Skip to content

Commit bdcd5c2

Browse files
stephenfinasottile
authored andcommitted
Handle escaped braces in f-strings
To use a curly brace in an f-string, you must escape it. For example: >>> k = 1 >>> f'{{{k}' '{1' Saving this as a script and running the 'tokenize' module highlights something odd around the counting of tokens: ❯ python -m tokenize wow.py 0,0-0,0: ENCODING 'utf-8' 1,0-1,1: NAME 'k' 1,2-1,3: OP '=' 1,4-1,5: NUMBER '1' 1,5-1,6: NEWLINE '\n' 2,0-2,2: FSTRING_START "f'" 2,2-2,3: FSTRING_MIDDLE '{' # <-- here... 2,4-2,5: OP '{' # <-- and here 2,5-2,6: NAME 'k' 2,6-2,7: OP '}' 2,7-2,8: FSTRING_END "'" 2,8-2,9: NEWLINE '\n' 3,0-3,0: ENDMARKER '' The FSTRING_MIDDLE character we have is the escaped/post-parse single curly brace rather than the raw double curly brace, however, while our end index of this token accounts for the parsed form, the start index of the next token does not (put another way, it jumps from 3 -> 4). This triggers some existing, unrelated code that we need to bypass. Do just that. Signed-off-by: Stephen Finucane <[email protected]> Closes: #1948
1 parent 2a811cc commit bdcd5c2

File tree

2 files changed

+42
-1
lines changed

2 files changed

+42
-1
lines changed

src/flake8/processor.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,13 @@ def build_logical_line_tokens(self) -> _Logical: # noqa: C901
203203
if token_type == tokenize.STRING:
204204
text = mutate_string(text)
205205
elif token_type == FSTRING_MIDDLE: # pragma: >=3.12 cover
206-
text = "x" * len(text)
206+
# A curly brace in an FSTRING_MIDDLE token must be an escaped
207+
# curly brace. Both 'text' and 'end' will account for the
208+
# escaped version of the token (i.e. a single brace) rather
209+
# than the raw double brace version, so we must counteract this
210+
brace_offset = text.count("{") + text.count("}")
211+
text = "x" * (len(text) + brace_offset)
212+
end = (end[0], end[1] + brace_offset)
207213
if previous_row:
208214
(start_row, start_column) = start
209215
if previous_row != start_row:

tests/integration/test_plugins.py

+35
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Integration tests for plugin loading."""
22
from __future__ import annotations
33

4+
import sys
5+
46
import pytest
57

68
from flake8.main.cli import main
@@ -261,3 +263,36 @@ def test_logical_line_plugin(tmpdir, capsys):
261263
"""
262264
out, err = capsys.readouterr()
263265
assert out == expected
266+
267+
268+
def test_escaping_of_fstrings_in_string_redacter(tmpdir, capsys):
269+
cfg_s = f"""\
270+
[flake8]
271+
extend-ignore = F
272+
[flake8:local-plugins]
273+
extension =
274+
T = {yields_logical_line.__module__}:{yields_logical_line.__name__}
275+
"""
276+
277+
cfg = tmpdir.join("tox.ini")
278+
cfg.write(cfg_s)
279+
280+
src = """\
281+
f'{{"{hello}": "{world}"}}'
282+
"""
283+
t_py = tmpdir.join("t.py")
284+
t_py.write_binary(src.encode())
285+
286+
with tmpdir.as_cwd():
287+
assert main(("t.py", "--config", str(cfg))) == 1
288+
289+
if sys.version_info >= (3, 12): # pragma: >=3.12 cover
290+
expected = """\
291+
t.py:1:1: T001 "f'xxx{hello}xxxx{world}xxx'"
292+
"""
293+
else: # pragma: <3.12 cover
294+
expected = """\
295+
t.py:1:1: T001 "f'xxxxxxxxxxxxxxxxxxxxxxxx'"
296+
"""
297+
out, err = capsys.readouterr()
298+
assert out == expected

0 commit comments

Comments
 (0)