Skip to content

Commit 66ffcbc

Browse files
Add Consider-using-f-string checker (#4796)
* Add ``consider-using-f-string`` checker This adds a checker for normal strings which are formatted with ``.format()`` or '%'. The message is a convention to nudge users towards using f-strings. This closes #3592 * Update pylint code to use f-strings After adding `consider-using-f-strings` the codebase showed numerous cases of formatting which could be f-strings. This commit changes most of these to become f-strings, or adds ignores. * Apply suggestions from code review Co-authored-by: Pierre Sassoulas <[email protected]>
1 parent 1a19421 commit 66ffcbc

File tree

84 files changed

+525
-384
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+525
-384
lines changed

ChangeLog

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ Release date: TBA
1010
..
1111
Put new features here and also in 'doc/whatsnew/2.11.rst'
1212

13+
* Added ``consider-using-f-string``: Emitted when .format() or '%' is being used to format a string.
14+
15+
Closes #3592
16+
1317

1418
What's New in Pylint 2.10.3?
1519
============================
@@ -193,7 +197,6 @@ Release date: 2021-08-20
193197

194198
* Allow ``true`` and ``false`` values in ``pylintrc`` for better compatibility with ``toml`` config.
195199

196-
197200
* Class methods' signatures are ignored the same way as functions' with similarities "ignore-signatures" option enabled
198201

199202
Closes #4653

doc/exts/pylint_extensions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def builder_inited(app):
3737
if name[0] == "_" or name in DEPRECATED_MODULES:
3838
continue
3939
if ext == ".py":
40-
modules.append("pylint.extensions.%s" % name)
40+
modules.append(f"pylint.extensions.{name}")
4141
elif ext == ".rst":
4242
doc_files["pylint.extensions." + name] = os.path.join(ext_path, filename)
4343
modules.sort()

doc/whatsnew/2.11.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ Summary -- Release highlights
1212
New checkers
1313
============
1414

15+
* Added ``consider-using-f-string``: Emitted when .format() or '%' is being used to format a string.
16+
17+
Closes #3592
18+
1519

1620
Extensions
1721
==========

pylint/checkers/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ def table_lines_from_stats(stats, old_stats, columns):
6565
diff_str = diff_string(old, new)
6666
else:
6767
old, diff_str = "NC", "NC"
68-
new = "%.3f" % new if isinstance(new, float) else str(new)
69-
old = "%.3f" % old if isinstance(old, float) else str(old)
68+
new = f"{new:.3f}" if isinstance(new, float) else str(new)
69+
old = f"{old:.3f}" if isinstance(old, float) else str(old)
7070
lines += (m_type.replace("_", " "), new, old, diff_str)
7171
return lines
7272

pylint/checkers/base.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ class AnyStyle(NamingStyle):
193193
["set()", "{}", "[]"],
194194
),
195195
**{
196-
x: "%s()" % x
196+
x: f"{x}()"
197197
for x in (
198198
"collections.deque",
199199
"collections.ChainMap",
@@ -404,12 +404,12 @@ def report_by_type_stats(sect, stats, old_stats):
404404
try:
405405
documented = total - stats["undocumented_" + node_type]
406406
percent = (documented * 100.0) / total
407-
nice_stats[node_type]["percent_documented"] = "%.2f" % percent
407+
nice_stats[node_type]["percent_documented"] = f"{percent:.2f}"
408408
except KeyError:
409409
nice_stats[node_type]["percent_documented"] = "NC"
410410
try:
411411
percent = (stats["badname_" + node_type] * 100.0) / total
412-
nice_stats[node_type]["percent_badname"] = "%.2f" % percent
412+
nice_stats[node_type]["percent_badname"] = f"{percent:.2f}"
413413
except KeyError:
414414
nice_stats[node_type]["percent_badname"] = "NC"
415415
lines = ("type", "number", "old number", "difference", "%documented", "%badname")
@@ -1703,8 +1703,7 @@ def _create_naming_options():
17031703
"type": "choice",
17041704
"choices": list(NAMING_STYLES.keys()),
17051705
"metavar": "<style>",
1706-
"help": "Naming style matching correct %s names."
1707-
% (human_readable_name,),
1706+
"help": f"Naming style matching correct {human_readable_name} names.",
17081707
},
17091708
)
17101709
)
@@ -1715,8 +1714,7 @@ def _create_naming_options():
17151714
"default": None,
17161715
"type": "regexp",
17171716
"metavar": "<regexp>",
1718-
"help": "Regular expression matching correct %s names. Overrides %s-naming-style."
1719-
% (human_readable_name, name_type),
1717+
"help": f"Regular expression matching correct {human_readable_name} names. Overrides {name_type}-naming-style.",
17201718
},
17211719
)
17221720
)
@@ -1888,9 +1886,9 @@ def _create_naming_rules(self):
18881886
regexps[name_type] = custom_regex
18891887

