@@ -117,6 +117,14 @@ def __init__(
117
117
end_lno = exc_value .end_lineno
118
118
self .end_lineno = str (end_lno ) if end_lno is not None else None
119
119
self .end_offset = exc_value .end_offset
120
+ elif (
121
+ exc_type
122
+ and issubclass (exc_type , (NameError , AttributeError ))
123
+ and getattr (exc_value , "name" , None ) is not None
124
+ ):
125
+ suggestion = _compute_suggestion_error (exc_value , exc_traceback )
126
+ if suggestion :
127
+ self ._str += f". Did you mean: '{ suggestion } '?"
120
128
121
129
if lookup_lines :
122
130
# Force all lines in the stack to be loaded
@@ -416,3 +424,127 @@ def print_exc(
416
424
) -> None :
417
425
value = sys .exc_info ()[1 ]
418
426
print_exception (value , limit , file , chain )
427
+
428
+
429
+ # Python levenshtein edit distance code for NameError/AttributeError
430
+ # suggestions, backported from 3.12
431
+
432
+ _MAX_CANDIDATE_ITEMS = 750
433
+ _MAX_STRING_SIZE = 40
434
+ _MOVE_COST = 2
435
+ _CASE_COST = 1
436
+
437
+
438
+ def _substitution_cost (ch_a , ch_b ):
439
+ if ch_a == ch_b :
440
+ return 0
441
+ if ch_a .lower () == ch_b .lower ():
442
+ return _CASE_COST
443
+ return _MOVE_COST
444
+
445
+
446
+ def _compute_suggestion_error (exc_value , tb ):
447
+ wrong_name = getattr (exc_value , "name" , None )
448
+ if wrong_name is None or not isinstance (wrong_name , str ):
449
+ return None
450
+ if isinstance (exc_value , AttributeError ):
451
+ obj = exc_value .obj
452
+ try :
453
+ d = dir (obj )
454
+ except Exception :
455
+ return None
456
+ else :
457
+ assert isinstance (exc_value , NameError )
458
+ # find most recent frame
459
+ if tb is None :
460
+ return None
461
+ while tb .tb_next is not None :
462
+ tb = tb .tb_next
463
+ frame = tb .tb_frame
464
+
465
+ d = list (frame .f_locals ) + list (frame .f_globals ) + list (frame .f_builtins )
466
+ if len (d ) > _MAX_CANDIDATE_ITEMS :
467
+ return None
468
+ wrong_name_len = len (wrong_name )
469
+ if wrong_name_len > _MAX_STRING_SIZE :
470
+ return None
471
+ best_distance = wrong_name_len
472
+ suggestion = None
473
+ for possible_name in d :
474
+ if possible_name == wrong_name :
475
+ # A missing attribute is "found". Don't suggest it (see GH-88821).
476
+ continue
477
+ # No more than 1/3 of the involved characters should need changed.
478
+ max_distance = (len (possible_name ) + wrong_name_len + 3 ) * _MOVE_COST // 6
479
+ # Don't take matches we've already beaten.
480
+ max_distance = min (max_distance , best_distance - 1 )
481
+ current_distance = _levenshtein_distance (
482
+ wrong_name , possible_name , max_distance
483
+ )
484
+ if current_distance > max_distance :
485
+ continue
486
+ if not suggestion or current_distance < best_distance :
487
+ suggestion = possible_name
488
+ best_distance = current_distance
489
+ return suggestion
490
+
491
+
492
+ def _levenshtein_distance (a , b , max_cost ):
493
+ # A Python implementation of Python/suggestions.c:levenshtein_distance.
494
+
495
+ # Both strings are the same
496
+ if a == b :
497
+ return 0
498
+
499
+ # Trim away common affixes
500
+ pre = 0
501
+ while a [pre :] and b [pre :] and a [pre ] == b [pre ]:
502
+ pre += 1
503
+ a = a [pre :]
504
+ b = b [pre :]
505
+ post = 0
506
+ while a [: post or None ] and b [: post or None ] and a [post - 1 ] == b [post - 1 ]:
507
+ post -= 1
508
+ a = a [: post or None ]
509
+ b = b [: post or None ]
510
+ if not a or not b :
511
+ return _MOVE_COST * (len (a ) + len (b ))
512
+ if len (a ) > _MAX_STRING_SIZE or len (b ) > _MAX_STRING_SIZE :
513
+ return max_cost + 1
514
+
515
+ # Prefer shorter buffer
516
+ if len (b ) < len (a ):
517
+ a , b = b , a
518
+
519
+ # Quick fail when a match is impossible
520
+ if (len (b ) - len (a )) * _MOVE_COST > max_cost :
521
+ return max_cost + 1
522
+
523
+ # Instead of producing the whole traditional len(a)-by-len(b)
524
+ # matrix, we can update just one row in place.
525
+ # Initialize the buffer row
526
+ row = list (range (_MOVE_COST , _MOVE_COST * (len (a ) + 1 ), _MOVE_COST ))
527
+
528
+ result = 0
529
+ for bindex in range (len (b )):
530
+ bchar = b [bindex ]
531
+ distance = result = bindex * _MOVE_COST
532
+ minimum = sys .maxsize
533
+ for index in range (len (a )):
534
+ # 1) Previous distance in this row is cost(b[:b_index], a[:index])
535
+ substitute = distance + _substitution_cost (bchar , a [index ])
536
+ # 2) cost(b[:b_index], a[:index+1]) from previous row
537
+ distance = row [index ]
538
+ # 3) existing result is cost(b[:b_index+1], a[index])
539
+
540
+ insert_delete = min (result , distance ) + _MOVE_COST
541
+ result = min (insert_delete , substitute )
542
+
543
+ # cost(b[:b_index+1], a[:index+1])
544
+ row [index ] = result
545
+ if result < minimum :
546
+ minimum = result
547
+ if minimum > max_cost :
548
+ # Everything in this row is too big, so bail early.
549
+ return max_cost + 1
550
+ return result
0 commit comments