blob: dc4c9c0554e4471717f552e62d14bf5e7048a0ef [file] [log] [blame]
lowRISC Contributors802543a2019-08-31 12:12:56 +01001#!/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
7import argparse
8import os
9import subprocess
10import sys
11
12import pkg_resources
13
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000014# 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 Contributors802543a2019-08-31 12:12:56 +010023
24# include here because in hook case don't want to import reggen
25def show_and_exit(clitool, packages):
26 util_path = os.path.dirname(os.path.realpath(clitool))
27 os.chdir(util_path)
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000028 ver = subprocess.check_output(
lowRISC Contributors802543a2019-08-31 12:12:56 +010029 ["git", "describe", "--always", "--dirty", "--broken"],
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000030 universal_newlines=True).strip()
31 if not ver:
lowRISC Contributors802543a2019-08-31 12:12:56 +010032 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 Swarbrickb2b5b432020-03-24 17:06:15 +000036 sys.exit(0)
lowRISC Contributors802543a2019-08-31 12:12:56 +010037
38
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000039def 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 Contributors802543a2019-08-31 12:12:56 +010051 if verbose:
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000052 print('Running {}'.format(' '.join(check)))
53
54 check_cmd = check + files
lowRISC Contributors802543a2019-08-31 12:12:56 +010055 try:
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000056 assert check
57 subprocess.check_output(check_cmd, stderr=subprocess.STDOUT)
58 return (False, False)
lowRISC Contributors802543a2019-08-31 12:12:56 +010059 except FileNotFoundError:
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000060 print('{} not found: do you need to install it?'.format(check[0]))
61 return (True, True)
lowRISC Contributors802543a2019-08-31 12:12:56 +010062 except subprocess.CalledProcessError as exc:
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000063 sys.stderr.write('Lint failed:\n {}\n'.format(' '.join(check_cmd)))
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000064
lowRISC Contributors802543a2019-08-31 12:12:56 +010065 if exc.output:
66 output = exc.output.decode(sys.getfilesystemencoding())
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000067 print('\t',
68 '\n\t'.join(output.splitlines()),
69 sep='',
70 file=sys.stderr)
71
Philipp Wagner7b4e26a2021-05-18 15:42:02 +010072 if not dofix or fix is None:
73 return (True, True)
74
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000075 print("Fixing...", file=sys.stderr)
76 subprocess.check_call(fix + files,
77 stderr=subprocess.STDOUT,
78 stdout=subprocess.DEVNULL)
79 return (True, False)
lowRISC Contributors802543a2019-08-31 12:12:56 +010080
81
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000082def lint_files(tools, input_files, dofix, verbose):
83 '''Run each tool on each file in input_files.'''
84 something_bad = False
lowRISC Contributors802543a2019-08-31 12:12:56 +010085
lowRISC Contributors802543a2019-08-31 12:12:56 +010086 if verbose:
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000087 print('Changed files: ' + ', '.join(input_files))
lowRISC Contributors802543a2019-08-31 12:12:56 +010088
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000089 for tool in tools:
90 bad, stop = run_linter(tool, dofix, verbose, input_files)
91 if bad:
92 something_bad = True
93 if stop:
94 break
lowRISC Contributors802543a2019-08-31 12:12:56 +010095
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000096 return 1 if something_bad else 0
97
98
99def get_files_from_git(show_cached):
100 '''Return a list of paths to work on
101
102 Use git diff to find the list of Python files that have changed. If
103 show_cached is True, this operates on the staging tree, rather than the
104 work tree.
105
106 '''
107 # This git diff command will return all the paths that have changed and end
108 # in .py
109 cmd = (['git', 'diff', '--name-only', '--diff-filter=ACM'] +
110 (['--cached'] if show_cached else []) + ['*.py'])
111
112 lines = subprocess.check_output(cmd, universal_newlines=True).split('\n')
113 paths = []
114 for line in lines:
115 path = line.strip()
116 if path:
117 paths.append(path)
118
119 return paths
120
121
122def parse_tool_list(as_string):
123 '''Parse a list of tools (as passed to the --tools argument)
124
125 Returns a nonempty list of strings, one for each tool included.'''
126 tools = []
127 for name in as_string.split(','):
128 name = name.strip()
129 if name not in _KNOWN_TOOLS:
130 raise argparse.ArgumentTypeError('Unknown tool: {}.'.format(name))
131 tools.append(name)
132
133 assert tools
134 return tools
135
136
137def install_commit_hook():
138 '''Install this script as a pre-commit hook in this repository'''
139 git_dir = subprocess.check_output(['git', 'rev-parse', '--git-dir'],
140 universal_newlines=True).strip()
141 hooks_dir = os.path.join(git_dir, 'hooks')
142 os.makedirs(hooks_dir, exist_ok=True)
143
144 hook_path = os.path.join(hooks_dir, 'pre-commit')
145 if os.path.exists(hook_path):
146 raise RuntimeError('There is already a hook at {}.'.format(hook_path))
147
148 # Find a relative path from hooks_dir to __file__ (so we can move the whole
149 # repo in the future without breaking anything).
150 rel_path = os.path.relpath(__file__, hooks_dir)
151
152 print('Installing hook at {}, pointing at {}.'.format(hook_path, rel_path))
153 os.symlink(rel_path, hook_path)
lowRISC Contributors802543a2019-08-31 12:12:56 +0100154
155
156def main():
157 parser = argparse.ArgumentParser(
158 description=__doc__,
159 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
160 parser.add_argument(
161 '--version', action='store_true', help='Show version and exit')
162 parser.add_argument(
163 '-v',
164 '--verbose',
165 action='store_true',
166 help='Verbose output: ls the output directories')
167 parser.add_argument(
168 '-c',
169 '--commit',
170 action='store_true',
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000171 help=('Only check files staged for commit rather than'
172 'all modified files (forced when run as git hook)'))
173 parser.add_argument('--fix',
174 action='store_true',
175 help='Fix files detected with problems')
176 parser.add_argument('--hook',
177 action='store_true',
178 help='Install as ../.git/hooks/pre-commit and exit')
179 parser.add_argument('-f',
180 '--file',
181 metavar='file',
182 nargs='+',
183 default=[],
184 help='File(s) to check instead of deriving from git')
185 parser.add_argument('--tools',
186 type=parse_tool_list,
187 default=['yapf', 'isort'],
188 help='Comma separated list of linting tools to use')
lowRISC Contributors802543a2019-08-31 12:12:56 +0100189
190 args = parser.parse_args()
191 if args.version:
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000192 show_and_exit(__file__, ['yapf', 'isort', 'flake8'])
lowRISC Contributors802543a2019-08-31 12:12:56 +0100193
lowRISC Contributors802543a2019-08-31 12:12:56 +0100194 running_hook = sys.argv[0].endswith('hooks/pre-commit')
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000195 if running_hook and args.verbose:
196 print('argv[0] is ' + sys.argv[0] + ' so running_hook is True')
lowRISC Contributors802543a2019-08-31 12:12:56 +0100197
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000198 if args.hook:
199 if args.file:
200 raise RuntimeError('Cannot specify both --hook and a file list.')
201 install_commit_hook()
202 return 0
lowRISC Contributors802543a2019-08-31 12:12:56 +0100203
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000204 if args.file:
205 input_files = args.file
206 if args.commit:
207 raise RuntimeError('Cannot specify both --commit and a file list.')
lowRISC Contributors802543a2019-08-31 12:12:56 +0100208 else:
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000209 input_files = get_files_from_git(running_hook or args.commit)
lowRISC Contributors802543a2019-08-31 12:12:56 +0100210
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000211 if not input_files:
212 print('No input files. Exiting.')
213 return 0
lowRISC Contributors802543a2019-08-31 12:12:56 +0100214
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000215 return lint_files(args.tools, input_files, args.fix, args.verbose)
lowRISC Contributors802543a2019-08-31 12:12:56 +0100216
217
218if __name__ == "__main__":
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000219 try:
220 sys.exit(main())
221 except RuntimeError as err:
222 sys.stderr.write('Error: {}\n'.format(err))
223 sys.exit(1)