Skip to content

Commit 7adad8b

Browse files
committed
Add cross-language method suggestions for builtin AttributeError
When Levenshtein-based suggestions find no match for an AttributeError on list, str, or dict, check a static table of common method names from JavaScript, Java, C#, and Ruby. For example, [].push() now suggests .append(), "".toUpperCase() suggests .upper(), and {}.keySet() suggests .keys(). The list.add() case suggests using a set instead of suggesting .append(), since .add() is a set method and the user may have passed a list where a set was expected (per discussion with Serhiy Storchaka, Terry Reedy, and Paul Moore). Design: flat (type, attr) -> suggestion text table, no runtime introspection. Only exact builtin types are matched to avoid false positives on subclasses. Discussion: https://discuss.python.org/t/106632
1 parent 3364e7e commit 7adad8b

File tree

3 files changed

+127
-0
lines changed

3 files changed

+127
-0
lines changed

Lib/test/test_traceback.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4564,6 +4564,67 @@ def __init__(self):
45644564
actual = self.get_suggestion(Outer(), 'target')
45654565
self.assertIn("'.normal.target'", actual)
45664566

4567+
def test_cross_language_list_push_suggests_append(self):
4568+
actual = self.get_suggestion([], 'push')
4569+
self.assertIn("'.append'", actual)
4570+
4571+
def test_cross_language_list_concat_suggests_extend(self):
4572+
actual = self.get_suggestion([], 'concat')
4573+
self.assertIn("'.extend'", actual)
4574+
4575+
def test_cross_language_list_addAll_suggests_extend(self):
4576+
actual = self.get_suggestion([], 'addAll')
4577+
self.assertIn("'.extend'", actual)
4578+
4579+
def test_cross_language_list_add_suggests_set(self):
4580+
actual = self.get_suggestion([], 'add')
4581+
self.assertIn("Did you mean to use a set?", actual)
4582+
4583+
def test_cross_language_str_toUpperCase_suggests_upper(self):
4584+
actual = self.get_suggestion('', 'toUpperCase')
4585+
self.assertIn("'.upper'", actual)
4586+
4587+
def test_cross_language_str_toLowerCase_suggests_lower(self):
4588+
actual = self.get_suggestion('', 'toLowerCase')
4589+
self.assertIn("'.lower'", actual)
4590+
4591+
def test_cross_language_str_trimStart_suggests_lstrip(self):
4592+
actual = self.get_suggestion('', 'trimStart')
4593+
self.assertIn("'.lstrip'", actual)
4594+
4595+
def test_cross_language_str_trimEnd_suggests_rstrip(self):
4596+
actual = self.get_suggestion('', 'trimEnd')
4597+
self.assertIn("'.rstrip'", actual)
4598+
4599+
def test_cross_language_dict_keySet_suggests_keys(self):
4600+
actual = self.get_suggestion({}, 'keySet')
4601+
self.assertIn("'.keys'", actual)
4602+
4603+
def test_cross_language_dict_entrySet_suggests_items(self):
4604+
actual = self.get_suggestion({}, 'entrySet')
4605+
self.assertIn("'.items'", actual)
4606+
4607+
def test_cross_language_dict_putAll_suggests_update(self):
4608+
actual = self.get_suggestion({}, 'putAll')
4609+
self.assertIn("'.update'", actual)
4610+
4611+
def test_cross_language_levenshtein_takes_priority(self):
4612+
# Levenshtein catches trim->strip and indexOf->index before
4613+
# the cross-language table is consulted
4614+
actual = self.get_suggestion('', 'trim')
4615+
self.assertIn("'.strip'", actual)
4616+
4617+
def test_cross_language_no_hint_for_unknown_attr(self):
4618+
actual = self.get_suggestion([], 'completely_unknown_method')
4619+
self.assertNotIn("Did you mean", actual)
4620+
4621+
def test_cross_language_not_triggered_for_subclasses(self):
4622+
# Only exact builtin types, not subclasses
4623+
class MyList(list):
4624+
pass
4625+
actual = self.get_suggestion(MyList(), 'push')
4626+
self.assertNotIn("append", actual)
4627+
45674628
def make_module(self, code):
45684629
tmpdir = Path(tempfile.mkdtemp())
45694630
self.addCleanup(shutil.rmtree, tmpdir)

