diff --git a/Lib/idlelib/idle_test/test_run.py b/Lib/idlelib/idle_test/test_run.py index 83ecbffa2a197e..9a9d3b7b4e219c 100644 --- a/Lib/idlelib/idle_test/test_run.py +++ b/Lib/idlelib/idle_test/test_run.py @@ -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 diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index a4a49fd44bb2e0..b12da52b2a841b 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4176,25 +4176,25 @@ 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: @@ -4202,13 +4202,13 @@ class B: 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): @@ -4256,7 +4256,7 @@ class A: fiⁿₐˡᵢᶻₐᵗᵢᵒₙ = None suggestion = self.get_suggestion(A(), 'fiⁿₐˡᵢᶻₐᵗᵢᵒₙ') - self.assertIn("'finalization'", suggestion) + self.assertIn("'.finalization'", suggestion) self.assertNotIn("analization", suggestion) class B: @@ -4264,8 +4264,10 @@ class B: 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) @@ -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 @@ -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): @@ -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) @@ -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 @@ -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()) diff --git a/Lib/traceback.py b/Lib/traceback.py index 97d83f3ddd3297..cb3ba6940fd230 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -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) @@ -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__ = \ diff --git a/Misc/NEWS.d/next/Library/2026-02-07-16-31-42.gh-issue-144285.iyH9iL.rst b/Misc/NEWS.d/next/Library/2026-02-07-16-31-42.gh-issue-144285.iyH9iL.rst new file mode 100644 index 00000000000000..e1119a85e9c1f3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-07-16-31-42.gh-issue-144285.iyH9iL.rst @@ -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.