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