Skip to content

Commit 3ef307f

Browse files
committed
gh-153569: consolidate tokenizer state around source spans
Keep active formatted-string frames with expression and comment spans, and carry raw-string context with emitted and cached parser tokens. Let the reader own input-specific state and distinguish retained source from streaming windows. Derive locations and failures from existing scanner state, and keep buffer relocation inside the reader.
1 parent 09117bc commit 3ef307f

34 files changed

Lines changed: 846 additions & 862 deletions

Lib/test/test_fstring.py

Lines changed: 18 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\'')
@@ -1876,6 +1891,9 @@ def __format__(self, format):
18761891
self.assertEqual(f"{UnchangedFormat():{r'\xFF'}}", '\\xFF')
18771892
self.assertEqual(rf"{UnchangedFormat():{r'\xFF'}}", '\\xFF')
18781893

1894+
self.assertEqual(rf"{UnchangedFormat():{f'\xFF'}}\n", 'ÿ\\n')
1895+
self.assertEqual(f"{UnchangedFormat():{rf'\xFF'}}\n", '\\xFF\n')
1896+
18791897
# Test continuation character in format specs
18801898
self.assertEqual(f"""{UnchangedFormat():{'a'\
18811899
'b'}}""", 'ab')

Lib/test/test_repl.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,8 @@ def read_until(marker, start=0):
184184

185185
@cpython_only
186186
def test_lexer_buffer_realloc_with_null_start(self):
187-
# gh-144759: NULL pointer arithmetic in the lexer when start and
188-
# multi_line_start are NULL (uninitialized in tok_mode_stack[0])
189-
# and the lexer buffer is reallocated while parsing long input.
187+
# gh-144759: NULL pointer arithmetic when the lexer buffer grows
188+
# while parsing long input.
190189
long_value = "a" * 2000
191190
user_input = dedent(f"""\
192191
x = f'{{{long_value!r}}}'

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: 38 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"
@@ -150,6 +159,14 @@ def test_raw_tstrings(self):
150159
t = tr"{path}\Documents"
151160
self.assertTStringEqual(t, ("", r"\Documents"), [(path, "path")])
152161

162+
value = 42
163+
t = rt"{value:{f'\xFF'}}\n"
164+
self.assertTStringEqual(
165+
t, ("", "\\n"), [(value, "value", None, 'ÿ')])
166+
t = t"{value:{rf'\xFF'}}\n"
167+
self.assertTStringEqual(
168+
t, ("", "\n"), [(value, "value", None, '\\xFF')])
169+
153170
def test_template_concatenation(self):
154171
# Test template + template
155172
t1 = t"Hello, "
@@ -217,6 +234,10 @@ def test_syntax_errors(self):
217234
("t'{x=!}'", "t-string: missing conversion character"),
218235
("t'{x!z}'", "t-string: invalid conversion character 'z': "
219236
"expected 's', 'r', or 'a'"),
237+
("f\"{t'{x!z}'}\"", "t-string: invalid conversion character 'z': "
238+
"expected 's', 'r', or 'a'"),
239+
("t'{f\"{x!z}\"}'", "f-string: invalid conversion character 'z': "
240+
"expected 's', 'r', or 'a'"),
220241
("t'{lambda:1}'", "t-string: lambda expressions are not allowed "
221242
"without parentheses"),
222243
("t'{x:{;}}'", "t-string: expecting a valid expression after '{'"),
@@ -287,5 +308,22 @@ def test_triple_quoted(self):
287308
)
288309
self.assertEqual(fstring(t), "\n Hello,\n Python\n ")
289310

311+
t = t'{"""a" # inside"""}'
312+
self.assertEqual(t.interpolations[0].expression,
313+
'"""a" # inside"""')
314+
315+
t = t'{"""a""""#" # outside
316+
}'
317+
self.assertEqual(t.interpolations[0].expression, '"""a""""#"')
318+
319+
x, y = 1, 2
320+
t = t'{x != y # outside
321+
}'
322+
self.assertEqual(t.interpolations[0].expression, 'x != y')
323+
324+
d = {'a#b': 42}
325+
t = t'''{f"{d["a#b"]}"}'''
326+
self.assertEqual(t.interpolations[0].expression, 'f"{d["a#b"]}"')
327+
290328
if __name__ == '__main__':
291329
unittest.main()

Makefile.pre.in

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,6 @@ PEGEN_OBJS= \
394394
Parser/peg_api.o
395395

396396
TOKENIZER_OBJS= \
397-
Parser/lexer/buffer.o \
398397
Parser/lexer/lexer.o \
399398
Parser/lexer/number.o \
400399
Parser/lexer/state.o \
@@ -411,7 +410,6 @@ PEGEN_HEADERS= \
411410
$(srcdir)/Parser/string_parser.h
412411

413412
TOKENIZER_HEADERS= \
414-
Parser/lexer/buffer.h \
415413
Parser/lexer/lexer.h \
416414
Parser/lexer/lexer_internal.h \
417415
Parser/lexer/state.h \

Modules/_testinternalcapi/tokenizer.c

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ check_system_error(int failed, const char *message)
2323
return 0;
2424
}
2525

