Skip to content

Commit b6b72e7

Browse files
authored
gh-144285: Improve AttributeError attribute suggestions (#144299)
1 parent 072cd7c commit b6b72e7

File tree

4 files changed

+49
-37
lines changed

4 files changed

+49
-37
lines changed

Lib/idlelib/idle_test/test_run.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def __eq__(self, other):
4444
"Or did you forget to import 'abc'?\n"),
4545
('int.reel', AttributeError,
4646
"type object 'int' has no attribute 'reel'. "
47-
"Did you mean: 'real'?\n"),
47+
"Did you mean '.real' instead of '.reel'?\n"),
4848
)
4949

5050
@force_not_colorized

Lib/test/test_traceback.py

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4176,39 +4176,39 @@ class CaseChangeOverSubstitution:
41764176
BLuch = None
41774177

41784178
for cls, suggestion in [
4179-
(Addition, "'bluchin'?"),
4180-
(Substitution, "'blech'?"),
4181-
(Elimination, "'blch'?"),
4182-
(Addition, "'bluchin'?"),
4183-
(SubstitutionOverElimination, "'blach'?"),
4184-
(SubstitutionOverAddition, "'blach'?"),
4185-
(EliminationOverAddition, "'bluc'?"),
4186-
(CaseChangeOverSubstitution, "'BLuch'?"),
4179+
(Addition, "'.bluchin'"),
4180+
(Substitution, "'.blech'"),
4181+
(Elimination, "'.blch'"),
4182+
(Addition, "'.bluchin'"),
4183+
(SubstitutionOverElimination, "'.blach'"),
4184+
(SubstitutionOverAddition, "'.blach'"),
4185+
(EliminationOverAddition, "'.bluc'"),
4186+
(CaseChangeOverSubstitution, "'.BLuch'"),
41874187
]:
41884188
actual = self.get_suggestion(cls(), 'bluch')
4189-
self.assertIn(suggestion, actual)
4189+
self.assertIn('Did you mean ' + suggestion, actual)
41904190

41914191
def test_suggestions_underscored(self):
41924192
class A:
41934193
bluch = None
41944194

4195-
self.assertIn("'bluch'", self.get_suggestion(A(), 'blach'))
4196-
self.assertIn("'bluch'", self.get_suggestion(A(), '_luch'))
4197-
self.assertIn("'bluch'", self.get_suggestion(A(), '_bluch'))
4195+
self.assertIn("'.bluch'", self.get_suggestion(A(), 'blach'))
4196+
self.assertIn("'.bluch'", self.get_suggestion(A(), '_luch'))
4197+
self.assertIn("'.bluch'", self.get_suggestion(A(), '_bluch'))
41984198

41994199
attr_function = self.attr_function
42004200
class B:
42014201
_bluch = None
42024202
def method(self, name):
42034203
attr_function(self, name)
42044204

4205-
self.assertIn("'_bluch'", self.get_suggestion(B(), '_blach'))
4206-
self.assertIn("'_bluch'", self.get_suggestion(B(), '_luch'))
4207-
self.assertNotIn("'_bluch'", self.get_suggestion(B(), 'bluch'))
4205+
self.assertIn("'._bluch'", self.get_suggestion(B(), '_blach'))
4206+
self.assertIn("'._bluch'", self.get_suggestion(B(), '_luch'))
4207+
self.assertNotIn("'._bluch'", self.get_suggestion(B(), 'bluch'))
42084208

4209-
self.assertIn("'_bluch'", self.get_suggestion(partial(B().method, '_blach')))
4210-
self.assertIn("'_bluch'", self.get_suggestion(partial(B().method, '_luch')))
4211-
self.assertIn("'_bluch'", self.get_suggestion(partial(B().method, 'bluch')))
4209+
self.assertIn("'._bluch'", self.get_suggestion(partial(B().method, '_blach')))
4210+
self.assertIn("'._bluch'", self.get_suggestion(partial(B().method, '_luch')))
4211+
self.assertIn("'._bluch'", self.get_suggestion(partial(B().method, 'bluch')))
42124212

42134213

42144214
def test_do_not_trigger_for_long_attributes(self):
@@ -4256,16 +4256,18 @@ class A:
42564256
fiⁿₐˡᵢᶻₐᵗᵢᵒₙ = None
42574257

42584258
suggestion = self.get_suggestion(A(), 'fiⁿₐˡᵢᶻₐᵗᵢᵒₙ')
4259-
self.assertIn("'finalization'", suggestion)
4259+
self.assertIn("'.finalization'", suggestion)
42604260
self.assertNotIn("analization", suggestion)
42614261

42624262
class B:
42634263
attr_a = None
42644264
attr_µ = None # attr_\xb5
42654265

42664266
suggestion = self.get_suggestion(B(), 'attr_\xb5')
4267-
self.assertIn("'attr_\u03bc'", suggestion)
4268-
self.assertIn(r"'attr_\u03bc'", suggestion)
4267+
self.assertIn(
4268+
"'.attr_\u03bc' ('attr_\\u03bc') "
4269+
"instead of '.attr_\xb5' ('attr_\\xb5')",
4270+
suggestion)
42694271
self.assertNotIn("attr_a", suggestion)
42704272