Lib/traceback.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,11 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
11531153
self._str += f". Did you mean '.{suggestion}' instead of '.{wrong_name}'?"
11541154
else:
11551155
self._str += f". Did you mean '.{suggestion}' ({suggestion!a}) instead of '.{wrong_name}' ({wrong_name!a})?"
1156+
elif hasattr(exc_value, 'obj'):
1157+
with suppress(Exception):
1158+
hint = _get_cross_language_hint(exc_value.obj, wrong_name)
1159+
if hint:
1160+
self._str += f". {hint}"
11561161
elif exc_type and issubclass(exc_type, NameError) and \
11571162
getattr(exc_value, "name", None) is not None:
11581163
wrong_name = getattr(exc_value, "name", None)
@@ -1649,6 +1654,45 @@ def print(self, *, file=None, chain=True, **kwargs):
16491654
_MOVE_COST = 2
16501655
_CASE_COST = 1
16511656

1657+
# Cross-language method suggestions for builtin types.
1658+
# Consulted as a fallback when Levenshtein-based suggestions find no match.
1659+
#
1660+
# Inclusion criteria:
1661+
# 1. Must have evidence of real cross-language confusion (Stack Overflow
1662+
# traffic, bug reports in production repos, developer survey data)
1663+
# 2. Must not be catchable by Levenshtein distance (too different from
1664+
# the correct Python method name)
1665+
# 3. Must be from a top-4 language by Python co-usage: JavaScript, Java,
1666+
# C#, or Ruby (JetBrains/PSF Developer Survey 2024)
1667+
#
1668+
# Each entry maps (builtin_type, wrong_name) to a suggestion string.
1669+
# If the suggestion is a Python method name, the standard "Did you mean"
1670+
# format is used. If it contains a space, it's rendered as a full hint.
1671+
#
1672+
# See https://discuss.python.org/t/106632 for the design discussion.
1673+
_CROSS_LANGUAGE_HINTS = {
1674+
# list -- JavaScript/Ruby equivalents
1675+
(list, "push"): "append",
1676+
(list, "concat"): "extend",
1677+
# list -- Java/C# equivalents
1678+
(list, "addAll"): "extend",
1679+
# list -- wrong-type suggestion (per Serhiy Storchaka, Terry Reedy,
1680+
# Paul Moore: list.add() more likely means the user expected a set)
1681+
(list, "add"): "Did you mean to use a set? Sets have an .add() method",
1682+
# str -- JavaScript equivalents
1683+
(str, "toUpperCase"): "upper",
1684+
(str, "toLowerCase"): "lower",
1685+
(str, "trimStart"): "lstrip",
1686+
(str, "trimEnd"): "rstrip",
1687+
# dict -- Java equivalents
1688+
(dict, "keySet"): "keys",
1689+
(dict, "entrySet"): "items",
1690+
(dict, "putAll"): "update",
1691+
# Note: indexOf, trim, and getOrDefault are not included because
1692+
# Levenshtein distance already catches them (indexOf->index,
1693+
# trim->strip, getOrDefault->setdefault).
1694+
}
1695+
16521696

16531697
def _substitution_cost(ch_a, ch_b):
16541698
if ch_a == ch_b:
@@ -1711,6 +1755,23 @@ def _check_for_nested_attribute(obj, wrong_name, attrs):
17111755
return None
17121756

17131757

1758+
def _get_cross_language_hint(obj, wrong_name):
1759+
"""Check if wrong_name is a common method name from another language.
1760+
1761+
Only checks exact builtin types (list, str, dict) to avoid false
1762+
positives on subclasses that may intentionally lack these methods.
1763+
Returns a formatted hint string, or None.
1764+
"""
1765+
hint = _CROSS_LANGUAGE_HINTS.get((type(obj), wrong_name))
1766+
if hint is None:
1767+
return None
1768+
if ' ' in hint:
1769+
# Full custom hint (e.g., wrong-type suggestion for list.add)
1770+
return hint
1771+
# Direct method equivalent -- format like Levenshtein suggestions
1772+
return f"Did you mean '.{hint}' instead of '.{wrong_name}'?"
1773+
1774+
17141775
def _get_safe___dir__(obj):
17151776
# Use obj.__dir__() to avoid a TypeError when calling dir(obj).
17161777
# See gh-131001 and gh-139933.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Cross-language method suggestions are now shown for :exc:`AttributeError` on
2+
builtin types when the existing Levenshtein-based suggestions find no match.
3+
For example, ``[].push()`` now suggests ``append`` (JavaScript), and
4+
``"".toUpperCase()`` suggests ``upper``. The ``list.add()`` case suggests
5+
using a set instead, following feedback from the community discussion.

0 commit comments

Comments
 (0)