From 966ae8886174a9d56b6769d81e153757679999ab Mon Sep 17 00:00:00 2001 From: lipengyu Date: Thu, 15 Oct 2026 23:00:14 +0800 Subject: [PATCH] gh-157491: Restore format-specifier state after nested replacement fields --- Lib/test/test_fstring.py | 21 +++++++++++++++++++ Lib/test/test_tstring.py | 18 +++++++++++++++- ...-10-15-11-22-44.gh-issue-157491.5BfNWA.rst | 2 ++ Parser/lexer/lexer.c | 2 +- 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-11-22-44.gh-issue-157491.5BfNWA.rst diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 0f87deebf7a8dd..de2b4ccc788c7e 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -869,6 +869,26 @@ def test_format_specifier_expressions(self): """f'{"s"!{"r"}}'""", ]) + def test_nested_replacement_field_restores_format_specifier(self): + class CaptureFormat: + def __format__(self, format_spec): + self.format_spec = format_spec + return "" + + x = CaptureFormat() + y = "Y" + z = "Z" + + def check_format_spec(result, expected): + self.assertEqual(x.format_spec, expected) + self.assertEqual(result, "") + + check_format_spec(f'{x:{{y}}}', "{'Y'}") + check_format_spec(f'{x:{y}{{z}}}', "Y{'Z'}") + check_format_spec(f'{x:{y!s}{{z}}}', "Y{'Z'}") + check_format_spec(f'{x:{y=}{{z}}}', "y='Y'{'Z'}") + check_format_spec(f'''{x:{y}{{z}}}''', "Y{'Z'}") + def test_custom_format_specifier(self): class CustomFormat: def __format__(self, format_spec): @@ -1871,6 +1891,7 @@ def test_gh129093(self): def test_newlines_in_format_specifiers(self): cases = [ """f'{1:d\n}'""", + """f'{x:{y}\n}'""", """f'__{ 1:d }__'""", diff --git a/Lib/test/test_tstring.py b/Lib/test/test_tstring.py index 1e5f14ca007d1e..6ba2f997cb04a5 100644 --- a/Lib/test/test_tstring.py +++ b/Lib/test/test_tstring.py @@ -138,6 +138,19 @@ def test_format_specifiers(self): ) self.assertEqual(fstring(t), " 1") + x = object() + y = "Y" + z = "Z" + for template, expected in ( + (t"{x:{y}{{z}}}", "Y{'Z'}"), + (t"{x:{y!s}{{z}}}", "Y{'Z'}"), + (t"{x:{y=}{{z}}}", "y='Y'{'Z'}"), + (t'''{x:{y}{{z}}}''', "Y{'Z'}"), + ): + with self.subTest(template=template): + self.assertEqual(template.interpolations[0].format_spec, + expected) + def test_conversions(self): # Test !s conversion (str) obj = object() @@ -342,7 +355,10 @@ def test_syntax_errors(self): ("t'{lambda:1}'", "t-string: lambda expressions are not allowed " "without parentheses"), ("t'{x:{;}}'", "t-string: expecting a valid expression after '{'"), - ("t'{1:d\n}'", "t-string: newlines are not allowed in format specifiers") + ("t'{1:d\n}'", + "t-string: newlines are not allowed in format specifiers"), + ("t'{x:{y}\n}'", + "t-string: newlines are not allowed in format specifiers"), ): with self.subTest(case), self.assertRaisesRegex(SyntaxError, err): eval(case) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-11-22-44.gh-issue-157491.5BfNWA.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-11-22-44.gh-issue-157491.5BfNWA.rst new file mode 100644 index 00000000000000..6fd2f557d6dc08 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-11-22-44.gh-issue-157491.5BfNWA.rst @@ -0,0 +1,2 @@ +Correctly parse braces and report invalid newlines after nested replacement +fields in f-string and t-string format specifiers. diff --git a/Parser/lexer/lexer.c b/Parser/lexer/lexer.c index af57deb12cbefb..d3ffcc29f0d284 100644 --- a/Parser/lexer/lexer.c +++ b/Parser/lexer/lexer.c @@ -1323,7 +1323,7 @@ tok_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, struct t if (c == '}' && current_tok->curly_bracket_depth == current_tok->curly_bracket_expr_start_depth) { current_tok->curly_bracket_expr_start_depth--; current_tok->kind = TOK_FSTRING_MODE; - current_tok->in_format_spec = 0; + current_tok->in_format_spec = INSIDE_FSTRING_EXPR(current_tok); current_tok->in_debug = 0; } }