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;