Skip to content

Commit 7511697

Browse files
committed
gh-137967: Restore suggestions on nested attribute access
1 parent e39255e commit 7511697

File tree

3 files changed

+120
-1
lines changed

3 files changed

+120
-1
lines changed

Lib/test/test_traceback.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4262,6 +4262,87 @@ def __getattribute__(self, attr):
42624262
self.assertIn("Did you mean", actual)
42634263
self.assertIn("bluch", actual)
42644264

4265+
def test_getattr_nested_attribute_suggestions(self):
4266+
# Test that nested attributes are suggested when no direct match
4267+
class Inner:
4268+
def __init__(self):
4269+
self.value = 42
4270+
self.data = "test"
4271+
4272+
class Outer:
4273+
def __init__(self):
4274+
self.inner = Inner()
4275+
4276+
# Should suggest 'inner.value'
4277+
actual = self.get_suggestion(Outer(), 'value')
4278+
self.assertIn("Did you mean: 'inner.value'", actual)
4279+
4280+
# Should suggest 'inner.data'
4281+
actual = self.get_suggestion(Outer(), 'data')
4282+
self.assertIn("Did you mean: 'inner.data'", actual)
4283+
4284+
def test_getattr_nested_prioritizes_direct_matches(self):
4285+
# Test that direct attribute matches are prioritized over nested ones
4286+
class Inner:
4287+
def __init__(self):
4288+
self.foo = 42
4289+
4290+
class Outer:
4291+
def __init__(self):
4292+
self.inner = Inner()
4293+
self.fooo = 100 # Similar to 'foo'
4294+
4295+
# Should suggest 'fooo' (direct) not 'inner.foo' (nested)
4296+
actual = self.get_suggestion(Outer(), 'foo')
4297+
self.assertIn("Did you mean: 'fooo'", actual)
4298+
self.assertNotIn("inner.foo", actual)
4299+
4300+
def test_getattr_nested_with_property(self):
4301+
# Test nested suggestions work with properties
4302+
class Inner:
4303+
@property
4304+
def computed(self):
4305+
return 42
4306+
4307+
class Outer:
4308+
def __init__(self):
4309+
self.inner = Inner()
4310+
4311+
actual = self.get_suggestion(Outer(), 'computed')
4312+
self.assertIn("Did you mean: 'inner.computed'", actual)
4313+
4314+
def test_getattr_nested_no_suggestion_for_deep_nesting(self):
4315+
# Test that deeply nested attributes (2+ levels) are not suggested
4316+
class Deep:
4317+
def __init__(self):
4318+
self.value = 42
4319+
4320+
class Middle:
4321+
def __init__(self):
4322+
self.deep = Deep()
4323+
4324+
class Outer:
4325+
def __init__(self):
4326+
self.middle = Middle()
4327+
4328+
# Should not suggest 'middle.deep.value' (too deep)
4329+
actual = self.get_suggestion(Outer(), 'value')
4330+
self.assertNotIn("Did you mean", actual)
4331+
4332+
def test_getattr_nested_ignores_private_attributes(self):
4333+
# Test that nested suggestions ignore private attributes
4334+
class Inner:
4335+
def __init__(self):
4336+
self.public_value = 42
4337+
4338+
class Outer:
4339+
def __init__(self):
4340+
self._private_inner = Inner()
4341+
4342+
# Should not suggest '_private_inner.public_value'
4343+
actual = self.get_suggestion(Outer(), 'public_value')
4344+
self.assertNotIn("Did you mean", actual)
4345+
42654346
def make_module(self, code):
42664347
tmpdir = Path(tempfile.mkdtemp())
42674348
self.addCleanup(shutil.rmtree, tmpdir)

Lib/traceback.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1601,6 +1601,31 @@ def _substitution_cost(ch_a, ch_b):
16011601
return _MOVE_COST
16021602

16031603

1604+
def _check_for_nested_attribute(obj, wrong_name, attrs):
1605+
"""Check if any attribute of obj has the wrong_name as a nested attribute.
1606+
1607+
Returns the first nested attribute suggestion found, or None.
1608+
Limited to checking 20 attributes and returning up to 5 suggestions.
1609+
"""
1610+
nested_suggestions = []
1611+
max_nested_suggestions = 5
1612+
1613+
# Check for nested attributes (only one level deep)
1614+
attrs_to_check = [x for x in attrs if not x.startswith('_')][:20] # Limit number of attributes to check
1615+
for attr_name in attrs_to_check:
1616+
try:
1617+
attr_obj = getattr(obj, attr_name)
1618+
# Check if the nested attribute has the wrong_name
1619+
if hasattr(attr_obj, wrong_name):
1620+
nested_suggestions.append(f"{attr_name}.{wrong_name}")
1621+
if len(nested_suggestions) >= max_nested_suggestions:
1622+
break
1623+
except:
1624+
pass
1625+
1626+
return nested_suggestions[0] if nested_suggestions else None
1627+
1628+
16041629
def _compute_suggestion_error(exc_value, tb, wrong_name):
16051630
if wrong_name is None or not isinstance(wrong_name, str):
16061631
return None
@@ -1666,7 +1691,9 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
16661691
except ImportError:
16671692
pass
16681693
else:
1669-
return _suggestions._generate_suggestions(d, wrong_name)
1694+
suggestion = _suggestions._generate_suggestions(d, wrong_name)
1695+
if suggestion:
1696+
return suggestion
16701697

16711698
# Compute closest match
16721699

@@ -1691,6 +1718,16 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
16911718
if not suggestion or current_distance < best_distance:
16921719
suggestion = possible_name
16931720
best_distance = current_distance
1721+
1722+
# If no direct attribute match found, check for nested attributes
1723+
if not suggestion and isinstance(exc_value, AttributeError):
1724+
try:
1725+
nested_suggestion = _check_for_nested_attribute(exc_value.obj, wrong_name, d)
1726+
if nested_suggestion:
1727+
return nested_suggestion
1728+
except:
1729+
pass
1730+
16941731
return suggestion
16951732

16961733

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Show error suggestions on nested attribute access. Patch by Pablo Galindo

0 commit comments

Comments
 (0)