summaryrefslogtreecommitdiff
path: root/pylint/checkers
diff options
context:
space:
mode:
authorAlan Chan <achan961117@gmail.com>2018-10-04 15:28:36 +0800
committerClaudiu Popa <pcmanticore@gmail.com>2018-10-04 09:28:36 +0200
commit0dd573faae1ad339c07f789270002f8faf2a3691 (patch)
tree5b139a624a01a1e86a2cbe0eb920d80ca167bc41 /pylint/checkers
parent540e26db2e05c4f4f555db54d4c046ceff0e7763 (diff)
downloadpylint-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.py33
-rw-r--r--pylint/checkers/strings.py97
-rw-r--r--pylint/checkers/utils.py74
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.