diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 74b979d009664d..bdc34a29022ffe 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4020,8 +4020,7 @@ def test_dont_swallow_subexceptions_of_falsey_exceptiongroup(self): global_for_suggestions = None - -class SuggestionFormattingTestBase: +class SuggestionFormattingTestBaseParent: def get_suggestion(self, obj, attr_name=None): if attr_name is not None: def callable(): @@ -4033,8 +4032,12 @@ def callable(): callable, slice_start=-1, slice_end=None ) return result_lines[0] - - def test_getattr_suggestions(self): + +class BaseSuggestionTests(SuggestionFormattingTestBaseParent): + """ + Subclasses need to implement the get_suggestion method. + """ + def test_suggestions(self): class Substitution: noise = more_noise = a = bc = None blech = None @@ -4074,44 +4077,50 @@ class CaseChangeOverSubstitution: (EliminationOverAddition, "'bluc'?"), (CaseChangeOverSubstitution, "'BLuch'?"), ]: - actual = self.get_suggestion(cls(), 'bluch') + obj = cls() + actual = self.get_suggestion(obj, 'bluch') self.assertIn(suggestion, actual) - def test_getattr_suggestions_underscored(self): + 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')) + obj = A() + self.assertIn("'bluch'", self.get_suggestion(obj, 'blach')) + self.assertIn("'bluch'", self.get_suggestion(obj, '_luch')) + self.assertIn("'bluch'", self.get_suggestion(obj, '_bluch')) class B: _bluch = None def method(self, name): getattr(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')) + obj = B() + self.assertIn("'_bluch'", self.get_suggestion(obj, '_blach')) + self.assertIn("'_bluch'", self.get_suggestion(obj, '_luch')) + self.assertNotIn("'_bluch'", self.get_suggestion(obj, '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'))) + if hasattr(self, 'test_with_method_call'): + self.assertIn("'_bluch'", self.get_suggestion(partial(obj.method, '_blach'))) + self.assertIn("'_bluch'", self.get_suggestion(partial(obj.method, '_luch'))) + self.assertIn("'_bluch'", self.get_suggestion(partial(obj.method, 'bluch'))) - def test_getattr_suggestions_do_not_trigger_for_long_attributes(self): + def test_do_not_trigger_for_long_attributes(self): class A: blech = None - actual = self.get_suggestion(A(), 'somethingverywrong') + obj = A() + actual = self.get_suggestion(obj, 'somethingverywrong') self.assertNotIn("blech", actual) - def test_getattr_error_bad_suggestions_do_not_trigger_for_small_names(self): + def test_do_not_trigger_for_small_names(self): class MyClass: vvv = mom = w = id = pytho = None + obj = MyClass() for name in ("b", "v", "m", "py"): with self.subTest(name=name): - actual = self.get_suggestion(MyClass, name) + actual = self.get_suggestion(obj, name) self.assertNotIn("Did you mean", actual) self.assertNotIn("'vvv", actual) self.assertNotIn("'mom'", actual) @@ -4119,7 +4128,7 @@ class MyClass: self.assertNotIn("'w'", actual) self.assertNotIn("'pytho'", actual) - def test_getattr_suggestions_do_not_trigger_for_big_dicts(self): + def test_do_not_trigger_for_big_dicts(self): class A: blech = None # A class with a very big __dict__ will not be considered @@ -4127,10 +4136,29 @@ class A: for index in range(2000): setattr(A, f"index_{index}", None) - actual = self.get_suggestion(A(), 'bluch') - self.assertNotIn("blech", actual) + obj = A() + actual = self.get_suggestion(obj, 'bluch') + self.assertNotIn("blech", actual) - def test_getattr_suggestions_no_args(self): +class GetattrSuggestionTests(BaseSuggestionTests): + def get_suggestion(self, obj, attr_name=None): + if attr_name is not None: + def callable(): + getattr(obj, attr_name) + else: + callable = obj + + result_lines = self.get_exception( + callable, slice_start=-1, slice_end=None + ) + return result_lines[0] + + def test_with_method_call(self): + # This is a placeholder method to make + # hasattr(self, 'test_with_method_call') return True + pass + + def test_suggestions_no_args(self): class A: blech = None def __getattr__(self, attr): @@ -4147,7 +4175,7 @@ def __getattr__(self, attr): actual = self.get_suggestion(A(), 'bluch') self.assertIn("blech", actual) - def test_getattr_suggestions_invalid_args(self): + def test_suggestions_invalid_args(self): class NonStringifyClass: __str__ = None __repr__ = None @@ -4171,13 +4199,24 @@ def __getattr__(self, attr): actual = self.get_suggestion(cls(), 'bluch') self.assertIn("blech", actual) - def test_getattr_suggestions_for_same_name(self): + def test_suggestions_for_same_name(self): class A: def __dir__(self): return ['blech'] actual = self.get_suggestion(A(), 'blech') self.assertNotIn("Did you mean", actual) +class DelattrSuggestionTests(BaseSuggestionTests): + def get_suggestion(self, obj, attr_name): + def callable(): + delattr(obj, attr_name) + + result_lines = self.get_exception( + callable, slice_start=-1, slice_end=None + ) + return result_lines[0] + +class SuggestionFormattingTestBase(SuggestionFormattingTestBaseParent): def test_attribute_error_with_failing_dict(self): class T: bluch = 1 @@ -4655,6 +4694,49 @@ class CPythonSuggestionFormattingTests( """ +class PurePythonGetattrSuggestionFormattingTests( + PurePythonExceptionFormattingMixin, + GetattrSuggestionTests, + unittest.TestCase, +): + """ + Same set of tests (for attribute access) as above using the pure Python implementation of + traceback printing in traceback.py. + """ + + +class PurePythonDelattrSuggestionFormattingTests( + PurePythonExceptionFormattingMixin, + DelattrSuggestionTests, + unittest.TestCase, +): + """ + Same set of tests (for attribute deletion) as above using the pure Python implementation of + traceback printing in traceback.py. + """ + + +@cpython_only +class CPythonGetattrSuggestionFormattingTests( + CAPIExceptionFormattingMixin, + GetattrSuggestionTests, + unittest.TestCase, +): + """ + Same set of tests (for attribute access) as above but with Python's internal traceback printing. + """ + + +@cpython_only +class CPythonDelattrSuggestionFormattingTests( + CAPIExceptionFormattingMixin, + DelattrSuggestionTests, + unittest.TestCase, +): + """ + Same set of tests (for attribute deletion) as above but with Python's internal traceback printing. + """ + class MiscTest(unittest.TestCase): def test_all(self): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-02-22-01-23-23.gh-issue-130425.x5SNQ8.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-22-01-23-23.gh-issue-130425.x5SNQ8.rst new file mode 100644 index 00000000000000..a655cf2f2a765b --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-22-01-23-23.gh-issue-130425.x5SNQ8.rst @@ -0,0 +1,2 @@ +Add ``"Did you mean: 'attr'?"`` suggestion when using ``del obj.attr`` if ``attr`` +does not exist. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index be62ae5eefd00d..f8e63a9315d9c0 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -6939,6 +6939,7 @@ store_instance_attr_lock_held(PyObject *obj, PyDictValues *values, PyErr_Format(PyExc_AttributeError, "'%.100s' object has no attribute '%U'", Py_TYPE(obj)->tp_name, name); + _PyObject_SetAttributeErrorContext(obj, name); return -1; } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy