"Fossies" - the Fresh Open Source Software Archive

Member "keystone-17.0.0/keystone/tests/hacking/checks.py" (13 May 2020, 12094 Bytes) of package /linux/misc/openstack/keystone-17.0.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "checks.py": 16.0.1_vs_17.0.0.

    1 # Licensed under the Apache License, Version 2.0 (the "License"); you may
    2 # not use this file except in compliance with the License. You may obtain
    3 # a copy of the License at
    4 #
    5 #      http://www.apache.org/licenses/LICENSE-2.0
    6 #
    7 # Unless required by applicable law or agreed to in writing, software
    8 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    9 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   10 # License for the specific language governing permissions and limitations
   11 # under the License.
   12 
   13 """Keystone's pep8 extensions.
   14 
   15 In order to make the review process faster and easier for core devs we are
   16 adding some Keystone specific pep8 checks. This will catch common errors
   17 so that core devs don't have to.
   18 
   19 There are two types of pep8 extensions. One is a function that takes either
   20 a physical or logical line. The physical or logical line is the first param
   21 in the function definition and can be followed by other parameters supported
   22 by pycodestyle. The second type is a class that parses AST trees. For more info
   23 please see pycodestyle.py.
   24 """
   25 
   26 import ast
   27 import re
   28 
   29 
   30 class BaseASTChecker(ast.NodeVisitor):
   31     """Provides a simple framework for writing AST-based checks.
   32 
   33     Subclasses should implement visit_* methods like any other AST visitor
   34     implementation. When they detect an error for a particular node the
   35     method should call ``self.add_error(offending_node)``. Details about
   36     where in the code the error occurred will be pulled from the node
   37     object.
   38 
   39     Subclasses should also provide a class variable named CHECK_DESC to
   40     be used for the human readable error message.
   41 
   42     """
   43 
   44     def __init__(self, tree, filename):
   45         """Created object automatically by pep8.
   46 
   47         :param tree: an AST tree
   48         :param filename: name of the file being analyzed
   49                          (ignored by our checks)
   50         """
   51         self._tree = tree
   52         self._errors = []
   53 
   54     def run(self):
   55         """Called automatically by pep8."""
   56         self.visit(self._tree)
   57         return self._errors
   58 
   59     def add_error(self, node, message=None):
   60         """Add an error caused by a node to the list of errors for pep8."""
   61         message = message or self.CHECK_DESC
   62         error = (node.lineno, node.col_offset, message, self.__class__)
   63         self._errors.append(error)
   64 
   65 
   66 class CheckForMutableDefaultArgs(BaseASTChecker):
   67     """Check for the use of mutable objects as function/method defaults.
   68 
   69     We are only checking for list and dict literals at this time. This means
   70     that a developer could specify an instance of their own and cause a bug.
   71     The fix for this is probably more work than it's worth because it will
   72     get caught during code review.
   73 
   74     """
   75 
   76     CHECK_DESC = 'K001 Using mutable as a function/method default'
   77     MUTABLES = (
   78         ast.List, ast.ListComp,
   79         ast.Dict, ast.DictComp,
   80         ast.Set, ast.SetComp,
   81         ast.Call)
   82 
   83     def visit_FunctionDef(self, node):
   84         for arg in node.args.defaults:
   85             if isinstance(arg, self.MUTABLES):
   86                 self.add_error(arg)
   87 
   88         super(CheckForMutableDefaultArgs, self).generic_visit(node)
   89 
   90 
   91 def block_comments_begin_with_a_space(physical_line, line_number):
   92     """There should be a space after the # of block comments.
   93 
   94     There is already a check in pep8 that enforces this rule for
   95     inline comments.
   96 
   97     Okay: # this is a comment
   98     Okay: #!/usr/bin/python
   99     Okay: #  this is a comment
  100     K002: #this is a comment
  101 
  102     """
  103     MESSAGE = "K002 block comments should start with '# '"
  104 
  105     # shebangs are OK
  106     if line_number == 1 and physical_line.startswith('#!'):
  107         return
  108 
  109     text = physical_line.strip()
  110     if text.startswith('#'):  # look for block comments
  111         if len(text) > 1 and not text[1].isspace():
  112             return physical_line.index('#'), MESSAGE
  113 
  114 
  115 class CheckForTranslationIssues(BaseASTChecker):
  116 
  117     LOGGING_CHECK_DESC = 'K005 Using translated string in logging'
  118     USING_DEPRECATED_WARN = 'K009 Using the deprecated Logger.warn'
  119     LOG_MODULES = ('logging', 'oslo_log.log')
  120     I18N_MODULES = (
  121         'keystone.i18n._',
  122     )
  123     TRANS_HELPER_MAP = {
  124         'debug': None,
  125         'info': '_LI',
  126         'warning': '_LW',
  127         'error': '_LE',
  128         'exception': '_LE',
  129         'critical': '_LC',
  130     }
  131 
  132     def __init__(self, tree, filename):
  133         super(CheckForTranslationIssues, self).__init__(tree, filename)
  134 
  135         self.logger_names = []
  136         self.logger_module_names = []
  137         self.i18n_names = {}
  138 
  139         # NOTE(dstanek): this kinda accounts for scopes when talking
  140         # about only leaf node in the graph
  141         self.assignments = {}
  142 
  143     def generic_visit(self, node):
  144         """Called if no explicit visitor function exists for a node."""
  145         for field, value in ast.iter_fields(node):
  146             if isinstance(value, list):
  147                 for item in value:
  148                     if isinstance(item, ast.AST):
  149                         item._parent = node
  150                         self.visit(item)
  151             elif isinstance(value, ast.AST):
  152                 value._parent = node
  153                 self.visit(value)
  154 
  155     def _filter_imports(self, module_name, alias):
  156         """Keep lists of logging and i18n imports."""
  157         if module_name in self.LOG_MODULES:
  158             self.logger_module_names.append(alias.asname or alias.name)
  159         elif module_name in self.I18N_MODULES:
  160             self.i18n_names[alias.asname or alias.name] = alias.name
  161 
  162     def visit_Import(self, node):
  163         for alias in node.names:
  164             self._filter_imports(alias.name, alias)
  165         return super(CheckForTranslationIssues, self).generic_visit(node)
  166 
  167     def visit_ImportFrom(self, node):
  168         for alias in node.names:
  169             full_name = '%s.%s' % (node.module, alias.name)
  170             self._filter_imports(full_name, alias)
  171         return super(CheckForTranslationIssues, self).generic_visit(node)
  172 
  173     def _find_name(self, node):
  174         """Return the fully qualified name or a Name or Attribute."""
  175         if isinstance(node, ast.Name):
  176             return node.id
  177         elif (isinstance(node, ast.Attribute)
  178                 and isinstance(node.value, (ast.Name, ast.Attribute))):
  179             method_name = node.attr
  180             obj_name = self._find_name(node.value)
  181             if obj_name is None:
  182                 return None
  183             return obj_name + '.' + method_name
  184         elif isinstance(node, str):
  185             return node
  186         else:  # could be Subscript, Call or many more
  187             return None
  188 
  189     def visit_Assign(self, node):
  190         """Look for 'LOG = logging.getLogger'.
  191 
  192         This handles the simple case:
  193           name = [logging_module].getLogger(...)
  194 
  195           - or -
  196 
  197           name = [i18n_name](...)
  198 
  199         And some much more comple ones:
  200           name = [i18n_name](...) % X
  201 
  202           - or -
  203 
  204           self.name = [i18n_name](...) % X
  205 
  206         """
  207         attr_node_types = (ast.Name, ast.Attribute)
  208 
  209         if (len(node.targets) != 1
  210                 or not isinstance(node.targets[0], attr_node_types)):
  211             # say no to: "x, y = ..."
  212             return super(CheckForTranslationIssues, self).generic_visit(node)
  213 
  214         target_name = self._find_name(node.targets[0])
  215 
  216         if (isinstance(node.value, ast.BinOp) and
  217                 isinstance(node.value.op, ast.Mod)):
  218             if (isinstance(node.value.left, ast.Call) and
  219                     isinstance(node.value.left.func, ast.Name) and
  220                     node.value.left.func.id in self.i18n_names):
  221                 # NOTE(dstanek): this is done to match cases like:
  222                 # `msg = _('something %s') % x`
  223                 node = ast.Assign(value=node.value.left)
  224 
  225         if not isinstance(node.value, ast.Call):
  226             # node.value must be a call to getLogger
  227             self.assignments.pop(target_name, None)
  228             return super(CheckForTranslationIssues, self).generic_visit(node)
  229 
  230         # is this a call to an i18n function?
  231         if (isinstance(node.value.func, ast.Name)
  232                 and node.value.func.id in self.i18n_names):
  233             self.assignments[target_name] = node.value.func.id
  234             return super(CheckForTranslationIssues, self).generic_visit(node)
  235 
  236         if (not isinstance(node.value.func, ast.Attribute)
  237                 or not isinstance(node.value.func.value, attr_node_types)):
  238             # function must be an attribute on an object like
  239             # logging.getLogger
  240             return super(CheckForTranslationIssues, self).generic_visit(node)
  241 
  242         object_name = self._find_name(node.value.func.value)
  243         func_name = node.value.func.attr
  244 
  245         if (object_name in self.logger_module_names
  246                 and func_name == 'getLogger'):
  247             self.logger_names.append(target_name)
  248 
  249         return super(CheckForTranslationIssues, self).generic_visit(node)
  250 
  251     def visit_Call(self, node):
  252         """Look for the 'LOG.*' calls."""
  253         # obj.method
  254         if isinstance(node.func, ast.Attribute):
  255             obj_name = self._find_name(node.func.value)
  256             if isinstance(node.func.value, ast.Name):
  257                 method_name = node.func.attr
  258             elif isinstance(node.func.value, ast.Attribute):
  259                 obj_name = self._find_name(node.func.value)
  260                 method_name = node.func.attr
  261             else:  # could be Subscript, Call or many more
  262                 return (super(CheckForTranslationIssues, self)
  263                         .generic_visit(node))
  264 
  265             # if dealing with a logger the method can't be "warn"
  266             if obj_name in self.logger_names and method_name == 'warn':
  267                 msg = node.args[0]  # first arg to a logging method is the msg
  268                 self.add_error(msg, message=self.USING_DEPRECATED_WARN)
  269 
  270             # must be a logger instance and one of the support logging methods
  271             if (obj_name not in self.logger_names
  272                     or method_name not in self.TRANS_HELPER_MAP):
  273                 return (super(CheckForTranslationIssues, self)
  274                         .generic_visit(node))
  275 
  276             # the call must have arguments
  277             if not node.args:
  278                 return (super(CheckForTranslationIssues, self)
  279                         .generic_visit(node))
  280 
  281             self._process_log_messages(node)
  282 
  283         return super(CheckForTranslationIssues, self).generic_visit(node)
  284 
  285     def _process_log_messages(self, node):
  286         msg = node.args[0]  # first arg to a logging method is the msg
  287 
  288         # if first arg is a call to a i18n name
  289         if (isinstance(msg, ast.Call)
  290                 and isinstance(msg.func, ast.Name)
  291                 and msg.func.id in self.i18n_names):
  292             self.add_error(msg, message=self.LOGGING_CHECK_DESC)
  293 
  294         # if the first arg is a reference to a i18n call
  295         elif (isinstance(msg, ast.Name)
  296                 and msg.id in self.assignments):
  297             self.add_error(msg, message=self.LOGGING_CHECK_DESC)
  298 
  299 
  300 def dict_constructor_with_sequence_copy(logical_line):
  301     """Should use a dict comprehension instead of a dict constructor.
  302 
  303     PEP-0274 introduced dict comprehension with performance enhancement
  304     and it also makes code more readable.
  305 
  306     Okay: lower_res = {k.lower(): v for k, v in res[1].items()}
  307     Okay: fool = dict(a='a', b='b')
  308     K008: lower_res = dict((k.lower(), v) for k, v in res[1].items())
  309     K008:     attrs = dict([(k, _from_json(v))
  310     K008: dict([[i,i] for i in range(3)])
  311 
  312     """
  313     MESSAGE = ("K008 Must use a dict comprehension instead of a dict"
  314                " constructor with a sequence of key-value pairs.")
  315 
  316     dict_constructor_with_sequence_re = (
  317         re.compile(r".*\bdict\((\[)?(\(|\[)(?!\{)"))
  318 
  319     if dict_constructor_with_sequence_re.match(logical_line):
  320         yield (0, MESSAGE)
  321 
  322 
  323 def factory(register):
  324     register(CheckForMutableDefaultArgs)
  325     register(block_comments_begin_with_a_space)
  326     register(CheckForTranslationIssues)
  327     register(dict_constructor_with_sequence_copy)