diff options
Diffstat (limited to 'venv/Lib/site-packages/pylint/checkers/logging.py')
-rw-r--r-- | venv/Lib/site-packages/pylint/checkers/logging.py | 384 |
1 files changed, 384 insertions, 0 deletions
diff --git a/venv/Lib/site-packages/pylint/checkers/logging.py b/venv/Lib/site-packages/pylint/checkers/logging.py new file mode 100644 index 0000000..5ad0e76 --- /dev/null +++ b/venv/Lib/site-packages/pylint/checkers/logging.py @@ -0,0 +1,384 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2009-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr> +# Copyright (c) 2009, 2012, 2014 Google, Inc. +# Copyright (c) 2012 Mike Bryant <leachim@leachim.info> +# Copyright (c) 2014 Brett Cannon <brett@python.org> +# Copyright (c) 2014 Arun Persaud <arun@nubati.net> +# Copyright (c) 2015-2018 Claudiu Popa <pcmanticore@gmail.com> +# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro> +# Copyright (c) 2016 Chris Murray <chris@chrismurray.scot> +# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk> +# Copyright (c) 2017 guillaume2 <guillaume.peillex@gmail.col> +# Copyright (c) 2017 Ćukasz Rogalski <rogalski.91@gmail.com> +# Copyright (c) 2018 Mike Frysinger <vapier@gmail.com> +# Copyright (c) 2018 Mariatta Wijaya <mariatta@python.org> +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/master/COPYING + +"""checker for use of Python logging +""" +import string + +import astroid + +from pylint import checkers, interfaces +from pylint.checkers import utils +from pylint.checkers.utils import check_messages + +MSGS = { + "W1201": ( + "Specify string format arguments as logging function parameters", + "logging-not-lazy", + "Used when a logging statement has a call form of " + '"logging.<logging method>(format_string % (format_args...))". ' + "Such calls should leave string interpolation to the logging " + "method itself and be written " + '"logging.<logging method>(format_string, format_args...)" ' + "so that the program may avoid incurring the cost of the " + "interpolation in those cases in which no message will be " + "logged. For more, see " + "http://www.python.org/dev/peps/pep-0282/.", + ), + "W1202": ( + "Use %s formatting in logging functions%s", + "logging-format-interpolation", + "Used when a logging statement has a call form of " + '"logging.<logging method>(<string formatting>)".' + " with invalid string formatting. " + "Use another way for format the string instead.", + ), + "E1200": ( + "Unsupported logging format character %r (%#02x) at index %d", + "logging-unsupported-format", + "Used when an unsupported format character is used in a logging " + "statement format string.", + ), + "E1201": ( + "Logging format string ends in middle of conversion specifier", + "logging-format-truncated", + "Used when a logging statement format string terminates before " + "the end of a conversion specifier.", + ), + "E1205": ( + "Too many arguments for logging format string", + "logging-too-many-args", + "Used when a logging format string is given too many arguments.", + ), + "E1206": ( + "Not enough arguments for logging format string", + "logging-too-few-args", + "Used when a logging format string is given too few arguments.", + ), +} + + +CHECKED_CONVENIENCE_FUNCTIONS = { + "critical", + "debug", + "error", + "exception", + "fatal", + "info", + "warn", + "warning", +} + + +def is_method_call(func, types=(), methods=()): + """Determines if a BoundMethod node represents a method call. + + Args: + func (astroid.BoundMethod): The BoundMethod AST node to check. + types (Optional[String]): Optional sequence of caller type names to restrict check. + methods (Optional[String]): Optional sequence of method names to restrict check. + + Returns: + bool: true if the node represents a method call for the given type and + method names, False otherwise. + """ + return ( + isinstance(func, astroid.BoundMethod) + and isinstance(func.bound, astroid.Instance) + and (func.bound.name in types if types else True) + and (func.name in methods if methods else True) + ) + + +class LoggingChecker(checkers.BaseChecker): + """Checks use of the logging module.""" + + __implements__ = interfaces.IAstroidChecker + name = "logging" + msgs = MSGS + + options = ( + ( + "logging-modules", + { + "default": ("logging",), + "type": "csv", + "metavar": "<comma separated list>", + "help": "Logging modules to check that the string format " + "arguments are in logging function parameter format.", + }, + ), + ( + "logging-format-style", + { + "default": "old", + "type": "choice", + "metavar": "<old (%) or new ({) or fstr (f'')>", + "choices": ["old", "new", "fstr"], + "help": "Format style used to check logging format string. " + "`old` means using % formatting, `new` is for `{}` formatting," + "and `fstr` is for f-strings.", + }, + ), + ) + + def visit_module(self, node): # pylint: disable=unused-argument + """Clears any state left in this checker from last module checked.""" + # The code being checked can just as easily "import logging as foo", + # so it is necessary to process the imports and store in this field + # what name the logging module is actually given. + self._logging_names = set() + logging_mods = self.config.logging_modules + + self._format_style = self.config.logging_format_style + format_styles = {"old": "%", "new": "{", "fstr": "f-string"} + format_style_help = "" + if self._format_style == "old": + format_style_help = " and pass the % parameters as arguments" + + self._format_style_args = (format_styles[self._format_style], format_style_help) + + self._logging_modules = set(logging_mods) + self._from_imports = {} + for logging_mod in logging_mods: + parts = logging_mod.rsplit(".", 1) + if len(parts) > 1: + self._from_imports[parts[0]] = parts[1] + + def visit_importfrom(self, node): + """Checks to see if a module uses a non-Python logging module.""" + try: + logging_name = self._from_imports[node.modname] + for module, as_name in node.names: + if module == logging_name: + self._logging_names.add(as_name or module) + except KeyError: + pass + + def visit_import(self, node): + """Checks to see if this module uses Python's built-in logging.""" + for module, as_name in node.names: + if module in self._logging_modules: + self._logging_names.add(as_name or module) + + @check_messages(*MSGS) + def visit_call(self, node): + """Checks calls to logging methods.""" + + def is_logging_name(): + return ( + isinstance(node.func, astroid.Attribute) + and isinstance(node.func.expr, astroid.Name) + and node.func.expr.name in self._logging_names + ) + + def is_logger_class(): + try: + for inferred in node.func.infer(): + if isinstance(inferred, astroid.BoundMethod): + parent = inferred._proxied.parent + if isinstance(parent, astroid.ClassDef) and ( + parent.qname() == "logging.Logger" + or any( + ancestor.qname() == "logging.Logger" + for ancestor in parent.ancestors() + ) + ): + return True, inferred._proxied.name + except astroid.exceptions.InferenceError: + pass + return False, None + + if is_logging_name(): + name = node.func.attrname + else: + result, name = is_logger_class() + if not result: + return + self._check_log_method(node, name) + + def _check_log_method(self, node, name): + """Checks calls to logging.log(level, format, *format_args).""" + if name == "log": + if node.starargs or node.kwargs or len(node.args) < 2: + # Either a malformed call, star args, or double-star args. Beyond + # the scope of this checker. + return + format_pos = 1 + elif name in CHECKED_CONVENIENCE_FUNCTIONS: + if node.starargs or node.kwargs or not node.args: + # Either no args, star args, or double-star args. Beyond the + # scope of this checker. + return + format_pos = 0 + else: + return + + if isinstance(node.args[format_pos], astroid.BinOp): + binop = node.args[format_pos] + emit = binop.op == "%" + if binop.op == "+": + total_number_of_strings = sum( + 1 + for operand in (binop.left, binop.right) + if self._is_operand_literal_str(utils.safe_infer(operand)) + ) + emit = total_number_of_strings > 0 + if emit: + self.add_message("logging-not-lazy", node=node) + elif isinstance(node.args[format_pos], astroid.Call): + self._check_call_func(node.args[format_pos]) + elif isinstance(node.args[format_pos], astroid.Const): + self._check_format_string(node, format_pos) + elif isinstance( + node.args[format_pos], (astroid.FormattedValue, astroid.JoinedStr) + ): + if self._format_style != "fstr": + self.add_message( + "logging-format-interpolation", + node=node, + args=self._format_style_args, + ) + + @staticmethod + def _is_operand_literal_str(operand): + """ + Return True if the operand in argument is a literal string + """ + return isinstance(operand, astroid.Const) and operand.name == "str" + + def _check_call_func(self, node): + """Checks that function call is not format_string.format(). + + Args: + node (astroid.node_classes.Call): + Call AST node to be checked. + """ + func = utils.safe_infer(node.func) + types = ("str", "unicode") + methods = ("format",) + if is_method_call(func, types, methods) and not is_complex_format_str( + func.bound + ): + self.add_message( + "logging-format-interpolation", node=node, args=self._format_style_args + ) + + def _check_format_string(self, node, format_arg): + """Checks that format string tokens match the supplied arguments. + + Args: + node (astroid.node_classes.NodeNG): AST node to be checked. + format_arg (int): Index of the format string in the node arguments. + """ + num_args = _count_supplied_tokens(node.args[format_arg + 1 :]) + if not num_args: + # If no args were supplied the string is not interpolated and can contain + # formatting characters - it's used verbatim. Don't check any further. + return + + format_string = node.args[format_arg].value + required_num_args = 0 + if isinstance(format_string, bytes): + format_string = format_string.decode() + if isinstance(format_string, str): + try: + if self._format_style == "old": + keyword_args, required_num_args, _, _ = utils.parse_format_string( + format_string + ) + if keyword_args: + # Keyword checking on logging strings is complicated by + # special keywords - out of scope. + return + elif self._format_style == "new": + keyword_arguments, implicit_pos_args, explicit_pos_args = utils.parse_format_method_string( + format_string + ) + + keyword_args_cnt = len( + set(k for k, l in keyword_arguments if not isinstance(k, int)) + ) + required_num_args = ( + keyword_args_cnt + implicit_pos_args + explicit_pos_args + ) + else: + self.add_message( + "logging-format-interpolation", + node=node, + args=self._format_style_args, + ) + except utils.UnsupportedFormatCharacter as ex: + char = format_string[ex.index] + self.add_message( + "logging-unsupported-format", + node=node, + args=(char, ord(char), ex.index), + ) + return + except utils.IncompleteFormatString: + self.add_message("logging-format-truncated", node=node) + return + if num_args > required_num_args: + self.add_message("logging-too-many-args", node=node) + elif num_args < required_num_args: + self.add_message("logging-too-few-args", node=node) + + +def is_complex_format_str(node): + """Checks if node represents a string with complex formatting specs. + + Args: + node (astroid.node_classes.NodeNG): AST node to check + Returns: + bool: True if inferred string uses complex formatting, False otherwise + """ + inferred = utils.safe_infer(node) + if inferred is None or not ( + isinstance(inferred, astroid.Const) and isinstance(inferred.value, str) + ): + return True + try: + parsed = list(string.Formatter().parse(inferred.value)) + except ValueError: + # This format string is invalid + return False + for _, _, format_spec, _ in parsed: + if format_spec: + return True + return False + + +def _count_supplied_tokens(args): + """Counts the number of tokens in an args list. + + The Python log functions allow for special keyword arguments: func, + exc_info and extra. To handle these cases correctly, we only count + arguments that aren't keywords. + + Args: + args (list): AST nodes that are arguments for a log format string. + + Returns: + int: Number of AST nodes that aren't keywords. + """ + return sum(1 for arg in args if not isinstance(arg, astroid.Keyword)) + + +def register(linter): + """Required method to auto-register this checker.""" + linter.register_checker(LoggingChecker(linter)) |