blob: 81c522e5a12b43ac7fe4b7990c4ea883fa19b636 [file] [log] [blame]
mbligh99d2ded2008-06-23 16:17:36 +00001#!/usr/bin/python -u
beeps48fc6f52013-01-07 14:36:40 -08002"""
3Wrapper to patch pylint library functions to suit autotest.
4
5This script is invoked as part of the presubmit checks for autotest python
6files. It runs pylint on a list of files that it obtains either through
7the command line or from an environment variable set in pre-upload.py.
8
9Example:
10run_pylint.py filename.py
11"""
mbligh99d2ded2008-06-23 16:17:36 +000012
beeps98365d82013-02-20 20:08:07 -080013import fnmatch, os, re, sys
mbligh99d2ded2008-06-23 16:17:36 +000014
beeps98365d82013-02-20 20:08:07 -080015import common
16from autotest_lib.client.common_lib import autotemp, revision_control
beeps48fc6f52013-01-07 14:36:40 -080017
18# Do a basic check to see if pylint is even installed.
mbligh65e06b12008-08-22 18:12:49 +000019try:
20 import pylint
Eric Li861b2d52011-02-04 14:50:35 -080021 from pylint.__pkginfo__ import version as pylint_version
mbligh65e06b12008-08-22 18:12:49 +000022except ImportError:
beeps48fc6f52013-01-07 14:36:40 -080023 print ("Unable to import pylint, it may need to be installed."
24 " Run 'sudo aptitude install pylint' if you haven't already.")
mbligh65e06b12008-08-22 18:12:49 +000025 sys.exit(1)
26
Eric Li861b2d52011-02-04 14:50:35 -080027major, minor, release = pylint_version.split('.')
28pylint_version = float("%s.%s" % (major, minor))
mbligh99d2ded2008-06-23 16:17:36 +000029
beeps48fc6f52013-01-07 14:36:40 -080030# some files make pylint blow up, so make sure we ignore them
31BLACKLIST = ['/contrib/*', '/frontend/afe/management.py']
jadmanski94a64932008-07-22 14:03:10 +000032
33# patch up the logilab module lookup tools to understand autotest_lib.* trash
34import logilab.common.modutils
35_ffm = logilab.common.modutils.file_from_modpath
36def file_from_modpath(modpath, path=None, context_file=None):
beeps48fc6f52013-01-07 14:36:40 -080037 """
38 Wrapper to eliminate autotest_lib from modpath.
39
40 @param modpath: name of module splitted on '.'
41 @param path: optional list of paths where module should be searched for.
42 @param context_file: path to file doing the importing.
43 @return The path to the module as returned by the parent method invocation.
44 @raises: ImportError if these is no such module.
45 """
jadmanski94a64932008-07-22 14:03:10 +000046 if modpath[0] == "autotest_lib":
47 return _ffm(modpath[1:], path, context_file)
48 else:
49 return _ffm(modpath, path, context_file)
50logilab.common.modutils.file_from_modpath = file_from_modpath
51
52
mbligh99d2ded2008-06-23 16:17:36 +000053import pylint.lint
beeps2c669642013-01-14 18:30:57 -080054from pylint.checkers import base, imports, variables
mbligh99d2ded2008-06-23 16:17:36 +000055
56# need to put autotest root dir on sys.path so pylint will be happy
57autotest_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
58sys.path.insert(0, autotest_root)
59
60# patch up pylint import checker to handle our importing magic
beeps48fc6f52013-01-07 14:36:40 -080061ROOT_MODULE = 'autotest_lib.'
62COMMON_MODULE = 'common'
63
64
beeps98365d82013-02-20 20:08:07 -080065class pylint_error(Exception):
66 """
67 Error raised when pylint complains about a file.
68 """
69
70
71class run_pylint_error(pylint_error):
72 """
73 Error raised when an assumption made in this file is violated.
74 """
75
76
beeps48fc6f52013-01-07 14:36:40 -080077def patch_modname(modname):
78 """
79 Patches modname so we can make sense of autotest_lib modules.
80
81 @param modname: name of a module, contains '.'
82 @return modified modname string.
83 """
84 if modname.startswith(ROOT_MODULE) or modname.startswith(ROOT_MODULE[:-1]):
85 modname = modname[len(ROOT_MODULE):]
86 return modname
87
88
89def patch_consumed_list(to_consume=None, consumed=None):
90 """
91 Patches consumed modules list.
92
93 Prevents pylint from flagging 'common' as an unused import if we're
94 importing from autotest_lib. to_consume and consumed are dictionaries pylint
95 uses to record what 'names' (functions/modules/classes) it sees in a given
96 scope. When a name is referenced it's moved from one dictionary to the other
97 and after all visitors of the ast have been visited the entries left in
98 to_consume are reported by pylint as unused.
99
100 @param modname: name of a module, contains '.'
101 @param to_consume: a dictionary of names pylint needs to see referenced.
102 @param consumed: a dictionary of names that pylint has seen referenced.
103 @return modified modname string.
104 """
105 if (to_consume is not None and
106 consumed is not None and
107 COMMON_MODULE in to_consume):
108 consumed[COMMON_MODULE] = to_consume[COMMON_MODULE]
109 del to_consume[COMMON_MODULE]
110
mbligh99d2ded2008-06-23 16:17:36 +0000111
112class CustomImportsChecker(imports.ImportsChecker):
beeps48fc6f52013-01-07 14:36:40 -0800113 """Modifies stock imports checker to suit autotest."""
mbligh99d2ded2008-06-23 16:17:36 +0000114 def visit_from(self, node):
beeps48fc6f52013-01-07 14:36:40 -0800115 node.modname = patch_modname(node.modname)
116 return super(CustomImportsChecker, self).visit_from(node)
117
118
119class CustomVariablesChecker(variables.VariablesChecker):
120 """Modifies stock variables checker to suit autotest."""
121
122 def visit_module(self, node):
123 """
124 Unflag 'import common'.
125
126 _to_consume eg: [({to reference}, {referenced}, 'scope type')]
127 Enteries are appended to this list as we drill deeper in scope.
128 If we ever come across an 'import common' we immediately move it
129 to the consumed list.
130
131 @param node: node of the ast we're currently checking.
132 """
133 super(CustomVariablesChecker, self).visit_module(node)
134 scoped_names = self._to_consume.pop()
135 patch_consumed_list(scoped_names[0],scoped_names[1])
136 self._to_consume.append(scoped_names)
137
138 def visit_from(self, node):
139 """Patches modnames so pylints understands autotest_lib."""
140 node.modname = patch_modname(node.modname)
141 return super(CustomVariablesChecker, self).visit_from(node)
mbligh99d2ded2008-06-23 16:17:36 +0000142
beeps2c669642013-01-14 18:30:57 -0800143
144class CustomDocStringChecker(base.DocStringChecker):
145 """Modifies stock docstring checker to suit Autotest doxygen style."""
146
beeps1b6433b2013-01-31 17:46:50 -0800147 def visit_module(self, node):
148 """
149 Don't visit imported modules when checking for docstrings.
150
151 @param node: the node we're visiting.
152 """
153 pass
154
155
beeps98365d82013-02-20 20:08:07 -0800156 def visit_function(self, node):
157 """
158 Don't request docstrings for commonly overridden autotest functions.
159
160 @param node: node of the ast we're currently checking.
161 """
162 if (node.name in ('run_once', 'initialize', 'cleanup') and
163 any(ancestor.name == 'base_test' for ancestor in
164 node.parent.frame().ancestors())):
165 return
166
167 super(CustomDocStringChecker, self).visit_function(node)
168
169
Chris Sosaae6acd92013-02-06 15:08:04 -0800170 @staticmethod
171 def _should_skip_arg(arg):
beeps98365d82013-02-20 20:08:07 -0800172 """
173 @return: True if the argument given by arg is whitelisted, and does
174 not require a "@param" docstring.
175 """
Chris Sosaae6acd92013-02-06 15:08:04 -0800176 return arg in ('self', 'cls', 'args', 'kwargs', 'dargs')
177
178
beeps2c669642013-01-14 18:30:57 -0800179 def _check_docstring(self, node_type, node):
180 """
181 Teaches pylint to look for @param with each argument in the
182 function/method signature.
183
184 @param node_type: type of the node we're currently checking.
185 @param node: node of the ast we're currently checking.
186 """
187 super(CustomDocStringChecker, self)._check_docstring(node_type, node)
188 docstring = node.doc
189 if (docstring is not None and
190 (node_type is 'method' or
191 node_type is 'function')):
192 args = node.argnames()
193 old_msg = self.linter._messages['C0111'].msg
194 for arg in args:
195 arg_docstring_rgx = '.*@param '+arg+'.*'
196 line = re.search(arg_docstring_rgx, node.doc)
Chris Sosaae6acd92013-02-06 15:08:04 -0800197 if not line and not self._should_skip_arg(arg):
beeps2c669642013-01-14 18:30:57 -0800198 self.linter._messages['C0111'].msg = ('Docstring needs '
199 '"@param '+arg+':"')
200 self.add_message('C0111', node=node)
201 self.linter._messages['C0111'].msg = old_msg
202
203base.DocStringChecker = CustomDocStringChecker
mbligh99d2ded2008-06-23 16:17:36 +0000204imports.ImportsChecker = CustomImportsChecker
beeps48fc6f52013-01-07 14:36:40 -0800205variables.VariablesChecker = CustomVariablesChecker
mbligh99d2ded2008-06-23 16:17:36 +0000206
207
beeps98365d82013-02-20 20:08:07 -0800208def batch_check_files(file_paths, base_opts):
209 """
210 Run pylint on a list of files so we get consolidated errors.
211
212 @param file_paths: a list of file paths.
213 @param base_opts: a list of pylint config options.
214
215 @raises: pylint_error if pylint finds problems with a file
216 in this commit.
217 """
218 pylint_runner = pylint.lint.Run(list(base_opts) + list(file_paths),
219 exit=False)
220 if pylint_runner.linter.msg_status:
221 raise pylint_error(pylint_runner.linter.msg_status)
222
223
224def should_check_file(file_path):
225 """
226 Don't check blacklisted or non .py files.
227
228 @param file_path: abs path of file to check.
229 @return: True if this file is a non-blacklisted python file.
230 """
231 file_path = os.path.abspath(file_path)
232 if file_path.endswith('.py'):
233 return all(not fnmatch.fnmatch(file_path, '*' + pattern)
234 for pattern in BLACKLIST)
235 return False
236
237
beeps48fc6f52013-01-07 14:36:40 -0800238def check_file(file_path, base_opts):
239 """
240 Invokes pylint on files after confirming that they're not black listed.
241
beeps2c669642013-01-14 18:30:57 -0800242 @param base_opts: pylint base options.
beeps48fc6f52013-01-07 14:36:40 -0800243 @param file_path: path to the file we need to run pylint on.
244 """
beeps98365d82013-02-20 20:08:07 -0800245 if not isinstance(file_path, basestring):
246 raise TypeError('expected a string as filepath, got %s'%
247 type(file_path))
248
249 if should_check_file(file_path):
250 pylint_runner = pylint.lint.Run(base_opts + [file_path], exit=False)
251 if pylint_runner.linter.msg_status:
252 pylint_error(pylint_runner.linter.msg_status)
mbligh99d2ded2008-06-23 16:17:36 +0000253
254
255def visit(arg, dirname, filenames):
beeps48fc6f52013-01-07 14:36:40 -0800256 """
257 Visit function invoked in check_dir.
258
259 @param arg: arg from os.walk.path
260 @param dirname: dir from os.walk.path
261 @param filenames: files in dir from os.walk.path
262 """
mbligh99d2ded2008-06-23 16:17:36 +0000263 for filename in filenames:
beeps48fc6f52013-01-07 14:36:40 -0800264 check_file(os.path.join(dirname, filename), arg)
mbligh99d2ded2008-06-23 16:17:36 +0000265
266
beeps48fc6f52013-01-07 14:36:40 -0800267def check_dir(dir_path, base_opts):
268 """
269 Calls visit on files in dir_path.
270
beeps2c669642013-01-14 18:30:57 -0800271 @param base_opts: pylint base options.
beeps48fc6f52013-01-07 14:36:40 -0800272 @param dir_path: path to directory.
273 """
274 os.path.walk(dir_path, visit, base_opts)
mbligh99d2ded2008-06-23 16:17:36 +0000275
276
beeps48fc6f52013-01-07 14:36:40 -0800277def extend_baseopts(base_opts, new_opt):
278 """
279 Replaces an argument in base_opts with a cmd line argument.
280
281 @param base_opts: original pylint_base_opts.
282 @param new_opt: new cmd line option.
283 """
284 for args in base_opts:
285 if new_opt in args:
286 base_opts.remove(args)
287 base_opts.append(new_opt)
288
289
290def get_cmdline_options(args_list, pylint_base_opts, rcfile):
291 """
292 Parses args_list and extends pylint_base_opts.
293
294 Command line arguments might include options mixed with files.
295 Go through this list and filter out the options, if the options are
296 specified in the pylintrc file we cannot replace them and the file
297 needs to be edited. If the options are already a part of
298 pylint_base_opts we replace them, and if not we append to
299 pylint_base_opts.
300
301 @param args_list: list of files/pylint args passed in through argv.
302 @param pylint_base_opts: default pylint options.
303 @param rcfile: text from pylint_rc.
304 """
305 for args in args_list:
306 if args.startswith('--'):
307 opt_name = args[2:].split('=')[0]
308 if opt_name in rcfile and pylint_version >= 0.21:
beeps98365d82013-02-20 20:08:07 -0800309 raise run_pylint_error('The rcfile already contains the %s '
310 'option. Please edit pylintrc instead.'
311 % opt_name)
beeps48fc6f52013-01-07 14:36:40 -0800312 else:
313 extend_baseopts(pylint_base_opts, args)
314 args_list.remove(args)
315
beeps98365d82013-02-20 20:08:07 -0800316
317def git_show_to_temp_file(commit, original_file, new_temp_file):
318 """
319 'Git shows' the file in original_file to a tmp file with
320 the name new_temp_file. We need to preserve the filename
321 as it gets reflected in pylints error report.
322
323 @param commit: commit hash of the commit we're running repo upload on.
324 @param original_file: the path to the original file we'd like to run
325 'git show' on.
326 @param new_temp_file: new_temp_file is the path to a temp file we write the
327 output of 'git show' into.
328 """
329 git_repo = revision_control.GitRepo(common.autotest_dir, None, None,
330 common.autotest_dir)
331
332 with open(new_temp_file, 'w') as f:
333 output = git_repo.gitcmd('show --no-ext-diff %s:%s'
334 % (commit, original_file),
335 ignore_status=False).stdout
336 f.write(output)
337
338
339def check_committed_files(work_tree_files, commit, pylint_base_opts):
340 """
341 Get a list of files corresponding to the commit hash.
342
343 The contents of a file in the git work tree can differ from the contents
344 of a file in the commit we mean to upload. To work around this we run
345 pylint on a temp file into which we've 'git show'n the committed version
346 of each file.
347
348 @param work_tree_files: list of files in this commit specified by their
349 absolute path.
350 @param commit: hash of the commit this upload applies to.
351 @param pylint_base_opts: a list of pylint config options.
352 """
353 files_to_check = filter(should_check_file, work_tree_files)
354
355 # Map the absolute path of each file so it's relative to the autotest repo.
356 # All files that are a part of this commit should have an abs path within
357 # the autotest repo, so this regex should never fail.
358 work_tree_files = [re.search(r'%s/(.*)' % common.autotest_dir, f).group(1)
359 for f in files_to_check]
360
361 tempdir = None
362 try:
363 tempdir = autotemp.tempdir()
364 temp_files = [os.path.join(tempdir.name, file_path.split('/')[-1:][0])
365 for file_path in work_tree_files]
366
367 for file_tuple in zip(work_tree_files, temp_files):
368 git_show_to_temp_file(commit, *file_tuple)
369 # Only check if we successfully git showed all files in the commit.
370 batch_check_files(temp_files, pylint_base_opts)
371 finally:
372 if tempdir:
373 tempdir.clean()
374
375
beeps48fc6f52013-01-07 14:36:40 -0800376def main():
377 """Main function checks each file in a commit for pylint violations."""
378
beeps1b6433b2013-01-31 17:46:50 -0800379 # For now all error/warning/refactor/convention exceptions except those in
380 # the enable string are disabled.
381 # W0611: All imported modules (except common) need to be used.
382 # W1201: Logging methods should take the form
383 # logging.<loggingmethod>(format_string, format_args...); and not
384 # logging.<loggingmethod>(format_string % (format_args...))
385 # C0111: Docstring needed. Also checks @param for each arg.
386 # C0112: Non-empty Docstring needed.
beeps48fc6f52013-01-07 14:36:40 -0800387 # Ideally we would like to enable as much as we can, but if we did so at
388 # this stage anyone who makes a tiny change to a file will be tasked with
389 # cleaning all the lint in it. See chromium-os:37364.
390
391 # Note: There are three major sources of E1101/E1103/E1120 false positives:
392 # * common_lib.enum.Enum objects
393 # * DB model objects (scheduler models are the worst, but Django models also
394 # generate some errors)
395 pylint_rc = os.path.join(os.path.dirname(os.path.abspath(__file__)),
396 'pylintrc')
397 if pylint_version >= 0.21:
398 pylint_base_opts = ['--rcfile=%s' % pylint_rc,
399 '--reports=no',
beeps55532562013-01-16 12:13:46 -0800400 '--disable=W,R,E,C,F',
beeps1b6433b2013-01-31 17:46:50 -0800401 '--enable=W0611,W1201,C0111,C0112',
402 '--no-docstring-rgx=_.*',]
beeps48fc6f52013-01-07 14:36:40 -0800403 else:
404 all_failures = 'error,warning,refactor,convention'
405 pylint_base_opts = ['--disable-msg-cat=%s' % all_failures,
406 '--reports=no',
407 '--include-ids=y',
beeps1b6433b2013-01-31 17:46:50 -0800408 '--ignore-docstrings=n',
409 '--no-docstring-rgx=_.*',]
beeps48fc6f52013-01-07 14:36:40 -0800410
411 # run_pylint can be invoked directly with command line arguments,
412 # or through a presubmit hook which uses the arguments in pylintrc. In the
413 # latter case no command line arguments are passed. If it is invoked
414 # directly without any arguments, it should check all files in the cwd.
415 args_list = sys.argv[1:]
416 if args_list:
417 get_cmdline_options(args_list,
418 pylint_base_opts,
419 open(pylint_rc).read())
beeps98365d82013-02-20 20:08:07 -0800420 batch_check_files(args_list, pylint_base_opts)
421 elif os.environ.get('PRESUBMIT_FILES') is not None:
422 check_committed_files(
423 os.environ.get('PRESUBMIT_FILES').split('\n'),
424 os.environ.get('PRESUBMIT_COMMIT'),
425 pylint_base_opts)
beeps48fc6f52013-01-07 14:36:40 -0800426 else:
beeps98365d82013-02-20 20:08:07 -0800427 check_dir('.', pylint_base_opts)
beeps48fc6f52013-01-07 14:36:40 -0800428
429
430if __name__ == '__main__':
431 main()