lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # Copyright lowRISC contributors. |
| 3 | # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| 4 | # SPDX-License-Identifier: Apache-2.0 |
| 5 | """Lint Python for lowRISC rules""" |
| 6 | |
| 7 | import argparse |
| 8 | import os |
| 9 | import subprocess |
| 10 | import sys |
| 11 | |
| 12 | import pkg_resources |
| 13 | |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 14 | # A map from tool name to the tuple (check, fix). These are two commands which |
| 15 | # should be run to check for and fix errors, respectively. If the tool doesn't |
| 16 | # support fixing in place, the "fix" command can be None. |
| 17 | _KNOWN_TOOLS = { |
| 18 | 'yapf': (['yapf', '-d'], ['yapf', '-i']), |
| 19 | 'isort': (['isort', '-c', '-w79'], ['isort', '-w79']), |
| 20 | 'flake8': (['flake8'], None) |
| 21 | } |
| 22 | |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 23 | |
| 24 | # include here because in hook case don't want to import reggen |
| 25 | def show_and_exit(clitool, packages): |
| 26 | util_path = os.path.dirname(os.path.realpath(clitool)) |
| 27 | os.chdir(util_path) |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 28 | ver = subprocess.check_output( |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 29 | ["git", "describe", "--always", "--dirty", "--broken"], |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 30 | universal_newlines=True).strip() |
| 31 | if not ver: |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 32 | ver = 'not found (not in Git repository?)' |
| 33 | sys.stderr.write(clitool + " Git version " + ver + '\n') |
| 34 | for p in packages: |
| 35 | sys.stderr.write(p + ' ' + pkg_resources.require(p)[0].version + '\n') |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 36 | sys.exit(0) |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 37 | |
| 38 | |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 39 | def run_linter(tool, dofix, verbose, files): |
| 40 | '''Run the given lint tool on a nonempty list of files |
| 41 | |
| 42 | Returns (bad, stop) where bad is true if the lint tool spotted any errors. |
| 43 | stop is true if the tool fails and either dofix is false or the tool |
| 44 | doesn't have a fix command. |
| 45 | |
| 46 | ''' |
| 47 | assert files |
| 48 | assert tool in _KNOWN_TOOLS |
| 49 | check, fix = _KNOWN_TOOLS[tool] |
| 50 | |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 51 | if verbose: |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 52 | print('Running {}'.format(' '.join(check))) |
| 53 | |
| 54 | check_cmd = check + files |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 55 | try: |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 56 | assert check |
| 57 | subprocess.check_output(check_cmd, stderr=subprocess.STDOUT) |
| 58 | return (False, False) |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 59 | except FileNotFoundError: |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 60 | print('{} not found: do you need to install it?'.format(check[0])) |
| 61 | return (True, True) |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 62 | except subprocess.CalledProcessError as exc: |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 63 | sys.stderr.write('Lint failed:\n {}\n'.format(' '.join(check_cmd))) |
| 64 | if not dofix or fix is None: |
| 65 | return (True, True) |
| 66 | |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 67 | if exc.output: |
| 68 | output = exc.output.decode(sys.getfilesystemencoding()) |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 69 | print('\t', |
| 70 | '\n\t'.join(output.splitlines()), |
| 71 | sep='', |
| 72 | file=sys.stderr) |
| 73 | |
| 74 | print("Fixing...", file=sys.stderr) |
| 75 | subprocess.check_call(fix + files, |
| 76 | stderr=subprocess.STDOUT, |
| 77 | stdout=subprocess.DEVNULL) |
| 78 | return (True, False) |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 79 | |
| 80 | |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 81 | def lint_files(tools, input_files, dofix, verbose): |
| 82 | '''Run each tool on each file in input_files.''' |
| 83 | something_bad = False |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 84 | |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 85 | if verbose: |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 86 | print('Changed files: ' + ', '.join(input_files)) |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 87 | |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 88 | for tool in tools: |
| 89 | bad, stop = run_linter(tool, dofix, verbose, input_files) |
| 90 | if bad: |
| 91 | something_bad = True |
| 92 | if stop: |
| 93 | break |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 94 | |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 95 | return 1 if something_bad else 0 |
| 96 | |
| 97 | |
| 98 | def get_files_from_git(show_cached): |
| 99 | '''Return a list of paths to work on |
| 100 | |
| 101 | Use git diff to find the list of Python files that have changed. If |
| 102 | show_cached is True, this operates on the staging tree, rather than the |
| 103 | work tree. |
| 104 | |
| 105 | ''' |
| 106 | # This git diff command will return all the paths that have changed and end |
| 107 | # in .py |
| 108 | cmd = (['git', 'diff', '--name-only', '--diff-filter=ACM'] + |
| 109 | (['--cached'] if show_cached else []) + ['*.py']) |
| 110 | |
| 111 | lines = subprocess.check_output(cmd, universal_newlines=True).split('\n') |
| 112 | paths = [] |
| 113 | for line in lines: |
| 114 | path = line.strip() |
| 115 | if path: |
| 116 | paths.append(path) |
| 117 | |
| 118 | return paths |
| 119 | |
| 120 | |
| 121 | def parse_tool_list(as_string): |
| 122 | '''Parse a list of tools (as passed to the --tools argument) |
| 123 | |
| 124 | Returns a nonempty list of strings, one for each tool included.''' |
| 125 | tools = [] |
| 126 | for name in as_string.split(','): |
| 127 | name = name.strip() |
| 128 | if name not in _KNOWN_TOOLS: |
| 129 | raise argparse.ArgumentTypeError('Unknown tool: {}.'.format(name)) |
| 130 | tools.append(name) |
| 131 | |
| 132 | assert tools |
| 133 | return tools |
| 134 | |
| 135 | |
| 136 | def install_commit_hook(): |
| 137 | '''Install this script as a pre-commit hook in this repository''' |
| 138 | git_dir = subprocess.check_output(['git', 'rev-parse', '--git-dir'], |
| 139 | universal_newlines=True).strip() |
| 140 | hooks_dir = os.path.join(git_dir, 'hooks') |
| 141 | os.makedirs(hooks_dir, exist_ok=True) |
| 142 | |
| 143 | hook_path = os.path.join(hooks_dir, 'pre-commit') |
| 144 | if os.path.exists(hook_path): |
| 145 | raise RuntimeError('There is already a hook at {}.'.format(hook_path)) |
| 146 | |
| 147 | # Find a relative path from hooks_dir to __file__ (so we can move the whole |
| 148 | # repo in the future without breaking anything). |
| 149 | rel_path = os.path.relpath(__file__, hooks_dir) |
| 150 | |
| 151 | print('Installing hook at {}, pointing at {}.'.format(hook_path, rel_path)) |
| 152 | os.symlink(rel_path, hook_path) |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 153 | |
| 154 | |
| 155 | def main(): |
| 156 | parser = argparse.ArgumentParser( |
| 157 | description=__doc__, |
| 158 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 159 | parser.add_argument( |
| 160 | '--version', action='store_true', help='Show version and exit') |
| 161 | parser.add_argument( |
| 162 | '-v', |
| 163 | '--verbose', |
| 164 | action='store_true', |
| 165 | help='Verbose output: ls the output directories') |
| 166 | parser.add_argument( |
| 167 | '-c', |
| 168 | '--commit', |
| 169 | action='store_true', |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 170 | help=('Only check files staged for commit rather than' |
| 171 | 'all modified files (forced when run as git hook)')) |
| 172 | parser.add_argument('--fix', |
| 173 | action='store_true', |
| 174 | help='Fix files detected with problems') |
| 175 | parser.add_argument('--hook', |
| 176 | action='store_true', |
| 177 | help='Install as ../.git/hooks/pre-commit and exit') |
| 178 | parser.add_argument('-f', |
| 179 | '--file', |
| 180 | metavar='file', |
| 181 | nargs='+', |
| 182 | default=[], |
| 183 | help='File(s) to check instead of deriving from git') |
| 184 | parser.add_argument('--tools', |
| 185 | type=parse_tool_list, |
| 186 | default=['yapf', 'isort'], |
| 187 | help='Comma separated list of linting tools to use') |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 188 | |
| 189 | args = parser.parse_args() |
| 190 | if args.version: |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 191 | show_and_exit(__file__, ['yapf', 'isort', 'flake8']) |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 192 | |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 193 | running_hook = sys.argv[0].endswith('hooks/pre-commit') |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 194 | if running_hook and args.verbose: |
| 195 | print('argv[0] is ' + sys.argv[0] + ' so running_hook is True') |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 196 | |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 197 | if args.hook: |
| 198 | if args.file: |
| 199 | raise RuntimeError('Cannot specify both --hook and a file list.') |
| 200 | install_commit_hook() |
| 201 | return 0 |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 202 | |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 203 | if args.file: |
| 204 | input_files = args.file |
| 205 | if args.commit: |
| 206 | raise RuntimeError('Cannot specify both --commit and a file list.') |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 207 | else: |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 208 | input_files = get_files_from_git(running_hook or args.commit) |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 209 | |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 210 | if not input_files: |
| 211 | print('No input files. Exiting.') |
| 212 | return 0 |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 213 | |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 214 | return lint_files(args.tools, input_files, args.fix, args.verbose) |
lowRISC Contributors | 802543a | 2019-08-31 12:12:56 +0100 | [diff] [blame] | 215 | |
| 216 | |
| 217 | if __name__ == "__main__": |
Rupert Swarbrick | b2b5b43 | 2020-03-24 17:06:15 +0000 | [diff] [blame] | 218 | try: |
| 219 | sys.exit(main()) |
| 220 | except RuntimeError as err: |
| 221 | sys.stderr.write('Error: {}\n'.format(err)) |
| 222 | sys.exit(1) |