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