Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Lib/test/test_annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1869,7 +1869,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}'"
)
Expand Down
8 changes: 8 additions & 0 deletions Lib/test/test_fstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -1695,6 +1695,14 @@ def __repr__(self):
self.assertEqual(f'''{f"{d["a#b"]}"=}''',
'f"{d["a#b"]}"=\'42\'')

result = f'''{(
1, # Force lexer metadata reconstruction.
"\"#")=}'''
self.assertEqual(
result,
'(\n 1, \n "\\"#")=(1, \'"#\')',
)

self.assertEqual(f'{ # some comment goes here
"""hello"""=}', ' \n """hello"""=\'hello\'')
self.assertEqual(f'{"""# this is not a comment
Expand Down
78 changes: 75 additions & 3 deletions Lib/test/test_tstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,47 @@ 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")

# Explicit line continuations after the debug marker are part of
# the debug text, not the interpolation expression.
for template, strings, interpolation, rendered in (
(
t"""Value: {value =\
}""",
("Value: value =\\\n", ""),
(value, "value ", "r"),
"Value: value =\\\n42",
),
(
t"""Value: {value =\
!r}""",
("Value: value =\\\n", ""),
(value, "value ", "r"),
"Value: value =\\\n42",
),
(
t"""Value: {value =\
:04}""",
("Value: value =\\\n", ""),
(value, "value ", None, "04"),
"Value: value =\\\n0042",
),
(
t"""Value: {value =\
\
}""",
("Value: value =\\\n\\\n", ""),
(value, "value ", "r"),
"Value: value =\\\n\\\n42",
),
):
with self.subTest(template=template):
self.assertTStringEqual(template, strings, [interpolation])
self.assertEqual(fstring(template), rendered)

class C:
def __format__(self, spec):
return f"FORMAT-{spec}"
Expand All @@ -149,6 +186,41 @@ def __format__(self, spec):
self.assertEqual(t.interpolations[0].format_spec,
"FORMAT-value=42")

def test_interpolation_expression_whitespace(self):
x = 42
for template, expected in (
(t"{x}", "x"),
(t"{x }", "x "),
(t"{ x}", " x"),
(t"{ x }", " x "),
(t"{ x }", " x "),
(t"""{
x
}""", "\n x\n"),
(t"{ x !r}", " x "),
(t"{ x :.2f}", " x "),
(t"{ x = }", " x "),
(t"{ x = !r}", " x "),
(t"{ x = :.2f}", " x "),
(t"{x == 42 = }", "x == 42 "),
):
with self.subTest(template=template):
self.assertEqual(
template.interpolations[0].expression,
expected,
)

def test_interpolation_expression_with_reconstructed_metadata(self):
regular = t'''{(
1, # Force lexer metadata reconstruction.
"\"#")}'''
debug = t'''{(
1, # Force lexer metadata reconstruction.
"\"#")=}'''
expected = '(\n 1, \n "\\"#")'
self.assertEqual(regular.interpolations[0].expression, expected)
self.assertEqual(debug.interpolations[0].expression, expected)

def test_raw_tstrings(self):
path = r"C:\Users"
t = rt"{path}\Documents"
Expand Down Expand Up @@ -302,12 +374,12 @@ def test_triple_quoted(self):

t = t'{"""a""""#" # outside
}'
self.assertEqual(t.interpolations[0].expression, '"""a""""#"')
self.assertEqual(t.interpolations[0].expression, '"""a""""#" \n')

x, y = 1, 2
t = t'{x != y # outside
}'
self.assertEqual(t.interpolations[0].expression, 'x != y')
self.assertEqual(t.interpolations[0].expression, 'x != y \n')

d = {'a#b': 42}
t = t'''{f"{d["a#b"]}"}'''
Expand Down
9 changes: 9 additions & 0 deletions Lib/test/test_unparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,15 @@ def test_tstrings(self):
self.check_ast_roundtrip('t""')
self.check_ast_roundtrip("t'{(lambda x: x)}'")
self.check_ast_roundtrip("t'{t'{x}'}'")
self.check_ast_roundtrip(
r"""t'''{(
1, # Force lexer metadata reconstruction.
"\"#")}'''"""
)
self.check_ast_roundtrip(
r'''t"""Value: {value =\
}"""'''
)

def test_tstring_with_nonsensical_str_field(self):
# `value` suggests that the original code is `t'{test1}`, but `str` suggests otherwise
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Trailing whitespace in a t-string interpolation expression is now preserved
in :attr:`string.templatelib.Interpolation.expression`, up to the closing ``}``
or the conversion (``!``), format (``:``), or debug (``=``) delimiter.
Explicit line continuations following a debug ``=`` remain part of the debug
text and are excluded from :attr:`~string.templatelib.Interpolation.expression`.
33 changes: 26 additions & 7 deletions Parser/action_helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -1544,21 +1544,38 @@ _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 == '=') {
/* Discard whitespace and explicit line continuations after the debug "="
but preserve whitespace before it. */
while (len > 0) {
int has_newline = 0;
while (len > 0) {
Py_UCS4 c = PyUnicode_READ_CHAR(exprstr, len - 1);
if (!_PyUnicode_IsWhitespace(c)) {
break;
}
if (c == '\r' || c == '\n') {
has_newline = 1;
}
len--;
}
else {
if (!has_newline || len == 0 ||
PyUnicode_READ_CHAR(exprstr, len - 1) != '\\')
{
break;
}
len--;
}

/* Preserve unexpected metadata instead of dropping source text. */
if (len == 0 || PyUnicode_READ_CHAR(exprstr, len - 1) != '=') {
return Py_NewRef(exprstr);
}

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,
Expand Down Expand Up @@ -1589,7 +1606,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;
Expand Down
Loading