diff options
author | Alan Chan <achan961117@gmail.com> | 2018-10-04 15:28:36 +0800 |
---|---|---|
committer | Claudiu Popa <pcmanticore@gmail.com> | 2018-10-04 09:28:36 +0200 |
commit | 0dd573faae1ad339c07f789270002f8faf2a3691 (patch) | |
tree | 5b139a624a01a1e86a2cbe0eb920d80ca167bc41 /pylint/checkers | |
parent | 540e26db2e05c4f4f555db54d4c046ceff0e7763 (diff) | |
download | pylint-git-0dd573faae1ad339c07f789270002f8faf2a3691.tar.gz |
New option: logging-format-style for logging checker (#2521)
logging-format-style accepts one of '%' or '{', (defaults to '%'). When '{' is selected, logging
checker assumes str.format() style format strings for calls to the logging.
pylint was unable to count the required number of args for the format string when the
format string was using the `{` format. The new feature indirectly fixes that by allowing
the proper interpretation of that format string.
Diffstat (limited to 'pylint/checkers')
-rw-r--r-- | pylint/checkers/logging.py | 33 | ||||
-rw-r--r-- | pylint/checkers/strings.py | 97 | ||||
-rw-r--r-- | pylint/checkers/utils.py | 74 |
3 files changed, 103 insertions, 101 deletions
diff --git a/pylint/checkers/logging.py b/pylint/checkers/logging.py index 92a4a0428..139faf9f4 100644 --- a/pylint/checkers/logging.py +++ b/pylint/checkers/logging.py @@ -136,6 +136,16 @@ class LoggingChecker(checkers.BaseChecker): "arguments are in logging function parameter format.", }, ), + ( + "logging-format-style", + { + "default": "%", + "type": "choice", + "metavar": "<% or {>", + "choices": ["%", "{"], + "help": "Format style used to check logging format string", + }, + ), ) def visit_module(self, node): # pylint: disable=unused-argument @@ -146,6 +156,7 @@ class LoggingChecker(checkers.BaseChecker): self._logging_names = set() logging_mods = self.config.logging_modules + self._format_style = self.config.logging_format_style self._logging_modules = set(logging_mods) self._from_imports = {} for logging_mod in logging_mods: @@ -284,13 +295,21 @@ class LoggingChecker(checkers.BaseChecker): required_num_args = 0 else: try: - 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 + if self._format_style == "%": + 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 == "{": + keys, num_args, manual_pos_arg = utils.parse_format_method_string( + format_string + ) + + kargs = len(set(k for k, l in keys if not isinstance(k, int))) + required_num_args = kargs + num_args + manual_pos_arg except utils.UnsupportedFormatCharacter as ex: char = format_string[ex.index] self.add_message( diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index f14673cab..0f7123a71 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -25,7 +25,6 @@ import builtins import sys import tokenize -import string import numbers from collections import Counter @@ -171,98 +170,6 @@ BUILTINS_STR = builtins.__name__ + ".str" BUILTINS_FLOAT = builtins.__name__ + ".float" BUILTINS_INT = builtins.__name__ + ".int" -if _PY3K: - import _string # pylint: disable=wrong-import-position, wrong-import-order - - def split_format_field_names(format_string): - try: - return _string.formatter_field_name_split(format_string) - except ValueError: - raise utils.IncompleteFormatString() - - -else: - - def _field_iterator_convertor(iterator): - for is_attr, key in iterator: - if isinstance(key, numbers.Number): - yield is_attr, int(key) - else: - yield is_attr, key - - def split_format_field_names(format_string): - try: - keyname, fielditerator = format_string._formatter_field_name_split() - except ValueError: - raise utils.IncompleteFormatString - # it will return longs, instead of ints, which will complicate - # the output - return keyname, _field_iterator_convertor(fielditerator) - - -def collect_string_fields(format_string): - """ Given a format string, return an iterator - of all the valid format fields. It handles nested fields - as well. - """ - - formatter = string.Formatter() - try: - parseiterator = formatter.parse(format_string) - for result in parseiterator: - if all(item is None for item in result[1:]): - # not a replacement format - continue - name = result[1] - nested = result[2] - yield name - if nested: - for field in collect_string_fields(nested): - yield field - except ValueError as exc: - # Probably the format string is invalid. - if exc.args[0].startswith("cannot switch from manual"): - # On Jython, parsing a string with both manual - # and automatic positions will fail with a ValueError, - # while on CPython it will simply return the fields, - # the validation being done in the interpreter (?). - # We're just returning two mixed fields in order - # to trigger the format-combined-specification check. - yield "" - yield "1" - return - raise utils.IncompleteFormatString(format_string) - - -def parse_format_method_string(format_string): - """ - Parses a PEP 3101 format string, returning a tuple of - (keys, num_args, manual_pos_arg), - where keys is the set of mapping keys in the format string, num_args - is the number of arguments required by the format string and - manual_pos_arg is the number of arguments passed with the position. - """ - keys = [] - num_args = 0 - manual_pos_arg = set() - for name in collect_string_fields(format_string): - if name and str(name).isdigit(): - manual_pos_arg.add(str(name)) - elif name: - keyname, fielditerator = split_format_field_names(name) - if isinstance(keyname, numbers.Number): - # In Python 2 it will return long which will lead - # to different output between 2 and 3 - manual_pos_arg.add(str(keyname)) - keyname = int(keyname) - try: - keys.append((keyname, list(fielditerator))) - except ValueError: - raise utils.IncompleteFormatString() - else: - num_args += 1 - return keys, num_args, len(manual_pos_arg) - def get_access_path(key, parts): """ Given a list of format specifiers, returns @@ -487,7 +394,9 @@ class StringFormatChecker(BaseChecker): return try: - fields, num_args, manual_pos = parse_format_method_string(strnode.value) + fields, num_args, manual_pos = utils.parse_format_method_string( + strnode.value + ) except utils.IncompleteFormatString: self.add_message("bad-format-string", node=node) return diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index f4d10006b..98a892f90 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -33,6 +33,7 @@ import builtins from functools import lru_cache, partial, singledispatch import itertools +import numbers import re import sys import string @@ -48,6 +49,7 @@ from typing import ( List, Type, ) +import _string # pylint: disable=wrong-import-position, wrong-import-order import astroid from astroid import bases as _bases @@ -535,6 +537,78 @@ def parse_format_string( return keys, num_args, key_types, pos_types +def split_format_field_names(format_string) -> Tuple[str, Iterable[Tuple[bool, str]]]: + try: + return _string.formatter_field_name_split(format_string) + except ValueError: + raise IncompleteFormatString() + + +def collect_string_fields(format_string) -> Iterable[Optional[str]]: + """ Given a format string, return an iterator + of all the valid format fields. It handles nested fields + as well. + """ + formatter = string.Formatter() + try: + parseiterator = formatter.parse(format_string) + for result in parseiterator: + if all(item is None for item in result[1:]): + # not a replacement format + continue + name = result[1] + nested = result[2] + yield name + if nested: + for field in collect_string_fields(nested): + yield field + except ValueError as exc: + # Probably the format string is invalid. + if exc.args[0].startswith("cannot switch from manual"): + # On Jython, parsing a string with both manual + # and automatic positions will fail with a ValueError, + # while on CPython it will simply return the fields, + # the validation being done in the interpreter (?). + # We're just returning two mixed fields in order + # to trigger the format-combined-specification check. + yield "" + yield "1" + return + raise IncompleteFormatString(format_string) + + +def parse_format_method_string( + format_string: str +) -> Tuple[List[Tuple[str, List[Tuple[bool, str]]]], int, int]: + """ + Parses a PEP 3101 format string, returning a tuple of + (keys, num_args, manual_pos_arg), + where keys is the set of mapping keys in the format string, num_args + is the number of arguments required by the format string and + manual_pos_arg is the number of arguments passed with the position. + """ + keys = [] + num_args = 0 + manual_pos_arg = set() + for name in collect_string_fields(format_string): + if name and str(name).isdigit(): + manual_pos_arg.add(str(name)) + elif name: + keyname, fielditerator = split_format_field_names(name) + if isinstance(keyname, numbers.Number): + # In Python 2 it will return long which will lead + # to different output between 2 and 3 + manual_pos_arg.add(str(keyname)) + keyname = int(keyname) + try: + keys.append((keyname, list(fielditerator))) + except ValueError: + raise IncompleteFormatString() + else: + num_args += 1 + return keys, num_args, len(manual_pos_arg) + + def is_attr_protected(attrname: str) -> bool: """return True if attribute name is protected (start with _ and some other details), False otherwise. |