Skip to content

Commit dfbde8c

Browse files
[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 2258026) Co-authored-by: Lysandros Nikolaou <lisandrosnik@gmail.com>
1 parent 53002b9 commit dfbde8c

5 files changed

Lines changed: 41 additions & 3 deletions

File tree

‎Lib/test/test_fstring.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,24 @@ def test_misformed_unicode_character_name(self):
10461046
r"'\N{GREEK CAPITAL LETTER DELTA'",
10471047
])
10481048

1049+
def test_invalid_escape_location(self):
1050+
cases = [
1051+
(r'f"\xZZ"', (1, 3, 1, 7)),
1052+
(r'f"{x}\xZZ"', (1, 6, 1, 10)),
1053+
(r'f"a{x}b\N{NOPE}{y}"', (1, 7, 1, 16)),
1054+
(r'f"ok" f"\xZZ"', (1, 9, 1, 13)),
1055+
('f"""\\xZZ\nabc"""', (1, 5, 2, 4)),
1056+
('f"""abc\ndef\\xZZ"""', (1, 5, 2, 8)),
1057+
('x = (1,\n f"\\xZZ")', (2, 5, 2, 9)),
1058+
]
1059+
for source, location in cases:
1060+
with self.subTest(source=source):
1061+
with self.assertRaisesRegex(SyntaxError, r'\(unicode error\)') as cm:
1062+
compile(source, '<string>', 'exec')
1063+
error = cm.exception
1064+
self.assertEqual((error.lineno, error.offset,
1065+
error.end_lineno, error.end_offset), location)
1066+
10491067
def test_backslashes_in_expression_part(self):
10501068
self.assertEqual(f"{(
10511069
1 +
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix the location reported for invalid escape sequences in f-strings and
2+
t-strings. The :exc:`SyntaxError` now points at the literal part that
3+
contains the escape, instead of at the closing quote.

‎Parser/action_helpers.c‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1302,7 +1302,7 @@ _PyPegen_decode_fstring_part(Parser* p, int is_raw, expr_ty constant, Token* tok
13021302
is_raw = is_raw || strchr(bstr, '\\') == NULL;
13031303
PyObject *str = _PyPegen_decode_string(p, is_raw, bstr, len, token);
13041304
if (str == NULL) {
1305-
_Pypegen_raise_decode_error(p);
1305+
_Pypegen_raise_decode_error_known_location(p, constant);
13061306
return NULL;
13071307
}
13081308
if (_PyArena_AddPyObject(p->arena, str) < 0) {

‎Parser/pegen.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ typedef enum {
175175
} TARGETS_TYPE;
176176

177177
int _Pypegen_raise_decode_error(Parser *p);
178+
int _Pypegen_raise_decode_error_known_location(Parser *p, expr_ty a);
178179
int _Pypegen_tokenizer_error(Parser *p);
179180
void *_PyPegen_raise_error(Parser *p, PyObject *errtype, int use_mark, const char *errmsg, ...);
180181
void *_PyPegen_raise_error_known_location(Parser *p, PyObject *errtype,

‎Parser/pegen_errors.c‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ _Pypegen_tokenizer_error(Parser *p)
8181

8282
int
8383
_Pypegen_raise_decode_error(Parser *p)
84+
{
85+
return _Pypegen_raise_decode_error_known_location(p, NULL);
86+
}
87+
88+
int
89+
_Pypegen_raise_decode_error_known_location(Parser *p, expr_ty a)
8490
{
8591
assert(PyErr_Occurred());
8692
const char *errtype = NULL;
@@ -98,12 +104,22 @@ _Pypegen_raise_decode_error(Parser *p)
98104
PyErr_Fetch(&type, &value, &tback);
99105
errstr = PyObject_Str(value);
100106
if (errstr) {
101-
RAISE_SYNTAX_ERROR("(%s) %U", errtype, errstr);
107+
if (a != NULL) {
108+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "(%s) %U", errtype, errstr);
109+
}
110+
else {
111+
RAISE_SYNTAX_ERROR("(%s) %U", errtype, errstr);
112+
}
102113
Py_DECREF(errstr);
103114
}
104115
else {
105116
PyErr_Clear();
106-
RAISE_SYNTAX_ERROR("(%s) unknown error", errtype);
117+
if (a != NULL) {
118+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "(%s) unknown error", errtype);
119+
}
120+
else {
121+
RAISE_SYNTAX_ERROR("(%s) unknown error", errtype);
122+
}
107123
}
108124
Py_XDECREF(type);
109125
Py_XDECREF(value);

0 commit comments

Comments
 (0)