Skip to content

Commit 2ddc218

Browse files
authored
gh-153569: centralize formatted-string state and source spans (#156484)
Formatted-string expressions and comments can outlive the buffer window where scanning began. Pointer boundaries require buffer relocation to repair tokenizer state. Record logical source ranges instead. Retain the active input window while a formatted string is open, give each mode ownership of its comment spans, and materialize text through shared span views.
1 parent e757c06 commit 2ddc218

19 files changed

Lines changed: 668 additions & 607 deletions

Lib/test/test_fstring.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1657,6 +1657,7 @@ def __repr__(self):
16571657
self.assertEqual(f'{C()=:x}', 'C()=FORMAT-x')
16581658
self.assertEqual(f'{C()=!r:*^20}', 'C()=********REPR********')
16591659
self.assertEqual(f"{C():{20=}}", 'FORMAT-20=20')
1660+
self.assertEqual(f"{C():{C():{4=}}}", 'FORMAT-FORMAT-4=4')
16601661

16611662
self.assertRaises(SyntaxError, eval, "f'{C=]'")
16621663

@@ -1679,6 +1680,20 @@ def __repr__(self):
16791680

16801681
self.assertEqual(f'{" # nooo "=}', '" # nooo "=\' # nooo \'')
16811682
self.assertEqual(f'{" \" # nooo \" "=}', '" \\" # nooo \\" "=\' " # nooo " \'')
1683+
self.assertEqual(f'{"""a" # inside"""=}',
1684+
'"""a" # inside"""=\'a" # inside\'')
1685+
self.assertEqual(f"{'''a' # inside'''=}",
1686+
"'''a' # inside'''=\"a' # inside\"")
1687+
self.assertEqual(f'{"""a""""#" # outside
1688+
=}', '"""a""""#" \n=\'a#\'')
1689+
1690+
x, y = 1, 2
1691+
self.assertEqual(f'{x != y # outside
1692+
=}', 'x != y \n=True')
1693+
1694+
d = {'a#b': 42}
1695+
self.assertEqual(f'''{f"{d["a#b"]}"=}''',
1696+
'f"{d["a#b"]}"=\'42\'')
16821697

16831698
self.assertEqual(f'{ # some comment goes here
16841699
"""hello"""=}', ' \n """hello"""=\'hello\'')

Lib/test/test_syntax.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3393,6 +3393,10 @@ def test_invalid_line_continuation_error_position(self):
33933393
self._check_error('\nfgdfgf\n1,\\#\n2\n',
33943394
"unexpected character after line continuation character",
33953395
lineno=3, offset=4)
3396+
for prefix in ("f", "t"):
3397+
self._check_error(f'{prefix}"""{{\n\\ x}}"""',
3398+
"unexpected character after line continuation character",
3399+
lineno=2, offset=2)
33963400

33973401
def test_invalid_line_continuation_left_recursive(self):
33983402
# Check bpo-42218: SyntaxErrors following left-recursive rules

Lib/test/test_tokenize.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2573,6 +2573,40 @@ def test_degraded_fstring_format_spec(self):
25732573
("f-string: single '}' is not allowed", (1, 11)),
25742574
)
25752575

2576+
def test_carriage_return_after_debug_comment(self):
2577+
for prefix in ("f", "t"):
2578+
with self.subTest(prefix=prefix):
2579+
tokens = self._get_tokens(f"{prefix}'''{{x=# comment\r}}'''")
2580+
self.assertEqual(tokens[4].string, "# comment\r}")
2581+
2582+
def test_incomplete_formatted_string_comment_after_carriage_return(self):
2583+
for prefix in ("f", "t"):
2584+
with self.subTest(prefix=prefix):
2585+
for extra_tokens in (False, True):
2586+
with self.assertRaises(tokenize.TokenError) as caught:
2587+
self._get_tokens(
2588+
f"{prefix}'{{#\r!", extra_tokens=extra_tokens
2589+
)
2590+
self.assertEqual(
2591+
caught.exception.args,
2592+
("unexpected EOF in multi-line statement", (1, 7)),
2593+
)
2594+
2595+
def test_formatted_string_nesting_limit(self):
2596+
def nested_string(depth, prefix):
2597+
source = "'x'"
2598+
for _ in range(depth):
2599+
source = f'{prefix}"{{{source}}}"'
2600+
return source
2601+
2602+
for prefix in ("f", "t"):
2603+
with self.subTest(prefix=prefix):
2604+
self._get_tokens(nested_string(149, prefix))
2605+
with self.assertRaisesRegex(
2606+
tokenize.TokenError,
2607+
"too many nested f-strings or t-strings"):
2608+
self._get_tokens(nested_string(150, prefix))
2609+
25762610
def test_escaped_fstring_brace_has_a_position_gap(self):
25772611
tokens = self._get_tokens('f"a{{"', extra_tokens=True)
25782612
self.assertEqual(

Lib/test/test_tstring.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@ def test_debug_specifier(self):
140140
)
141141
self.assertEqual(fstring(t), "Value: value = 42")
142142

