Skip to content
Merged
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
28 changes: 28 additions & 0 deletions Lib/test/test_fstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,34 @@ def test_unterminated_string(self):
r"""f'{("x}'""",
])

def test_unclosed_multiline_replacement_field(self):
for prefix in ('f', 't', 'rf', 'rt'):
for quote in ('"', "'"):
triple = quote * 3
cases = (
# The apparent closing quotes open a string in the field.
('var = "abc"\na = PREFIXQUOTE {var} extern "C" { QUOTE\n'
'b = QUOTE string QUOTE', 2),
# Parentheses and dictionaries inside the field must not
# change which opening brace the diagnostic identifies.
('a = PREFIXQUOTE{\n(QUOTE', 1),
('a = PREFIXQUOTE{\n{0: QUOTE', 1),
# Use the innermost format field or formatted string.
('a = PREFIXQUOTE{0:\n{1\nQUOTE', 2),
('a = f"{\nPREFIXQUOTE{1\nQUOTE', 2),
# Account for parentheses outside the formatted string.
('a = (PREFIXQUOTE{1\nQUOTE', 1),
)
for source, lineno in cases:
source = source.replace('PREFIX', prefix).replace('QUOTE', triple)
with self.subTest(source=source):
with self.assertRaises(SyntaxError) as cm:
compile(source, '<test>', 'exec')
self.assertEqual(
cm.exception.msg,
f"{prefix[-1]}-string: expecting '}}' to close '{{' "
f"on line {lineno}")

@unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
def test_mismatched_parens(self):
self.assertAllRaise(SyntaxError, r"closing parenthesis '\}' "
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Include the line number of the opening brace in the :exc:`SyntaxError` for
an unclosed f-string or t-string replacement field when a matching quote
on a later line is interpreted as the start of an unterminated string.
10 changes: 10 additions & 0 deletions Parser/lexer/lexer.c
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,16 @@ tok_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, struct t
tokenizer_mode *the_current_tok = TOK_GET_MODE(tok);
if (the_current_tok->quote == quote &&
the_current_tok->quote_size == quote_size) {
int level = tok->level - the_current_tok->curly_bracket_depth
+ the_current_tok->curly_bracket_expr_start_depth;
assert(level >= 0 && level < tok->level);
assert(tok->parenstack[level] == '{');
int lineno = tok->parenlinenostack[level];
if (lineno != tok->lineno) {
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok,
"%c-string: expecting '}' to close '{' on line %d",
TOK_GET_STRING_PREFIX(tok), lineno));
}
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok,
"%c-string: expecting '}'", TOK_GET_STRING_PREFIX(tok)));
}
Expand Down
Loading