@@ -1147,16 +1147,20 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
11471147 elif exc_type and issubclass (exc_type , AttributeError ) and \
11481148 getattr (exc_value , "name" , None ) is not None :
11491149 wrong_name = getattr (exc_value , "name" , None )
1150- suggestion = _compute_suggestion_error (exc_value , exc_traceback , wrong_name )
1151- if suggestion :
1152- if suggestion .isascii ():
1153- self ._str += f". Did you mean '.{ suggestion } ' instead of '.{ wrong_name } '?"
1154- else :
1155- self ._str += f". Did you mean '.{ suggestion } ' ({ suggestion !a} ) instead of '.{ wrong_name } ' ({ wrong_name !a} )?"
1156- elif hasattr (exc_value , 'obj' ):
1150+ # Check cross-language/wrong-type hints first (more specific),
1151+ # then fall back to Levenshtein distance suggestions.
1152+ hint = None
1153+ if hasattr (exc_value , 'obj' ):
11571154 hint = _get_cross_language_hint (exc_value .obj , wrong_name )
1158- if hint :
1159- self ._str += f". { hint } "
1155+ if hint :
1156+ self ._str += f". { hint } "
1157+ else :
1158+ suggestion = _compute_suggestion_error (exc_value , exc_traceback , wrong_name )
1159+ if suggestion :
1160+ if suggestion .isascii ():
1161+ self ._str += f". Did you mean '.{ suggestion } ' instead of '.{ wrong_name } '?"
1162+ else :
1163+ self ._str += f". Did you mean '.{ suggestion } ' ({ suggestion !a} ) instead of '.{ wrong_name } ' ({ wrong_name !a} )?"
11601164 elif exc_type and issubclass (exc_type , NameError ) and \
11611165 getattr (exc_value , "name" , None ) is not None :
11621166 wrong_name = getattr (exc_value , "name" , None )
@@ -1663,32 +1667,61 @@ def print(self, *, file=None, chain=True, **kwargs):
16631667# 2. Must not be catchable by Levenshtein distance (too different from
16641668# the correct Python method name).
16651669#
1666- # Each entry maps (builtin_type, wrong_name) to a (suggestion, is_raw) tuple.
1670+ # Each entry maps a wrong method name to a list of (type, suggestion, is_raw)
1671+ # tuples. The lookup checks isinstance() so subclasses are also matched.
16671672# If is_raw is False, the suggestion is wrapped in "Did you mean '.X'?".
16681673# If is_raw is True, the suggestion is rendered as-is.
16691674#
16701675# See https://github.com/python/cpython/issues/146406.
1671- _CROSS_LANGUAGE_HINTS = frozendict ( {
1676+ _CROSS_LANGUAGE_HINTS = {
16721677 # list -- JavaScript/Ruby equivalents
1673- ( list , "push" ): ( "append" , False ),
1674- ( list , "concat" ): ( "extend" , False ),
1678+ "push" : [( list , "append" , False )] ,
1679+ "concat" : [( list , "extend" , False )] ,
16751680 # list -- Java/C# equivalents
1676- (list , "addAll" ): ("extend" , False ),
1677- (list , "contains" ): ("Use 'x in list'." , True ),
1678- # list -- wrong-type suggestion more likely means the user expected a set
1679- (list , "add" ): ("Did you mean to use a 'set' object?" , True ),
1681+ "addAll" : [(list , "extend" , False )],
1682+ "contains" : [(list , "Use 'x in list'." , True )],
1683+ # list -- wrong-type suggestion (user expected a set)
1684+ "add" : [(list , "Did you mean to use a 'set' object?" , True ),
1685+ (frozenset , "Did you mean to use a 'set' object?" , True )],
16801686 # str -- JavaScript equivalents
1681- ( str , "toUpperCase" ): ( "upper" , False ),
1682- ( str , "toLowerCase" ): ( "lower" , False ),
1683- ( str , "trimStart" ): ( "lstrip" , False ),
1684- ( str , "trimEnd" ): ( "rstrip" , False ),
1687+ "toUpperCase" : [( str , "upper" , False )] ,
1688+ "toLowerCase" : [( str , "lower" , False )] ,
1689+ "trimStart" : [( str , "lstrip" , False )] ,
1690+ "trimEnd" : [( str , "rstrip" , False )] ,
16851691 # dict -- Java/JavaScript equivalents
1686- (dict , "keySet" ): ("keys" , False ),
1687- (dict , "entrySet" ): ("items" , False ),
1688- (dict , "entries" ): ("items" , False ),
1689- (dict , "putAll" ): ("update" , False ),
1690- (dict , "put" ): ("Use d[k] = v." , True ),
1691- })
1692+ "keySet" : [(dict , "keys" , False )],
1693+ "entrySet" : [(dict , "items" , False )],
1694+ "entries" : [(dict , "items" , False )],
1695+ "putAll" : [(dict , "update" , False )],
1696+ "put" : [(dict , "Use d[k] = v." , True )],
1697+ # tuple -- mutable method on immutable type (user expected a list)
1698+ "append" : [(tuple , "Did you mean to use a 'list' object?" , True )],
1699+ "extend" : [(tuple , "Did you mean to use a 'list' object?" , True )],
1700+ "insert" : [(tuple , "Did you mean to use a 'list' object?" , True )],
1701+ "remove" : [(tuple , "Did you mean to use a 'list' object?" , True ),
1702+ (frozenset , "Did you mean to use a 'set' object?" , True )],
1703+ # frozenset -- mutable method on immutable type (user expected a set)
1704+ "discard" : [(frozenset , "Did you mean to use a 'set' object?" , True )],
1705+ # frozendict -- mutable method on immutable type (user expected a dict)
1706+ "update" : [(frozenset , "Did you mean to use a 'set' object?" , True ),
1707+ (frozendict , "Did you mean to use a 'dict' object?" , True )],
1708+ # float -- bitwise operators belong to int
1709+ "__or__" : [(float , "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'." , True )],
1710+ "__and__" : [(float , "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'." , True )],
1711+ "__xor__" : [(float , "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'." , True )],
1712+ "__lshift__" : [(float , "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'." , True )],
1713+ "__rshift__" : [(float , "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'." , True )],
1714+ # NoneType -- common methods tried on None (got None instead of expected type)
1715+ "keys" : [(type (None ), "Did you expect a 'dict'?" , True )],
1716+ "values" : [(type (None ), "Did you expect a 'dict'?" , True )],
1717+ "items" : [(type (None ), "Did you expect a 'dict'?" , True )],
1718+ "upper" : [(type (None ), "Did you expect a 'str'?" , True )],
1719+ "lower" : [(type (None ), "Did you expect a 'str'?" , True )],
1720+ "strip" : [(type (None ), "Did you expect a 'str'?" , True )],
1721+ "split" : [(type (None ), "Did you expect a 'str'?" , True )],
1722+ "sort" : [(type (None ), "Did you expect a 'list'?" , True )],
1723+ "pop" : [(type (None ), "Did you expect a 'list' or 'dict'?" , True )],
1724+ }
16921725
16931726
16941727def _substitution_cost (ch_a , ch_b ):
@@ -1753,19 +1786,21 @@ def _check_for_nested_attribute(obj, wrong_name, attrs):
17531786
17541787
17551788def _get_cross_language_hint (obj , wrong_name ):
1756- """Check if wrong_name is a common method name from another language.
1789+ """Check if wrong_name is a common method name from another language,
1790+ a mutable method on an immutable type, or a method tried on None.
17571791
1758- Only checks exact builtin types (list, str, dict) to avoid false
1759- positives on subclasses that may intentionally lack these methods.
1792+ Uses isinstance() so subclasses of builtin types also get hints.
17601793 Returns a formatted hint string, or None.
17611794 """
1762- entry = _CROSS_LANGUAGE_HINTS .get (( type ( obj ), wrong_name ) )
1763- if entry is None :
1795+ entries = _CROSS_LANGUAGE_HINTS .get (wrong_name )
1796+ if entries is None :
17641797 return None
1765- hint , is_raw = entry
1766- if is_raw :
1767- return hint
1768- return f"Did you mean '.{ hint } '?"
1798+ for check_type , hint , is_raw in entries :
1799+ if isinstance (obj , check_type ):
1800+ if is_raw :
1801+ return hint
1802+ return f"Did you mean '.{ hint } '?"
1803+ return None
17691804
17701805
17711806def _get_safe___dir__ (obj ):
0 commit comments