From 5804a09edecd63eee39941a12cc9b43a4a664675 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 1 Aug 2024 11:08:06 +0100 Subject: [PATCH] Resolve #1948 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 higlights something odd around our counting: ❯ 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 --- src/flake8/processor.py | 14 +++++++++++--- tests/integration/test_plugins.py | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/flake8/processor.py b/src/flake8/processor.py index 21a25e0f..29c5929c 100644 --- a/src/flake8/processor.py +++ b/src/flake8/processor.py @@ -203,9 +203,17 @@ def build_logical_line_tokens(self) -> _Logical: # noqa: C901 if token_type == tokenize.STRING: text = mutate_string(text) elif token_type == FSTRING_MIDDLE: # pragma: >=3.12 cover - text = "x" * len(text) + # A curly brace in an FSTRING_MIDDLE token must be an escaped + # curly brace. Both 'text' and 'end' will account for the + # escaped version of the token (i.e. a single brace) rather + # than the raw double brace version, so we must counteract this + fstring_offset = 0 + if "{" in text or "}" in text: + fstring_offset = text.count("{") + text.count("}") + text = "x" * (len(text) + fstring_offset) + end = (end[0], end[1] + fstring_offset) if previous_row: - (start_row, start_column) = start + start_row, start_column = start if previous_row != start_row: row_index = previous_row - 1 column_index = previous_column - 1 @@ -219,7 +227,7 @@ def build_logical_line_tokens(self) -> _Logical: # noqa: C901 logical.append(text) length += len(text) mapping.append((length, end)) - (previous_row, previous_column) = end + previous_row, previous_column = end return comments, logical, mapping def build_ast(self) -> ast.AST: diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index d4c22b0b..360f7559 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -248,7 +248,7 @@ def test_logical_line_plugin(tmpdir, capsys): cfg.write(cfg_s) src = """\ -f'hello world' +f'{{"{hello}": "{world}"}}' """ t_py = tmpdir.join("t.py") t_py.write_binary(src.encode()) @@ -257,7 +257,7 @@ def test_logical_line_plugin(tmpdir, capsys): assert main(("t.py", "--config", str(cfg))) == 1 expected = """\ -t.py:1:1: T001 "f'xxxxxxxxxxx'" +t.py:1:1: T001 "f'xxx{hello}xxxx{world}xxx'" """ out, err = capsys.readouterr() assert out == expected