diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 8e4c28562a6cbe..cd5f513503d9f7 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1827,6 +1827,7 @@ class TestKeywordTypoSuggestions(unittest.TestCase): ("function f():", "def"), ("func f():", "def"), ("void f():", "def"), + (f"{"a="*10}0;tpye x = int;{"z="*10}1", "type"), ] def test_keyword_suggestions_from_file(self): diff --git a/Lib/traceback.py b/Lib/traceback.py index dcdab1f12e9a16..1c4427c164170a 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -2,6 +2,7 @@ import collections.abc import functools +import heapq import itertools import linecache import os @@ -19,6 +20,7 @@ from contextlib import suppress lazy import _colorize +lazy import difflib try: from _missing_stdlib_info import _MISSING_STDLIB_MODULE_MESSAGES @@ -1427,10 +1429,9 @@ def _find_keyword_typos(self): if not self._exc_metadata: return - line, offset, source = self._exc_metadata + line, _, source = self._exc_metadata end_line = int(self.lineno) if self.lineno is not None else 0 lines = None - from_filename = False if source is None: if self.filename: @@ -1438,9 +1439,7 @@ def _find_keyword_typos(self): with open(self.filename) as f: lines = f.read().splitlines() except Exception: - line, end_line, offset = 0,1,0 - else: - from_filename = True + line, end_line, _ = 0,1,0 lines = lines if lines is not None else self.text.splitlines() else: lines = source.splitlines() @@ -1462,26 +1461,32 @@ def _find_keyword_typos(self): return # Original code compiles or is incomplete - can't validate fixes error_lines = error_code.splitlines() - tokens = tokenize.generate_tokens(io.StringIO(error_code).readline) + tokens = [] + offset = self.end_offset + try: + for token in tokenize.generate_tokens(io.StringIO(error_code).readline): + if token.type != tokenize.NAME: + continue + if keyword.iskeyword(token.string): + continue + # Only consider NAME tokens on the same line as the error + the_end = end_line if line == 0 else end_line + 1 + if token.start[0] + line != the_end: + continue + rank = abs(offset - token.end[1]) + heapq.heappush(tokens, (rank, token)) + except Exception: + pass tokens_left_to_process = 10 - import difflib - for token in tokens: - start, end = token.start, token.end - if token.type != tokenize.NAME: - continue - # Only consider NAME tokens on the same line as the error - the_end = end_line if line == 0 else end_line + 1 - if from_filename and token.start[0]+line != the_end: - continue + while tokens: + rank, token = heapq.heappop(tokens) wrong_name = token.string - if wrong_name in keyword.kwlist: - continue - # Limit the number of valid tokens to consider to not spend # to much time in this function tokens_left_to_process -= 1 if tokens_left_to_process < 0: break + start, end = token.start, token.end # Limit the number of possible matches to try max_matches = 3 matches = [] diff --git a/Misc/NEWS.d/next/Library/2026-08-19-13-07-18.gh-issue-156047.4l4U9N.rst b/Misc/NEWS.d/next/Library/2026-08-19-13-07-18.gh-issue-156047.4l4U9N.rst new file mode 100644 index 00000000000000..7e77a2ca279cb7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-19-13-07-18.gh-issue-156047.4l4U9N.rst @@ -0,0 +1,2 @@ +When looking for possibly misspelled Python keywords after a :exc:`SyntaxError`, +candidate names are now ranked to improve accuracy. Patch by Bartosz Sławecki.