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