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
2 changes: 1 addition & 1 deletion Lib/idlelib/idle_test/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def __eq__(self, other):
"Or did you forget to import 'abc'?\n"),
('int.reel', AttributeError,
"type object 'int' has no attribute 'reel'. "
"Did you mean: 'real'?\n"),
"Did you mean '.real' instead of '.reel'?\n"),
)

@force_not_colorized
Expand Down
58 changes: 30 additions & 28 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -4176,39 +4176,39 @@ class CaseChangeOverSubstitution:
BLuch = None

for cls, suggestion in [
(Addition, "'bluchin'?"),
(Substitution, "'blech'?"),
(Elimination, "'blch'?"),
(Addition, "'bluchin'?"),
(SubstitutionOverElimination, "'blach'?"),
(SubstitutionOverAddition, "'blach'?"),
(EliminationOverAddition, "'bluc'?"),
(CaseChangeOverSubstitution, "'BLuch'?"),
(Addition, "'.bluchin'"),
(Substitution, "'.blech'"),
(Elimination, "'.blch'"),
(Addition, "'.bluchin'"),
(SubstitutionOverElimination, "'.blach'"),
(SubstitutionOverAddition, "'.blach'"),
(EliminationOverAddition, "'.bluc'"),
(CaseChangeOverSubstitution, "'.BLuch'"),
]:
actual = self.get_suggestion(cls(), 'bluch')
self.assertIn(suggestion, actual)
self.assertIn('Did you mean ' + suggestion, actual)

def test_suggestions_underscored(self):
class A:
bluch = None

self.assertIn("'bluch'", self.get_suggestion(A(), 'blach'))
self.assertIn("'bluch'", self.get_suggestion(A(), '_luch'))
self.assertIn("'bluch'", self.get_suggestion(A(), '_bluch'))
self.assertIn("'.bluch'", self.get_suggestion(A(), 'blach'))
self.assertIn("'.bluch'", self.get_suggestion(A(), '_luch'))
self.assertIn("'.bluch'", self.get_suggestion(A(), '_bluch'))

attr_function = self.attr_function
class B:
_bluch = None
def method(self, name):
attr_function(self, name)

self.assertIn("'_bluch'", self.get_suggestion(B(), '_blach'))
self.assertIn("'_bluch'", self.get_suggestion(B(), '_luch'))
self.assertNotIn("'_bluch'", self.get_suggestion(B(), 'bluch'))
self.assertIn("'._bluch'", self.get_suggestion(B(), '_blach'))
self.assertIn("'._bluch'", self.get_suggestion(B(), '_luch'))
self.assertNotIn("'._bluch'", self.get_suggestion(B(), 'bluch'))

self.assertIn("'_bluch'", self.get_suggestion(partial(B().method, '_blach')))
self.assertIn("'_bluch'", self.get_suggestion(partial(B().method, '_luch')))
self.assertIn("'_bluch'", self.get_suggestion(partial(B().method, 'bluch')))
self.assertIn("'._bluch'", self.get_suggestion(partial(B().method, '_blach')))
self.assertIn("'._bluch'", self.get_suggestion(partial(B().method, '_luch')))
self.assertIn("'._bluch'", self.get_suggestion(partial(B().method, 'bluch')))


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

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

class B:
attr_a = None
attr_µ = None # attr_\xb5

suggestion = self.get_suggestion(B(), 'attr_\xb5')
self.assertIn("'attr_\u03bc'", suggestion)
self.assertIn(r"'attr_\u03bc'", suggestion)
self.assertIn(
"'.attr_\u03bc' ('attr_\\u03bc') "
"instead of '.attr_\xb5' ('attr_\\xb5')",
suggestion)
self.assertNotIn("attr_a", suggestion)


Expand Down Expand Up @@ -4371,11 +4373,11 @@ def __init__(self):

# Should suggest 'inner.value'
actual = self.get_suggestion(Outer(), 'value')
self.assertIn("Did you mean: 'inner.value'", actual)
self.assertIn("Did you mean '.inner.value' instead of '.value'", actual)

# Should suggest 'inner.data'
actual = self.get_suggestion(Outer(), 'data')
self.assertIn("Did you mean: 'inner.data'", actual)
self.assertIn("Did you mean '.inner.data' instead of '.data'", actual)

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

# Should suggest 'fooo' (direct) not 'inner.foo' (nested)
actual = self.get_suggestion(Outer(), 'foo')
self.assertIn("Did you mean: 'fooo'", actual)
self.assertIn("Did you mean '.fooo'", actual)
self.assertNotIn("inner.foo", actual)

def test_getattr_nested_with_property(self):
Expand Down Expand Up @@ -4487,7 +4489,7 @@ def __init__(self):

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

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

# Should still suggest 'safe_inner.target' without crashing
# Should still suggest '.safe_inner.target' without crashing
# even though accessing exploder.target would raise an exception
actual = self.get_suggestion(Outer(), 'target')
self.assertIn("'safe_inner.target'", actual)
self.assertIn("'.safe_inner.target'", actual)

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

# Should still find 'normal.target' even though weird.target check fails
actual = self.get_suggestion(Outer(), 'target')
self.assertIn("'normal.target'", actual)
self.assertIn("'.normal.target'", actual)

def make_module(self, code):
tmpdir = Path(tempfile.mkdtemp())
Expand Down
23 changes: 15 additions & 8 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -1127,7 +1127,16 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
self._str += (". Site initialization is disabled, did you forget to "
+ "add the site-packages directory to sys.path "
+ "or to enable your virtual environment?")
elif exc_type and issubclass(exc_type, (NameError, AttributeError)) and \
elif exc_type and issubclass(exc_type, AttributeError) and \
getattr(exc_value, "name", None) is not None:
wrong_name = getattr(exc_value, "name", None)
suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
if suggestion:
if suggestion.isascii():
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 exc_type and issubclass(exc_type, NameError) and \
getattr(exc_value, "name", None) is not None:
wrong_name = getattr(exc_value, "name", None)
suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
Expand All @@ -1136,13 +1145,11 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
self._str += f". Did you mean: '{suggestion}'?"
else:
self._str += f". Did you mean: '{suggestion}' ({suggestion!a})?"
if issubclass(exc_type, NameError):
wrong_name = getattr(exc_value, "name", None)
if wrong_name is not None and wrong_name in sys.stdlib_module_names:
if suggestion:
self._str += f" Or did you forget to import '{wrong_name}'?"
else:
self._str += f". Did you forget to import '{wrong_name}'?"
if wrong_name is not None and wrong_name in sys.stdlib_module_names:
if suggestion:
self._str += f" Or did you forget to import '{wrong_name}'?"
else:
self._str += f". Did you forget to import '{wrong_name}'?"
if lookup_lines:
self._load_lines()
self.__suppress_context__ = \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Attribute suggestions in :exc:`AttributeError` tracebacks are now formatted differently
to make them easier to understand, for example: ``Did you mean '.datetime.now' instead of '.now'``.
Contributed by Bartosz Sławecki.
Loading