143+
class C:
144+
def __format__(self, spec):
145+
return f"FORMAT-{spec}"
146+
147+
x = y = C()
148+
t = t"{x:{y:{value=}}}"
149+
self.assertEqual(t.interpolations[0].format_spec,
150+
"FORMAT-value=42")
151+
143152
def test_raw_tstrings(self):
144153
path = r"C:\Users"
145154
t = rt"{path}\Documents"
@@ -287,5 +296,22 @@ def test_triple_quoted(self):
287296
)
288297
self.assertEqual(fstring(t), "\n Hello,\n Python\n ")
289298

299+
t = t'{"""a" # inside"""}'
300+
self.assertEqual(t.interpolations[0].expression,
301+
'"""a" # inside"""')
302+
303+
t = t'{"""a""""#" # outside
304+
}'
305+
self.assertEqual(t.interpolations[0].expression, '"""a""""#"')
306+
307+
x, y = 1, 2
308+
t = t'{x != y # outside
309+
}'
310+
self.assertEqual(t.interpolations[0].expression, 'x != y')
311+
312+
d = {'a#b': 42}
313+
t = t'''{f"{d["a#b"]}"}'''
314+
self.assertEqual(t.interpolations[0].expression, 'f"{d["a#b"]}"')
315+
290316
if __name__ == '__main__':
291317
unittest.main()

Parser/action_helpers.c

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,14 +1001,21 @@ result_token_with_metadata(Parser *p, void *result, PyObject *metadata)
10011001
return res;
10021002
}
10031003

1004+
static char
1005+
formatted_string_prefix(const Parser *p)
1006+
{
1007+
const ftstring_state *state = _PyLexer_CurrentFTString(p->tok);
1008+
return state == NULL ? 'f' : _PyLexer_StringPrefix(state->kind);
1009+
}
1010+
10041011
ResultTokenWithMetadata *
10051012
_PyPegen_check_fstring_conversion(Parser *p, Token* conv_token, expr_ty conv)
10061013
{
10071014
if (conv_token->lineno != conv->lineno || conv_token->end_col_offset != conv->col_offset) {
10081015
return RAISE_SYNTAX_ERROR_KNOWN_RANGE(
10091016
conv_token, conv,
10101017
"%c-string: conversion type must come right after the exclamation mark",
1011-
TOK_GET_STRING_PREFIX(p->tok)
1018+
formatted_string_prefix(p)
10121019
);
10131020
}
10141021

@@ -1017,7 +1024,7 @@ _PyPegen_check_fstring_conversion(Parser *p, Token* conv_token, expr_ty conv)
10171024
!(first == 's' || first == 'r' || first == 'a')) {
10181025
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(conv,
10191026
"%c-string: invalid conversion character %R: expected 's', 'r', or 'a'",
1020-
TOK_GET_STRING_PREFIX(p->tok),
1027+
formatted_string_prefix(p),
10211028
conv->v.Name.id);
10221029
return NULL;
10231030
}
@@ -1344,7 +1351,8 @@ _PyPegen_decode_fstring_part(Parser* p, int is_raw, expr_ty constant, Token* tok
13441351
}
13451352

