From 3cfd699519cc058025c1863998e969128b8b2fe3 Mon Sep 17 00:00:00 2001 From: lipengyu Date: Sun, 26 Jul 2026 17:16:01 +0800 Subject: [PATCH 1/3] gh-154719: Preserve trailing whitespace in t-string interpolation expressions --- Lib/test/test_tstring.py | 24 ++++++++++++++++++- ...-07-26-17-20-49.gh-issue-154719.eI88Gs.rst | 3 +++ Parser/action_helpers.c | 20 +++++++++------- 3 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-26-17-20-49.gh-issue-154719.eI88Gs.rst diff --git a/Lib/test/test_tstring.py b/Lib/test/test_tstring.py index 74653c77c55de17..5a15824bb364ae5 100644 --- a/Lib/test/test_tstring.py +++ b/Lib/test/test_tstring.py @@ -79,6 +79,28 @@ def upper(self): ) self.assertEqual(fstring(t), "Name: Bob, Age: 30") + def test_interpolation_expression_whitespace(self): + x = 42 + cases = [ + (t"{x}", "x"), + (t"{x }", "x "), + (t"{ x}", " x"), + (t"{ x }", " x "), + (t"{ x }", " x "), + (t"{x !r}", "x "), + (t"{x :}", "x "), + (t"{x = }", "x "), + (t"{x == 42 = }", "x == 42 "), + (t"""{ + x +}""", "\n x\n"), + ] + for template, expected in cases: + with self.subTest(template=template): + self.assertEqual( + template.interpolations[0].expression, expected + ) + def test_format_specifiers(self): # Test basic format specifiers value = 3.14159 @@ -136,7 +158,7 @@ def test_debug_specifier(self): # Test white space in debug specifier t = t"Value: {value = }" self.assertTStringEqual( - t, ("Value: value = ", ""), [(value, "value", "r")] + t, ("Value: value = ", ""), [(value, "value ", "r")] ) self.assertEqual(fstring(t), "Value: value = 42") diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-26-17-20-49.gh-issue-154719.eI88Gs.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-26-17-20-49.gh-issue-154719.eI88Gs.rst new file mode 100644 index 000000000000000..bdbdda1689b0ad5 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-26-17-20-49.gh-issue-154719.eI88Gs.rst @@ -0,0 +1,3 @@ +Whitespace immediately before ``}``, ``!``, ``:``, or ``=`` is now included +in :attr:`string.templatelib.Interpolation.expression` for interpolations +created from t-string literals. diff --git a/Parser/action_helpers.c b/Parser/action_helpers.c index 809f3c0a7b270e8..d2da7eee423e277 100644 --- a/Parser/action_helpers.c +++ b/Parser/action_helpers.c @@ -1540,21 +1540,21 @@ _get_interpolation_conversion(Parser *p, Token *debug, ResultTokenWithMetadata * } static PyObject * -_strip_interpolation_expr(PyObject *exprstr) +_strip_interpolation_debug_expr(PyObject *exprstr) { Py_ssize_t len = PyUnicode_GET_LENGTH(exprstr); - for (Py_ssize_t i = len - 1; i >= 0; i--) { - Py_UCS4 c = PyUnicode_READ_CHAR(exprstr, i); - if (_PyUnicode_IsWhitespace(c) || c == '=') { - len--; - } - else { + /* Discard whitespace after the debug "=" but preserve whitespace before it. */ + while (len > 0) { + Py_UCS4 c = PyUnicode_READ_CHAR(exprstr, len - 1); + if (!_PyUnicode_IsWhitespace(c)) { break; } + len--; } + assert(len > 0 && PyUnicode_READ_CHAR(exprstr, len - 1) == '='); - return PyUnicode_Substring(exprstr, 0, len); + return PyUnicode_Substring(exprstr, 0, len - 1); } expr_ty _PyPegen_interpolation(Parser *p, expr_ty expression, Token *debug, ResultTokenWithMetadata *conversion, @@ -1585,7 +1585,9 @@ expr_ty _PyPegen_interpolation(Parser *p, expr_ty expression, Token *debug, Resu } assert(exprstr != NULL); - PyObject *final_exprstr = _strip_interpolation_expr(exprstr); + PyObject *final_exprstr = debug + ? _strip_interpolation_debug_expr(exprstr) + : Py_NewRef(exprstr); if (!final_exprstr || _PyArena_AddPyObject(arena, final_exprstr) < 0) { Py_XDECREF(final_exprstr); return NULL; From 255fb8c4f7756b6f6c7a62b3c585ba612900c412 Mon Sep 17 00:00:00 2001 From: lipengyu Date: Mon, 27 Jul 2026 09:16:18 +0800 Subject: [PATCH 2/3] fix test failed --- Lib/test/test_annotationlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 5087c3ca425f1fc..4892d34a7623279 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1862,7 +1862,7 @@ def nested(): self.assertEqual(type_repr(t'''{ 0 & 1 | 2 - }'''), 't"""{ 0\n & 1\n | 2}"""') + }'''), 't"""{ 0\n & 1\n | 2\n }"""') self.assertEqual( type_repr(Template("hi", Interpolation(42, "42"))), "t'hi{42}'" ) From 76478c24cbd4ca468979c070171bdc51221c19a5 Mon Sep 17 00:00:00 2001 From: lipengyu Date: Mon, 27 Jul 2026 10:30:45 +0800 Subject: [PATCH 3/3] fix fuzz test failed --- Lib/test/test_tstring.py | 12 ++++++++++++ Parser/action_helpers.c | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_tstring.py b/Lib/test/test_tstring.py index 5a15824bb364ae5..08937c2f9a5a860 100644 --- a/Lib/test/test_tstring.py +++ b/Lib/test/test_tstring.py @@ -162,6 +162,18 @@ def test_debug_specifier(self): ) self.assertEqual(fstring(t), "Value: value = 42") + def test_debug_specifier_with_reconstructed_metadata(self): + regular = t'''{( + 1, # Force lexer metadata reconstruction. + "\"#")}''' + debug = t'''{( + 1, # Force lexer metadata reconstruction. + "\"#")=}''' + self.assertEqual( + debug.interpolations[0].expression, + regular.interpolations[0].expression, + ) + def test_raw_tstrings(self): path = r"C:\Users" t = rt"{path}\Documents" diff --git a/Parser/action_helpers.c b/Parser/action_helpers.c index d2da7eee423e277..80cb09bb2ec5971 100644 --- a/Parser/action_helpers.c +++ b/Parser/action_helpers.c @@ -1552,7 +1552,10 @@ _strip_interpolation_debug_expr(PyObject *exprstr) } len--; } - assert(len > 0 && PyUnicode_READ_CHAR(exprstr, len - 1) == '='); + /* The debug marker may be absent from reconstructed lexer metadata. */ + if (len == 0 || PyUnicode_READ_CHAR(exprstr, len - 1) != '=') { + return Py_NewRef(exprstr); + } return PyUnicode_Substring(exprstr, 0, len - 1); }