42714273

@@ -4371,11 +4373,11 @@ def __init__(self):
43714373

43724374
# Should suggest 'inner.value'
43734375
actual = self.get_suggestion(Outer(), 'value')
4374-
self.assertIn("Did you mean: 'inner.value'", actual)
4376+
self.assertIn("Did you mean '.inner.value' instead of '.value'", actual)
43754377

43764378
# Should suggest 'inner.data'
43774379
actual = self.get_suggestion(Outer(), 'data')
4378-
self.assertIn("Did you mean: 'inner.data'", actual)
4380+
self.assertIn("Did you mean '.inner.data' instead of '.data'", actual)
43794381

43804382
def test_getattr_nested_prioritizes_direct_matches(self):
43814383
# Test that direct attribute matches are prioritized over nested ones
@@ -4390,7 +4392,7 @@ def __init__(self):
43904392

43914393
# Should suggest 'fooo' (direct) not 'inner.foo' (nested)
43924394
actual = self.get_suggestion(Outer(), 'foo')
4393-
self.assertIn("Did you mean: 'fooo'", actual)
4395+
self.assertIn("Did you mean '.fooo'", actual)
43944396
self.assertNotIn("inner.foo", actual)
43954397

43964398
def test_getattr_nested_with_property(self):
@@ -4487,7 +4489,7 @@ def __init__(self):
44874489

44884490
# Should suggest only the first match (alphabetically)
44894491
actual = self.get_suggestion(Outer(), 'value')
4490-
self.assertIn("'a_inner.value'", actual)
4492+
self.assertIn("'.a_inner.value'", actual)
44914493
# Verify it's a single suggestion, not multiple
44924494
self.assertEqual(actual.count("Did you mean"), 1)
44934495

@@ -4510,10 +4512,10 @@ def __init__(self):
45104512
self.exploder = ExplodingProperty() # Accessing attributes will raise
45114513
self.safe_inner = SafeInner()
45124514

4513-
# Should still suggest 'safe_inner.target' without crashing
4515+
# Should still suggest '.safe_inner.target' without crashing
45144516
# even though accessing exploder.target would raise an exception
45154517
actual = self.get_suggestion(Outer(), 'target')
4516-
self.assertIn("'safe_inner.target'", actual)
4518+
self.assertIn("'.safe_inner.target'", actual)
45174519

45184520
def test_getattr_nested_handles_hasattr_exceptions(self):
45194521
# Test that exceptions in hasattr don't crash the system
@@ -4534,7 +4536,7 @@ def __init__(self):
45344536

45354537
# Should still find 'normal.target' even though weird.target check fails
45364538
actual = self.get_suggestion(Outer(), 'target')
4537-
self.assertIn("'normal.target'", actual)
4539+
self.assertIn("'.normal.target'", actual)
45384540

45394541
def make_module(self, code):
45404542
tmpdir = Path(tempfile.mkdtemp())

Lib/traceback.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,16 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
11281128
self._str += (". Site initialization is disabled, did you forget to "
11291129
+ "add the site-packages directory to sys.path "
11301130
+ "or to enable your virtual environment?")
1131-
elif exc_type and issubclass(exc_type, (NameError, AttributeError)) and \
1131+
elif exc_type and issubclass(exc_type, AttributeError) and \
1132+
getattr(exc_value, "name", None) is not None:
1133+
wrong_name = getattr(exc_value, "name", None)
1134+
suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
1135+
if suggestion:
1136+
if suggestion.isascii():
1137+
self._str += f". Did you mean '.{suggestion}' instead of '.{wrong_name}'?"
1138+
else:
1139+
self._str += f". Did you mean '.{suggestion}' ({suggestion!a}) instead of '.{wrong_name}' ({wrong_name!a})?"
1140+
elif exc_type and issubclass(exc_type, NameError) and \
11321141
getattr(exc_value, "name", None) is not None:
11331142
wrong_name = getattr(exc_value, "name", None)
11341143
suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
@@ -1137,13 +1146,11 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
11371146
self._str += f". Did you mean: '{suggestion}'?"
11381147
else:
11391148
self._str += f". Did you mean: '{suggestion}' ({suggestion!a})?"
1140-
if issubclass(exc_type, NameError):
1141-
wrong_name = getattr(exc_value, "name", None)
1142-
if wrong_name is not None and wrong_name in sys.stdlib_module_names:
1143-
if suggestion:
1144-
self._str += f" Or did you forget to import '{wrong_name}'?"
1145-
else:
1146-
self._str += f". Did you forget to import '{wrong_name}'?"
1149+
if wrong_name is not None and wrong_name in sys.stdlib_module_names:
1150+
if suggestion:
1151+
self._str += f" Or did you forget to import '{wrong_name}'?"
1152+
else:
1153+
self._str += f". Did you forget to import '{wrong_name}'?"
11471154
if lookup_lines:
11481155
self._load_lines()
11491156
self.__suppress_context__ = \
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Attribute suggestions in :exc:`AttributeError` tracebacks are now formatted differently
2+
to make them easier to understand, for example: ``Did you mean '.datetime.now' instead of '.now'``.
3+
Contributed by Bartosz Sławecki.

0 commit comments

Comments
 (0)