Skip to content

Commit 86758d1

Browse files
authored
gh-160268: Fix tokenize on a mismatched closing bracket in an f-string (#160272)
Since gh-105042, `tokenize` tolerates mismatched brackets. That also let a mismatched closing bracket pop the brace that opened an f-string replacement field, which the f-string state relies on. In `f'{]a'`, this triggered an assertion failure in _PyLexer_scan_string. Check the replacement field brace in both modes.
1 parent 1bc0ed1 commit 86758d1

3 files changed

Lines changed: 21 additions & 14 deletions

File tree

‎Lib/test/test_tokenize.py‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2675,15 +2675,17 @@ def test_tolerant_incompatible_prefix_position_after_non_ascii(self):
26752675
)
26762676

26772677
def test_tolerant_fstring_closer_at_expression_entry_depth(self):
2678-
source = "f'1:{]]{}}r}''"
2679-
for extra_tokens, position in ((False, (1, 6)), (True, (1, 7))):
2680-
with self.subTest(extra_tokens=extra_tokens):
2681-
with self.assertRaises(tokenize.TokenError) as caught:
2682-
self._get_tokens(source, extra_tokens=extra_tokens)
2683-
self.assertEqual(
2684-
caught.exception.args,
2685-
("f-string: unmatched ']'", position),
2686-
)
2678+
cases = [
2679+
("f'1:{]]{}}r}''", ("f-string: unmatched ']'", (1, 6))),
2680+
("f'{]a'", ("f-string: unmatched ']'", (1, 4))),
2681+
("t'{\nx)'", ("t-string: unmatched ')'", (2, 2))),
2682+
]
2683+
for source, error in cases:
2684+
for extra_tokens in (False, True):
2685+
with self.subTest(source=source, extra_tokens=extra_tokens):
2686+
with self.assertRaises(tokenize.TokenError) as caught:
2687+
self._get_tokens(source, extra_tokens=extra_tokens)
2688+
self.assertEqual(caught.exception.args, error)
26872689

26882690
def test_int(self):
26892691

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a crash in :mod:`tokenize` when a mismatched closing bracket follows the opening brace of an f-string or t-string replacement field, as in ``f'{]a'``. It now raises the same "unmatched" error as the parser.

‎Parser/lexer/lexer.c‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -447,11 +447,12 @@ _PyLexer_get_normal(struct tok_state *tok, ftstring_state *current, struct token
447447
if (tok->level > 0) {
448448
tok->level--;
449449
int opening = tok->parenstack[tok->level];
450-
if (!tok->tok_extra_tokens && !((opening == '(' && c == ')') ||
451-
(opening == '[' && c == ']') ||
452-
(opening == '{' && c == '}'))) {
450+
if (!((opening == '(' && c == ')') ||
451+
(opening == '[' && c == ']') ||
452+
(opening == '{' && c == '}'))) {
453453
/* Do not match a closer against the brace that opened the
454-
* current replacement field. */
454+
* current replacement field. The f-string state relies on
455+
* that brace, so this is checked even with extra tokens. */
455456
if (current != NULL && opening == '{') {
456457
int bracket_depth =
457458
_PyLexer_FTStringBracketDepth(tok, current);
@@ -461,7 +462,10 @@ _PyLexer_get_normal(struct tok_state *tok, ftstring_state *current, struct token
461462
_PyLexer_StringPrefix(current->kind), c));
462463
}
463464
}
464-
if (tok->parenlinenostack[tok->level] != tok->lineno) {
465+
if (tok->tok_extra_tokens) {
466+
/* Allow other mismatched brackets. */
467+
}
468+
else if (tok->parenlinenostack[tok->level] != tok->lineno) {
465469
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok,
466470
"closing parenthesis '%c' does not match "
467471
"opening parenthesis '%c' on line %d",

0 commit comments

Comments
 (0)