Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit af0798e

Browse files
authored
[3.14] gh-154719: Preserve trailing whitespace in t-string interpolation expressions (#154762) (#157392)
1 parent f66a784 commit af0798e

7 files changed

Lines changed: 134 additions & 10 deletions

File tree

Lib/test/test_annotationlib.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1869,7 +1869,7 @@ def nested():
18691869
self.assertEqual(type_repr(t'''{ 0
18701870
& 1
18711871
| 2
1872-
}'''), 't"""{ 0\n & 1\n | 2}"""')
1872+
}'''), 't"""{ 0\n & 1\n | 2\n }"""')
18731873
self.assertEqual(
18741874
type_repr(Template("hi", Interpolation(42, "42"))), "t'hi{42}'"
18751875
)

Lib/test/test_fstring.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1668,6 +1668,14 @@ def __repr__(self):
16681668
self.assertEqual(f'{" # nooo "=}', '" # nooo "=\' # nooo \'')
16691669
self.assertEqual(f'{" \" # nooo \" "=}', '" \\" # nooo \\" "=\' " # nooo " \'')
16701670

1671+
result = f'''{(
1672+
1, # Force lexer metadata reconstruction.
1673+
"\"#")=}'''
1674+
self.assertEqual(
1675+
result,
1676+
'(\n 1, \n "\\"#")=(1, \'"#\')',
1677+
)
1678+
16711679
self.assertEqual(f'{ # some comment goes here
16721680
"""hello"""=}', ' \n """hello"""=\'hello\'')
16731681
self.assertEqual(f'{"""# this is not a comment

Lib/test/test_tstring.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,82 @@ def test_debug_specifier(self):
136136
# Test white space in debug specifier
137137
t = t"Value: {value = }"
138138
self.assertTStringEqual(
139-
t, ("Value: value = ", ""), [(value, "value", "r")]
139+
t, ("Value: value = ", ""), [(value, "value ", "r")]
140140
)
141141
self.assertEqual(fstring(t), "Value: value = 42")
142142

143+
# Explicit line continuations after the debug marker are part of
144+
# the debug text, not the interpolation expression.
145+
for template, strings, interpolation, rendered in (
146+
(
147+
t"""Value: {value =\
148+
}""",
149+
("Value: value =\\\n", ""),
150+
(value, "value ", "r"),
151+
"Value: value =\\\n42",
152+
),
153+
(
154+
t"""Value: {value =\
155+
!r}""",
156+
("Value: value =\\\n", ""),
157+
(value, "value ", "r"),
158+
"Value: value =\\\n42",
159+
),
160+
(
161+
t"""Value: {value =\
162+
:04}""",
163+
("Value: value =\\\n", ""),
164+
(value, "value ", None, "04"),
165+
"Value: value =\\\n0042",
166+
),
167+
(
168+
t"""Value: {value =\
169+
\
170+
}""",
171+
("Value: value =\\\n\\\n", ""),
172+
(value, "value ", "r"),
173+
"Value: value =\\\n\\\n42",
174+
),
175+
):
176+
with self.subTest(template=template):
177+
self.assertTStringEqual(template, strings, [interpolation])
178+
self.assertEqual(fstring(template), rendered)
179+
180+
def test_interpolation_expression_whitespace(self):
181+
x = 42
182+
for template, expected in (
183+
(t"{x}", "x"),
184+
(t"{x }", "x "),
185+
(t"{ x}", " x"),
186+
(t"{ x }", " x "),
187+
(t"{ x }", " x "),
188+
(t"""{
189+
x
190+
}""", "\n x\n"),
191+
(t"{ x !r}", " x "),
192+
(t"{ x :.2f}", " x "),
193+
(t"{ x = }", " x "),
194+
(t"{ x = !r}", " x "),
195+
(t"{ x = :.2f}", " x "),
196+
(t"{x == 42 = }", "x == 42 "),
197+
):
198+
with self.subTest(template=template):
199+
self.assertEqual(
200+
template.interpolations[0].expression,
201+
expected,
202+
)
203+
204+
def test_interpolation_expression_with_reconstructed_metadata(self):
205+
regular = t'''{(
206+
1, # Force lexer metadata reconstruction.
207+
"\"#")}'''
208+
debug = t'''{(
209+
1, # Force lexer metadata reconstruction.
210+
"\"#")=}'''
211+
expected = '(\n 1, \n "\\"#")'
212+
self.assertEqual(regular.interpolations[0].expression, expected)
213+
self.assertEqual(debug.interpolations[0].expression, expected)
214+
143215
def test_raw_tstrings(self):
144216
path = r"C:\Users"
145217
t = rt"{path}\Documents"

Lib/test/test_unparse.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,15 @@ def test_tstrings(self):
216216
self.check_ast_roundtrip('t""')
217217
self.check_ast_roundtrip("t'{(lambda x: x)}'")
218218
self.check_ast_roundtrip("t'{t'{x}'}'")
219+
self.check_ast_roundtrip(
220+
r"""t'''{(
221+
1, # Force lexer metadata reconstruction.
222+
"\"#")}'''"""
223+
)
224+
self.check_ast_roundtrip(
225+
r'''t"""Value: {value =\
226+
}"""'''
227+
)
219228

220229
def test_tstring_with_nonsensical_str_field(self):
221230
# `value` suggests that the original code is `t'{test1}`, but `str` suggests otherwise
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Trailing whitespace in a t-string interpolation expression is now preserved
2+
in :attr:`string.templatelib.Interpolation.expression`, up to the closing ``}``
3+
or the conversion (``!``), format (``:``), or debug (``=``) delimiter.
4+
Explicit line continuations following a debug ``=`` remain part of the debug
5+
text and are excluded from :attr:`~string.templatelib.Interpolation.expression`.
6+
Escaped quotes are now preserved while reconstructing t-string interpolation
7+
metadata and f-string debug text containing comments.

Parser/action_helpers.c

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,21 +1510,38 @@ _get_interpolation_conversion(Parser *p, Token *debug, ResultTokenWithMetadata *
15101510
}
15111511

15121512
static PyObject *
1513-
_strip_interpolation_expr(PyObject *exprstr)
1513+
_strip_interpolation_debug_expr(PyObject *exprstr)
15141514
{
15151515
Py_ssize_t len = PyUnicode_GET_LENGTH(exprstr);
15161516

1517-
for (Py_ssize_t i = len - 1; i >= 0; i--) {
1518-
Py_UCS4 c = PyUnicode_READ_CHAR(exprstr, i);
1519-
if (_PyUnicode_IsWhitespace(c) || c == '=') {
1517+
/* Discard whitespace and explicit line continuations after the debug "="
1518+
but preserve whitespace before it. */
1519+
while (len > 0) {
1520+
int has_newline = 0;
1521+
while (len > 0) {
1522+
Py_UCS4 c = PyUnicode_READ_CHAR(exprstr, len - 1);
1523+
if (!_PyUnicode_IsWhitespace(c)) {
1524+
break;
1525+
}
1526+
if (c == '\r' || c == '\n') {
1527+
has_newline = 1;
1528+
}
15201529
len--;
15211530
}
1522-
else {
1531+
if (!has_newline || len == 0 ||
1532+
PyUnicode_READ_CHAR(exprstr, len - 1) != '\\')
1533+
{
15231534
break;
15241535
}
1536+
len--;
1537+
}
1538+
1539+
/* Preserve unexpected metadata instead of dropping source text. */
1540+
if (len == 0 || PyUnicode_READ_CHAR(exprstr, len - 1) != '=') {
1541+
return Py_NewRef(exprstr);
15251542
}
15261543

1527-
return PyUnicode_Substring(exprstr, 0, len);
1544+
return PyUnicode_Substring(exprstr, 0, len - 1);
15281545
}
15291546

15301547
expr_ty _PyPegen_interpolation(Parser *p, expr_ty expression, Token *debug, ResultTokenWithMetadata *conversion,
@@ -1555,7 +1572,9 @@ expr_ty _PyPegen_interpolation(Parser *p, expr_ty expression, Token *debug, Resu
15551572
}
15561573

15571574
assert(exprstr != NULL);
1558-
PyObject *final_exprstr = _strip_interpolation_expr(exprstr);
1575+
PyObject *final_exprstr = debug
1576+
? _strip_interpolation_debug_expr(exprstr)
1577+
: Py_NewRef(exprstr);
15591578
if (!final_exprstr || _PyArena_AddPyObject(arena, final_exprstr) < 0) {
15601579
Py_XDECREF(final_exprstr);
15611580
return NULL;

Parser/lexer/lexer.c

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,17 @@ set_ftstring_expr(struct tok_state* tok, struct token *token, char c) {
177177
while (i < tok_mode->last_expr_size - tok_mode->last_expr_end) {
178178
char ch = tok_mode->last_expr_buffer[i];
179179

180+
// Copy escaped characters without interpreting the escaped
181+
// character as a quote or comment marker.
182+
if (ch == '\\') {
183+
result[j++] = ch;
184+
i++;
185+
if (i < tok_mode->last_expr_size - tok_mode->last_expr_end) {
186+
result[j++] = tok_mode->last_expr_buffer[i];
187+
}
188+
}
180189
// Handle string quotes
181-
if (ch == '"' || ch == '\'') {
190+
else if (ch == '"' || ch == '\'') {
182191
// See comment above to understand this part
183192
if (!in_string) {
184193
in_string = 1;

0 commit comments

Comments
 (0)