Home | History | Annotate | Download | only in utils
      1 #!/usr/bin/python -u
      2 """
      3 Wrapper to patch pylint library functions to suit autotest.
      4 
      5 This script is invoked as part of the presubmit checks for autotest python
      6 files. It runs pylint on a list of files that it obtains either through
      7 the command line or from an environment variable set in pre-upload.py.
      8 
      9 Example:
     10 run_pylint.py filename.py
     11 """
     12 
     13 import fnmatch
     14 import logging
     15 import os
     16 import re
     17 import sys
     18 
     19 import common
     20 from autotest_lib.client.common_lib import autotemp, revision_control
     21 
     22 # Do a basic check to see if pylint is even installed.
     23 try:
     24     import pylint
     25     from pylint.__pkginfo__ import version as pylint_version
     26 except 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 
     31 major, minor, release = pylint_version.split('.')
     32 pylint_version = float("%s.%s" % (major, minor))
     33 
     34 # some files make pylint blow up, so make sure we ignore them
     35 BLACKLIST = ['/site-packages/*', '/contrib/*', '/frontend/afe/management.py']
     36 
     37 import pylint.lint
     38 from pylint.checkers import base, imports, variables
     39 
     40 # need to put autotest root dir on sys.path so pylint will be happy
     41 autotest_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
     42 sys.path.insert(0, autotest_root)
     43 
     44 # patch up pylint import checker to handle our importing magic
     45 ROOT_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 
     55 class pylint_error(Exception):
     56     """
     57     Error raised when pylint complains about a file.
     58     """
     59 
     60 
     61 class run_pylint_error(pylint_error):
     62     """
     63     Error raised when an assumption made in this file is violated.
     64     """
     65 
     66 
     67 def 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 
     79 def 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 
    102 class 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 
    110 class 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 
    135 class 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 
    178 base.DocStringChecker = CustomDocStringChecker
    179 imports.ImportsChecker = CustomImportsChecker
    180 variables.VariablesChecker = CustomVariablesChecker
    181 
    182 
    183 def 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 
    203 def 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 
    217 def 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 
    238 def 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 
    250 def 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 
    266 def 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 
    279 def 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 
    301 def 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 
    323 def 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 
    362 def _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 
    377 def 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 
    436 if __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