From 16061e1855caf1334d42e1063c478389449c1026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Mon, 2 Aug 2021 15:51:52 +0200 Subject: [PATCH 01/18] Add ``possible-f-string-as-string`` checker This checks if text in strings in between `{}`'s are variables. The var the string is assigned to is also checked for a format() call. If this does not happen, the string should probably be a f-string and a message is emitted. This closes #2507 --- ChangeLog | 4 ++ doc/whatsnew/2.10.rst | 4 ++ pylint/checkers/strings.py | 58 +++++++++++++++++++ .../p/possible_f_string_as_string.py | 12 ++++ .../p/possible_f_string_as_string.txt | 3 + 5 files changed, 81 insertions(+) create mode 100644 tests/functional/p/possible_f_string_as_string.py create mode 100644 tests/functional/p/possible_f_string_as_string.txt diff --git a/ChangeLog b/ChangeLog index aaefea2374..d14a7bb5c8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -68,6 +68,10 @@ Release date: TBA Closes #4681 +* Added ``possible-f-string-as-string``: Emitted when variables are used in normal strings in between "{}" + + Closes #2507 + What's New in Pylint 2.9.6? =========================== diff --git a/doc/whatsnew/2.10.rst b/doc/whatsnew/2.10.rst index 26b792a6ab..4c90242b2a 100644 --- a/doc/whatsnew/2.10.rst +++ b/doc/whatsnew/2.10.rst @@ -28,6 +28,10 @@ New checkers Closes #3692 +* Added ``possible-f-string-as-string``: Emitted when variables are used in normal strings in between "{}" + + Closes #2507 + Extensions ========== diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 2fe3cc83ac..03ad0a2c0c 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -26,6 +26,7 @@ # Copyright (c) 2020 Anthony # Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com> # Copyright (c) 2021 Peter Kolbus +# Copyright (c) 2021 Daniel van Noord <13665637+DanielNoord@users.noreply.github.com> # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html @@ -204,6 +205,12 @@ "Used when we detect an f-string that does not use any interpolation variables, " "in which case it can be either a normal string or a bug in the code.", ), + "W1310": ( + "Using an string which should probably be a f-string", + "possible-f-string-as-string", + "Used when we detect a string that uses '{}' with a local variable inside. " + "This string is probably meant to be an f-string.", + ), } OTHER_NODES = ( @@ -892,6 +899,57 @@ def process_non_raw_string_token( # character can never be the start of a new backslash escape. index += 2 + @check_messages("possible-f-string-as-string") + def visit_const(self, node: astroid.Const): + self._detect_possible_f_string(node) + + def _detect_possible_f_string(self, node: astroid.Const): + """Check whether strings include local/global variables in '{}' + Those should probably be f-strings's + """ + + def detect_if_used_in_format(node: astroid.Const, assign_name: str) -> bool: + """Check if the node is used in a call to format() if so return True""" + # the skip_class is to make sure we don't go into inner scopes, but might not be needed per se + for attr in node.scope().nodes_of_class( + astroid.Attribute, skip_klass=(astroid.FunctionDef,) + ): + if isinstance(attr.expr, astroid.Name): + if attr.expr.name == assign_name and attr.attrname == "format": + return True + return False + + if node.pytype() == "builtins.str" and not isinstance( + node.parent, astroid.JoinedStr + ): + # Find all pairs of '{}' within a string + inner_matches = re.findall(r"(?<=\{).*?(?=\})", node.value) + if len(inner_matches) != len(set(inner_matches)): + return + if inner_matches: + for match in inner_matches: + # Check if match is a local or global variable + if match := node.scope().locals.get( + match + ) or node.root().locals.get(match): + assign_node = node + while not isinstance(assign_node, astroid.Assign): + assign_node = assign_node.parent + if isinstance(assign_node.value, astroid.Tuple): + node_index = assign_node.value.elts.index(node) + assign_name = assign_node.targets[0].elts[node_index].name + else: + assign_name = assign_node.targets[0].name + if not detect_if_used_in_format(node, assign_name): + self.add_message( + "possible-f-string-as-string", + line=node.lineno, + node=node, + ) + return + else: + return + def register(linter): """required method to auto register this checker""" diff --git a/tests/functional/p/possible_f_string_as_string.py b/tests/functional/p/possible_f_string_as_string.py new file mode 100644 index 0000000000..6882657392 --- /dev/null +++ b/tests/functional/p/possible_f_string_as_string.py @@ -0,0 +1,12 @@ +# pylint: disable=missing-module-docstring, invalid-name +var = "string" +var_two = "extra string" + +x = f"This is a {var} which should be a f-string" +x = "This is a {var} used twice, see {var}" +x = "This is a {var} which should be a f-string" # [possible-f-string-as-string] +x = "This is a {var} and {var_two} which should be a f-string" # [possible-f-string-as-string] +x1, x2, x3 = (1, 2, "This is a {var} which should be a f-string") # [possible-f-string-as-string] + +y = "This is a {var} used for formatting later" +z = y.format(var="string") diff --git a/tests/functional/p/possible_f_string_as_string.txt b/tests/functional/p/possible_f_string_as_string.txt new file mode 100644 index 0000000000..a3ce06f37c --- /dev/null +++ b/tests/functional/p/possible_f_string_as_string.txt @@ -0,0 +1,3 @@ +possible-f-string-as-string:7:4::Using an string which should probably be a f-string:HIGH +possible-f-string-as-string:8:4::Using an string which should probably be a f-string:HIGH +possible-f-string-as-string:9:20::Using an string which should probably be a f-string:HIGH From 92b89598f436425b208282e651669c236f3a457b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Mon, 2 Aug 2021 16:08:04 +0200 Subject: [PATCH 02/18] Remove walrus operator --- pylint/checkers/strings.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 03ad0a2c0c..a7e5c8fa10 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -929,9 +929,7 @@ def detect_if_used_in_format(node: astroid.Const, assign_name: str) -> bool: if inner_matches: for match in inner_matches: # Check if match is a local or global variable - if match := node.scope().locals.get( - match - ) or node.root().locals.get(match): + if node.scope().locals.get(match) or node.root().locals.get(match): assign_node = node while not isinstance(assign_node, astroid.Assign): assign_node = assign_node.parent From 240b4da6228b33a177e3e41259bcd109a3efc3fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Mon, 2 Aug 2021 19:02:06 +0200 Subject: [PATCH 03/18] Apply suggestions from code review Co-authored-by: Pierre Sassoulas --- pylint/checkers/strings.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index a7e5c8fa10..6d6cf235e1 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -206,8 +206,8 @@ "in which case it can be either a normal string or a bug in the code.", ), "W1310": ( - "Using an string which should probably be a f-string", - "possible-f-string-as-string", + "The %s syntax imply an f-string but the leading 'f' is missing", + "possible-forgotten-f-prefix" "Used when we detect a string that uses '{}' with a local variable inside. " "This string is probably meant to be an f-string.", ), @@ -901,7 +901,10 @@ def process_non_raw_string_token( @check_messages("possible-f-string-as-string") def visit_const(self, node: astroid.Const): - self._detect_possible_f_string(node) + if node.pytype() == "builtins.str" and not isinstance( + node.parent, astroid.JoinedStr + ): + self._detect_possible_f_string(node) def _detect_possible_f_string(self, node: astroid.Const): """Check whether strings include local/global variables in '{}' @@ -929,7 +932,9 @@ def detect_if_used_in_format(node: astroid.Const, assign_name: str) -> bool: if inner_matches: for match in inner_matches: # Check if match is a local or global variable - if node.scope().locals.get(match) or node.root().locals.get(match): + if not(node.scope().locals.get(match) or node.root().locals.get(match)): + return + ... assign_node = node while not isinstance(assign_node, astroid.Assign): assign_node = assign_node.parent From 3c2cf2405d28250964a497e6fdd329770bd45446 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Aug 2021 17:02:47 +0000 Subject: [PATCH 04/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pylint/checkers/strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 6d6cf235e1..6b5fa6296b 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -934,7 +934,7 @@ def detect_if_used_in_format(node: astroid.Const, assign_name: str) -> bool: # Check if match is a local or global variable if not(node.scope().locals.get(match) or node.root().locals.get(match)): return - ... + ... assign_node = node while not isinstance(assign_node, astroid.Assign): assign_node = assign_node.parent From f7f4f89e5633ce351482e4631388634abed5d72f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Mon, 2 Aug 2021 19:27:19 +0200 Subject: [PATCH 05/18] Add review changes --- ChangeLog | 2 +- doc/whatsnew/2.10.rst | 2 +- pylint/checkers/strings.py | 39 ++++++++++--------- .../p/possible_f_string_as_string.py | 12 ------ .../p/possible_f_string_as_string.txt | 3 -- .../p/possible_forgotten_f_prefix.py | 16 ++++++++ .../p/possible_forgotten_f_prefix.txt | 4 ++ 7 files changed, 42 insertions(+), 36 deletions(-) delete mode 100644 tests/functional/p/possible_f_string_as_string.py delete mode 100644 tests/functional/p/possible_f_string_as_string.txt create mode 100644 tests/functional/p/possible_forgotten_f_prefix.py create mode 100644 tests/functional/p/possible_forgotten_f_prefix.txt diff --git a/ChangeLog b/ChangeLog index d14a7bb5c8..18dee2ea70 100644 --- a/ChangeLog +++ b/ChangeLog @@ -68,7 +68,7 @@ Release date: TBA Closes #4681 -* Added ``possible-f-string-as-string``: Emitted when variables are used in normal strings in between "{}" +* Added ``possible-forgotten-f-prefix``: Emitted when variables are used in normal strings in between "{}" Closes #2507 diff --git a/doc/whatsnew/2.10.rst b/doc/whatsnew/2.10.rst index 4c90242b2a..12d98de0d7 100644 --- a/doc/whatsnew/2.10.rst +++ b/doc/whatsnew/2.10.rst @@ -28,7 +28,7 @@ New checkers Closes #3692 -* Added ``possible-f-string-as-string``: Emitted when variables are used in normal strings in between "{}" +* Added ``possible-forgotten-f-prefix``: Emitted when variables are used in normal strings in between "{}" Closes #2507 diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 6b5fa6296b..ce7276d39d 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -207,7 +207,7 @@ ), "W1310": ( "The %s syntax imply an f-string but the leading 'f' is missing", - "possible-forgotten-f-prefix" + "possible-forgotten-f-prefix", "Used when we detect a string that uses '{}' with a local variable inside. " "This string is probably meant to be an f-string.", ), @@ -899,7 +899,7 @@ def process_non_raw_string_token( # character can never be the start of a new backslash escape. index += 2 - @check_messages("possible-f-string-as-string") + @check_messages("possible-forgotten-f-prefix") def visit_const(self, node: astroid.Const): if node.pytype() == "builtins.str" and not isinstance( node.parent, astroid.JoinedStr @@ -932,24 +932,25 @@ def detect_if_used_in_format(node: astroid.Const, assign_name: str) -> bool: if inner_matches: for match in inner_matches: # Check if match is a local or global variable - if not(node.scope().locals.get(match) or node.root().locals.get(match)): + if not ( + node.scope().locals.get(match) or node.root().locals.get(match) + ): return - ... - assign_node = node - while not isinstance(assign_node, astroid.Assign): - assign_node = assign_node.parent - if isinstance(assign_node.value, astroid.Tuple): - node_index = assign_node.value.elts.index(node) - assign_name = assign_node.targets[0].elts[node_index].name - else: - assign_name = assign_node.targets[0].name - if not detect_if_used_in_format(node, assign_name): - self.add_message( - "possible-f-string-as-string", - line=node.lineno, - node=node, - ) - return + assign_node = node + while not isinstance(assign_node, astroid.Assign): + assign_node = assign_node.parent + if isinstance(assign_node.value, astroid.Tuple): + node_index = assign_node.value.elts.index(node) + assign_name = assign_node.targets[0].elts[node_index].name + else: + assign_name = assign_node.targets[0].name + if not detect_if_used_in_format(node, assign_name): + self.add_message( + "possible-forgotten-f-prefix", + line=node.lineno, + node=node, + args=(f"{{{match}}}",), + ) else: return diff --git a/tests/functional/p/possible_f_string_as_string.py b/tests/functional/p/possible_f_string_as_string.py deleted file mode 100644 index 6882657392..0000000000 --- a/tests/functional/p/possible_f_string_as_string.py +++ /dev/null @@ -1,12 +0,0 @@ -# pylint: disable=missing-module-docstring, invalid-name -var = "string" -var_two = "extra string" - -x = f"This is a {var} which should be a f-string" -x = "This is a {var} used twice, see {var}" -x = "This is a {var} which should be a f-string" # [possible-f-string-as-string] -x = "This is a {var} and {var_two} which should be a f-string" # [possible-f-string-as-string] -x1, x2, x3 = (1, 2, "This is a {var} which should be a f-string") # [possible-f-string-as-string] - -y = "This is a {var} used for formatting later" -z = y.format(var="string") diff --git a/tests/functional/p/possible_f_string_as_string.txt b/tests/functional/p/possible_f_string_as_string.txt deleted file mode 100644 index a3ce06f37c..0000000000 --- a/tests/functional/p/possible_f_string_as_string.txt +++ /dev/null @@ -1,3 +0,0 @@ -possible-f-string-as-string:7:4::Using an string which should probably be a f-string:HIGH -possible-f-string-as-string:8:4::Using an string which should probably be a f-string:HIGH -possible-f-string-as-string:9:20::Using an string which should probably be a f-string:HIGH diff --git a/tests/functional/p/possible_forgotten_f_prefix.py b/tests/functional/p/possible_forgotten_f_prefix.py new file mode 100644 index 0000000000..9493952878 --- /dev/null +++ b/tests/functional/p/possible_forgotten_f_prefix.py @@ -0,0 +1,16 @@ +# pylint: disable=missing-module-docstring, invalid-name, line-too-long +var = "string" +var_two = "extra string" + +x = f"This is a {var} which should be a f-string" +x = "This is a {var} used twice, see {var}" +x = "This is a {var} which should be a f-string" # [possible-forgotten-f-prefix] +x = "This is a {var} and {var_two} which should be a f-string" # [possible-forgotten-f-prefix, possible-forgotten-f-prefix] +x1, x2, x3 = (1, 2, "This is a {var} which should be a f-string") # [possible-forgotten-f-prefix] + +y = "This is a {var} used for formatting later" +z = y.format(var="string") + +examples = [var, var_two] +x = f"This is an example with a list: {''.join(examples) + 'well...' }" +x = "This is an example with a list: {''.join(examples) + 'well...' }" # [possible-forgotten-f-prefix] diff --git a/tests/functional/p/possible_forgotten_f_prefix.txt b/tests/functional/p/possible_forgotten_f_prefix.txt new file mode 100644 index 0000000000..f252badf13 --- /dev/null +++ b/tests/functional/p/possible_forgotten_f_prefix.txt @@ -0,0 +1,4 @@ +possible-forgotten-f-prefix:7:4::The {var} syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:8:4::The {var_two} syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:8:4::The {var} syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:9:20::The {var} syntax imply an f-string but the leading 'f' is missing:HIGH From c91a14f26644f36227f311635b12ce4e3976df17 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Tue, 17 Aug 2021 08:40:04 +0200 Subject: [PATCH 06/18] Fix bad conflict resolution --- pylint/checkers/strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index b2aba0c9bb..9f0353c12e 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -926,7 +926,7 @@ def visit_const(self, node: astroid.Const): node.parent, astroid.JoinedStr ): self._detect_possible_f_string(node) - self._detect_u_string_prefix(node) + self._detect_u_string_prefix(node) def _detect_possible_f_string(self, node: astroid.Const): """Check whether strings include local/global variables in '{}' From 20600377d556aff229164c03109dd0553b496a8d Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Tue, 17 Aug 2021 08:55:09 +0200 Subject: [PATCH 07/18] Add more test cases with invalid python code --- .../p/possible_forgotten_f_prefix.py | 20 ++++++++++++++++++- .../p/possible_forgotten_f_prefix.txt | 14 +++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/tests/functional/p/possible_forgotten_f_prefix.py b/tests/functional/p/possible_forgotten_f_prefix.py index 9493952878..64e53c52f5 100644 --- a/tests/functional/p/possible_forgotten_f_prefix.py +++ b/tests/functional/p/possible_forgotten_f_prefix.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-module-docstring, invalid-name, line-too-long +# pylint: disable=missing-docstring, invalid-name, line-too-long, multiple-statements, pointless-string-statement, pointless-statement var = "string" var_two = "extra string" @@ -11,6 +11,24 @@ y = "This is a {var} used for formatting later" z = y.format(var="string") +g = "This is a {another_var} used for formatting later" +h = y.format(another_var="string") + +i = "This is {invalid /// python /// inside}" + examples = [var, var_two] x = f"This is an example with a list: {''.join(examples) + 'well...' }" x = "This is an example with a list: {''.join(examples) + 'well...' }" # [possible-forgotten-f-prefix] + +param = "string" +"This is a string" # good +"This is a {param}" # [possible-forgotten-f-prefix] +f"This is a {param}" # good + +"This is a calculation: 1 + 1" # good +"This is a calculation: {1 + 1}" # [possible-forgotten-f-prefix] +f"This is a calculation: {1 + 1}" # good + +"This is a nice string" # good +"This is a {'nice' + param}" # [possible-forgotten-f-prefix] +f"This is a {'nice' + param}" # good diff --git a/tests/functional/p/possible_forgotten_f_prefix.txt b/tests/functional/p/possible_forgotten_f_prefix.txt index f252badf13..760812c8ed 100644 --- a/tests/functional/p/possible_forgotten_f_prefix.txt +++ b/tests/functional/p/possible_forgotten_f_prefix.txt @@ -1,4 +1,10 @@ -possible-forgotten-f-prefix:7:4::The {var} syntax imply an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:8:4::The {var_two} syntax imply an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:8:4::The {var} syntax imply an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:9:20::The {var} syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:7:4::The '{var}' syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:8:4::The '{var_two}' syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:8:4::The '{var}' syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:9:20::The '{var}' syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:11:4::The '{var}' syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:14:4::The '{another_var}' syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:25:4::The '{''.join(examples) + 'well...' }' syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:29:0::The '{param}' syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:33:0::The '{1 + 1}' syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:37:0::The '{'nice' + param}' syntax imply an f-string but the leading 'f' is missing:HIGH From 8def6c5b3664436d742ab6d4b00175791a05106f Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Tue, 17 Aug 2021 09:43:32 +0200 Subject: [PATCH 08/18] Use ast.parse() work in progress --- pylint/checkers/strings.py | 73 ++++++++----------- .../p/possible_forgotten_f_prefix.py | 10 ++- 2 files changed, 38 insertions(+), 45 deletions(-) diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 9f0353c12e..776f990b85 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -34,7 +34,7 @@ """Checker for string formatting operations. """ - +import ast import collections import numbers import re @@ -151,7 +151,7 @@ "E1310": ( "Suspicious argument in %s.%s call", "bad-str-strip-call", - "The argument to a str.{l,r,}strip call contains a duplicate character, ", + "The argument to a str.{l,r,}strip call contains a duplicate character, ", # pylint: disable=possible-forgotten-f-prefix ), "W1302": ( "Invalid format string", @@ -189,7 +189,7 @@ "W1307": ( "Using invalid lookup key %r in format specifier %r", "invalid-format-index", - "Used when a PEP 3101 format string uses a lookup specifier " + "Used when a PEP 3101 format string uses a lookup specifier " # pylint: disable=possible-forgotten-f-prefix "({a[1]}), but the argument passed for formatting " "doesn't contain or doesn't have that key as an attribute.", ), @@ -933,48 +933,37 @@ def _detect_possible_f_string(self, node: astroid.Const): Those should probably be f-strings's """ - def detect_if_used_in_format(node: astroid.Const, assign_name: str) -> bool: + def detect_if_used_in_format(node: astroid.Const) -> bool: """Check if the node is used in a call to format() if so return True""" - # the skip_class is to make sure we don't go into inner scopes, but might not be needed per se - for attr in node.scope().nodes_of_class( - astroid.Attribute, skip_klass=(astroid.FunctionDef,) - ): - if isinstance(attr.expr, astroid.Name): - if attr.expr.name == assign_name and attr.attrname == "format": - return True + print(node) return False - if node.pytype() == "builtins.str" and not isinstance( - node.parent, astroid.JoinedStr - ): - # Find all pairs of '{}' within a string - inner_matches = re.findall(r"(?<=\{).*?(?=\})", node.value) - if len(inner_matches) != len(set(inner_matches)): - return - if inner_matches: - for match in inner_matches: - # Check if match is a local or global variable - if not ( - node.scope().locals.get(match) or node.root().locals.get(match) - ): - return - assign_node = node - while not isinstance(assign_node, astroid.Assign): - assign_node = assign_node.parent - if isinstance(assign_node.value, astroid.Tuple): - node_index = assign_node.value.elts.index(node) - assign_name = assign_node.targets[0].elts[node_index].name - else: - assign_name = assign_node.targets[0].name - if not detect_if_used_in_format(node, assign_name): - self.add_message( - "possible-forgotten-f-prefix", - line=node.lineno, - node=node, - args=(f"{{{match}}}",), - ) - else: - return + # Find all pairs of '{}' within a string + inner_matches = re.findall(r"(?<=\{).*?(?=\})", node.value) + if len(inner_matches) != len(set(inner_matches)): + return + if inner_matches: + for match in inner_matches: + try: + ast.parse(match, "", "eval") + except SyntaxError: + # Not valid python + continue + # if not isinstance(parsed_match, ast.Expression): + # # Not a proper expression, won't work in f-string + # continue + # for ast_node in ast.walk(parsed_match): + # if isinstance(ast_node, ast.Name): + # print( + # f"TODO check that the name {ast_node.id} exists in the scope ?" + # ) + if not detect_if_used_in_format(node): + self.add_message( + "possible-forgotten-f-prefix", + line=node.lineno, + node=node, + args=(f"'{{{match}}}'",), + ) def _detect_u_string_prefix(self, node: astroid.Const): """Check whether strings include a 'u' prefix like u'String'""" diff --git a/tests/functional/p/possible_forgotten_f_prefix.py b/tests/functional/p/possible_forgotten_f_prefix.py index 64e53c52f5..8077ca97a1 100644 --- a/tests/functional/p/possible_forgotten_f_prefix.py +++ b/tests/functional/p/possible_forgotten_f_prefix.py @@ -8,13 +8,17 @@ x = "This is a {var} and {var_two} which should be a f-string" # [possible-forgotten-f-prefix, possible-forgotten-f-prefix] x1, x2, x3 = (1, 2, "This is a {var} which should be a f-string") # [possible-forgotten-f-prefix] -y = "This is a {var} used for formatting later" +y = "This is a {var} used for formatting later" # [possible-forgotten-f-prefix] z = y.format(var="string") -g = "This is a {another_var} used for formatting later" -h = y.format(another_var="string") +g = "This is a {another_var} used for formatting later" # [possible-forgotten-f-prefix] +h = g.format(another_var="string") i = "This is {invalid /// python /// inside}" +j = "This is {not */ valid python.}" +k = "This is {def function(): return 42} valid python but not an expression" + +def function(): return 42 examples = [var, var_two] x = f"This is an example with a list: {''.join(examples) + 'well...' }" From f803cafd25c5eb24222d40aa523a7877250bcd1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 17 Aug 2021 17:19:18 +0200 Subject: [PATCH 09/18] Fix pre-commit messages --- pylint/checkers/classes.py | 4 ++-- pylint/checkers/python3.py | 2 +- pylint/checkers/refactoring/refactoring_checker.py | 2 +- pylint/checkers/stdlib.py | 2 +- pylint/checkers/typecheck.py | 4 ++-- pylint/lint/pylinter.py | 2 +- script/bump_changelog.py | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pylint/checkers/classes.py b/pylint/checkers/classes.py index ec0312eba6..fccfd64eb6 100644 --- a/pylint/checkers/classes.py +++ b/pylint/checkers/classes.py @@ -999,10 +999,10 @@ def _check_unused_private_attributes(self, node: astroid.ClassDef) -> None: if attribute.attrname != assign_attr.attrname: continue - if assign_attr.expr.name == "cls" and attribute.expr.name in [ + if assign_attr.expr.name == "cls" and attribute.expr.name in ( "cls", "self", - ]: + ): # If assigned to cls.attrib, can be accessed by cls/self break diff --git a/pylint/checkers/python3.py b/pylint/checkers/python3.py index ccce989017..3fd635a23d 100644 --- a/pylint/checkers/python3.py +++ b/pylint/checkers/python3.py @@ -156,7 +156,7 @@ def _in_iterating_context(node): elif ( isinstance(parent, astroid.Compare) and len(parent.ops) == 1 - and parent.ops[0][0] in ["in", "not in"] + and parent.ops[0][0] in ("in", "not in") ): return True # Also if it's an `yield from`, that's fair diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py index 77d157ae66..793b27cc88 100644 --- a/pylint/checkers/refactoring/refactoring_checker.py +++ b/pylint/checkers/refactoring/refactoring_checker.py @@ -965,7 +965,7 @@ def _check_consider_using_generator(self, node): # remove square brackets '[]' inside_comp = node.args[0].as_string()[1:-1] call_name = node.func.name - if call_name in ["any", "all"]: + if call_name in ("any", "all"): self.add_message( "use-a-generator", node=node, diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index 5ff77ff83f..7eabe9bb40 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -568,7 +568,7 @@ def _check_redundant_assert(self, node, infer): isinstance(infer, astroid.BoundMethod) and node.args and isinstance(node.args[0], astroid.Const) - and infer.name in ["assertTrue", "assertFalse"] + and infer.name in ("assertTrue", "assertFalse") ): self.add_message( "redundant-unittest-assert", diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 3291393a43..a3b0709602 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -518,7 +518,7 @@ def _emit_no_member(node, owner, owner_name, ignored_mixins=True, ignored_none=T and isinstance(owner.parent, astroid.ClassDef) and owner.parent.name == "EnumMeta" and owner_name == "__members__" - and node.attrname in ["items", "values", "keys"] + and node.attrname in ("items", "values", "keys") ): # Avoid false positive on Enum.__members__.{items(), values, keys} # See https://github.com/PyCQA/pylint/issues/4123 @@ -1780,7 +1780,7 @@ def visit_compare(self, node): return op, right = node.ops[0] - if op in ["in", "not in"]: + if op in ("in", "not in"): self._check_membership_test(right) @check_messages( diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py index e4ed47683b..286b7e1b34 100644 --- a/pylint/lint/pylinter.py +++ b/pylint/lint/pylinter.py @@ -728,7 +728,7 @@ def any_fail_on_issues(self): def disable_noerror_messages(self): for msgcat, msgids in self.msgs_store._msgs_by_category.items(): # enable only messages with 'error' severity and above ('fatal') - if msgcat in ["E", "F"]: + if msgcat in ("E", "F"): for msgid in msgids: self.enable(msgid) else: diff --git a/script/bump_changelog.py b/script/bump_changelog.py index 41c8c3c8a9..71c9c88333 100644 --- a/script/bump_changelog.py +++ b/script/bump_changelog.py @@ -136,7 +136,7 @@ def do_checks(content, next_version, version, version_type): wn_next_version = get_whats_new(next_version) wn_this_version = get_whats_new(version) # There is only one field where the release date is TBA - if version_type in [VersionType.MAJOR, VersionType.MINOR]: + if version_type in (VersionType.MAJOR, VersionType.MINOR): assert ( content.count(RELEASE_DATE_TEXT) <= 1 ), f"There should be only one release date 'TBA' ({version}) {err}" From 971c29d51d629a161bb41c61e1b3a8b185ec925b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 17 Aug 2021 17:21:09 +0200 Subject: [PATCH 10/18] Update checks and tests --- pylint/checkers/strings.py | 57 ++++++++++++--- .../p/possible_forgotten_f_prefix.py | 73 +++++++++++-------- .../p/possible_forgotten_f_prefix.txt | 19 +++-- 3 files changed, 98 insertions(+), 51 deletions(-) diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 776f990b85..4586d00167 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -151,7 +151,7 @@ "E1310": ( "Suspicious argument in %s.%s call", "bad-str-strip-call", - "The argument to a str.{l,r,}strip call contains a duplicate character, ", # pylint: disable=possible-forgotten-f-prefix + "The argument to a str.{l,r,}strip call contains a duplicate character, ", ), "W1302": ( "Invalid format string", @@ -189,7 +189,7 @@ "W1307": ( "Using invalid lookup key %r in format specifier %r", "invalid-format-index", - "Used when a PEP 3101 format string uses a lookup specifier " # pylint: disable=possible-forgotten-f-prefix + "Used when a PEP 3101 format string uses a lookup specifier " "({a[1]}), but the argument passed for formatting " "doesn't contain or doesn't have that key as an attribute.", ), @@ -212,7 +212,7 @@ "in which case it can be either a normal string without formatting or a bug in the code.", ), "W1311": ( - "The %s syntax imply an f-string but the leading 'f' is missing", + "The '%s' syntax implies an f-string but the leading 'f' is missing", "possible-forgotten-f-prefix", "Used when we detect a string that uses '{}' with a local variable inside. " "This string is probably meant to be an f-string.", @@ -933,15 +933,24 @@ def _detect_possible_f_string(self, node: astroid.Const): Those should probably be f-strings's """ - def detect_if_used_in_format(node: astroid.Const) -> bool: + def detect_if_used_in_format(node: astroid.Const, assign_name: str) -> bool: """Check if the node is used in a call to format() if so return True""" - print(node) + # the skip_class is to make sure we don't go into inner scopes, but might not be needed per se + for attr in node.scope().nodes_of_class( + astroid.Attribute, skip_klass=(astroid.FunctionDef,) + ): + if isinstance(attr.expr, astroid.Name): + if attr.expr.name == assign_name and attr.attrname == "format": + return True return False # Find all pairs of '{}' within a string inner_matches = re.findall(r"(?<=\{).*?(?=\})", node.value) + + # If a variable is used twice it is probably used for formatting later on if len(inner_matches) != len(set(inner_matches)): return + if inner_matches: for match in inner_matches: try: @@ -957,13 +966,37 @@ def detect_if_used_in_format(node: astroid.Const) -> bool: # print( # f"TODO check that the name {ast_node.id} exists in the scope ?" # ) - if not detect_if_used_in_format(node): - self.add_message( - "possible-forgotten-f-prefix", - line=node.lineno, - node=node, - args=(f"'{{{match}}}'",), - ) + + # Get the assign node, if there is any + assign_node = node + while not isinstance(assign_node, astroid.Assign): + assign_node = assign_node.parent + if isinstance(assign_node, astroid.Module): + break + else: + # Get the assign name and handle the case of tuple assignment + if isinstance(assign_node.value, astroid.Tuple): + node_index = assign_node.value.elts.index(node) + assign_name = assign_node.targets[0].elts[node_index].name + else: + assign_name = assign_node.targets[0].name + + # Detect calls to .format() + if not detect_if_used_in_format(node, assign_name): + self.add_message( + "possible-forgotten-f-prefix", + line=node.lineno, + node=node, + args=(f"{{{match}}}",), + ) + continue + + self.add_message( + "possible-forgotten-f-prefix", + line=node.lineno, + node=node, + args=(f"{{{match}}}",), + ) def _detect_u_string_prefix(self, node: astroid.Const): """Check whether strings include a 'u' prefix like u'String'""" diff --git a/tests/functional/p/possible_forgotten_f_prefix.py b/tests/functional/p/possible_forgotten_f_prefix.py index 8077ca97a1..62b156a94a 100644 --- a/tests/functional/p/possible_forgotten_f_prefix.py +++ b/tests/functional/p/possible_forgotten_f_prefix.py @@ -1,38 +1,53 @@ -# pylint: disable=missing-docstring, invalid-name, line-too-long, multiple-statements, pointless-string-statement, pointless-statement -var = "string" -var_two = "extra string" +"""Check various forms of strings which could be f-strings without a prefix""" +# pylint: disable=invalid-name, line-too-long, pointless-string-statement, pointless-statement +# pylint: disable=missing-function-docstring -x = f"This is a {var} which should be a f-string" -x = "This is a {var} used twice, see {var}" -x = "This is a {var} which should be a f-string" # [possible-forgotten-f-prefix] -x = "This is a {var} and {var_two} which should be a f-string" # [possible-forgotten-f-prefix, possible-forgotten-f-prefix] -x1, x2, x3 = (1, 2, "This is a {var} which should be a f-string") # [possible-forgotten-f-prefix] +# Check for local variable interpolation +PARAM = "string" +PARAM_TWO = "extra string" -y = "This is a {var} used for formatting later" # [possible-forgotten-f-prefix] -z = y.format(var="string") +A = f"This is a {PARAM} which should be a f-string" +B = "This is a {PARAM} used twice, see {PARAM}" +C = "This is a {PARAM} which should be a f-string" # [possible-forgotten-f-prefix] +D = "This is a {PARAM} and {PARAM_TWO} which should be a f-string" # [possible-forgotten-f-prefix, possible-forgotten-f-prefix] +E1, E2, E3 = (1, 2, "This is a {PARAM} which should be a f-string") # [possible-forgotten-f-prefix] -g = "This is a {another_var} used for formatting later" # [possible-forgotten-f-prefix] -h = g.format(another_var="string") +# Check for use of .format() +F = "This is a {parameter} used for formatting later" +G = F.format(parameter="string") -i = "This is {invalid /// python /// inside}" -j = "This is {not */ valid python.}" -k = "This is {def function(): return 42} valid python but not an expression" +H = "This is a {another_parameter} used for formatting later" +I = H.format(another_parameter="string") -def function(): return 42 +# Check for use of variables within functions +PARAM_LIST = [PARAM, PARAM_TWO] +J = f"This is an example with a list: {''.join(PARAM_LIST) + 'well...'}" +K = "This is an example with a list: {''.join(PARAM_LIST) + 'well...'}" # [possible-forgotten-f-prefix] -examples = [var, var_two] -x = f"This is an example with a list: {''.join(examples) + 'well...' }" -x = "This is an example with a list: {''.join(examples) + 'well...' }" # [possible-forgotten-f-prefix] +# Check for calculations without variables +L = f"This is a calculation: {1 + 1}" +M = "This is a calculation: {1 + 1}" # [possible-forgotten-f-prefix] -param = "string" -"This is a string" # good -"This is a {param}" # [possible-forgotten-f-prefix] -f"This is a {param}" # good +# Check invalid Python code +N = "This is {invalid /// python /// inside}" +O = "This is {not */ valid python.}" +P = "This is {def function(): return 42} valid python but not an expression" -"This is a calculation: 1 + 1" # good -"This is a calculation: {1 + 1}" # [possible-forgotten-f-prefix] -f"This is a calculation: {1 + 1}" # good -"This is a nice string" # good -"This is a {'nice' + param}" # [possible-forgotten-f-prefix] -f"This is a {'nice' + param}" # good +def function(): + return 42 + + +# Check strings without assignment +PARAM_THREE = "string" +f"This is a {PARAM_THREE}" +"This is a string" +"This is a {PARAM_THREE}" # [possible-forgotten-f-prefix] + +f"This is a calculation: {1 + 1}" +"This is a calculation: 1 + 1" +"This is a calculation: {1 + 1}" # [possible-forgotten-f-prefix] + +f"This is a {'nice' + PARAM_THREE}" +"This is a nice string" +"This is a {'nice' + PARAM_THREE}" # [possible-forgotten-f-prefix] diff --git a/tests/functional/p/possible_forgotten_f_prefix.txt b/tests/functional/p/possible_forgotten_f_prefix.txt index 760812c8ed..0ed22994be 100644 --- a/tests/functional/p/possible_forgotten_f_prefix.txt +++ b/tests/functional/p/possible_forgotten_f_prefix.txt @@ -1,10 +1,9 @@ -possible-forgotten-f-prefix:7:4::The '{var}' syntax imply an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:8:4::The '{var_two}' syntax imply an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:8:4::The '{var}' syntax imply an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:9:20::The '{var}' syntax imply an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:11:4::The '{var}' syntax imply an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:14:4::The '{another_var}' syntax imply an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:25:4::The '{''.join(examples) + 'well...' }' syntax imply an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:29:0::The '{param}' syntax imply an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:33:0::The '{1 + 1}' syntax imply an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:37:0::The '{'nice' + param}' syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:11:4::The '{PARAM}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:12:4::The '{PARAM_TWO}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:12:4::The '{PARAM}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:13:20::The '{PARAM}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:25:4::The '{''.join(PARAM_LIST) + 'well...'}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:29:4::The '{1 + 1}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:45:0::The '{PARAM_THREE}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:49:0::The '{1 + 1}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:53:0::The '{'nice' + PARAM_THREE}' syntax implies an f-string but the leading 'f' is missing:HIGH From 817bf995bbbcffe7903844ce7f86d671ab046769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 17 Aug 2021 21:26:22 +0200 Subject: [PATCH 11/18] Update tests/functional/p/possible_forgotten_f_prefix.py Co-authored-by: Pierre Sassoulas --- tests/functional/p/possible_forgotten_f_prefix.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/functional/p/possible_forgotten_f_prefix.py b/tests/functional/p/possible_forgotten_f_prefix.py index 62b156a94a..b76d636361 100644 --- a/tests/functional/p/possible_forgotten_f_prefix.py +++ b/tests/functional/p/possible_forgotten_f_prefix.py @@ -34,8 +34,6 @@ P = "This is {def function(): return 42} valid python but not an expression" -def function(): - return 42 # Check strings without assignment From b351fe934fdf8e50faabe7942d6b62ed008d93fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 18 Aug 2021 14:24:08 +0200 Subject: [PATCH 12/18] Fix some tests --- pylint/checkers/strings.py | 14 +++++------ .../p/possible_forgotten_f_prefix.py | 25 ++++++++++++++----- .../p/possible_forgotten_f_prefix.txt | 16 ++++++------ 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 4586d00167..95d7853c69 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -951,6 +951,12 @@ def detect_if_used_in_format(node: astroid.Const, assign_name: str) -> bool: if len(inner_matches) != len(set(inner_matches)): return + if ( + isinstance(node.parent, astroid.Attribute) + and node.parent.attrname == "format" + ): + return + if inner_matches: for match in inner_matches: try: @@ -958,14 +964,6 @@ def detect_if_used_in_format(node: astroid.Const, assign_name: str) -> bool: except SyntaxError: # Not valid python continue - # if not isinstance(parsed_match, ast.Expression): - # # Not a proper expression, won't work in f-string - # continue - # for ast_node in ast.walk(parsed_match): - # if isinstance(ast_node, ast.Name): - # print( - # f"TODO check that the name {ast_node.id} exists in the scope ?" - # ) # Get the assign node, if there is any assign_node = node diff --git a/tests/functional/p/possible_forgotten_f_prefix.py b/tests/functional/p/possible_forgotten_f_prefix.py index b76d636361..4f71779ee2 100644 --- a/tests/functional/p/possible_forgotten_f_prefix.py +++ b/tests/functional/p/possible_forgotten_f_prefix.py @@ -1,6 +1,7 @@ """Check various forms of strings which could be f-strings without a prefix""" # pylint: disable=invalid-name, line-too-long, pointless-string-statement, pointless-statement -# pylint: disable=missing-function-docstring +# pylint: disable=missing-function-docstring, missing-class-docstring, too-few-public-methods +# pylint: disable=useless-object-inheritance # Check for local variable interpolation PARAM = "string" @@ -15,9 +16,24 @@ # Check for use of .format() F = "This is a {parameter} used for formatting later" G = F.format(parameter="string") +H = "{0}, {1}".format(1, 2) + + +def func_one(): + return "{0}, {1}".format(1, 2) + + +def func_two(): + I = "{0}, {1}" + return I.format(1, 2) + + +class Class(object): + attr = 0 + + def __str__(self): + return "{self.attr}".format(self=self) -H = "This is a {another_parameter} used for formatting later" -I = H.format(another_parameter="string") # Check for use of variables within functions PARAM_LIST = [PARAM, PARAM_TWO] @@ -33,9 +49,6 @@ O = "This is {not */ valid python.}" P = "This is {def function(): return 42} valid python but not an expression" - - - # Check strings without assignment PARAM_THREE = "string" f"This is a {PARAM_THREE}" diff --git a/tests/functional/p/possible_forgotten_f_prefix.txt b/tests/functional/p/possible_forgotten_f_prefix.txt index 0ed22994be..9a23ecafb3 100644 --- a/tests/functional/p/possible_forgotten_f_prefix.txt +++ b/tests/functional/p/possible_forgotten_f_prefix.txt @@ -1,9 +1,9 @@ -possible-forgotten-f-prefix:11:4::The '{PARAM}' syntax implies an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:12:4::The '{PARAM_TWO}' syntax implies an f-string but the leading 'f' is missing:HIGH possible-forgotten-f-prefix:12:4::The '{PARAM}' syntax implies an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:13:20::The '{PARAM}' syntax implies an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:25:4::The '{''.join(PARAM_LIST) + 'well...'}' syntax implies an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:29:4::The '{1 + 1}' syntax implies an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:45:0::The '{PARAM_THREE}' syntax implies an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:49:0::The '{1 + 1}' syntax implies an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:53:0::The '{'nice' + PARAM_THREE}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:13:4::The '{PARAM_TWO}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:13:4::The '{PARAM}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:14:20::The '{PARAM}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:41:4::The '{''.join(PARAM_LIST) + 'well...'}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:45:4::The '{1 + 1}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:61:0::The '{PARAM_THREE}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:65:0::The '{1 + 1}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:69:0::The '{'nice' + PARAM_THREE}' syntax implies an f-string but the leading 'f' is missing:HIGH From f04237792606a147f0f63e11e08b7013a39310a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 18 Aug 2021 16:20:55 +0200 Subject: [PATCH 13/18] Fix tests --- pylint/checkers/stdlib.py | 2 +- pylint/checkers/strings.py | 109 +++++++++++------- .../p/possible_forgotten_f_prefix.py | 30 +++-- .../p/possible_forgotten_f_prefix.txt | 10 +- 4 files changed, 93 insertions(+), 58 deletions(-) diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index 95bec2e5d9..3a63aefb5a 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -569,7 +569,7 @@ def _check_redundant_assert(self, node, infer): isinstance(infer, astroid.BoundMethod) and node.args and isinstance(node.args[0], nodes.Const) - and infer.name in ["assertTrue", "assertFalse"] + and infer.name in ("assertTrue", "assertFalse") ): self.add_message( "redundant-unittest-assert", diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 4cc71c4b06..9189036977 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -39,7 +39,7 @@ import numbers import re import tokenize -from typing import TYPE_CHECKING, Iterable +from typing import TYPE_CHECKING, Iterable, Union import astroid from astroid import nodes @@ -215,8 +215,8 @@ "W1311": ( "The '%s' syntax implies an f-string but the leading 'f' is missing", "possible-forgotten-f-prefix", - "Used when we detect a string that uses '{}' with a local variable inside. " - "This string is probably meant to be an f-string.", + "Used when we detect a string that uses '{}' with a local variable or valid " + "expression inside. This string is probably meant to be an f-string.", ), } @@ -929,20 +929,66 @@ def visit_const(self, node: nodes.Const): self._detect_possible_f_string(node) self._detect_u_string_prefix(node) - def _detect_possible_f_string(self, node: astroid.Const): + def _detect_possible_f_string(self, node: nodes.Const): """Check whether strings include local/global variables in '{}' Those should probably be f-strings's """ - def detect_if_used_in_format(node: astroid.Const, assign_name: str) -> bool: - """Check if the node is used in a call to format() if so return True""" - # the skip_class is to make sure we don't go into inner scopes, but might not be needed per se - for attr in node.scope().nodes_of_class( - astroid.Attribute, skip_klass=(astroid.FunctionDef,) - ): - if isinstance(attr.expr, astroid.Name): - if attr.expr.name == assign_name and attr.attrname == "format": + def detect_if_used_in_format(node: nodes.Const) -> bool: + """A helper function that checks if the node is used + in a call to format() if so returns True""" + + def get_all_format_calls(node: nodes.Const) -> set: + """Return a set of all calls to format()""" + calls = set() + # The skip_class is to make sure we don't go into inner scopes + for call in node.scope().nodes_of_class( + nodes.Attribute, skip_klass=(nodes.FunctionDef,) + ): + if call.attrname == "format": + if isinstance(call.expr, nodes.Name): + calls.add(call.expr.name) + elif isinstance(call.expr, nodes.Subscript): + slice_repr = [call.expr.value, call.expr.slice] + while not isinstance(slice_repr[0], nodes.Name): + slice_repr = [ + slice_repr[0].value, + slice_repr[0].slice, + ] + slice_repr[1:] + calls.add( + [slice_repr[0].name] + [i.value for i in slice_repr[1:]] + ) + return calls + + def check_match_in_calls( + assignment: Union[nodes.AssignName, nodes.Subscript] + ) -> bool: + """Check if the node to which is being assigned is used in a call to format()""" + format_calls = get_all_format_calls(node) + if isinstance(assignment, nodes.AssignName): + if assignment.name in format_calls: + return True + elif isinstance(assignment, nodes.Subscript): + slice_repr = [assignment.value, assignment.slice] + while not isinstance(slice_repr[0], nodes.Name): + slice_repr = [ + slice_repr[0].value, + slice_repr[0].slice, + ] + slice_repr[1:] + slice_repr = [slice_repr[0].name] + [ + i.value for i in slice_repr[1:] + ] + if slice_repr in format_calls: return True + return False + + if isinstance(node.parent, nodes.Assign): + return check_match_in_calls(node.parent.targets[0]) + if isinstance(node.parent, nodes.Tuple): + node_index = node.parent.elts.index(node) + return check_match_in_calls( + node.parent.parent.targets[0].elts[node_index] + ) return False # Find all pairs of '{}' within a string @@ -953,7 +999,7 @@ def detect_if_used_in_format(node: astroid.Const, assign_name: str) -> bool: return if ( - isinstance(node.parent, astroid.Attribute) + isinstance(node.parent, nodes.Attribute) and node.parent.attrname == "format" ): return @@ -966,36 +1012,13 @@ def detect_if_used_in_format(node: astroid.Const, assign_name: str) -> bool: # Not valid python continue - # Get the assign node, if there is any - assign_node = node - while not isinstance(assign_node, astroid.Assign): - assign_node = assign_node.parent - if isinstance(assign_node, astroid.Module): - break - else: - # Get the assign name and handle the case of tuple assignment - if isinstance(assign_node.value, astroid.Tuple): - node_index = assign_node.value.elts.index(node) - assign_name = assign_node.targets[0].elts[node_index].name - else: - assign_name = assign_node.targets[0].name - - # Detect calls to .format() - if not detect_if_used_in_format(node, assign_name): - self.add_message( - "possible-forgotten-f-prefix", - line=node.lineno, - node=node, - args=(f"{{{match}}}",), - ) - continue - - self.add_message( - "possible-forgotten-f-prefix", - line=node.lineno, - node=node, - args=(f"{{{match}}}",), - ) + if not detect_if_used_in_format(node): + self.add_message( + "possible-forgotten-f-prefix", + line=node.lineno, + node=node, + args=(f"{{{match}}}",), + ) def _detect_u_string_prefix(self, node: nodes.Const): """Check whether strings include a 'u' prefix like u'String'""" diff --git a/tests/functional/p/possible_forgotten_f_prefix.py b/tests/functional/p/possible_forgotten_f_prefix.py index 4f71779ee2..7df43109d6 100644 --- a/tests/functional/p/possible_forgotten_f_prefix.py +++ b/tests/functional/p/possible_forgotten_f_prefix.py @@ -15,8 +15,20 @@ # Check for use of .format() F = "This is a {parameter} used for formatting later" +F.format(parameter="string") G = F.format(parameter="string") +"{0}, {1}".format(1, 2) H = "{0}, {1}".format(1, 2) +I1, I2, I3 = (1, 2, "This is a {PARAM} which is later formatted") +I3.format(PARAM) + +J = {"key_one": "", "key_two": {"inner_key": ""}} +J["key_one"] = "This is a {parameter} used for formatting later" +J["key_one"].format(PARAM) +K = J["key_one"].format(PARAM) +J["key_two"]["inner_key"] = "This is a {parameter} used for formatting later" +J["key_two"]["inner_key"].format(PARAM) +L = J["key_two"]["inner_key"].format(PARAM) def func_one(): @@ -24,8 +36,8 @@ def func_one(): def func_two(): - I = "{0}, {1}" - return I.format(1, 2) + x = "{0}, {1}" + return x.format(1, 2) class Class(object): @@ -37,17 +49,17 @@ def __str__(self): # Check for use of variables within functions PARAM_LIST = [PARAM, PARAM_TWO] -J = f"This is an example with a list: {''.join(PARAM_LIST) + 'well...'}" -K = "This is an example with a list: {''.join(PARAM_LIST) + 'well...'}" # [possible-forgotten-f-prefix] +M = f"This is an example with a list: {''.join(PARAM_LIST) + 'well...'}" +N = "This is an example with a list: {''.join(PARAM_LIST) + 'well...'}" # [possible-forgotten-f-prefix] # Check for calculations without variables -L = f"This is a calculation: {1 + 1}" -M = "This is a calculation: {1 + 1}" # [possible-forgotten-f-prefix] +O = f"This is a calculation: {1 + 1}" +P = "This is a calculation: {1 + 1}" # [possible-forgotten-f-prefix] # Check invalid Python code -N = "This is {invalid /// python /// inside}" -O = "This is {not */ valid python.}" -P = "This is {def function(): return 42} valid python but not an expression" +Q = "This is {invalid /// python /// inside}" +R = "This is {not */ valid python.}" +S = "This is {def function(): return 42} valid python but not an expression" # Check strings without assignment PARAM_THREE = "string" diff --git a/tests/functional/p/possible_forgotten_f_prefix.txt b/tests/functional/p/possible_forgotten_f_prefix.txt index 9a23ecafb3..e1f91ed615 100644 --- a/tests/functional/p/possible_forgotten_f_prefix.txt +++ b/tests/functional/p/possible_forgotten_f_prefix.txt @@ -2,8 +2,8 @@ possible-forgotten-f-prefix:12:4::The '{PARAM}' syntax implies an f-string but t possible-forgotten-f-prefix:13:4::The '{PARAM_TWO}' syntax implies an f-string but the leading 'f' is missing:HIGH possible-forgotten-f-prefix:13:4::The '{PARAM}' syntax implies an f-string but the leading 'f' is missing:HIGH possible-forgotten-f-prefix:14:20::The '{PARAM}' syntax implies an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:41:4::The '{''.join(PARAM_LIST) + 'well...'}' syntax implies an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:45:4::The '{1 + 1}' syntax implies an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:61:0::The '{PARAM_THREE}' syntax implies an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:65:0::The '{1 + 1}' syntax implies an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:69:0::The '{'nice' + PARAM_THREE}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:53:4::The '{''.join(PARAM_LIST) + 'well...'}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:57:4::The '{1 + 1}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:68:0::The '{PARAM_THREE}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:72:0::The '{1 + 1}' syntax implies an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:76:0::The '{'nice' + PARAM_THREE}' syntax implies an f-string but the leading 'f' is missing:HIGH From bfd075c9f148e63e9326b67a15f4300a301269ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 18 Aug 2021 16:36:39 +0200 Subject: [PATCH 14/18] Fix hashing of list --- pylint/checkers/strings.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 9189036977..9fedefee7f 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -949,15 +949,13 @@ def get_all_format_calls(node: nodes.Const) -> set: if isinstance(call.expr, nodes.Name): calls.add(call.expr.name) elif isinstance(call.expr, nodes.Subscript): - slice_repr = [call.expr.value, call.expr.slice] + slice_repr = [call.expr.value, call.expr.slice.value] while not isinstance(slice_repr[0], nodes.Name): slice_repr = [ slice_repr[0].value, - slice_repr[0].slice, + slice_repr[0].slice.value, ] + slice_repr[1:] - calls.add( - [slice_repr[0].name] + [i.value for i in slice_repr[1:]] - ) + calls.add(tuple(slice_repr[0].name) + tuple(slice_repr[1:])) return calls def check_match_in_calls( @@ -969,16 +967,16 @@ def check_match_in_calls( if assignment.name in format_calls: return True elif isinstance(assignment, nodes.Subscript): - slice_repr = [assignment.value, assignment.slice] + slice_repr = [assignment.value, assignment.slice.value] while not isinstance(slice_repr[0], nodes.Name): slice_repr = [ slice_repr[0].value, - slice_repr[0].slice, + slice_repr[0].slice.value, ] + slice_repr[1:] - slice_repr = [slice_repr[0].name] + [ - i.value for i in slice_repr[1:] - ] - if slice_repr in format_calls: + if ( + tuple(slice_repr[0].name) + tuple(slice_repr[1:]) + in format_calls + ): return True return False From 8e01ff2de89daec9036f9594e6146498c1d35025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 18 Aug 2021 16:44:12 +0200 Subject: [PATCH 15/18] Apply suggestions from code review --- ChangeLog | 2 +- doc/whatsnew/2.10.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index e722eec47e..ded44dea9d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -85,7 +85,7 @@ Release date: TBA Closes #4681 -* Added ``possible-forgotten-f-prefix``: Emitted when variables are used in normal strings in between "{}" +* Added ``possible-forgotten-f-prefix``: Emitted when variables or valid expressions are used in normal strings in between "{}" Closes #2507 diff --git a/doc/whatsnew/2.10.rst b/doc/whatsnew/2.10.rst index 2fa02accc1..99f43ed23c 100644 --- a/doc/whatsnew/2.10.rst +++ b/doc/whatsnew/2.10.rst @@ -28,7 +28,7 @@ New checkers Closes #3692 -* Added ``possible-forgotten-f-prefix``: Emitted when variables are used in normal strings in between "{}" +* Added ``possible-forgotten-f-prefix``: Emitted when variables or valid expressions are used in normal strings in between "{}" Closes #2507 From 5d13efd12eff4eef7f563af6233c366e7ff4eab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 18 Aug 2021 20:02:24 +0200 Subject: [PATCH 16/18] Remove non-relevant changes --- ChangeLog | 2 +- doc/whatsnew/2.10.rst | 3 +-- pylint/checkers/classes.py | 4 ++-- pylint/checkers/python3.py | 2 +- pylint/checkers/refactoring/refactoring_checker.py | 2 +- pylint/checkers/stdlib.py | 2 +- pylint/checkers/typecheck.py | 4 ++-- pylint/lint/pylinter.py | 2 +- script/bump_changelog.py | 2 +- tests/functional/p/possible_forgotten_f_prefix.py | 10 +++++----- 10 files changed, 16 insertions(+), 17 deletions(-) diff --git a/ChangeLog b/ChangeLog index e722eec47e..ded44dea9d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -85,7 +85,7 @@ Release date: TBA Closes #4681 -* Added ``possible-forgotten-f-prefix``: Emitted when variables are used in normal strings in between "{}" +* Added ``possible-forgotten-f-prefix``: Emitted when variables or valid expressions are used in normal strings in between "{}" Closes #2507 diff --git a/doc/whatsnew/2.10.rst b/doc/whatsnew/2.10.rst index 2fa02accc1..2e3bb1c2c7 100644 --- a/doc/whatsnew/2.10.rst +++ b/doc/whatsnew/2.10.rst @@ -28,14 +28,13 @@ New checkers Closes #3692 -* Added ``possible-forgotten-f-prefix``: Emitted when variables are used in normal strings in between "{}" +* Added ``possible-forgotten-f-prefix``: Emitted when variables or valid expressions are used in normal strings in between "{}" Closes #2507 * Added ``use-sequence-for-iteration``: Emitted when iterating over an in-place defined ``set``. - Extensions ========== diff --git a/pylint/checkers/classes.py b/pylint/checkers/classes.py index e96c630db8..3ecc738e5b 100644 --- a/pylint/checkers/classes.py +++ b/pylint/checkers/classes.py @@ -1000,10 +1000,10 @@ def _check_unused_private_attributes(self, node: nodes.ClassDef) -> None: if attribute.attrname != assign_attr.attrname: continue - if assign_attr.expr.name == "cls" and attribute.expr.name in ( + if assign_attr.expr.name == "cls" and attribute.expr.name in [ "cls", "self", - ): + ]: # If assigned to cls.attrib, can be accessed by cls/self break diff --git a/pylint/checkers/python3.py b/pylint/checkers/python3.py index 52d4e6414f..d5cf029262 100644 --- a/pylint/checkers/python3.py +++ b/pylint/checkers/python3.py @@ -157,7 +157,7 @@ def _in_iterating_context(node): elif ( isinstance(parent, nodes.Compare) and len(parent.ops) == 1 - and parent.ops[0][0] in ("in", "not in") + and parent.ops[0][0] in ["in", "not in"] ): return True # Also if it's an `yield from`, that's fair diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py index 793b27cc88..77d157ae66 100644 --- a/pylint/checkers/refactoring/refactoring_checker.py +++ b/pylint/checkers/refactoring/refactoring_checker.py @@ -965,7 +965,7 @@ def _check_consider_using_generator(self, node): # remove square brackets '[]' inside_comp = node.args[0].as_string()[1:-1] call_name = node.func.name - if call_name in ("any", "all"): + if call_name in ["any", "all"]: self.add_message( "use-a-generator", node=node, diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index 3a63aefb5a..95bec2e5d9 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -569,7 +569,7 @@ def _check_redundant_assert(self, node, infer): isinstance(infer, astroid.BoundMethod) and node.args and isinstance(node.args[0], nodes.Const) - and infer.name in ("assertTrue", "assertFalse") + and infer.name in ["assertTrue", "assertFalse"] ): self.add_message( "redundant-unittest-assert", diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 96811d970c..502a0390c2 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -519,7 +519,7 @@ def _emit_no_member(node, owner, owner_name, ignored_mixins=True, ignored_none=T and isinstance(owner.parent, nodes.ClassDef) and owner.parent.name == "EnumMeta" and owner_name == "__members__" - and node.attrname in ("items", "values", "keys") + and node.attrname in ["items", "values", "keys"] ): # Avoid false positive on Enum.__members__.{items(), values, keys} # See https://github.com/PyCQA/pylint/issues/4123 @@ -1778,7 +1778,7 @@ def visit_compare(self, node): return op, right = node.ops[0] - if op in ("in", "not in"): + if op in ["in", "not in"]: self._check_membership_test(right) @check_messages( diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py index 286b7e1b34..e4ed47683b 100644 --- a/pylint/lint/pylinter.py +++ b/pylint/lint/pylinter.py @@ -728,7 +728,7 @@ def any_fail_on_issues(self): def disable_noerror_messages(self): for msgcat, msgids in self.msgs_store._msgs_by_category.items(): # enable only messages with 'error' severity and above ('fatal') - if msgcat in ("E", "F"): + if msgcat in ["E", "F"]: for msgid in msgids: self.enable(msgid) else: diff --git a/script/bump_changelog.py b/script/bump_changelog.py index 71c9c88333..41c8c3c8a9 100644 --- a/script/bump_changelog.py +++ b/script/bump_changelog.py @@ -136,7 +136,7 @@ def do_checks(content, next_version, version, version_type): wn_next_version = get_whats_new(next_version) wn_this_version = get_whats_new(version) # There is only one field where the release date is TBA - if version_type in (VersionType.MAJOR, VersionType.MINOR): + if version_type in [VersionType.MAJOR, VersionType.MINOR]: assert ( content.count(RELEASE_DATE_TEXT) <= 1 ), f"There should be only one release date 'TBA' ({version}) {err}" diff --git a/tests/functional/p/possible_forgotten_f_prefix.py b/tests/functional/p/possible_forgotten_f_prefix.py index 7df43109d6..5738c86d5e 100644 --- a/tests/functional/p/possible_forgotten_f_prefix.py +++ b/tests/functional/p/possible_forgotten_f_prefix.py @@ -20,15 +20,15 @@ "{0}, {1}".format(1, 2) H = "{0}, {1}".format(1, 2) I1, I2, I3 = (1, 2, "This is a {PARAM} which is later formatted") -I3.format(PARAM) +I3.format(PARAM=PARAM) J = {"key_one": "", "key_two": {"inner_key": ""}} J["key_one"] = "This is a {parameter} used for formatting later" -J["key_one"].format(PARAM) -K = J["key_one"].format(PARAM) +J["key_one"].format(parameter=PARAM) +K = J["key_one"].format(parameter=PARAM) J["key_two"]["inner_key"] = "This is a {parameter} used for formatting later" -J["key_two"]["inner_key"].format(PARAM) -L = J["key_two"]["inner_key"].format(PARAM) +J["key_two"]["inner_key"].format(parameter=PARAM) +L = J["key_two"]["inner_key"].format(parameter=PARAM) def func_one(): From f64e4fda384c48906aa57d48a5af2b48e3c7e3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 18 Aug 2021 20:24:04 +0200 Subject: [PATCH 17/18] Additional tests.. --- pylint/reporters/text.py | 3 +++ .../p/possible_forgotten_f_prefix.py | 22 +++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/pylint/reporters/text.py b/pylint/reporters/text.py index 865b3c84df..6fb106ece4 100644 --- a/pylint/reporters/text.py +++ b/pylint/reporters/text.py @@ -129,6 +129,7 @@ class TextReporter(BaseReporter): __implements__ = IReporter name = "text" extension = "txt" + #pylint: disable-next=possible-forgotten-f-prefix line_format = "{path}:{line}:{column}: {msg_id}: {msg} ({symbol})" def __init__(self, output=None): @@ -167,6 +168,7 @@ class ParseableTextReporter(TextReporter): """ name = "parseable" + #pylint: disable-next=possible-forgotten-f-prefix line_format = "{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" def __init__(self, output=None): @@ -182,6 +184,7 @@ class VSTextReporter(ParseableTextReporter): """Visual studio text reporter""" name = "msvs" + #pylint: disable-next=possible-forgotten-f-prefix line_format = "{path}({line}): [{msg_id}({symbol}){obj}] {msg}" diff --git a/tests/functional/p/possible_forgotten_f_prefix.py b/tests/functional/p/possible_forgotten_f_prefix.py index 5738c86d5e..bd4b373722 100644 --- a/tests/functional/p/possible_forgotten_f_prefix.py +++ b/tests/functional/p/possible_forgotten_f_prefix.py @@ -30,6 +30,7 @@ J["key_two"]["inner_key"].format(parameter=PARAM) L = J["key_two"]["inner_key"].format(parameter=PARAM) +M = "This is a {parameter} used for formatting later" def func_one(): return "{0}, {1}".format(1, 2) @@ -40,6 +41,10 @@ def func_two(): return x.format(1, 2) +def func_three(): + x = M.format(parameter=PARAM) + return x + class Class(object): attr = 0 @@ -49,17 +54,17 @@ def __str__(self): # Check for use of variables within functions PARAM_LIST = [PARAM, PARAM_TWO] -M = f"This is an example with a list: {''.join(PARAM_LIST) + 'well...'}" -N = "This is an example with a list: {''.join(PARAM_LIST) + 'well...'}" # [possible-forgotten-f-prefix] +N = f"This is an example with a list: {''.join(PARAM_LIST) + 'well...'}" +O = "This is an example with a list: {''.join(PARAM_LIST) + 'well...'}" # [possible-forgotten-f-prefix] # Check for calculations without variables -O = f"This is a calculation: {1 + 1}" -P = "This is a calculation: {1 + 1}" # [possible-forgotten-f-prefix] +P = f"This is a calculation: {1 + 1}" +Q = "This is a calculation: {1 + 1}" # [possible-forgotten-f-prefix] # Check invalid Python code -Q = "This is {invalid /// python /// inside}" -R = "This is {not */ valid python.}" -S = "This is {def function(): return 42} valid python but not an expression" +R = "This is {invalid /// python /// inside}" +S = "This is {not */ valid python.}" +T = "This is {def function(): return 42} valid python but not an expression" # Check strings without assignment PARAM_THREE = "string" @@ -74,3 +79,6 @@ def __str__(self): f"This is a {'nice' + PARAM_THREE}" "This is a nice string" "This is a {'nice' + PARAM_THREE}" # [possible-forgotten-f-prefix] + +# Check raw strings +U = r"a{1}" From 4851d9e449e63b7e60b790872aa4e25c432e5e6a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 18 Aug 2021 18:24:56 +0000 Subject: [PATCH 18/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pylint/reporters/text.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pylint/reporters/text.py b/pylint/reporters/text.py index 6fb106ece4..93ce9bc291 100644 --- a/pylint/reporters/text.py +++ b/pylint/reporters/text.py @@ -129,7 +129,7 @@ class TextReporter(BaseReporter): __implements__ = IReporter name = "text" extension = "txt" - #pylint: disable-next=possible-forgotten-f-prefix + # pylint: disable-next=possible-forgotten-f-prefix line_format = "{path}:{line}:{column}: {msg_id}: {msg} ({symbol})" def __init__(self, output=None): @@ -168,7 +168,7 @@ class ParseableTextReporter(TextReporter): """ name = "parseable" - #pylint: disable-next=possible-forgotten-f-prefix + # pylint: disable-next=possible-forgotten-f-prefix line_format = "{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" def __init__(self, output=None): @@ -184,7 +184,7 @@ class VSTextReporter(ParseableTextReporter): """Visual studio text reporter""" name = "msvs" - #pylint: disable-next=possible-forgotten-f-prefix + # pylint: disable-next=possible-forgotten-f-prefix line_format = "{path}({line}): [{msg_id}({symbol}){obj}] {msg}"