66import sys
77import textwrap
88import warnings
9+ import codeop
10+ import keyword
11+ import tokenize
12+ import io
913from contextlib import suppress
1014import _colorize
1115from _colorize import ANSIColors
@@ -1090,6 +1094,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
10901094 self .end_offset = exc_value .end_offset
10911095 self .msg = exc_value .msg
10921096 self ._is_syntax_error = True
1097+ self ._exc_metadata = getattr (exc_value , "_metadata" , None )
10931098 elif exc_type and issubclass (exc_type , ImportError ) and \
10941099 getattr (exc_value , "name_from" , None ) is not None :
10951100 wrong_name = getattr (exc_value , "name_from" , None )
@@ -1273,6 +1278,98 @@ def format_exception_only(self, *, show_group=False, _depth=0, **kwargs):
12731278 for ex in self .exceptions :
12741279 yield from ex .format_exception_only (show_group = show_group , _depth = _depth + 1 , colorize = colorize )
12751280
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+
12761373 def _format_syntax_error (self , stype , ** kwargs ):
12771374 """Format SyntaxError exceptions (internal helper)."""
12781375 # Show exactly where the problem was found.
@@ -1299,6 +1396,9 @@ def _format_syntax_error(self, stype, **kwargs):
12991396 # text = " foo\n"
13001397 # rtext = " foo"
13011398 # ltext = "foo"
1399+ with suppress (Exception ):
1400+ self ._find_keyword_typos ()
1401+ text = self .text
13021402 rtext = text .rstrip ('\n ' )
13031403 ltext = rtext .lstrip (' \n \f ' )
13041404 spaces = len (rtext ) - len (ltext )
0 commit comments