26+
static int
27+
check_line_view(const _PyTok_SourceText *source, Py_ssize_t lineno,
28+
const char *expected)
29+
{
30+
Py_ssize_t len;
31+
const char *line = _PyTok_SourceLineView(source, lineno, &len);
32+
return check(len == (Py_ssize_t)strlen(expected) &&
33+
memcmp(line, expected, len) == 0,
34+
"wrong source line view");
35+
}
36+
2637
static int
2738
same_cursor(const _PyTok_Cursor *left, const _PyTok_Cursor *right)
2839
{
@@ -40,6 +51,10 @@ test_tokenizer_source(PyObject *Py_UNUSED(module),
4051
_PyTok_SourceText source;
4152
_PyTok_SourceInit(&source);
4253

54+
if (check_line_view(&source, 1, "") < 0) {
55+
goto error;
56+
}
57+
4358
_PyTok_Loc loc;
4459
_PyTok_Line line;
4560
if (check(_PyTok_SourceLocation(
@@ -67,10 +82,20 @@ test_tokenizer_source(PyObject *Py_UNUSED(module),
6782
"wrong first source offset") < 0 ||
6883
check(_PyTok_SourceAppendLine(
6984
&source, "\xce\xb2\n", 3, 1) == 6,
70-
"wrong second source offset") < 0 ||
71-
check(_PyTok_SourceAppendLine(
72-
&source, "nul\0x\n", 6, 0) == 9,
73-
"wrong third source offset") < 0) {
85+
"wrong second source offset") < 0) {
86+
goto error;
87+
}
88+
89+
if (check_line_view(&source, PY_SSIZE_T_MIN, "alpha") < 0 ||
90+
check_line_view(&source, 1, "alpha") < 0 ||
91+
check_line_view(&source, 2, "\xce\xb2") < 0 ||
92+
check_line_view(&source, 3, "") < 0 ||
93+
check_line_view(&source, PY_SSIZE_T_MAX, "") < 0) {
94+
goto error;
95+
}
96+
97+
if (check(_PyTok_SourceAppendLine(&source, "nul\0x\n", 6, 0) == 9,
98+
"wrong third source offset") < 0) {
7499
goto error;
75100
}
76101

@@ -195,6 +220,11 @@ test_tokenizer_source(PyObject *Py_UNUSED(module),
195220
goto error;
196221
}
197222

223+
if (check_line_view(&source, 1, "tail") < 0 ||
224+
check_line_view(&source, PY_SSIZE_T_MAX, "tail") < 0) {
225+
goto error;
226+
}
227+
198228
_PyTok_SourceDiscard(&source);
199229
if (check(_PyTok_SourceAppendLine(&source, "a\n", 2, 0) == 4,
200230
"wrong retained source offset") < 0 ||

PCbuild/_freeze_module.vcxproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,6 @@
181181
<ClCompile Include="..\Parser\action_helpers.c" />
182182
<ClCompile Include="..\Parser\string_parser.c" />
183183
<ClCompile Include="..\Parser\token.c" />
184-
<ClCompile Include="..\Parser\lexer\buffer.c" />
185184
<ClCompile Include="..\Parser\lexer\state.c" />
186185
<ClCompile Include="..\Parser\lexer\lexer.c" />
187186
<ClCompile Include="..\Parser\lexer\number.c" />

PCbuild/_freeze_module.vcxproj.filters

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -469,9 +469,6 @@
469469
<ClCompile Include="..\Parser\lexer\string.c">
470470
<Filter>Source Files</Filter>
471471
</ClCompile>
472-
<ClCompile Include="..\Parser\lexer\buffer.c">
473-
<Filter>Source Files</Filter>
474-
</ClCompile>
475472
<ClCompile Include="..\Parser\lexer\state.c">
476473
<Filter>Source Files</Filter>
477474
</ClCompile>

PCbuild/pythoncore.vcxproj

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -423,10 +423,9 @@
423423
<ClInclude Include="..\Parser\lexer\state.h" />
424424
<ClInclude Include="..\Parser\lexer\lexer.h" />
425425
<ClInclude Include="..\Parser\lexer\lexer_internal.h" />
426-
<ClInclude Include="..\Parser\lexer\buffer.h" />
427-
<ClInclude Include="..\Parser\tokenizer\cursor.h" />
428426
<ClInclude Include="..\Parser\tokenizer\reader.h" />
429427
<ClInclude Include="..\Parser\tokenizer\reader_internal.h" />
428+
<ClInclude Include="..\Parser\tokenizer\cursor.h" />
430429
<ClInclude Include="..\Parser\tokenizer\source.h" />
431430
<ClInclude Include="..\Parser\tokenizer\helpers.h" />
432431
<ClInclude Include="..\Parser\tokenizer\tokenizer.h" />
@@ -593,7 +592,6 @@
593592
<ClCompile Include="..\Parser\lexer\lexer.c" />
594593
<ClCompile Include="..\Parser\lexer\number.c" />
595594
<ClCompile Include="..\Parser\lexer\string.c" />
596-
<ClCompile Include="..\Parser\lexer\buffer.c" />
597595
<ClCompile Include="..\Parser\tokenizer\cursor.c" />
598596
<ClCompile Include="..\Parser\tokenizer\source.c" />
599597
<ClCompile Include="..\Parser\tokenizer\decoder.c" />

0 commit comments

Comments
 (0)