diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 0cee756958f3ad1..c53bb35c4987769 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -279,14 +279,14 @@ def testSyntaxErrorOffset(self): check('try:\n pass\nexcept*:\n pass\nexcept* ValueError:\n pass', 3, 8) # Errors thrown by the tokenizer - check('(0x+1)', 1, 3) - check('x = 0xI', 1, 6) + check('(0x+1)', 1, 4) + check('x = 0xI', 1, 7) check('0010 + 2', 1, 1) check('x = 32e-+4', 1, 8) check('x = 0o9', 1, 7) - check('\u03b1 = 0xI', 1, 6) - check(b'\xce\xb1 = 0xI', 1, 6) - check(b'# -*- coding: iso8859-7 -*-\n\xe1 = 0xI', 2, 6, + check('\u03b1 = 0xI', 1, 7) + check(b'\xce\xb1 = 0xI', 1, 7) + check(b'# -*- coding: iso8859-7 -*-\n\xe1 = 0xI', 2, 7, encoding='iso8859-7') check(b"""if 1: def foo(): diff --git a/Lib/test/test_grammar.py b/Lib/test/test_grammar.py index ebcd98a0a37776d..1861249f1409fb3 100644 --- a/Lib/test/test_grammar.py +++ b/Lib/test/test_grammar.py @@ -21,6 +21,7 @@ import_helper, skip_emscripten_stack_overflow, skip_wasi_stack_overflow, + subTests, ) from test.support.numbers import ( VALID_UNDERSCORE_LITERALS, @@ -180,6 +181,27 @@ def check(test, error=False): check("[0x1for x in ()]") check("[0xfor x in ()]") + @subTests('source,offset,msg', + [("0xfg", 4, "hexadecimal"), + ("0x9g", 4, "hexadecimal"), + ("0b1z", 4, "binary"), + ("0o7q", 4, "octal"), + ("9spam", 2, " decimal"), + ("0xfspam", 4, "hexadecimal"), + ("1.0x", 4, " decimal"), + ("1e3w", 4, " decimal"), + ("1jz", 3, "imaginary"), + ("0xI", 3, "hexadecimal"), + ("0bz", 3, "binary"), + ]) + def test_end_of_numerical_literals_offset(self, source, offset, msg): + # gh-149277: verify the error caret points at the first invalid + # character, not the last valid digit. + with self.assertRaises(SyntaxError) as cm: + compile(source, "", "eval") + self.assertEqual(cm.exception.offset, offset) + self.assertIn(msg, cm.exception.msg) + def test_string_literals(self): x = ''; y = ""; self.assertTrue(len(x) == 0 and x == y) x = '\''; y = "'"; self.assertTrue(len(x) == 1 and x == y and ord(x) == 39) diff --git a/Lib/test/test_tokenize.py b/Lib/test/test_tokenize.py index e2db09d61f409bd..39e70bd82c4c556 100644 --- a/Lib/test/test_tokenize.py +++ b/Lib/test/test_tokenize.py @@ -2277,7 +2277,7 @@ def test_extra_tokens_relaxes_lexer_errors(self): cases = [ ( "2sin(x)", - ("invalid decimal literal", (1, 1)), + ("invalid decimal literal", (1, 2)), [ (token.NUMBER, "2", (1, 0), (1, 1)), (token.NAME, "sin", (1, 1), (1, 4)), diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-11-08-16-20.gh-issue-149277.dYRtaF.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-11-08-16-20.gh-issue-149277.dYRtaF.rst new file mode 100644 index 000000000000000..c8f4bc682f526f4 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-11-08-16-20.gh-issue-149277.dYRtaF.rst @@ -0,0 +1,2 @@ +Fix the :exc:`SyntaxError` caret position for invalid numeric literals to +point at the first invalid character. diff --git a/Parser/lexer/number.c b/Parser/lexer/number.c index 8bca8cbb9adfe5e..02e11d12616b04a 100644 --- a/Parser/lexer/number.c +++ b/Parser/lexer/number.c @@ -80,7 +80,6 @@ verify_end_of_number(struct tok_state *tok, int c, const char *kind) { } else /* In future releases, only error will remain. */ if (c < 128 && is_potential_identifier_char(c)) { - tok_backup(tok, c); _PyTokenizer_syntaxerror(tok, "invalid %s literal", kind); return 0; } @@ -130,7 +129,6 @@ _PyLexer_scan_number(struct tok_state *tok, struct token *token, int c, c = tok_nextc(tok); } if (!Py_ISXDIGIT(c)) { - tok_backup(tok, c); return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok, "invalid hexadecimal literal")); } do { @@ -154,7 +152,6 @@ _PyLexer_scan_number(struct tok_state *tok, struct token *token, int c, "invalid digit '%c' in octal literal", c)); } else { - tok_backup(tok, c); return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok, "invalid octal literal")); } } @@ -182,7 +179,6 @@ _PyLexer_scan_number(struct tok_state *tok, struct token *token, int c, return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok, "invalid digit '%c' in binary literal", c)); } else { - tok_backup(tok, c); return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok, "invalid binary literal")); } }