Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,35 @@ Improved error messages
^^^^^^^^^^^^^^
AttributeError: 'Container' object has no attribute 'area'. Did you mean '.inner.area' instead of '.area'?

* When an :exc:`AttributeError` on a builtin type has no close match via
Levenshtein distance, the error message now checks a static table of common
method names from other languages (JavaScript, Java, Ruby, C#) and suggests
the Python equivalent:

.. doctest::

>>> [1, 2, 3].push(4) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
AttributeError: 'list' object has no attribute 'push'. Did you mean '.append'?

>>> 'hello'.toUpperCase() # doctest: +ELLIPSIS
Traceback (most recent call last):
...
AttributeError: 'str' object has no attribute 'toUpperCase'. Did you mean '.upper'?

When the Python equivalent is a language construct rather than a method,
the hint describes the construct directly:

.. doctest::

>>> {}.put("a", 1) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
AttributeError: 'dict' object has no attribute 'put'. Use d[k] = v for item assignment.

(Contributed by Matt Van Horn in :gh:`146406`.)


Other language changes
======================
Expand Down
74 changes: 74 additions & 0 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -4564,6 +4564,80 @@ def __init__(self):
actual = self.get_suggestion(Outer(), 'target')
self.assertIn("'.normal.target'", actual)

def test_cross_language_list_push_suggests_append(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, it's kind of verbose to have one test per method. What about having a single test_cross_language() which runs all these tests?

actual = self.get_suggestion([], 'push')
self.assertIn("'.append'", actual)
self.assertNotIn("instead of", actual)

def test_cross_language_list_concat_suggests_extend(self):
actual = self.get_suggestion([], 'concat')
self.assertIn("'.extend'", actual)

def test_cross_language_list_addAll_suggests_extend(self):
actual = self.get_suggestion([], 'addAll')
self.assertIn("'.extend'", actual)

def test_cross_language_list_contains_suggests_in(self):
actual = self.get_suggestion([], 'contains')
self.assertIn("Use 'x in list'", actual)

def test_cross_language_list_add_suggests_set(self):
actual = self.get_suggestion([], 'add')
self.assertIn("Did you mean to use a 'set' object?", actual)

def test_cross_language_str_toUpperCase_suggests_upper(self):
actual = self.get_suggestion('', 'toUpperCase')
self.assertIn("'.upper'", actual)

def test_cross_language_str_toLowerCase_suggests_lower(self):
actual = self.get_suggestion('', 'toLowerCase')
self.assertIn("'.lower'", actual)

def test_cross_language_str_trimStart_suggests_lstrip(self):
actual = self.get_suggestion('', 'trimStart')
self.assertIn("'.lstrip'", actual)

def test_cross_language_str_trimEnd_suggests_rstrip(self):
actual = self.get_suggestion('', 'trimEnd')
self.assertIn("'.rstrip'", actual)

def test_cross_language_dict_keySet_suggests_keys(self):
actual = self.get_suggestion({}, 'keySet')
self.assertIn("'.keys'", actual)

def test_cross_language_dict_entrySet_suggests_items(self):
actual = self.get_suggestion({}, 'entrySet')
self.assertIn("'.items'", actual)

def test_cross_language_dict_putAll_suggests_update(self):
actual = self.get_suggestion({}, 'putAll')
self.assertIn("'.update'", actual)

def test_cross_language_dict_entries_suggests_items(self):
actual = self.get_suggestion({}, 'entries')
self.assertIn("'.items'", actual)

def test_cross_language_dict_put_suggests_bracket(self):
actual = self.get_suggestion({}, 'put')
self.assertIn("d[k] = v", actual)

def test_cross_language_levenshtein_takes_priority(self):
# Levenshtein catches trim->strip and indexOf->index before
# the cross-language table is consulted
actual = self.get_suggestion('', 'trim')
self.assertIn("'.strip'", actual)

def test_cross_language_no_hint_for_unknown_attr(self):
actual = self.get_suggestion([], 'completely_unknown_method')
self.assertNotIn("Did you mean", actual)

def test_cross_language_not_triggered_for_subclasses(self):
# Only exact builtin types, not subclasses
class MyList(list):
pass
actual = self.get_suggestion(MyList(), 'push')
self.assertNotIn("append", actual)

def make_module(self, code):
tmpdir = Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, tmpdir)
Expand Down
56 changes: 56 additions & 0 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,10 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
self._str += f". Did you mean '.{suggestion}' instead of '.{wrong_name}'?"
else:
self._str += f". Did you mean '.{suggestion}' ({suggestion!a}) instead of '.{wrong_name}' ({wrong_name!a})?"
elif hasattr(exc_value, 'obj'):
hint = _get_cross_language_hint(exc_value.obj, wrong_name)
if hint:
self._str += f". {hint}"
elif exc_type and issubclass(exc_type, NameError) and \
getattr(exc_value, "name", None) is not None:
wrong_name = getattr(exc_value, "name", None)
Expand Down Expand Up @@ -1649,6 +1653,42 @@ def print(self, *, file=None, chain=True, **kwargs):
_MOVE_COST = 2
_CASE_COST = 1

# Cross-language method suggestions for builtin types.
# Consulted as a fallback when Levenshtein-based suggestions find no match.
#
# Inclusion criteria:
# 1. Must have evidence of real cross-language confusion (Stack Overflow
# traffic, bug reports in production repos, developer survey data).
# 2. Must not be catchable by Levenshtein distance (too different from
# the correct Python method name).
#
# Each entry maps (builtin_type, wrong_name) to a (suggestion, is_raw) tuple.
# If is_raw is False, the suggestion is wrapped in "Did you mean '.X'?".
# If is_raw is True, the suggestion is rendered as-is.
#
# See https://github.com/python/cpython/issues/146406.
_CROSS_LANGUAGE_HINTS = {
# list -- JavaScript/Ruby equivalents
(list, "push"): ("append", False),
(list, "concat"): ("extend", False),
# list -- Java/C# equivalents
(list, "addAll"): ("extend", False),
(list, "contains"): ("Use 'x in list' to check membership.", True),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, maybe make it shorter:

Suggested change
(list, "contains"): ("Use 'x in list' to check membership.", True),
(list, "contains"): ("Use 'x in list'.", True),

# list -- wrong-type suggestion more likely means the user expected a set
(list, "add"): ("Did you mean to use a 'set' object?", True),
# str -- JavaScript equivalents
(str, "toUpperCase"): ("upper", False),
(str, "toLowerCase"): ("lower", False),
(str, "trimStart"): ("lstrip", False),
(str, "trimEnd"): ("rstrip", False),
# dict -- Java/JavaScript equivalents
(dict, "keySet"): ("keys", False),
(dict, "entrySet"): ("items", False),
(dict, "entries"): ("items", False),
(dict, "putAll"): ("update", False),
(dict, "put"): ("Use d[k] = v for item assignment.", True),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same suggestion here:

Suggested change
(dict, "put"): ("Use d[k] = v for item assignment.", True),
(dict, "put"): ("Use d[k] = v.", True),

}


def _substitution_cost(ch_a, ch_b):
if ch_a == ch_b:
Expand Down Expand Up @@ -1711,6 +1751,22 @@ def _check_for_nested_attribute(obj, wrong_name, attrs):
return None


def _get_cross_language_hint(obj, wrong_name):
"""Check if wrong_name is a common method name from another language.
Only checks exact builtin types (list, str, dict) to avoid false
positives on subclasses that may intentionally lack these methods.
Returns a formatted hint string, or None.
"""
entry = _CROSS_LANGUAGE_HINTS.get((type(obj), wrong_name))
if entry is None:
return None
hint, is_raw = entry
if is_raw:
return hint
return f"Did you mean '.{hint}'?"


def _get_safe___dir__(obj):
# Use obj.__dir__() to avoid a TypeError when calling dir(obj).
# See gh-131001 and gh-139933.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Cross-language method suggestions are now shown for :exc:`AttributeError` on
builtin types when the existing Levenshtein-based suggestions find no match.
For example, ``[].push()`` now suggests ``append`` and
``"".toUpperCase()`` suggests ``upper``.
Loading