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
21 changes: 21 additions & 0 deletions Lib/test/test_fstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,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):
Expand Down Expand Up @@ -1858,6 +1878,7 @@ def test_gh129093(self):
def test_newlines_in_format_specifiers(self):
cases = [
"""f'{1:d\n}'""",
"""f'{x:{y}\n}'""",
"""f'__{
1:d
}__'""",
Expand Down
6 changes: 1 addition & 5 deletions Lib/test/test_tokenize.py
Original file line number Diff line number Diff line change
Expand Up @@ -2552,11 +2552,7 @@ def test_degraded_fstring_format_spec(self):
self.assertEqual(
[(tok.string, tok.start, tok.end)
for tok in tokens if tok.type == token.FSTRING_MIDDLE],
[
("{", (1, 8), (1, 9)),
("3", (1, 10), (1, 11)),
("}", (1, 12), (1, 13)),
],
[("", (1, 13), (1, 13))],
)

tokens = self._get_tokens('f"{x:{y}}"')
Expand Down
18 changes: 17 additions & 1 deletion Lib/test/test_tstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ def test_format_specifiers(self):
)
self.assertEqual(fstring(t), "Pi: 3.14")

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()
Expand Down Expand Up @@ -313,7 +326,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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Correctly parse braces and report invalid newlines after nested replacement
fields in f-string and t-string format specifiers.
4 changes: 3 additions & 1 deletion Parser/lexer/string.c
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
PyObject *res;
if (comments != NULL && comments->count > 0) {
Py_ssize_t stripped_size = expr_len;
_PyTok_Off previous_end = state->expr_span.start;

Check warning on line 81 in Parser/lexer/string.c

View workflow job for this annotation

GitHub Actions / Address sanitizer (ubuntu-26.04)

variable ‘previous_end’ set but not used [-Wunused-but-set-variable]

Check warning on line 81 in Parser/lexer/string.c

View workflow job for this annotation

GitHub Actions / Cross build Linux

variable ‘previous_end’ set but not used [-Wunused-but-set-variable]

Check warning on line 81 in Parser/lexer/string.c

View workflow job for this annotation

GitHub Actions / Ubuntu (installed) / build, install and test

variable ‘previous_end’ set but not used [-Wunused-but-set-variable]
Py_ssize_t comment_count = 0;
for (Py_ssize_t i = 0; i < comments->count; i++) {
_PyTok_Span comment = comments->spans[i];
Expand Down Expand Up @@ -188,7 +188,9 @@
}
if (c == '}' && depth == state->replacement_depth - 1) {
state->replacement_depth--;
state->mode = FTSTRING_MODE_MIDDLE;
state->mode = state->replacement_depth
? FTSTRING_MODE_FORMAT_SPEC
: FTSTRING_MODE_MIDDLE;
state->debug_expr = 0;
}
return 0;
Expand Down
Loading