1#!/usr/bin/python -u 2""" 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""" 12 13import fnmatch 14import logging 15import os 16import re 17import sys 18 19import common 20from autotest_lib.client.common_lib import autotemp, revision_control 21 22# Do a basic check to see if pylint is even installed. 23try: 24 import pylint 25 from pylint.__pkginfo__ import version as pylint_version 26except ImportError: 27 print ("Unable to import pylint, it may need to be installed." 28 " Run 'sudo aptitude install pylint' if you haven't already.") 29 sys.exit(1) 30 31major, minor, release = pylint_version.split('.') 32pylint_version = float("%s.%s" % (major, minor)) 33 34# some files make pylint blow up, so make sure we ignore them 35BLACKLIST = ['/site-packages/*', '/contrib/*', '/frontend/afe/management.py'] 36 37# patch up the logilab module lookup tools to understand autotest_lib.* trash 38import logilab.common.modutils 39_ffm = logilab.common.modutils.file_from_modpath 40def file_from_modpath(modpath, path=None, context_file=None): 41 """ 42 Wrapper to eliminate autotest_lib from modpath. 43 44 @param modpath: name of module splitted on '.' 45 @param path: optional list of paths where module should be searched for. 46 @param context_file: path to file doing the importing. 47 @return The path to the module as returned by the parent method invocation. 48 @raises: ImportError if these is no such module. 49 """ 50 if modpath[0] == "autotest_lib": 51 return _ffm(modpath[1:], path, context_file) 52 else: 53 return _ffm(modpath, path, context_file) 54logilab.common.modutils.file_from_modpath = file_from_modpath 55 56 57import pylint.lint 58from pylint.checkers import base, imports, variables 59 60# need to put autotest root dir on sys.path so pylint will be happy 61autotest_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 62sys.path.insert(0, autotest_root) 63 64# patch up pylint import checker to handle our importing magic 65ROOT_MODULE = 'autotest_lib.' 66 67# A list of modules for pylint to ignore, specifically, these modules 68# are imported for their side-effects and are not meant to be used. 69_IGNORE_MODULES=['common', 'frontend_test_utils', 70 'setup_django_environment', 71 'setup_django_lite_environment', 72 'setup_django_readonly_environment', 'setup_test_environment',] 73 74 75class pylint_error(Exception): 76 """ 77 Error raised when pylint complains about a file. 78 """ 79 80 81class run_pylint_error(pylint_error): 82 """ 83 Error raised when an assumption made in this file is violated. 84 """ 85 86 87def patch_modname(modname): 88 """ 89 Patches modname so we can make sense of autotest_lib modules. 90 91 @param modname: name of a module, contains '.' 92 @return modified modname string. 93 """ 94 if modname.startswith(ROOT_MODULE) or modname.startswith(ROOT_MODULE[:-1]): 95 modname = modname[len(ROOT_MODULE):] 96 return modname 97 98 99def patch_consumed_list(to_consume=None, consumed=None): 100 """ 101 Patches the consumed modules list to ignore modules with side effects. 102 103 Autotest relies on importing certain modules solely for their side 104 effects. Pylint doesn't understand this and flags them as unused, since 105 they're not referenced anywhere in the code. To overcome this we need 106 to transplant said modules into the dictionary of modules pylint has 107 already seen, before pylint checks it. 108 109 @param to_consume: a dictionary of names pylint needs to see referenced. 110 @param consumed: a dictionary of names that pylint has seen referenced. 111 """ 112 ignore_modules = [] 113 if (to_consume is not None and consumed is not None): 114 ignore_modules = [module_name for module_name in _IGNORE_MODULES 115 if module_name in to_consume] 116 117 for module_name in ignore_modules: 118 consumed[module_name] = to_consume[module_name] 119 del to_consume[module_name] 120 121 122class CustomImportsChecker(imports.ImportsChecker): 123 """Modifies stock imports checker to suit autotest.""" 124 def visit_from(self, node): 125 node.modname = patch_modname(node.modname) 126 return super(CustomImportsChecker, self).visit_from(node) 127 128 129class CustomVariablesChecker(variables.VariablesChecker): 130 """Modifies stock variables checker to suit autotest.""" 131 132 def visit_module(self, node): 133 """ 134 Unflag 'import common'. 135 136 _to_consume eg: [({to reference}, {referenced}, 'scope type')] 137 Enteries are appended to this list as we drill deeper in scope. 138 If we ever come across a module to ignore, we immediately move it 139 to the consumed list. 140 141 @param node: node of the ast we're currently checking. 142 """ 143 super(CustomVariablesChecker, self).visit_module(node) 144 scoped_names = self._to_consume.pop() 145 patch_consumed_list(scoped_names[0],scoped_names[1]) 146 self._to_consume.append(scoped_names) 147 148 def visit_from(self, node): 149 """Patches modnames so pylints understands autotest_lib.""" 150 node.modname = patch_modname(node.modname) 151 return super(CustomVariablesChecker, self).visit_from(node) 152 153 154class CustomDocStringChecker(base.DocStringChecker): 155 """Modifies stock docstring checker to suit Autotest doxygen style.""" 156 157 def visit_module(self, node): 158 """ 159 Don't visit imported modules when checking for docstrings. 160 161 @param node: the node we're visiting. 162 """ 163 pass 164 165 166 def visit_function(self, node): 167 """ 168 Don't request docstrings for commonly overridden autotest functions. 169 170 @param node: node of the ast we're currently checking. 171 """ 172 173 # Even plain functions will have a parent, which is the 174 # module they're in, and a frame, which is the context 175 # of said module; They need not however, always have 176 # ancestors. 177 if (node.name in ('run_once', 'initialize', 'cleanup') and 178 hasattr(node.parent.frame(), 'ancestors') and 179 any(ancestor.name == 'base_test' for ancestor in 180 node.parent.frame().ancestors())): 181 return 182 183 if _is_test_case_method(node): 184 return 185 186 super(CustomDocStringChecker, self).visit_function(node) 187 188 189 @staticmethod 190 def _should_skip_arg(arg): 191 """ 192 @return: True if the argument given by arg is whitelisted, and does 193 not require a "@param" docstring. 194 """ 195 return arg in ('self', 'cls', 'args', 'kwargs', 'dargs') 196 197 198 def _check_docstring(self, node_type, node): 199 """ 200 Teaches pylint to look for @param with each argument in the 201 function/method signature. 202 203 @param node_type: type of the node we're currently checking. 204 @param node: node of the ast we're currently checking. 205 """ 206 super(CustomDocStringChecker, self)._check_docstring(node_type, node) 207 docstring = node.doc 208 if pylint_version >= 1.1: 209 key = 'missing-docstring' 210 else: 211 key = 'C0111' 212 213 if (docstring is not None and 214 (node_type is 'method' or 215 node_type is 'function')): 216 args = node.argnames() 217 old_msg = self.linter._messages[key].msg 218 for arg in args: 219 arg_docstring_rgx = '.*@param '+arg+'.*' 220 line = re.search(arg_docstring_rgx, node.doc) 221 if not line and not self._should_skip_arg(arg): 222 self.linter._messages[key].msg = ('Docstring needs ' 223 '"@param '+arg+':"') 224 self.add_message(key, node=node) 225 self.linter._messages[key].msg = old_msg 226 227base.DocStringChecker = CustomDocStringChecker 228imports.ImportsChecker = CustomImportsChecker 229variables.VariablesChecker = CustomVariablesChecker 230 231 232def batch_check_files(file_paths, base_opts): 233 """ 234 Run pylint on a list of files so we get consolidated errors. 235 236 @param file_paths: a list of file paths. 237 @param base_opts: a list of pylint config options. 238 239 @raises: pylint_error if pylint finds problems with a file 240 in this commit. 241 """ 242 if not file_paths: 243 return 244 245 pylint_runner = pylint.lint.Run(list(base_opts) + list(file_paths), 246 exit=False) 247 if pylint_runner.linter.msg_status: 248 raise pylint_error(pylint_runner.linter.msg_status) 249 250 251def should_check_file(file_path): 252 """ 253 Don't check blacklisted or non .py files. 254 255 @param file_path: abs path of file to check. 256 @return: True if this file is a non-blacklisted python file. 257 """ 258 file_path = os.path.abspath(file_path) 259 if file_path.endswith('.py'): 260 return all(not fnmatch.fnmatch(file_path, '*' + pattern) 261 for pattern in BLACKLIST) 262 return False 263 264 265def check_file(file_path, base_opts): 266 """ 267 Invokes pylint on files after confirming that they're not black listed. 268 269 @param base_opts: pylint base options. 270 @param file_path: path to the file we need to run pylint on. 271 """ 272 if not isinstance(file_path, basestring): 273 raise TypeError('expected a string as filepath, got %s'% 274 type(file_path)) 275 276 if should_check_file(file_path): 277 pylint_runner = pylint.lint.Run(base_opts + [file_path], exit=False) 278 if pylint_runner.linter.msg_status: 279 pylint_error(pylint_runner.linter.msg_status) 280 281 282def visit(arg, dirname, filenames): 283 """ 284 Visit function invoked in check_dir. 285 286 @param arg: arg from os.walk.path 287 @param dirname: dir from os.walk.path 288 @param filenames: files in dir from os.walk.path 289 """ 290 for filename in filenames: 291 check_file(os.path.join(dirname, filename), arg) 292 293 294def check_dir(dir_path, base_opts): 295 """ 296 Calls visit on files in dir_path. 297 298 @param base_opts: pylint base options. 299 @param dir_path: path to directory. 300 """ 301 os.path.walk(dir_path, visit, base_opts) 302 303 304def extend_baseopts(base_opts, new_opt): 305 """ 306 Replaces an argument in base_opts with a cmd line argument. 307 308 @param base_opts: original pylint_base_opts. 309 @param new_opt: new cmd line option. 310 """ 311 for args in base_opts: 312 if new_opt in args: 313 base_opts.remove(args) 314 base_opts.append(new_opt) 315 316 317def get_cmdline_options(args_list, pylint_base_opts, rcfile): 318 """ 319 Parses args_list and extends pylint_base_opts. 320 321 Command line arguments might include options mixed with files. 322 Go through this list and filter out the options, if the options are 323 specified in the pylintrc file we cannot replace them and the file 324 needs to be edited. If the options are already a part of 325 pylint_base_opts we replace them, and if not we append to 326 pylint_base_opts. 327 328 @param args_list: list of files/pylint args passed in through argv. 329 @param pylint_base_opts: default pylint options. 330 @param rcfile: text from pylint_rc. 331 """ 332 for args in args_list: 333 if args.startswith('--'): 334 opt_name = args[2:].split('=')[0] 335 if opt_name in rcfile and pylint_version >= 0.21: 336 raise run_pylint_error('The rcfile already contains the %s ' 337 'option. Please edit pylintrc instead.' 338 % opt_name) 339 else: 340 extend_baseopts(pylint_base_opts, args) 341 args_list.remove(args) 342 343 344def git_show_to_temp_file(commit, original_file, new_temp_file): 345 """ 346 'Git shows' the file in original_file to a tmp file with 347 the name new_temp_file. We need to preserve the filename 348 as it gets reflected in pylints error report. 349 350 @param commit: commit hash of the commit we're running repo upload on. 351 @param original_file: the path to the original file we'd like to run 352 'git show' on. 353 @param new_temp_file: new_temp_file is the path to a temp file we write the 354 output of 'git show' into. 355 """ 356 git_repo = revision_control.GitRepo(common.autotest_dir, None, None, 357 common.autotest_dir) 358 359 with open(new_temp_file, 'w') as f: 360 output = git_repo.gitcmd('show --no-ext-diff %s:%s' 361 % (commit, original_file), 362 ignore_status=False).stdout 363 f.write(output) 364 365 366def check_committed_files(work_tree_files, commit, pylint_base_opts): 367 """ 368 Get a list of files corresponding to the commit hash. 369 370 The contents of a file in the git work tree can differ from the contents 371 of a file in the commit we mean to upload. To work around this we run 372 pylint on a temp file into which we've 'git show'n the committed version 373 of each file. 374 375 @param work_tree_files: list of files in this commit specified by their 376 absolute path. 377 @param commit: hash of the commit this upload applies to. 378 @param pylint_base_opts: a list of pylint config options. 379 """ 380 files_to_check = filter(should_check_file, work_tree_files) 381 382 # Map the absolute path of each file so it's relative to the autotest repo. 383 # All files that are a part of this commit should have an abs path within 384 # the autotest repo, so this regex should never fail. 385 work_tree_files = [re.search(r'%s/(.*)' % common.autotest_dir, f).group(1) 386 for f in files_to_check] 387 388 tempdir = None 389 try: 390 tempdir = autotemp.tempdir() 391 temp_files = [os.path.join(tempdir.name, file_path.split('/')[-1:][0]) 392 for file_path in work_tree_files] 393 394 for file_tuple in zip(work_tree_files, temp_files): 395 git_show_to_temp_file(commit, *file_tuple) 396 # Only check if we successfully git showed all files in the commit. 397 batch_check_files(temp_files, pylint_base_opts) 398 finally: 399 if tempdir: 400 tempdir.clean() 401 402 403def _is_test_case_method(node): 404 """Determine if the given function node is a method of a TestCase. 405 406 We simply check for 'TestCase' being one of the parent classes in the mro of 407 the containing class. 408 409 @params node: A function node. 410 """ 411 if not hasattr(node.parent.frame(), 'ancestors'): 412 return False 413 414 parent_class_names = {x.name for x in node.parent.frame().ancestors()} 415 return 'TestCase' in parent_class_names 416 417 418def main(): 419 """Main function checks each file in a commit for pylint violations.""" 420 421 # For now all error/warning/refactor/convention exceptions except those in 422 # the enable string are disabled. 423 # W0611: All imported modules (except common) need to be used. 424 # W1201: Logging methods should take the form 425 # logging.<loggingmethod>(format_string, format_args...); and not 426 # logging.<loggingmethod>(format_string % (format_args...)) 427 # C0111: Docstring needed. Also checks @param for each arg. 428 # C0112: Non-empty Docstring needed. 429 # Ideally we would like to enable as much as we can, but if we did so at 430 # this stage anyone who makes a tiny change to a file will be tasked with 431 # cleaning all the lint in it. See chromium-os:37364. 432 433 # Note: 434 # 1. There are three major sources of E1101/E1103/E1120 false positives: 435 # * common_lib.enum.Enum objects 436 # * DB model objects (scheduler models are the worst, but Django models 437 # also generate some errors) 438 # 2. Docstrings are optional on private methods, and any methods that begin 439 # with either 'set_' or 'get_'. 440 pylint_rc = os.path.join(os.path.dirname(os.path.abspath(__file__)), 441 'pylintrc') 442 443 no_docstring_rgx = r'((_.*)|(set_.*)|(get_.*))' 444 if pylint_version >= 0.21: 445 pylint_base_opts = ['--rcfile=%s' % pylint_rc, 446 '--reports=no', 447 '--disable=W,R,E,C,F', 448 '--enable=W0611,W1201,C0111,C0112,E0602,W0601', 449 '--no-docstring-rgx=%s' % no_docstring_rgx,] 450 else: 451 all_failures = 'error,warning,refactor,convention' 452 pylint_base_opts = ['--disable-msg-cat=%s' % all_failures, 453 '--reports=no', 454 '--include-ids=y', 455 '--ignore-docstrings=n', 456 '--no-docstring-rgx=%s' % no_docstring_rgx,] 457 458 # run_pylint can be invoked directly with command line arguments, 459 # or through a presubmit hook which uses the arguments in pylintrc. In the 460 # latter case no command line arguments are passed. If it is invoked 461 # directly without any arguments, it should check all files in the cwd. 462 args_list = sys.argv[1:] 463 if args_list: 464 get_cmdline_options(args_list, 465 pylint_base_opts, 466 open(pylint_rc).read()) 467 batch_check_files(args_list, pylint_base_opts) 468 elif os.environ.get('PRESUBMIT_FILES') is not None: 469 check_committed_files( 470 os.environ.get('PRESUBMIT_FILES').split('\n'), 471 os.environ.get('PRESUBMIT_COMMIT'), 472 pylint_base_opts) 473 else: 474 check_dir('.', pylint_base_opts) 475 476 477if __name__ == '__main__': 478 try: 479 main() 480 except pylint_error as e: 481 logging.error(e) 482 sys.exit(1) 483