18901888
if custom_regex is not None:
1891-
hints[name_type] = "%r pattern" % custom_regex.pattern
1889+
hints[name_type] = f"{custom_regex.pattern!r} pattern"
18921890
else:
1893-
hints[name_type] = "%s naming style" % naming_style_name
1891+
hints[name_type] = f"{naming_style_name} naming style"
18941892

18951893
return regexps, hints
18961894

@@ -2023,7 +2021,7 @@ def _raise_name_warning(
20232021
type_label = HUMAN_READABLE_TYPES[node_type]
20242022
hint = self._name_hints[node_type]
20252023
if self.config.include_naming_hint:
2026-
hint += " (%r pattern)" % self._name_regexps[node_type].pattern
2024+
hint += f" ({self._name_regexps[node_type].pattern!r} pattern)"
20272025
args = (
20282026
(type_label.capitalize(), name, hint)
20292027
if warning == "invalid-name"

pylint/checkers/base_checker.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,35 +70,37 @@ def __str__(self):
7070

7171
def get_full_documentation(self, msgs, options, reports, doc=None, module=None):
7272
result = ""
73-
checker_title = "%s checker" % (self.name.replace("_", " ").title())
73+
checker_title = f"{self.name.replace('_', ' ').title()} checker"
7474
if module:
7575
# Provide anchor to link against
76-
result += ".. _%s:\n\n" % module
77-
result += "%s\n" % get_rst_title(checker_title, "~")
76+
result += f".. _{module}:\n\n"
77+
result += f"{get_rst_title(checker_title, '~')}\n"
7878
if module:
79-
result += "This checker is provided by ``%s``.\n" % module
80-
result += "Verbatim name of the checker is ``%s``.\n\n" % self.name
79+
result += f"This checker is provided by ``{module}``.\n"
80+
result += f"Verbatim name of the checker is ``{self.name}``.\n\n"
8181
if doc:
8282
# Provide anchor to link against
8383
result += get_rst_title(f"{checker_title} Documentation", "^")
84-
result += "%s\n\n" % cleandoc(doc)
84+
result += f"{cleandoc(doc)}\n\n"
8585
# options might be an empty generator and not be False when casted to boolean
8686
options = list(options)
8787
if options:
8888
result += get_rst_title(f"{checker_title} Options", "^")
89-
result += "%s\n" % get_rst_section(None, options)
89+
result += f"{get_rst_section(None, options)}\n"
9090
if msgs:
9191
result += get_rst_title(f"{checker_title} Messages", "^")
9292
for msgid, msg in sorted(
9393
msgs.items(), key=lambda kv: (_MSG_ORDER.index(kv[0][0]), kv[1])
9494
):
9595
msg = self.create_message_definition_from_tuple(msgid, msg)
96-
result += "%s\n" % msg.format_help(checkerref=False)
96+
result += f"{msg.format_help(checkerref=False)}\n"
9797
result += "\n"
9898
if reports:
9999
result += get_rst_title(f"{checker_title} Reports", "^")
100100
for report in reports:
101-
result += ":%s: %s\n" % report[:2]
101+
result += (
102+
":%s: %s\n" % report[:2] # pylint: disable=consider-using-f-string
103+
)
102104
result += "\n"
103105
result += "\n"
104106
return result

pylint/checkers/classes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2039,7 +2039,7 @@ class SpecialMethodsChecker(BaseChecker):
20392039
"__iter__ returns non-iterator",
20402040
"non-iterator-returned",
20412041
"Used when an __iter__ method returns something which is not an "
2042-
"iterable (i.e. has no `%s` method)" % NEXT_METHOD,
2042+
f"iterable (i.e. has no `{NEXT_METHOD}` method)",
20432043
{
20442044
"old_names": [
20452045
("W0234", "old-non-iterator-returned-1"),
@@ -2189,6 +2189,7 @@ def _check_unexpected_method_signature(self, node):
21892189
# tuple, although the user should implement the method
21902190
# to take all of them in consideration.
21912191
emit = mandatory not in expected_params
2192+
# pylint: disable-next=consider-using-f-string
21922193
expected_params = "between %d or %d" % expected_params
21932194
else:
21942195
# If the number of mandatory parameters doesn't

pylint/checkers/exceptions.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ class ExceptionsChecker(checkers.BaseChecker):
271271
"default": OVERGENERAL_EXCEPTIONS,
272272
"type": "csv",
273273
"metavar": "<comma-separated class names>",
274-
"help": "Exceptions that will emit a warning "
274+
"help": "Exceptions that will emit a warning " # pylint: disable=consider-using-f-string
275275
'when being caught. Defaults to "%s".'
276276
% (", ".join(OVERGENERAL_EXCEPTIONS),),
277277
},
@@ -488,20 +488,14 @@ def gather_exceptions_from_handler(
488488
def visit_binop(self, node):
489489
if isinstance(node.parent, nodes.ExceptHandler):
490490
# except (V | A)
491-
suggestion = "Did you mean '({}, {})' instead?".format(
492-
node.left.as_string(),
493-
node.right.as_string(),
494-
)
491+
suggestion = f"Did you mean '({node.left.as_string()}, {node.right.as_string()})' instead?"
495492
self.add_message("wrong-exception-operation", node=node, args=(suggestion,))
496493

497494
@utils.check_messages("wrong-exception-operation")
498495
def visit_compare(self, node):
499496
if isinstance(node.parent, nodes.ExceptHandler):
500497
# except (V < A)
501-
suggestion = "Did you mean '({}, {})' instead?".format(
502-
node.left.as_string(),
503-
", ".join(operand.as_string() for _, operand in node.ops),
504-
)
498+
suggestion = f"Did you mean '({node.left.as_string()}, {', '.join(operand.as_string() for _, operand in node.ops)})' instead?"
505499
self.add_message("wrong-exception-operation", node=node, args=(suggestion,))
506500

507501
@utils.check_messages(
@@ -561,10 +555,7 @@ def visit_tryexcept(self, node):
561555

562556
for previous_exc in exceptions_classes:
563557
if previous_exc in exc_ancestors:
564-
msg = "{} is an ancestor class of {}".format(
565-
previous_exc.name,
566-
exc.name,
567-
)
558+
msg = f"{previous_exc.name} is an ancestor class of {exc.name}"
568559
self.add_message(
569560
"bad-except-order", node=handler.type, args=msg
570561
)

pylint/checkers/imports.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -151,16 +151,16 @@ def _repr_tree_defs(data, indent_str=None):
151151
lines = []
152152
nodes_items = data.items()
153153
for i, (mod, (sub, files)) in enumerate(sorted(nodes_items, key=lambda x: x[0])):
154-
files = "" if not files else "(%s)" % ",".join(sorted(files))
154+
files = "" if not files else f"({','.join(sorted(files))})"
155155
if indent_str is None:
156156
lines.append(f"{mod} {files}")
157157
sub_indent_str = " "
158158
else:
159159
lines.append(fr"{indent_str}\-{mod} {files}")
160160
if i == len(nodes_items) - 1:
161-
sub_indent_str = "%s " % indent_str
161+
sub_indent_str = f"{indent_str} "
162162
else:
163-
sub_indent_str = "%s| " % indent_str
163+
sub_indent_str = f"{indent_str}| "
164164
if sub:
165165
lines.append(_repr_tree_defs(sub, sub_indent_str))
166166
return "\n".join(lines)
@@ -739,8 +739,8 @@ def _check_imports_order(self, _module_node):
739739
"wrong-import-order",
740740
node=node,
741741
args=(
742-
'standard import "%s"' % node.as_string(),
743-
'"%s"' % wrong_import[0][0].as_string(),
742+
f'standard import "{node.as_string()}"',
743+
f'"{wrong_import[0][0].as_string()}"',
744744
),
745745
)
746746
elif import_category == "THIRDPARTY":
@@ -754,8 +754,8 @@ def _check_imports_order(self, _module_node):
754754
"wrong-import-order",
755755
node=node,
756756
args=(
757-
'third party import "%s"' % node.as_string(),
758-
'"%s"' % wrong_import[0][0].as_string(),
757+
f'third party import "{node.as_string()}"',
758+
f'"{wrong_import[0][0].as_string()}"',
759759
),
760760
)
761761
elif import_category == "FIRSTPARTY":
@@ -769,8 +769,8 @@ def _check_imports_order(self, _module_node):
769769
"wrong-import-order",
770770
node=node,
771771
args=(
772-
'first party import "%s"' % node.as_string(),
773-
'"%s"' % wrong_import[0][0].as_string(),
772+
f'first party import "{node.as_string()}"',
773+
f'"{wrong_import[0][0].as_string()}"',
774774
),
775775
)
776776
elif import_category == "LOCALFOLDER":
@@ -787,9 +787,7 @@ def _get_imported_module(self, importnode, modname):
787787
return None
788788
self.add_message("relative-beyond-top-level", node=importnode)
789789
except astroid.AstroidSyntaxError as exc:
790-
message = "Cannot import {!r} due to syntax error {!r}".format(
791-
modname, str(exc.error) # pylint: disable=no-member; false positive
792-
)
790+
message = f"Cannot import {modname!r} due to syntax error {str(exc.error)!r}" # pylint: disable=no-member; false positive
793791
self.add_message("syntax-error", line=importnode.lineno, args=message)
794792

795793
except astroid.AstroidBuildingException:

pylint/checkers/misc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def open(self):
110110
if self.config.notes_rgx:
111111
regex_string = fr"#\s*({notes}|{self.config.notes_rgx})\b"
112112
else:
113-
regex_string = r"#\s*(%s)\b" % (notes)
113+
regex_string = fr"#\s*({notes})\b"
114114

115115
self._fixme_pattern = re.compile(regex_string, re.I)
116116

pylint/checkers/raw_metrics.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def report_raw_stats(sect, stats, old_stats):
2929
total_lines = stats["total_lines"]
3030
if not total_lines:
3131
raise EmptyReportError()
32-
sect.description = "%s lines have been analyzed" % total_lines
32+
sect.description = f"{total_lines} lines have been analyzed"
3333
lines = ("type", "number", "%", "previous", "difference")
3434
for node_type in ("code", "docstring", "comment", "empty"):
3535
key = node_type + "_lines"
@@ -40,7 +40,7 @@ def report_raw_stats(sect, stats, old_stats):
4040
diff_str = diff_string(old, total)
4141
else:
4242
old, diff_str = "NC", "NC"
43-
lines += (node_type, str(total), "%.2f" % percent, str(old), diff_str)
43+
lines += (node_type, str(total), f"{percent:.2f}", str(old), diff_str)
4444
sect.append(Table(children=lines, cols=5, rheaders=1))
4545

4646

pylint/checkers/refactoring/not_checker.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,8 @@ def visit_unaryop(self, node):
7575
and _type.qname() in self.skipped_classnames
7676
):
7777
return
78-
suggestion = "{} {} {}".format(
79-
left.as_string(),
80-
self.reverse_op[operator],
81-
right.as_string(),
78+
suggestion = (
79+
f"{left.as_string()} {self.reverse_op[operator]} {right.as_string()}"
8280
)
8381
self.add_message(
8482
"unneeded-not", node=node, args=(node.as_string(), suggestion)

pylint/checkers/refactoring/refactoring_checker.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,11 +1406,7 @@ def visit_return(self, node: nodes.Return) -> None:
14061406
suggestion = false_value.as_string()
14071407
else:
14081408
message = "consider-using-ternary"
1409-
suggestion = "{truth} if {cond} else {false}".format(
1410-
truth=truth_value.as_string(),
1411-
cond=cond.as_string(),
1412-
false=false_value.as_string(),
1413-
)
1409+
suggestion = f"{truth_value.as_string()} if {cond.as_string()} else {false_value.as_string()}"
14141410
self.add_message(message, node=node, args=(suggestion,))
14151411

14161412
def _append_context_managers_to_stack(self, node: nodes.Assign) -> None:

pylint/checkers/similar.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -457,11 +457,7 @@ def _get_similarity_report(
457457
report += f" {line.rstrip()}\n" if line.rstrip() else "\n"
458458
duplicated_line_number += number * (len(couples_l) - 1)
459459
total_line_number: int = sum(len(lineset) for lineset in self.linesets)
460-
report += "TOTAL lines={} duplicates={} percent={:.2f}\n".format(
461-
total_line_number,
462-
duplicated_line_number,
463-
duplicated_line_number * 100.0 / total_line_number,
464-
)
460+
report += f"TOTAL lines={total_line_number} duplicates={duplicated_line_number} percent={duplicated_line_number * 100.0 / total_line_number:.2f}\n"
465461
return report
466462

467463
def _find_common(
@@ -676,7 +672,7 @@ def __init__(
676672
)
677673

678674
def __str__(self):
679-
return "<Lineset for %s>" % self.name
675+
return f"<Lineset for {self.name}>"
680676

681677
def __len__(self):
682678
return len(self._real_lines)

pylint/checkers/spelling.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ class SpellingChecker(BaseTokenChecker):
233233
"metavar": "<dict name>",
234234
"choices": dict_choices,
235235
"help": "Spelling dictionary name. "
236-
"Available dictionaries: %s.%s" % (dicts, instr),
236+
f"Available dictionaries: {dicts}.{instr}",
237237
},
238238
),
239239
(
@@ -400,14 +400,14 @@ def _check_spelling(self, msgid, line, line_num):
400400
# Store word to private dict or raise a message.
401401
if self.config.spelling_store_unknown_words:
402402
if lower_cased_word not in self.unknown_words:
403-
self.private_dict_file.write("%s\n" % lower_cased_word)
403+
self.private_dict_file.write(f"{lower_cased_word}\n")
404404
self.unknown_words.add(lower_cased_word)
405405
else:
406406
# Present up to N suggestions.
407407
suggestions = self.spelling_dict.suggest(word)
408408
del suggestions[self.config.max_spelling_suggestions :]
409409
line_segment = line[word_start_at:]
410-
match = re.search(r"(\W|^)(%s)(\W|$)" % word, line_segment)
410+
match = re.search(fr"(\W|^)({word})(\W|$)", line_segment)
411411
if match:
412412
# Start position of second group in regex.
413413
col = match.regs[2][0]

0 commit comments

Comments
 (0)