blob: aa4d502703ea122dcf633695cb97bf1dd8f0b4cb [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)))
64 if not dofix or fix is None:
65 return (True, True)
66
lowRISC Contributors802543a2019-08-31 12:12:56 +010067 if exc.output:
68 output = exc.output.decode(sys.getfilesystemencoding())
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000069 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 Contributors802543a2019-08-31 12:12:56 +010079
80
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000081def lint_files(tools, input_files, dofix, verbose):
82 '''Run each tool on each file in input_files.'''
83 something_bad = False
lowRISC Contributors802543a2019-08-31 12:12:56 +010084
lowRISC Contributors802543a2019-08-31 12:12:56 +010085 if verbose:
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000086 print('Changed files: ' + ', '.join(input_files))
lowRISC Contributors802543a2019-08-31 12:12:56 +010087
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000088 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 Contributors802543a2019-08-31 12:12:56 +010094
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +000095 return 1 if something_bad else 0
96
97
98def 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
121def 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
136def 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 Contributors802543a2019-08-31 12:12:56 +0100153
154
155def 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 Swarbrickb2b5b432020-03-24 17:06:15 +0000170 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 Contributors802543a2019-08-31 12:12:56 +0100188
189 args = parser.parse_args()
190 if args.version:
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000191 show_and_exit(__file__, ['yapf', 'isort', 'flake8'])
lowRISC Contributors802543a2019-08-31 12:12:56 +0100192
lowRISC Contributors802543a2019-08-31 12:12:56 +0100193 running_hook = sys.argv[0].endswith('hooks/pre-commit')
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000194 if running_hook and args.verbose:
195 print('argv[0] is ' + sys.argv[0] + ' so running_hook is True')
lowRISC Contributors802543a2019-08-31 12:12:56 +0100196
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000197 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 Contributors802543a2019-08-31 12:12:56 +0100202
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000203 if args.file:
204 input_files = args.file
205 if args.commit:
206 raise RuntimeError('Cannot specify both --commit and a file list.')
lowRISC Contributors802543a2019-08-31 12:12:56 +0100207 else:
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000208 input_files = get_files_from_git(running_hook or args.commit)
lowRISC Contributors802543a2019-08-31 12:12:56 +0100209
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000210 if not input_files:
211 print('No input files. Exiting.')
212 return 0
lowRISC Contributors802543a2019-08-31 12:12:56 +0100213
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000214 return lint_files(args.tools, input_files, args.fix, args.verbose)
lowRISC Contributors802543a2019-08-31 12:12:56 +0100215
216
217if __name__ == "__main__":
Rupert Swarbrickb2b5b432020-03-24 17:06:15 +0000218 try:
219 sys.exit(main())
220 except RuntimeError as err:
221 sys.stderr.write('Error: {}\n'.format(err))
222 sys.exit(1)