[3.14] gh-160491: Fix location of invalid escapes in f-strings and t-strings (GH-160493) (#160531)

Literal parts of f-strings and t-strings are decoded only after the
whole string has been parsed, so a decode error took the location of
the last token. The resulting SyntaxError pointed at the closing quote,
and for multi-line strings it could be on the wrong line.

Raise the error at the literal part being decoded instead.
(cherry picked from commit 2258026edd)

Co-authored-by: Lysandros Nikolaou <lisandrosnik@gmail.com>
This commit is contained in:
Miss Islington (bot)andLysandros Nikolaou authored and GitHub committed 2026-10-23 16:32:56 -07:00
1 parent 53002b9221
commit dfbde8cd5b
5 files changed
+41 -3

No files matched your search

+18
View File
@@ -1046,6 +1046,24 @@ except Exception:
r"'\N{GREEK CAPITAL LETTER DELTA'",
])
def test_invalid_escape_location(self):
cases = [
(r'f"\xZZ"', (1, 3, 1, 7)),
(r'f"{x}\xZZ"', (1, 6, 1, 10)),
(r'f"a{x}b\N{NOPE}{y}"', (1, 7, 1, 16)),
(r'f"ok" f"\xZZ"', (1, 9, 1, 13)),
('f"""\\xZZ\nabc"""', (1, 5, 2, 4)),
('f"""abc\ndef\\xZZ"""', (1, 5, 2, 8)),
('x = (1,\n f"\\xZZ")', (2, 5, 2, 9)),
]
for source, location in cases:
with self.subTest(source=source):
with self.assertRaisesRegex(SyntaxError, r'\(unicode error\)') as cm:
compile(source, '<string>', 'exec')
error = cm.exception
self.assertEqual((error.lineno, error.offset,
error.end_lineno, error.end_offset), location)
def test_backslashes_in_expression_part(self):
self.assertEqual(f"{(
1 +
@@ -0,0 +1,3 @@
Fix the location reported for invalid escape sequences in f-strings and
t-strings. The :exc:`SyntaxError` now points at the literal part that
contains the escape, instead of at the closing quote.
+1 -1
View File
@@ -1302,7 +1302,7 @@ _PyPegen_decode_fstring_part(Parser* p, int is_raw, expr_ty constant, Token* tok
is_raw = is_raw || strchr(bstr, '\\') == NULL;
PyObject *str = _PyPegen_decode_string(p, is_raw, bstr, len, token);
if (str == NULL) {
_Pypegen_raise_decode_error(p);
_Pypegen_raise_decode_error_known_location(p, constant);
return NULL;
}
if (_PyArena_AddPyObject(p->arena, str) < 0) {
+1
View File
@@ -175,6 +175,7 @@ typedef enum {
} TARGETS_TYPE;
int _Pypegen_raise_decode_error(Parser *p);
int _Pypegen_raise_decode_error_known_location(Parser *p, expr_ty a);
int _Pypegen_tokenizer_error(Parser *p);
void *_PyPegen_raise_error(Parser *p, PyObject *errtype, int use_mark, const char *errmsg, ...);
void *_PyPegen_raise_error_known_location(Parser *p, PyObject *errtype,
+18 -2
View File
@@ -81,6 +81,12 @@ _Pypegen_tokenizer_error(Parser *p)
int
_Pypegen_raise_decode_error(Parser *p)
{
return _Pypegen_raise_decode_error_known_location(p, NULL);
}
int
_Pypegen_raise_decode_error_known_location(Parser *p, expr_ty a)
{
assert(PyErr_Occurred());
const char *errtype = NULL;
@@ -98,12 +104,22 @@ _Pypegen_raise_decode_error(Parser *p)
PyErr_Fetch(&type, &value, &tback);
errstr = PyObject_Str(value);
if (errstr) {
RAISE_SYNTAX_ERROR("(%s) %U", errtype, errstr);
if (a != NULL) {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "(%s) %U", errtype, errstr);
}
else {
RAISE_SYNTAX_ERROR("(%s) %U", errtype, errstr);
}
Py_DECREF(errstr);
}
else {
PyErr_Clear();
RAISE_SYNTAX_ERROR("(%s) unknown error", errtype);
if (a != NULL) {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "(%s) unknown error", errtype);
}
else {
RAISE_SYNTAX_ERROR("(%s) unknown error", errtype);
}
}
Py_XDECREF(type);
Py_XDECREF(value);