| #!/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) |