13461353
static asdl_expr_seq *
1347-
_get_resized_exprs(Parser *p, Token *a, asdl_expr_seq *raw_expressions, Token *b, enum string_kind_t string_kind)
1354+
_get_resized_exprs(Parser *p, Token *a, asdl_expr_seq *raw_expressions,
1355+
Token *b, ftstring_kind string_kind)
13481356
{
13491357
Py_ssize_t n_items = asdl_seq_LEN(raw_expressions);
13501358
Py_ssize_t total_items = n_items;
@@ -1370,15 +1378,13 @@ _get_resized_exprs(Parser *p, Token *a, asdl_expr_seq *raw_expressions, Token *b
13701378
for (Py_ssize_t i = 0; i < n_items; i++) {
13711379
expr_ty item = asdl_seq_GET(raw_expressions, i);
13721380

1373-
// This should correspond to a JoinedStr node of two elements
1374-
// created _PyPegen_formatted_value. This situation can only be the result of
1375-
// a (f|t)-string debug expression where the first element is a constant with the text and the second
1376-
// a formatted value with the expression.
1381+
/* Debug expressions arrive as JoinedStr(text, value); flatten them
1382+
into the surrounding string. */
13771383
if (item->kind == JoinedStr_kind) {
13781384
asdl_expr_seq *values = item->v.JoinedStr.values;
13791385
if (asdl_seq_LEN(values) != 2) {
13801386
PyErr_Format(PyExc_SystemError,
1381-
string_kind == TSTRING
1387+
_PyLexer_IsTString(string_kind)
13821388
? "unexpected TemplateStr node without debug data in t-string at line %d"
13831389
: "unexpected JoinedStr node without debug data in f-string at line %d",
13841390
item->lineno);
@@ -1390,7 +1396,9 @@ _get_resized_exprs(Parser *p, Token *a, asdl_expr_seq *raw_expressions, Token *b
13901396
asdl_seq_SET(seq, index++, first);
13911397

13921398
expr_ty second = asdl_seq_GET(values, 1);
1393-
assert((string_kind == TSTRING && second->kind == Interpolation_kind) || second->kind == FormattedValue_kind);
1399+
assert((_PyLexer_IsTString(string_kind) &&
1400+
second->kind == Interpolation_kind) ||
1401+
second->kind == FormattedValue_kind);
13941402
asdl_seq_SET(seq, index++, second);
13951403

13961404
continue;
@@ -1460,12 +1468,8 @@ expr_ty _PyPegen_decoded_constant_from_token(Parser* p, Token* tok) {
14601468
return NULL;
14611469
}
14621470

1463-
// Check if we're inside a raw f-string for format spec decoding
1464-
int is_raw = 0;
1465-
if (INSIDE_FSTRING(p->tok)) {
1466-
tokenizer_mode *mode = TOK_GET_MODE(p->tok);
1467-
is_raw = mode->raw;
1468-
}
1471+
const ftstring_state *state = _PyLexer_CurrentFTString(p->tok);
1472+
int is_raw = state != NULL && _PyLexer_IsRawString(state->kind);
14691473

14701474
PyObject* str = _PyPegen_decode_string(p, is_raw, bstr, bsize, tok);
14711475
if (str == NULL) {

Parser/lexer/buffer.c

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,6 @@ _PyLexer_SaveBufferPointers(struct tok_state *tok, const char *base,
1313
? -1 : tok->start - tok->buf;
1414
pointers->line_start_from_buf = tok->line_start == NULL
1515
? -1 : tok->line_start - tok->buf;
16-
pointers->multi_line_start_from_buf = tok->multi_line_start == NULL
17-
? -1 : tok->multi_line_start - tok->buf;
18-
for (int index = tok->tok_mode_stack_index; index > 0; --index) {
19-
tokenizer_mode *mode = &tok->tok_mode_stack[index];
20-
mode->start_offset = mode->start == NULL ? -1 : mode->start - tok->buf;
21-
mode->multi_line_start_offset = mode->multi_line_start == NULL
22-
? -1 : mode->multi_line_start - tok->buf;
23-
}
2416
}
2517

2618
void
@@ -34,13 +26,4 @@ _PyLexer_RestoreBufferPointers(struct tok_state *tok, char *base,
3426
? NULL : tok->buf + pointers->start_from_buf;
3527
tok->line_start = pointers->line_start_from_buf < 0
3628
? NULL : tok->buf + pointers->line_start_from_buf;
37-
tok->multi_line_start = pointers->multi_line_start_from_buf < 0
38-
? NULL : tok->buf + pointers->multi_line_start_from_buf;
39-
for (int index = tok->tok_mode_stack_index; index > 0; --index) {
40-
tokenizer_mode *mode = &tok->tok_mode_stack[index];
41-
mode->start = mode->start_offset < 0
42-
? NULL : tok->buf + mode->start_offset;
43-
mode->multi_line_start = mode->multi_line_start_offset < 0
44-
? NULL : tok->buf + mode->multi_line_start_offset;
45-
}
4629
}

Parser/lexer/buffer.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ typedef struct {
1111
Py_ssize_t inp_from_buf;
1212
Py_ssize_t start_from_buf;
1313
Py_ssize_t line_start_from_buf;
14-
Py_ssize_t multi_line_start_from_buf;
1514
} _PyLexer_BufferPointers;
1615

1716
void _PyLexer_SaveBufferPointers(

0 commit comments

Comments
 (0)