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 )
@@ -1273,6 +1278,98 @@ def format_exception_only(self, *, show_group=False, _depth=0, **kwargs):
1273
1278
for ex in self .exceptions :
1274
1279
yield from ex .format_exception_only (show_group = show_group , _depth = _depth + 1 , colorize = colorize )
1275
1280
1281
+ def _find_keyword_typos (self ):
1282
+ assert self ._is_syntax_error
1283
+ try :
1284
+ import _suggestions
1285
+ except ImportError :
1286
+ _suggestions = None
1287
+
1288
+ # Only try to find keyword typos if there is no custom message
1289
+ if self .msg != "invalid syntax" and "Perhaps you forgot a comma" not in self .msg :
1290
+ return
1291
+
1292
+ if not self ._exc_metadata :
1293
+ return
1294
+
1295
+ line , offset , source = self ._exc_metadata
1296
+ end_line = int (self .lineno ) if self .lineno is not None else 0
1297
+ lines = None
1298
+ from_filename = False
1299
+
1300
+ if source is None :
1301
+ if self .filename :
1302
+ try :
1303
+ with open (self .filename ) as f :
1304
+ lines = f .read ().splitlines ()
1305
+ except Exception :
1306
+ line , end_line , offset = 0 ,1 ,0
1307
+ else :
1308
+ from_filename = True
1309
+ lines = lines if lines is not None else self .text .splitlines ()
1310
+ else :
1311
+ lines = source .splitlines ()
1312
+
1313
+ error_code = lines [line - 1 if line > 0 else 0 :end_line ]
1314
+ error_code [0 ] = error_code [0 ][offset :]
1315
+ error_code = textwrap .dedent ('\n ' .join (error_code ))
1316
+
1317
+ # Do not continue if the source is too large
1318
+ if len (error_code ) > 1024 :
1319
+ return
1320
+
1321
+ error_lines = error_code .splitlines ()
1322
+ tokens = tokenize .generate_tokens (io .StringIO (error_code ).readline )
1323
+ tokens_left_to_process = 10
1324
+ import difflib
1325
+ for token in tokens :
1326
+ start , end = token .start , token .end
1327
+ if token .type != tokenize .NAME :
1328
+ continue
1329
+ # Only consider NAME tokens on the same line as the error
1330
+ if from_filename and token .start [0 ]+ line != end_line + 1 :
1331
+ continue
1332
+ wrong_name = token .string
1333
+ if wrong_name in keyword .kwlist :
1334
+ continue
1335
+
1336
+ # Limit the number of valid tokens to consider to not spend
1337
+ # to much time in this function
1338
+ tokens_left_to_process -= 1
1339
+ if tokens_left_to_process < 0 :
1340
+ break
1341
+ # Limit the number of possible matches to try
1342
+ matches = difflib .get_close_matches (wrong_name , keyword .kwlist , n = 3 )
1343
+ if not matches and _suggestions is not None :
1344
+ suggestion = _suggestions ._generate_suggestions (keyword .kwlist , wrong_name )
1345
+ matches = [suggestion ] if suggestion is not None else matches
1346
+ for suggestion in matches :
1347
+ if not suggestion or suggestion == wrong_name :
1348
+ continue
1349
+ # Try to replace the token with the keyword
1350
+ the_lines = error_lines .copy ()
1351
+ the_line = the_lines [start [0 ] - 1 ][:]
1352
+ chars = list (the_line )
1353
+ chars [token .start [1 ]:token .end [1 ]] = suggestion
1354
+ the_lines [start [0 ] - 1 ] = '' .join (chars )
1355
+ code = '\n ' .join (the_lines )
1356
+
1357
+ # Check if it works
1358
+ try :
1359
+ codeop .compile_command (code , symbol = "exec" , flags = codeop .PyCF_ONLY_AST )
1360
+ except SyntaxError :
1361
+ continue
1362
+
1363
+ # Keep token.line but handle offsets correctly
1364
+ self .text = token .line
1365
+ self .offset = token .start [1 ] + 1
1366
+ self .end_offset = token .end [1 ] + 1
1367
+ self .lineno = start [0 ]
1368
+ self .end_lineno = end [0 ]
1369
+ self .msg = f"invalid syntax. Did you mean '{ suggestion } '?"
1370
+ return
1371
+
1372
+
1276
1373
def _format_syntax_error (self , stype , ** kwargs ):
1277
1374
"""Format SyntaxError exceptions (internal helper)."""
1278
1375
# Show exactly where the problem was found.
@@ -1299,6 +1396,9 @@ def _format_syntax_error(self, stype, **kwargs):
1299
1396
# text = " foo\n"
1300
1397
# rtext = " foo"
1301
1398
# ltext = "foo"
1399
+ with suppress (Exception ):
1400
+ self ._find_keyword_typos ()
1401
+ text = self .text
1302
1402
rtext = text .rstrip ('\n ' )
1303
1403
ltext = rtext .lstrip (' \n \f ' )
1304
1404
spaces = len (rtext ) - len (ltext )
0 commit comments