|
6 | 6 | import sys
|
7 | 7 | import textwrap
|
8 | 8 | import warnings
|
| 9 | +import codeop |
| 10 | +import keyword |
| 11 | +import tokenize |
| 12 | +import io |
9 | 13 | from contextlib import suppress
|
10 | 14 | import _colorize
|
11 | 15 | from _colorize import ANSIColors
|
@@ -1090,6 +1094,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
|
1090 | 1094 | self.end_offset = exc_value.end_offset
|
1091 | 1095 | self.msg = exc_value.msg
|
1092 | 1096 | self._is_syntax_error = True
|
| 1097 | + self._exc_metadata = getattr(exc_value, "_metadata", None) |
1093 | 1098 | elif exc_type and issubclass(exc_type, ImportError) and \
|
1094 | 1099 | getattr(exc_value, "name_from", None) is not None:
|
1095 | 1100 | wrong_name = getattr(exc_value, "name_from", None)
|
@@ -1272,6 +1277,86 @@ def format_exception_only(self, *, show_group=False, _depth=0, **kwargs):
|
1272 | 1277 | if self.exceptions and show_group:
|
1273 | 1278 | for ex in self.exceptions:
|
1274 | 1279 | yield from ex.format_exception_only(show_group=show_group, _depth=_depth+1, colorize=colorize)
|
| 1280 | + |
| 1281 | + def _find_keyword_typos(self): |
| 1282 | + try: |
| 1283 | + import _suggestions |
| 1284 | + except ImportError: |
| 1285 | + return |
| 1286 | + |
| 1287 | + assert self._is_syntax_error |
| 1288 | + |
| 1289 | + # Only try to find keyword typos if there is no custom message |
| 1290 | + if self.msg != "invalid syntax": |
| 1291 | + return |
| 1292 | + |
| 1293 | + if not self._exc_metadata: |
| 1294 | + return |
| 1295 | + |
| 1296 | + line, offset, source = self._exc_metadata |
| 1297 | + end_line = int(self.lineno) if self.lineno is not None else 0 |
| 1298 | + lines = None |
| 1299 | + from_filename = False |
| 1300 | + |
| 1301 | + if source is None: |
| 1302 | + if self.filename: |
| 1303 | + try: |
| 1304 | + with open(self.filename) as f: |
| 1305 | + lines = f.readlines() |
| 1306 | + except Exception: |
| 1307 | + line, end_line, offset = 0,1,0 |
| 1308 | + else: |
| 1309 | + from_filename = True |
| 1310 | + lines = lines if lines is not None else self.text.splitlines() |
| 1311 | + else: |
| 1312 | + lines = source.splitlines() |
| 1313 | + |
| 1314 | + error_code = lines[line -1 if line > 0 else 0:end_line] |
| 1315 | + error_code[0] = error_code[0][offset:] |
| 1316 | + error_code = textwrap.dedent(''.join(error_code)) |
| 1317 | + |
| 1318 | + # Do not continue if the source is too large |
| 1319 | + if len(error_code) > 1024: |
| 1320 | + return |
| 1321 | + |
| 1322 | + tokens = tokenize.generate_tokens(io.StringIO(error_code).readline) |
| 1323 | + tokens_left_to_process = 10 |
| 1324 | + for token in tokens: |
| 1325 | + tokens_left_to_process -= 1 |
| 1326 | + if tokens_left_to_process < 0: |
| 1327 | + break |
| 1328 | + start, end = token.start, token.end |
| 1329 | + if token.type != tokenize.NAME: |
| 1330 | + continue |
| 1331 | + if from_filename and token.start[0]+line != end_line+1: |
| 1332 | + continue |
| 1333 | + wrong_name = token.string |
| 1334 | + if wrong_name in keyword.kwlist: |
| 1335 | + continue |
| 1336 | + suggestion = _suggestions._generate_suggestions(keyword.kwlist, wrong_name) |
| 1337 | + if not suggestion or suggestion == wrong_name: |
| 1338 | + continue |
| 1339 | + # Try to replace the token with the keyword |
| 1340 | + the_lines = error_code.splitlines() |
| 1341 | + the_line = the_lines[start[0] - 1] |
| 1342 | + chars = list(the_line) |
| 1343 | + chars[token.start[1]:token.end[1]] = suggestion |
| 1344 | + the_lines[start[0] - 1] = ''.join(chars) |
| 1345 | + code = ''.join(the_lines) |
| 1346 | + # Check if it works |
| 1347 | + try: |
| 1348 | + codeop.compile_command(code, symbol="exec", flags=codeop.PyCF_ONLY_AST) |
| 1349 | + except SyntaxError as e: |
| 1350 | + continue |
| 1351 | + # Keep token.line but handle offsets correctly |
| 1352 | + self.text = token.line |
| 1353 | + self.offset = token.start[1] + 1 |
| 1354 | + self.end_offset = token.end[1] + 1 |
| 1355 | + self.lineno = start[0] |
| 1356 | + self.end_lineno = end[0] |
| 1357 | + self.msg = f"invalid syntax. Did you mean '{suggestion}'?" |
| 1358 | + return |
| 1359 | + |
1275 | 1360 |
|
1276 | 1361 | def _format_syntax_error(self, stype, **kwargs):
|
1277 | 1362 | """Format SyntaxError exceptions (internal helper)."""
|
@@ -1299,6 +1384,9 @@ def _format_syntax_error(self, stype, **kwargs):
|
1299 | 1384 | # text = " foo\n"
|
1300 | 1385 | # rtext = " foo"
|
1301 | 1386 | # ltext = "foo"
|
| 1387 | + with suppress(Exception): |
| 1388 | + self._find_keyword_typos() |
| 1389 | + text = self.text |
1302 | 1390 | rtext = text.rstrip('\n')
|
1303 | 1391 | ltext = rtext.lstrip(' \n\f')
|
1304 | 1392 | spaces = len(rtext) - len(ltext)
|
|
0 commit comments