"Fossies" - the Fresh Open Source Software Archive

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