diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 72c80b8bfbfecc..af57b5fccc280f 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1096,6 +1096,24 @@ def test_misformed_unicode_character_name(self): 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, '', '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 + diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-23-20-15-24.gh-issue-160491.yIQ_Vf.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-23-20-15-24.gh-issue-160491.yIQ_Vf.rst new file mode 100644 index 00000000000000..2227224ba0fa2c --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-23-20-15-24.gh-issue-160491.yIQ_Vf.rst @@ -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. diff --git a/Parser/action_helpers.c b/Parser/action_helpers.c index 2f8a5d84dd9755..bb4005e2ed78e4 100644 --- a/Parser/action_helpers.c +++ b/Parser/action_helpers.c @@ -1373,7 +1373,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) { diff --git a/Parser/pegen.h b/Parser/pegen.h index 95ad2c9b7c0a84..db56e481220f52 100644 --- a/Parser/pegen.h +++ b/Parser/pegen.h @@ -212,6 +212,7 @@ typedef enum { // Error handling functions and APIs 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_incomplete_input(Parser *p); void *_PyPegen_raise_error(Parser *p, PyObject *errtype, int use_mark, const char *errmsg, ...); diff --git a/Parser/pegen_errors.c b/Parser/pegen_errors.c index 007397fa035822..ceb7a43e0ac1c5 100644 --- a/Parser/pegen_errors.c +++ b/Parser/pegen_errors.c @@ -103,6 +103,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; @@ -120,12 +126,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);