|  | #!/usr/bin/env python3 | 
|  | # Copyright lowRISC contributors. | 
|  | # Licensed under the Apache License, Version 2.0, see LICENSE for details. | 
|  | # SPDX-License-Identifier: Apache-2.0 | 
|  | """Lint Python for lowRISC rules""" | 
|  |  | 
|  | import argparse | 
|  | import os | 
|  | import subprocess | 
|  | import sys | 
|  |  | 
|  | import pkg_resources | 
|  |  | 
|  | # A map from tool name to the tuple (check, fix). These are two commands which | 
|  | # should be run to check for and fix errors, respectively. If the tool doesn't | 
|  | # support fixing in place, the "fix" command can be None. | 
|  | _KNOWN_TOOLS = { | 
|  | 'yapf': (['yapf', '-d'], ['yapf', '-i']), | 
|  | 'isort': (['isort', '-c', '-w79'], ['isort', '-w79']), | 
|  | 'flake8': (['flake8'], None) | 
|  | } | 
|  |  | 
|  |  | 
|  | # include here because in hook case don't want to import reggen | 
|  | def show_and_exit(clitool, packages): | 
|  | util_path = os.path.dirname(os.path.realpath(clitool)) | 
|  | os.chdir(util_path) | 
|  | ver = subprocess.check_output( | 
|  | ["git", "describe", "--always", "--dirty", "--broken"], | 
|  | universal_newlines=True).strip() | 
|  | if not ver: | 
|  | ver = 'not found (not in Git repository?)' | 
|  | sys.stderr.write(clitool + " Git version " + ver + '\n') | 
|  | for p in packages: | 
|  | sys.stderr.write(p + ' ' + pkg_resources.require(p)[0].version + '\n') | 
|  | sys.exit(0) | 
|  |  | 
|  |  | 
|  | def run_linter(tool, dofix, verbose, files): | 
|  | '''Run the given lint tool on a nonempty list of files | 
|  |  | 
|  | Returns (bad, stop) where bad is true if the lint tool spotted any errors. | 
|  | stop is true if the tool fails and either dofix is false or the tool | 
|  | doesn't have a fix command. | 
|  |  | 
|  | ''' | 
|  | assert files | 
|  | assert tool in _KNOWN_TOOLS | 
|  | check, fix = _KNOWN_TOOLS[tool] | 
|  |  | 
|  | if verbose: | 
|  | print('Running {}'.format(' '.join(check))) | 
|  |  | 
|  | check_cmd = check + files | 
|  | try: | 
|  | assert check | 
|  | subprocess.check_output(check_cmd, stderr=subprocess.STDOUT) | 
|  | return (False, False) | 
|  | except FileNotFoundError: | 
|  | print('{} not found: do you need to install it?'.format(check[0])) | 
|  | return (True, True) | 
|  | except subprocess.CalledProcessError as exc: | 
|  | sys.stderr.write('Lint failed:\n  {}\n'.format(' '.join(check_cmd))) | 
|  |  | 
|  | if exc.output: | 
|  | output = exc.output.decode(sys.getfilesystemencoding()) | 
|  | print('\t', | 
|  | '\n\t'.join(output.splitlines()), | 
|  | sep='', | 
|  | file=sys.stderr) | 
|  |  | 
|  | if not dofix or fix is None: | 
|  | return (True, True) | 
|  |  | 
|  | print("Fixing...", file=sys.stderr) | 
|  | subprocess.check_call(fix + files, | 
|  | stderr=subprocess.STDOUT, | 
|  | stdout=subprocess.DEVNULL) | 
|  | return (True, False) | 
|  |  | 
|  |  | 
|  | def lint_files(tools, input_files, dofix, verbose): | 
|  | '''Run each tool on each file in input_files.''' | 
|  | something_bad = False | 
|  |  | 
|  | if verbose: | 
|  | print('Changed files: ' + ', '.join(input_files)) | 
|  |  | 
|  | for tool in tools: | 
|  | bad, stop = run_linter(tool, dofix, verbose, input_files) | 
|  | if bad: | 
|  | something_bad = True | 
|  | if stop: | 
|  | break | 
|  |  | 
|  | return 1 if something_bad else 0 | 
|  |  | 
|  |  | 
|  | def get_files_from_git(show_cached): | 
|  | '''Return a list of paths to work on | 
|  |  | 
|  | Use git diff to find the list of Python files that have changed. If | 
|  | show_cached is True, this operates on the staging tree, rather than the | 
|  | work tree. | 
|  |  | 
|  | ''' | 
|  | # This git diff command will return all the paths that have changed and end | 
|  | # in .py | 
|  | cmd = (['git', 'diff', '--name-only', '--diff-filter=ACM'] + | 
|  | (['--cached'] if show_cached else []) + ['*.py']) | 
|  |  | 
|  | lines = subprocess.check_output(cmd, universal_newlines=True).split('\n') | 
|  | paths = [] | 
|  | for line in lines: | 
|  | path = line.strip() | 
|  | if path: | 
|  | paths.append(path) | 
|  |  | 
|  | return paths | 
|  |  | 
|  |  | 
|  | def parse_tool_list(as_string): | 
|  | '''Parse a list of tools (as passed to the --tools argument) | 
|  |  | 
|  | Returns a nonempty list of strings, one for each tool included.''' | 
|  | tools = [] | 
|  | for name in as_string.split(','): | 
|  | name = name.strip() | 
|  | if name not in _KNOWN_TOOLS: | 
|  | raise argparse.ArgumentTypeError('Unknown tool: {}.'.format(name)) | 
|  | tools.append(name) | 
|  |  | 
|  | assert tools | 
|  | return tools | 
|  |  | 
|  |  | 
|  | def install_commit_hook(): | 
|  | '''Install this script as a pre-commit hook in this repository''' | 
|  | git_dir = subprocess.check_output(['git', 'rev-parse', '--git-dir'], | 
|  | universal_newlines=True).strip() | 
|  | hooks_dir = os.path.join(git_dir, 'hooks') | 
|  | os.makedirs(hooks_dir, exist_ok=True) | 
|  |  | 
|  | hook_path = os.path.join(hooks_dir, 'pre-commit') | 
|  | if os.path.exists(hook_path): | 
|  | raise RuntimeError('There is already a hook at {}.'.format(hook_path)) | 
|  |  | 
|  | # Find a relative path from hooks_dir to __file__ (so we can move the whole | 
|  | # repo in the future without breaking anything). | 
|  | rel_path = os.path.relpath(__file__, hooks_dir) | 
|  |  | 
|  | print('Installing hook at {}, pointing at {}.'.format(hook_path, rel_path)) | 
|  | os.symlink(rel_path, hook_path) | 
|  |  | 
|  |  | 
|  | def main(): | 
|  | parser = argparse.ArgumentParser( | 
|  | description=__doc__, | 
|  | formatter_class=argparse.ArgumentDefaultsHelpFormatter) | 
|  | parser.add_argument( | 
|  | '--version', action='store_true', help='Show version and exit') | 
|  | parser.add_argument( | 
|  | '-v', | 
|  | '--verbose', | 
|  | action='store_true', | 
|  | help='Verbose output: ls the output directories') | 
|  | parser.add_argument( | 
|  | '-c', | 
|  | '--commit', | 
|  | action='store_true', | 
|  | help=('Only check files staged for commit rather than' | 
|  | 'all modified files (forced when run as git hook)')) | 
|  | parser.add_argument('--fix', | 
|  | action='store_true', | 
|  | help='Fix files detected with problems') | 
|  | parser.add_argument('--hook', | 
|  | action='store_true', | 
|  | help='Install as ../.git/hooks/pre-commit and exit') | 
|  | parser.add_argument('-f', | 
|  | '--file', | 
|  | metavar='file', | 
|  | nargs='+', | 
|  | default=[], | 
|  | help='File(s) to check instead of deriving from git') | 
|  | parser.add_argument('--tools', | 
|  | type=parse_tool_list, | 
|  | default=['yapf', 'isort'], | 
|  | help='Comma separated list of linting tools to use') | 
|  |  | 
|  | args = parser.parse_args() | 
|  | if args.version: | 
|  | show_and_exit(__file__, ['yapf', 'isort', 'flake8']) | 
|  |  | 
|  | running_hook = sys.argv[0].endswith('hooks/pre-commit') | 
|  | if running_hook and args.verbose: | 
|  | print('argv[0] is ' + sys.argv[0] + ' so running_hook is True') | 
|  |  | 
|  | if args.hook: | 
|  | if args.file: | 
|  | raise RuntimeError('Cannot specify both --hook and a file list.') | 
|  | install_commit_hook() | 
|  | return 0 | 
|  |  | 
|  | if args.file: | 
|  | input_files = args.file | 
|  | if args.commit: | 
|  | raise RuntimeError('Cannot specify both --commit and a file list.') | 
|  | else: | 
|  | input_files = get_files_from_git(running_hook or args.commit) | 
|  |  | 
|  | if not input_files: | 
|  | print('No input files. Exiting.') | 
|  | return 0 | 
|  |  | 
|  | return lint_files(args.tools, input_files, args.fix, args.verbose) | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | try: | 
|  | sys.exit(main()) | 
|  | except RuntimeError as err: | 
|  | sys.stderr.write('Error: {}\n'.format(err)) | 
|  | sys.exit(1) |