From ee4081fb0f6f24b31f2f8189b83fe00ecb87d1a6 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 22 Feb 2025 01:23:50 +0300 Subject: [PATCH 01/17] gh-130425: Add "Did you mean" suggestion for `del obj.attr` --- .../2025-02-22-01-23-23.gh-issue-130425.x5SNQ8.rst | 2 ++ Objects/dictobject.c | 1 + 2 files changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-02-22-01-23-23.gh-issue-130425.x5SNQ8.rst 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..7dbff8f76df6ea --- /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"`` 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; } From cf9a052aecbf15e54a7adf60a10bb3b7c1a90260 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 22 Feb 2025 09:56:57 +0300 Subject: [PATCH 02/17] Update NEWS --- .../2025-02-22-01-23-23.gh-issue-130425.x5SNQ8.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 7dbff8f76df6ea..a655cf2f2a765b 100644 --- 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 @@ -1,2 +1,2 @@ -Add ``"Did you mean"`` suggestion when using ``del obj.attr`` if ``attr`` +Add ``"Did you mean: 'attr'?"`` suggestion when using ``del obj.attr`` if ``attr`` does not exist. From c3698760b2281f1e9d1fe44c1693f677b73a8414 Mon Sep 17 00:00:00 2001 From: Pranjal095 Date: Sat, 22 Feb 2025 17:42:43 +0530 Subject: [PATCH 03/17] gh-130428: Add tests for delattr suggestions --- Lib/test/test_traceback.py | 98 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 74b979d009664d..be6d7ca5bcac2f 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4178,6 +4178,104 @@ def __dir__(self): actual = self.get_suggestion(A(), 'blech') self.assertNotIn("Did you mean", actual) + def test_delattr_suggestions(self): + class Substitution: + noise = more_noise = a = bc = None + blech = None + + class Elimination: + noise = more_noise = a = bc = None + blch = None + + class Addition: + noise = more_noise = a = bc = None + bluchin = None + + class SubstitutionOverElimination: + blach = None + bluc = None + + class SubstitutionOverAddition: + blach = None + bluchi = None + + class EliminationOverAddition: + blucha = None + bluc = None + + class CaseChangeOverSubstitution: + Luch = None + fluch = None + BLuch = None + + for cls, suggestion in [ + (Addition, "'bluchin'?"), + (Substitution, "'blech'?"), + (Elimination, "'blch'?"), + (Addition, "'bluchin'?"), + (SubstitutionOverElimination, "'blach'?"), + (SubstitutionOverAddition, "'blach'?"), + (EliminationOverAddition, "'bluc'?"), + (CaseChangeOverSubstitution, "'BLuch'?"), + ]: + obj = cls() + def callable(): + delattr(obj, 'bluch') + actual = self.get_suggestion(callable) + self.assertIn(suggestion, actual) + + def test_delattr_suggestions_underscored(self): + class A: + bluch = None + + obj = A() + self.assertIn("'bluch'", self.get_suggestion(lambda: delattr(obj, 'blach'))) + self.assertIn("'bluch'", self.get_suggestion(lambda: delattr(obj, '_luch'))) + self.assertIn("'bluch'", self.get_suggestion(lambda: delattr(obj, '_bluch'))) + + class B: + _bluch = None + + obj = B() + self.assertIn("'_bluch'", self.get_suggestion(lambda: delattr(obj, '_blach'))) + self.assertIn("'_bluch'", self.get_suggestion(lambda: delattr(obj, '_luch'))) + self.assertNotIn("'_bluch'", self.get_suggestion(lambda: delattr(obj, 'bluch'))) + + def test_delattr_suggestions_do_not_trigger_for_long_attributes(self): + class A: + blech = None + + obj = A() + actual = self.get_suggestion(lambda: delattr(obj, 'somethingverywrong')) + self.assertNotIn("blech", actual) + + def test_delattr_error_bad_suggestions_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(lambda: delattr(obj, name)) + self.assertNotIn("Did you mean", actual) + self.assertNotIn("'vvv", actual) + self.assertNotIn("'mom'", actual) + self.assertNotIn("'id'", actual) + self.assertNotIn("'w'", actual) + self.assertNotIn("'pytho'", actual) + + def test_delattr_suggestions_do_not_trigger_for_big_dicts(self): + class A: + blech = None + # A class with a very big __dict__ will not be considered + # for suggestions. + obj = A() + for index in range(2000): + setattr(obj, f"index_{index}", None) + + actual = self.get_suggestion(lambda: delattr(obj, 'bluch')) + self.assertNotIn("blech", actual) + def test_attribute_error_with_failing_dict(self): class T: bluch = 1 From 04254addb93c336306195e2c785837ec4b644c27 Mon Sep 17 00:00:00 2001 From: Pranjal095 Date: Sun, 23 Feb 2025 02:00:18 +0530 Subject: [PATCH 04/17] Refactored getattr and delattr tests --- Lib/test/test_traceback.py | 193 +++++++++++++++---------------------- 1 file changed, 77 insertions(+), 116 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index be6d7ca5bcac2f..f951c82062f1e4 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4034,7 +4034,7 @@ def callable(): ) return result_lines[0] - def test_getattr_suggestions(self): + def run_suggestion_tests(self, operation): class Substitution: noise = more_noise = a = bc = None blech = None @@ -4074,44 +4074,87 @@ class CaseChangeOverSubstitution: (EliminationOverAddition, "'bluc'?"), (CaseChangeOverSubstitution, "'BLuch'?"), ]: - actual = self.get_suggestion(cls(), 'bluch') + obj = cls() + + if operation == "getattr": + actual = self.get_suggestion(obj, 'bluch') + elif operation == "delattr": + actual = self.get_suggestion(lambda: delattr(obj, 'bluch')) + self.assertIn(suggestion, actual) - def test_getattr_suggestions_underscored(self): + def test_getattr_suggestions(self): + self.run_suggestion_tests("getattr") + + def test_delattr_suggestions(self): + self.run_suggestion_tests("delattr") + + def run_underscored_tests(self, operation): 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() + if operation == "getattr": + self.assertIn("'bluch'", self.get_suggestion(obj, 'blach')) + self.assertIn("'bluch'", self.get_suggestion(obj, '_luch')) + self.assertIn("'bluch'", self.get_suggestion(obj, '_bluch')) + elif operation == "delattr": + self.assertIn("'bluch'", self.get_suggestion(lambda: delattr(obj, 'blach'))) + self.assertIn("'bluch'", self.get_suggestion(lambda: delattr(obj, '_luch'))) + self.assertIn("'bluch'", self.get_suggestion(lambda: delattr(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() + if operation == "getattr": + 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(obj.method, '_blach'))) + self.assertIn("'_bluch'", self.get_suggestion(partial(obj.method, '_luch'))) + self.assertIn("'_bluch'", self.get_suggestion(partial(obj.method, 'bluch'))) + elif operation == "delattr": + self.assertIn("'_bluch'", self.get_suggestion(lambda: delattr(obj, '_blach'))) + self.assertIn("'_bluch'", self.get_suggestion(lambda: delattr(obj, '_luch'))) + self.assertNotIn("'_bluch'", self.get_suggestion(lambda: delattr(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'))) + def test_getattr_suggestions_underscored(self): + self.run_underscored_tests("getattr") - def test_getattr_suggestions_do_not_trigger_for_long_attributes(self): + def test_delattr_suggestions_underscored(self): + self.run_underscored_tests("delattr") + + def run_do_not_trigger_for_long_attributes_tests(self, operation): class A: blech = None - actual = self.get_suggestion(A(), 'somethingverywrong') + obj = A() + if operation == "getattr": + actual = self.get_suggestion(obj, 'somethingverywrong') + elif operation == "delattr": + actual = self.get_suggestion(lambda: delattr(obj, 'somethingverywrong')) self.assertNotIn("blech", actual) - def test_getattr_error_bad_suggestions_do_not_trigger_for_small_names(self): + def test_getattr_suggestions_do_not_trigger_for_long_attributes(self): + self.run_do_not_trigger_for_long_attributes_tests("getattr") + + def test_delattr_suggestions_do_not_trigger_for_long_attributes(self): + self.run_do_not_trigger_for_long_attributes_tests("delattr") + + def run_do_not_trigger_for_small_names_tests(self, operation): 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) + if operation == "getattr": + actual = self.get_suggestion(MyClass, name) + elif operation == "delattr": + actual = self.get_suggestion(lambda: delattr(obj, name)) self.assertNotIn("Did you mean", actual) self.assertNotIn("'vvv", actual) self.assertNotIn("'mom'", actual) @@ -4119,7 +4162,13 @@ class MyClass: self.assertNotIn("'w'", actual) self.assertNotIn("'pytho'", actual) - def test_getattr_suggestions_do_not_trigger_for_big_dicts(self): + def test_getattr_error_bad_suggestions_do_not_trigger_for_small_names(self): + self.run_do_not_trigger_for_small_names_tests("getattr") + + def test_delattr_error_bad_suggestions_do_not_trigger_for_small_names(self): + self.run_do_not_trigger_for_small_names_tests("delattr") + + def run_do_not_trigger_for_big_dicts_tests(self, operation): class A: blech = None # A class with a very big __dict__ will not be considered @@ -4127,9 +4176,19 @@ class A: for index in range(2000): setattr(A, f"index_{index}", None) - actual = self.get_suggestion(A(), 'bluch') + obj = A() + if operation == "getattr": + actual = self.get_suggestion(obj, 'bluch') + elif operation == "delattr": + actual = self.get_suggestion(lambda: delattr(obj, 'bluch')) self.assertNotIn("blech", actual) + def test_getattr_suggestions_do_not_trigger_for_big_dicts(self): + self.run_do_not_trigger_for_big_dicts_tests("getattr") + + def test_delattr_suggestions_do_not_trigger_for_big_dicts(self): + self.run_do_not_trigger_for_big_dicts_tests("delattr") + def test_getattr_suggestions_no_args(self): class A: blech = None @@ -4178,104 +4237,6 @@ def __dir__(self): actual = self.get_suggestion(A(), 'blech') self.assertNotIn("Did you mean", actual) - def test_delattr_suggestions(self): - class Substitution: - noise = more_noise = a = bc = None - blech = None - - class Elimination: - noise = more_noise = a = bc = None - blch = None - - class Addition: - noise = more_noise = a = bc = None - bluchin = None - - class SubstitutionOverElimination: - blach = None - bluc = None - - class SubstitutionOverAddition: - blach = None - bluchi = None - - class EliminationOverAddition: - blucha = None - bluc = None - - class CaseChangeOverSubstitution: - Luch = None - fluch = None - BLuch = None - - for cls, suggestion in [ - (Addition, "'bluchin'?"), - (Substitution, "'blech'?"), - (Elimination, "'blch'?"), - (Addition, "'bluchin'?"), - (SubstitutionOverElimination, "'blach'?"), - (SubstitutionOverAddition, "'blach'?"), - (EliminationOverAddition, "'bluc'?"), - (CaseChangeOverSubstitution, "'BLuch'?"), - ]: - obj = cls() - def callable(): - delattr(obj, 'bluch') - actual = self.get_suggestion(callable) - self.assertIn(suggestion, actual) - - def test_delattr_suggestions_underscored(self): - class A: - bluch = None - - obj = A() - self.assertIn("'bluch'", self.get_suggestion(lambda: delattr(obj, 'blach'))) - self.assertIn("'bluch'", self.get_suggestion(lambda: delattr(obj, '_luch'))) - self.assertIn("'bluch'", self.get_suggestion(lambda: delattr(obj, '_bluch'))) - - class B: - _bluch = None - - obj = B() - self.assertIn("'_bluch'", self.get_suggestion(lambda: delattr(obj, '_blach'))) - self.assertIn("'_bluch'", self.get_suggestion(lambda: delattr(obj, '_luch'))) - self.assertNotIn("'_bluch'", self.get_suggestion(lambda: delattr(obj, 'bluch'))) - - def test_delattr_suggestions_do_not_trigger_for_long_attributes(self): - class A: - blech = None - - obj = A() - actual = self.get_suggestion(lambda: delattr(obj, 'somethingverywrong')) - self.assertNotIn("blech", actual) - - def test_delattr_error_bad_suggestions_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(lambda: delattr(obj, name)) - self.assertNotIn("Did you mean", actual) - self.assertNotIn("'vvv", actual) - self.assertNotIn("'mom'", actual) - self.assertNotIn("'id'", actual) - self.assertNotIn("'w'", actual) - self.assertNotIn("'pytho'", actual) - - def test_delattr_suggestions_do_not_trigger_for_big_dicts(self): - class A: - blech = None - # A class with a very big __dict__ will not be considered - # for suggestions. - obj = A() - for index in range(2000): - setattr(obj, f"index_{index}", None) - - actual = self.get_suggestion(lambda: delattr(obj, 'bluch')) - self.assertNotIn("blech", actual) - def test_attribute_error_with_failing_dict(self): class T: bluch = 1 From 57deaf3c4051ec3ab31ee0c543959efb636520de Mon Sep 17 00:00:00 2001 From: Pranjal095 Date: Sun, 23 Feb 2025 02:14:36 +0530 Subject: [PATCH 05/17] Added else branches to handle unrecognized operations --- Lib/test/test_traceback.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index f951c82062f1e4..fe0865e2a942ad 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4075,12 +4075,14 @@ class CaseChangeOverSubstitution: (CaseChangeOverSubstitution, "'BLuch'?"), ]: obj = cls() - + if operation == "getattr": actual = self.get_suggestion(obj, 'bluch') elif operation == "delattr": actual = self.get_suggestion(lambda: delattr(obj, 'bluch')) - + else: + raise ValueError(f"operation '{operation}' not recognized") + self.assertIn(suggestion, actual) def test_getattr_suggestions(self): @@ -4102,6 +4104,8 @@ class A: self.assertIn("'bluch'", self.get_suggestion(lambda: delattr(obj, 'blach'))) self.assertIn("'bluch'", self.get_suggestion(lambda: delattr(obj, '_luch'))) self.assertIn("'bluch'", self.get_suggestion(lambda: delattr(obj, '_bluch'))) + else: + raise ValueError(f"operation '{operation}' not recognized") class B: _bluch = None @@ -4120,6 +4124,8 @@ def method(self, name): self.assertIn("'_bluch'", self.get_suggestion(lambda: delattr(obj, '_blach'))) self.assertIn("'_bluch'", self.get_suggestion(lambda: delattr(obj, '_luch'))) self.assertNotIn("'_bluch'", self.get_suggestion(lambda: delattr(obj, 'bluch'))) + else: + raise ValueError(f"operation '{operation}' not recognized") def test_getattr_suggestions_underscored(self): self.run_underscored_tests("getattr") @@ -4136,6 +4142,8 @@ class A: actual = self.get_suggestion(obj, 'somethingverywrong') elif operation == "delattr": actual = self.get_suggestion(lambda: delattr(obj, 'somethingverywrong')) + else: + raise ValueError(f"operation '{operation}' not recognized") self.assertNotIn("blech", actual) def test_getattr_suggestions_do_not_trigger_for_long_attributes(self): @@ -4155,6 +4163,8 @@ class MyClass: actual = self.get_suggestion(MyClass, name) elif operation == "delattr": actual = self.get_suggestion(lambda: delattr(obj, name)) + else: + raise ValueError(f"operation '{operation}' not recognized") self.assertNotIn("Did you mean", actual) self.assertNotIn("'vvv", actual) self.assertNotIn("'mom'", actual) @@ -4181,6 +4191,8 @@ class A: actual = self.get_suggestion(obj, 'bluch') elif operation == "delattr": actual = self.get_suggestion(lambda: delattr(obj, 'bluch')) + else: + raise ValueError(f"operation '{operation}' not recognized") self.assertNotIn("blech", actual) def test_getattr_suggestions_do_not_trigger_for_big_dicts(self): @@ -4188,7 +4200,7 @@ def test_getattr_suggestions_do_not_trigger_for_big_dicts(self): def test_delattr_suggestions_do_not_trigger_for_big_dicts(self): self.run_do_not_trigger_for_big_dicts_tests("delattr") - + def test_getattr_suggestions_no_args(self): class A: blech = None From bbae851b0c0c2aca4e528c52ebe9e94a8bf17e2b Mon Sep 17 00:00:00 2001 From: Pranjal095 Date: Tue, 4 Mar 2025 16:33:55 +0530 Subject: [PATCH 06/17] Refactor getattr and setattr suggestion tests --- Lib/test/test_traceback.py | 405 +++++++++++++++++++------------------ 1 file changed, 208 insertions(+), 197 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index fe0865e2a942ad..89eea5bf65975d 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4022,232 +4022,200 @@ def test_dont_swallow_subexceptions_of_falsey_exceptiongroup(self): class SuggestionFormattingTestBase: - 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 run_suggestion_tests(self, operation): - class Substitution: - noise = more_noise = a = bc = None - blech = None - - class Elimination: - noise = more_noise = a = bc = None - blch = None - - class Addition: - noise = more_noise = a = bc = None - bluchin = None - - class SubstitutionOverElimination: - blach = None - bluc = None - - class SubstitutionOverAddition: - blach = None - bluchi = None - - class EliminationOverAddition: - blucha = None - bluc = None - - class CaseChangeOverSubstitution: - Luch = None - fluch = None - BLuch = None - - for cls, suggestion in [ - (Addition, "'bluchin'?"), - (Substitution, "'blech'?"), - (Elimination, "'blch'?"), - (Addition, "'bluchin'?"), - (SubstitutionOverElimination, "'blach'?"), - (SubstitutionOverAddition, "'blach'?"), - (EliminationOverAddition, "'bluc'?"), - (CaseChangeOverSubstitution, "'BLuch'?"), - ]: - obj = cls() - - if operation == "getattr": + class BaseSuggestionTests: + """ + Subclasses need to implement the get_suggestion method. + """ + def test_suggestions(self): + class Substitution: + noise = more_noise = a = bc = None + blech = None + + class Elimination: + noise = more_noise = a = bc = None + blch = None + + class Addition: + noise = more_noise = a = bc = None + bluchin = None + + class SubstitutionOverElimination: + blach = None + bluc = None + + class SubstitutionOverAddition: + blach = None + bluchi = None + + class EliminationOverAddition: + blucha = None + bluc = None + + class CaseChangeOverSubstitution: + Luch = None + fluch = None + BLuch = None + + for cls, suggestion in [ + (Addition, "'bluchin'?"), + (Substitution, "'blech'?"), + (Elimination, "'blch'?"), + (Addition, "'bluchin'?"), + (SubstitutionOverElimination, "'blach'?"), + (SubstitutionOverAddition, "'blach'?"), + (EliminationOverAddition, "'bluc'?"), + (CaseChangeOverSubstitution, "'BLuch'?"), + ]: + obj = cls() actual = self.get_suggestion(obj, 'bluch') - elif operation == "delattr": - actual = self.get_suggestion(lambda: delattr(obj, 'bluch')) - else: - raise ValueError(f"operation '{operation}' not recognized") - - self.assertIn(suggestion, actual) + self.assertIn(suggestion, actual) - def test_getattr_suggestions(self): - self.run_suggestion_tests("getattr") + def test_suggestions_underscored(self): + class A: + bluch = None - def test_delattr_suggestions(self): - self.run_suggestion_tests("delattr") - - def run_underscored_tests(self, operation): - class A: - bluch = None - - obj = A() - if operation == "getattr": + 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')) - elif operation == "delattr": - self.assertIn("'bluch'", self.get_suggestion(lambda: delattr(obj, 'blach'))) - self.assertIn("'bluch'", self.get_suggestion(lambda: delattr(obj, '_luch'))) - self.assertIn("'bluch'", self.get_suggestion(lambda: delattr(obj, '_bluch'))) - else: - raise ValueError(f"operation '{operation}' not recognized") - class B: - _bluch = None - def method(self, name): - getattr(self, name) + class B: + _bluch = None + def method(self, name): + getattr(self, name) - obj = B() - if operation == "getattr": + 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(obj.method, '_blach'))) - self.assertIn("'_bluch'", self.get_suggestion(partial(obj.method, '_luch'))) - self.assertIn("'_bluch'", self.get_suggestion(partial(obj.method, 'bluch'))) - elif operation == "delattr": - self.assertIn("'_bluch'", self.get_suggestion(lambda: delattr(obj, '_blach'))) - self.assertIn("'_bluch'", self.get_suggestion(lambda: delattr(obj, '_luch'))) - self.assertNotIn("'_bluch'", self.get_suggestion(lambda: delattr(obj, 'bluch'))) - else: - raise ValueError(f"operation '{operation}' not recognized") - - def test_getattr_suggestions_underscored(self): - self.run_underscored_tests("getattr") - def test_delattr_suggestions_underscored(self): - self.run_underscored_tests("delattr") + 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 run_do_not_trigger_for_long_attributes_tests(self, operation): - class A: - blech = None + def test_do_not_trigger_for_long_attributes(self): + class A: + blech = None - obj = A() - if operation == "getattr": + obj = A() actual = self.get_suggestion(obj, 'somethingverywrong') - elif operation == "delattr": - actual = self.get_suggestion(lambda: delattr(obj, 'somethingverywrong')) - else: - raise ValueError(f"operation '{operation}' not recognized") - self.assertNotIn("blech", actual) - - def test_getattr_suggestions_do_not_trigger_for_long_attributes(self): - self.run_do_not_trigger_for_long_attributes_tests("getattr") - - def test_delattr_suggestions_do_not_trigger_for_long_attributes(self): - self.run_do_not_trigger_for_long_attributes_tests("delattr") - - def run_do_not_trigger_for_small_names_tests(self, operation): - class MyClass: - vvv = mom = w = id = pytho = None - - obj = MyClass() - for name in ("b", "v", "m", "py"): - with self.subTest(name=name): - if operation == "getattr": - actual = self.get_suggestion(MyClass, name) - elif operation == "delattr": - actual = self.get_suggestion(lambda: delattr(obj, name)) - else: - raise ValueError(f"operation '{operation}' not recognized") - self.assertNotIn("Did you mean", actual) - self.assertNotIn("'vvv", actual) - self.assertNotIn("'mom'", actual) - self.assertNotIn("'id'", actual) - self.assertNotIn("'w'", actual) - self.assertNotIn("'pytho'", actual) - - def test_getattr_error_bad_suggestions_do_not_trigger_for_small_names(self): - self.run_do_not_trigger_for_small_names_tests("getattr") - - def test_delattr_error_bad_suggestions_do_not_trigger_for_small_names(self): - self.run_do_not_trigger_for_small_names_tests("delattr") + self.assertNotIn("blech", actual) + + 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(obj, name) + self.assertNotIn("Did you mean", actual) + self.assertNotIn("'vvv", actual) + self.assertNotIn("'mom'", actual) + self.assertNotIn("'id'", actual) + self.assertNotIn("'w'", actual) + self.assertNotIn("'pytho'", actual) + + def test_do_not_trigger_for_big_dicts(self): + class A: + blech = None + # A class with a very big __dict__ will not be considered + # for suggestions. + for index in range(2000): + setattr(A, f"index_{index}", None) + + obj = A() + actual = self.get_suggestion(obj, 'bluch') + self.assertNotIn("blech", actual) - def run_do_not_trigger_for_big_dicts_tests(self, operation): - class A: - blech = None - # A class with a very big __dict__ will not be considered - # for suggestions. - for index in range(2000): - setattr(A, f"index_{index}", None) + 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 - obj = A() - if operation == "getattr": - actual = self.get_suggestion(obj, 'bluch') - elif operation == "delattr": - actual = self.get_suggestion(lambda: delattr(obj, 'bluch')) - else: - raise ValueError(f"operation '{operation}' not recognized") - self.assertNotIn("blech", actual) + result_lines = self.get_exception( + callable, slice_start=-1, slice_end=None + ) + return result_lines[0] - def test_getattr_suggestions_do_not_trigger_for_big_dicts(self): - self.run_do_not_trigger_for_big_dicts_tests("getattr") + def test_with_method_call(self): + # This is a placeholder method to make + # hasattr(self, 'test_with_method_call') return True + pass - def test_delattr_suggestions_do_not_trigger_for_big_dicts(self): - self.run_do_not_trigger_for_big_dicts_tests("delattr") + def test_suggestions_no_args(self): + class A: + blech = None + def __getattr__(self, attr): + raise AttributeError() - def test_getattr_suggestions_no_args(self): - class A: - blech = None - def __getattr__(self, attr): - raise AttributeError() + actual = self.get_suggestion(A(), 'bluch') + self.assertIn("blech", actual) - actual = self.get_suggestion(A(), 'bluch') - self.assertIn("blech", actual) + class A: + blech = None + def __getattr__(self, attr): + raise AttributeError - class A: - blech = None - def __getattr__(self, attr): - raise AttributeError + actual = self.get_suggestion(A(), 'bluch') + self.assertIn("blech", actual) - actual = self.get_suggestion(A(), 'bluch') - self.assertIn("blech", actual) + def test_suggestions_invalid_args(self): + class NonStringifyClass: + __str__ = None + __repr__ = None - def test_getattr_suggestions_invalid_args(self): - class NonStringifyClass: - __str__ = None - __repr__ = None + class A: + blech = None + def __getattr__(self, attr): + raise AttributeError(NonStringifyClass()) - class A: - blech = None - def __getattr__(self, attr): - raise AttributeError(NonStringifyClass()) + class B: + blech = None + def __getattr__(self, attr): + raise AttributeError("Error", 23) - class B: - blech = None - def __getattr__(self, attr): - raise AttributeError("Error", 23) + class C: + blech = None + def __getattr__(self, attr): + raise AttributeError(23) + + for cls in [A, B, C]: + actual = self.get_suggestion(cls(), 'bluch') + self.assertIn("blech", actual) + + 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) - class C: - blech = None - def __getattr__(self, attr): - raise AttributeError(23) + result_lines = self.get_exception( + callable, slice_start=-1, slice_end=None + ) + return result_lines[0] - for cls in [A, B, C]: - actual = self.get_suggestion(cls(), 'bluch') - self.assertIn("blech", actual) + def get_suggestion(self, obj, attr_name=None): + if attr_name is not None: + def callable(): + getattr(obj, attr_name) + else: + callable = obj - def test_getattr_suggestions_for_same_name(self): - class A: - def __dir__(self): - return ['blech'] - actual = self.get_suggestion(A(), 'blech') - self.assertNotIn("Did you mean", actual) + result_lines = self.get_exception( + callable, slice_start=-1, slice_end=None + ) + return result_lines[0] def test_attribute_error_with_failing_dict(self): class T: @@ -4726,6 +4694,49 @@ class CPythonSuggestionFormattingTests( """ +class PurePythonGetattrSuggestionFormattingTests( + PurePythonExceptionFormattingMixin, + SuggestionFormattingTestBase.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, + SuggestionFormattingTestBase.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, + SuggestionFormattingTestBase.GetattrSuggestionTests, + unittest.TestCase, +): + """ + Same set of tests (for attribute access) as above but with Python's internal traceback printing. + """ + + +@cpython_only +class CPythonDelattrSuggestionFormattingTests( + CAPIExceptionFormattingMixin, + SuggestionFormattingTestBase.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): From 8ba26c455bacb71544292f754d5a0f23284974a9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 28 Mar 2025 12:59:03 -0600 Subject: [PATCH 07/17] docs: -I also implies -P (#131539) From b1210ddc330998e2579ef407180944db84716e57 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 19 May 2025 18:39:43 +0300 Subject: [PATCH 08/17] GH-134236: make regen-all (GH-134237) From 60e6243235840c4bcd7e995a716bab5b759e7e27 Mon Sep 17 00:00:00 2001 From: "Tomas R." Date: Thu, 22 May 2025 14:05:43 -0400 Subject: [PATCH 09/17] GH-131798: Optimize away isinstance calls in the JIT (GH-134369) From 0ecb6d8d030f4c59d6e0f0b95affd3a973d91819 Mon Sep 17 00:00:00 2001 From: Pranjal095 Date: Sat, 12 Jul 2025 20:31:43 +0530 Subject: [PATCH 10/17] Refactor getattr and delattr suggestion tests --- Lib/test/test_traceback.py | 348 ++++++++++++++++++------------------- 1 file changed, 174 insertions(+), 174 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 89eea5bf65975d..bdc34a29022ffe 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4020,191 +4020,127 @@ def test_dont_swallow_subexceptions_of_falsey_exceptiongroup(self): global_for_suggestions = None +class SuggestionFormattingTestBaseParent: + def get_suggestion(self, obj, attr_name=None): + if attr_name is not None: + def callable(): + getattr(obj, attr_name) + else: + callable = obj -class SuggestionFormattingTestBase: - class BaseSuggestionTests: - """ - Subclasses need to implement the get_suggestion method. - """ - def test_suggestions(self): - class Substitution: - noise = more_noise = a = bc = None - blech = None - - class Elimination: - noise = more_noise = a = bc = None - blch = None - - class Addition: - noise = more_noise = a = bc = None - bluchin = None - - class SubstitutionOverElimination: - blach = None - bluc = None - - class SubstitutionOverAddition: - blach = None - bluchi = None - - class EliminationOverAddition: - blucha = None - bluc = None - - class CaseChangeOverSubstitution: - Luch = None - fluch = None - BLuch = None - - for cls, suggestion in [ - (Addition, "'bluchin'?"), - (Substitution, "'blech'?"), - (Elimination, "'blch'?"), - (Addition, "'bluchin'?"), - (SubstitutionOverElimination, "'blach'?"), - (SubstitutionOverAddition, "'blach'?"), - (EliminationOverAddition, "'bluc'?"), - (CaseChangeOverSubstitution, "'BLuch'?"), - ]: - obj = cls() - actual = self.get_suggestion(obj, 'bluch') - self.assertIn(suggestion, actual) - - def test_suggestions_underscored(self): - class A: - bluch = None - - 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')) + result_lines = self.get_exception( + callable, slice_start=-1, slice_end=None + ) + return result_lines[0] + +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 - class B: - _bluch = None - def method(self, name): - getattr(self, name) - - 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')) - - 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_do_not_trigger_for_long_attributes(self): - class A: - blech = None - - obj = A() - actual = self.get_suggestion(obj, 'somethingverywrong') - self.assertNotIn("blech", actual) - - 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(obj, name) - self.assertNotIn("Did you mean", actual) - self.assertNotIn("'vvv", actual) - self.assertNotIn("'mom'", actual) - self.assertNotIn("'id'", actual) - self.assertNotIn("'w'", actual) - self.assertNotIn("'pytho'", actual) - - def test_do_not_trigger_for_big_dicts(self): - class A: - blech = None - # A class with a very big __dict__ will not be considered - # for suggestions. - for index in range(2000): - setattr(A, f"index_{index}", None) - - obj = A() - actual = self.get_suggestion(obj, 'bluch') - self.assertNotIn("blech", actual) + class Elimination: + noise = more_noise = a = bc = None + blch = None - 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 + class Addition: + noise = more_noise = a = bc = None + bluchin = None - result_lines = self.get_exception( - callable, slice_start=-1, slice_end=None - ) - return result_lines[0] + class SubstitutionOverElimination: + blach = None + bluc = None - def test_with_method_call(self): - # This is a placeholder method to make - # hasattr(self, 'test_with_method_call') return True - pass + class SubstitutionOverAddition: + blach = None + bluchi = None - def test_suggestions_no_args(self): - class A: - blech = None - def __getattr__(self, attr): - raise AttributeError() + class EliminationOverAddition: + blucha = None + bluc = None - actual = self.get_suggestion(A(), 'bluch') - self.assertIn("blech", actual) + class CaseChangeOverSubstitution: + Luch = None + fluch = None + BLuch = None - class A: - blech = None - def __getattr__(self, attr): - raise AttributeError + for cls, suggestion in [ + (Addition, "'bluchin'?"), + (Substitution, "'blech'?"), + (Elimination, "'blch'?"), + (Addition, "'bluchin'?"), + (SubstitutionOverElimination, "'blach'?"), + (SubstitutionOverAddition, "'blach'?"), + (EliminationOverAddition, "'bluc'?"), + (CaseChangeOverSubstitution, "'BLuch'?"), + ]: + obj = cls() + actual = self.get_suggestion(obj, 'bluch') + self.assertIn(suggestion, actual) - actual = self.get_suggestion(A(), 'bluch') - self.assertIn("blech", actual) + def test_suggestions_underscored(self): + class A: + bluch = None - def test_suggestions_invalid_args(self): - class NonStringifyClass: - __str__ = None - __repr__ = None + 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 A: - blech = None - def __getattr__(self, attr): - raise AttributeError(NonStringifyClass()) + class B: + _bluch = None + def method(self, name): + getattr(self, name) - class B: - blech = None - def __getattr__(self, attr): - raise AttributeError("Error", 23) + 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')) - class C: - blech = None - def __getattr__(self, attr): - raise AttributeError(23) - - for cls in [A, B, C]: - actual = self.get_suggestion(cls(), 'bluch') - self.assertIn("blech", actual) - - 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) + 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'))) - result_lines = self.get_exception( - callable, slice_start=-1, slice_end=None - ) - return result_lines[0] + def test_do_not_trigger_for_long_attributes(self): + class A: + blech = None + obj = A() + actual = self.get_suggestion(obj, 'somethingverywrong') + self.assertNotIn("blech", actual) + + 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(obj, name) + self.assertNotIn("Did you mean", actual) + self.assertNotIn("'vvv", actual) + self.assertNotIn("'mom'", actual) + self.assertNotIn("'id'", actual) + self.assertNotIn("'w'", actual) + self.assertNotIn("'pytho'", actual) + + def test_do_not_trigger_for_big_dicts(self): + class A: + blech = None + # A class with a very big __dict__ will not be considered + # for suggestions. + for index in range(2000): + setattr(A, f"index_{index}", None) + + obj = A() + actual = self.get_suggestion(obj, 'bluch') + self.assertNotIn("blech", actual) + +class GetattrSuggestionTests(BaseSuggestionTests): def get_suggestion(self, obj, attr_name=None): if attr_name is not None: def callable(): @@ -4217,6 +4153,70 @@ def callable(): ) 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): + raise AttributeError() + + actual = self.get_suggestion(A(), 'bluch') + self.assertIn("blech", actual) + + class A: + blech = None + def __getattr__(self, attr): + raise AttributeError + + actual = self.get_suggestion(A(), 'bluch') + self.assertIn("blech", actual) + + def test_suggestions_invalid_args(self): + class NonStringifyClass: + __str__ = None + __repr__ = None + + class A: + blech = None + def __getattr__(self, attr): + raise AttributeError(NonStringifyClass()) + + class B: + blech = None + def __getattr__(self, attr): + raise AttributeError("Error", 23) + + class C: + blech = None + def __getattr__(self, attr): + raise AttributeError(23) + + for cls in [A, B, C]: + actual = self.get_suggestion(cls(), 'bluch') + self.assertIn("blech", actual) + + 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 @@ -4696,7 +4696,7 @@ class CPythonSuggestionFormattingTests( class PurePythonGetattrSuggestionFormattingTests( PurePythonExceptionFormattingMixin, - SuggestionFormattingTestBase.GetattrSuggestionTests, + GetattrSuggestionTests, unittest.TestCase, ): """ @@ -4707,7 +4707,7 @@ class PurePythonGetattrSuggestionFormattingTests( class PurePythonDelattrSuggestionFormattingTests( PurePythonExceptionFormattingMixin, - SuggestionFormattingTestBase.DelattrSuggestionTests, + DelattrSuggestionTests, unittest.TestCase, ): """ @@ -4719,7 +4719,7 @@ class PurePythonDelattrSuggestionFormattingTests( @cpython_only class CPythonGetattrSuggestionFormattingTests( CAPIExceptionFormattingMixin, - SuggestionFormattingTestBase.GetattrSuggestionTests, + GetattrSuggestionTests, unittest.TestCase, ): """ @@ -4730,7 +4730,7 @@ class CPythonGetattrSuggestionFormattingTests( @cpython_only class CPythonDelattrSuggestionFormattingTests( CAPIExceptionFormattingMixin, - SuggestionFormattingTestBase.DelattrSuggestionTests, + DelattrSuggestionTests, unittest.TestCase, ): """ From 9a4e53b03787b7894e0a571b645439bd8b44b9bc Mon Sep 17 00:00:00 2001 From: Pranjal095 Date: Sat, 12 Jul 2025 21:10:12 +0530 Subject: [PATCH 11/17] Fix linting issue --- Lib/test/test_traceback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index bdc34a29022ffe..48533435d9d235 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4032,7 +4032,7 @@ def callable(): callable, slice_start=-1, slice_end=None ) return result_lines[0] - + class BaseSuggestionTests(SuggestionFormattingTestBaseParent): """ Subclasses need to implement the get_suggestion method. @@ -4138,7 +4138,7 @@ class A: obj = A() actual = self.get_suggestion(obj, 'bluch') - self.assertNotIn("blech", actual) + self.assertNotIn("blech", actual) class GetattrSuggestionTests(BaseSuggestionTests): def get_suggestion(self, obj, attr_name=None): From d1e38c2913faa42229e477fae7becd1ed13bc653 Mon Sep 17 00:00:00 2001 From: Pranjal095 Date: Sat, 12 Jul 2025 23:08:52 +0530 Subject: [PATCH 12/17] Add requested changes --- Lib/test/test_traceback.py | 58 +++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 48533435d9d235..57d9e7c2ac45a6 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4020,7 +4020,8 @@ def test_dont_swallow_subexceptions_of_falsey_exceptiongroup(self): global_for_suggestions = None -class SuggestionFormattingTestBaseParent: + +class SuggestionFormattingTestMixin: def get_suggestion(self, obj, attr_name=None): if attr_name is not None: def callable(): @@ -4033,10 +4034,12 @@ def callable(): ) return result_lines[0] -class BaseSuggestionTests(SuggestionFormattingTestBaseParent): + +class BaseSuggestionTests(SuggestionFormattingTestMixin): """ Subclasses need to implement the get_suggestion method. """ + def test_suggestions(self): class Substitution: noise = more_noise = a = bc = None @@ -4077,50 +4080,40 @@ class CaseChangeOverSubstitution: (EliminationOverAddition, "'bluc'?"), (CaseChangeOverSubstitution, "'BLuch'?"), ]: - obj = cls() - actual = self.get_suggestion(obj, 'bluch') + actual = self.get_suggestion(cls(), 'bluch') self.assertIn(suggestion, actual) def test_suggestions_underscored(self): class A: bluch = None - 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')) + self.assertIn("'bluch'", self.get_suggestion(A(), 'blach')) + self.assertIn("'bluch'", self.get_suggestion(A(), '_luch')) + self.assertIn("'bluch'", self.get_suggestion(A(), '_bluch')) class B: _bluch = None def method(self, name): getattr(self, name) - 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')) - - 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'))) + self.assertIn("'_bluch'", self.get_suggestion(B(), '_blach')) + self.assertIn("'_bluch'", self.get_suggestion(B(), '_luch')) + self.assertNotIn("'_bluch'", self.get_suggestion(B(), 'bluch')) def test_do_not_trigger_for_long_attributes(self): class A: blech = None - obj = A() - actual = self.get_suggestion(obj, 'somethingverywrong') + actual = self.get_suggestion(A(), 'somethingverywrong') self.assertNotIn("blech", actual) 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(obj, name) + actual = self.get_suggestion(MyClass(), name) self.assertNotIn("Did you mean", actual) self.assertNotIn("'vvv", actual) self.assertNotIn("'mom'", actual) @@ -4136,10 +4129,10 @@ class A: for index in range(2000): setattr(A, f"index_{index}", None) - obj = A() - actual = self.get_suggestion(obj, 'bluch') + actual = self.get_suggestion(A(), 'bluch') self.assertNotIn("blech", actual) + class GetattrSuggestionTests(BaseSuggestionTests): def get_suggestion(self, obj, attr_name=None): if attr_name is not None: @@ -4153,11 +4146,6 @@ def callable(): ) 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 @@ -4206,6 +4194,17 @@ def __dir__(self): actual = self.get_suggestion(A(), 'blech') self.assertNotIn("Did you mean", actual) + def test_suggestions_with_method_call(self): + class B: + _bluch = None + def method(self, name): + getattr(self, name) + + 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'))) + + class DelattrSuggestionTests(BaseSuggestionTests): def get_suggestion(self, obj, attr_name): def callable(): @@ -4216,7 +4215,8 @@ def callable(): ) return result_lines[0] -class SuggestionFormattingTestBase(SuggestionFormattingTestBaseParent): + +class SuggestionFormattingTestBase(SuggestionFormattingTestMixin): def test_attribute_error_with_failing_dict(self): class T: bluch = 1 From fb654a4f351cc4309fa9cb8a9a4d2aef525c7c58 Mon Sep 17 00:00:00 2001 From: Pranjal095 Date: Sun, 13 Jul 2025 00:04:19 +0530 Subject: [PATCH 13/17] Add requested changes --- Doc/whatsnew/3.15.rst | 14 ++++++++++++++ Lib/test/test_traceback.py | 7 ++++--- Objects/dictobject.c | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 010abb7d9b9278..fa76249354d2b5 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -175,6 +175,20 @@ Other language changes * Several error messages incorrectly using the term "argument" have been corrected. (Contributed by Stan Ulbrych in :gh:`133382`.) +* The interpreter now provides helpful suggestions when :func:`delattr` fails due to a missing attribute. When an attribute name that closely resembles an existing attribute is used, the interpreter will suggest the correct attribute name in the error message. For example: + +.. doctest:: + + >>> class A: + ... pass + >>> a = A() + >>> a.abcde = 1 + >>> del a.abcdf + Traceback (most recent call last): + ... + AttributeError: 'A' object has no attribute 'abcdf'. Did you mean: 'abcde'? + +(Contributed by [sobolevn] and Pranjal Prajapati in :gh:`136588`.) New modules diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 57d9e7c2ac45a6..12b5930330f22d 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4200,9 +4200,10 @@ class B: def method(self, name): getattr(self, name) - 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'))) + obj = B() + 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'))) class DelattrSuggestionTests(BaseSuggestionTests): diff --git a/Objects/dictobject.c b/Objects/dictobject.c index f8e63a9315d9c0..f1227f269bb13f 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -6939,7 +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); + (void)_PyObject_SetAttributeErrorContext(obj, name); return -1; } From 023ad724dbd149eb9de978d16dbc1db89f1a60a1 Mon Sep 17 00:00:00 2001 From: Pranjal095 Date: Sun, 13 Jul 2025 00:27:53 +0530 Subject: [PATCH 14/17] Add requested changes --- Doc/whatsnew/3.15.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index fa76249354d2b5..42a9a39e4f5895 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -175,7 +175,12 @@ Other language changes * Several error messages incorrectly using the term "argument" have been corrected. (Contributed by Stan Ulbrych in :gh:`133382`.) -* The interpreter now provides helpful suggestions when :func:`delattr` fails due to a missing attribute. When an attribute name that closely resembles an existing attribute is used, the interpreter will suggest the correct attribute name in the error message. For example: +* The interpreter now provides helpful suggestions +when :func:`delattr` fails due to a missing attribute. + +When an attribute name that closely resembles an existing attribute is used, +the interpreter will suggest the correct attribute +name in the error message. For example: .. doctest:: @@ -183,12 +188,12 @@ Other language changes ... pass >>> a = A() >>> a.abcde = 1 - >>> del a.abcdf + >>> del a.abcdf # doctest: +ELLIPSIS Traceback (most recent call last): ... AttributeError: 'A' object has no attribute 'abcdf'. Did you mean: 'abcde'? -(Contributed by [sobolevn] and Pranjal Prajapati in :gh:`136588`.) +(Contributed by Nikita Sobolev and Pranjal Prajapati in :gh:`136588`.) New modules From 0b9b2e4c47941906d0df4a1ec90ed73e3f804329 Mon Sep 17 00:00:00 2001 From: Pranjal095 Date: Sun, 13 Jul 2025 00:38:18 +0530 Subject: [PATCH 15/17] Fix doc indentation issue --- Doc/whatsnew/3.15.rst | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 42a9a39e4f5895..b1376d5a806134 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -176,24 +176,23 @@ Other language changes (Contributed by Stan Ulbrych in :gh:`133382`.) * The interpreter now provides helpful suggestions -when :func:`delattr` fails due to a missing attribute. - -When an attribute name that closely resembles an existing attribute is used, -the interpreter will suggest the correct attribute -name in the error message. For example: - -.. doctest:: - - >>> class A: - ... pass - >>> a = A() - >>> a.abcde = 1 - >>> del a.abcdf # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - AttributeError: 'A' object has no attribute 'abcdf'. Did you mean: 'abcde'? - -(Contributed by Nikita Sobolev and Pranjal Prajapati in :gh:`136588`.) + when :func:`delattr` fails due to a missing attribute. + When an attribute name that closely resembles an existing attribute is used, + the interpreter will suggest the correct attribute + name in the error message. For example: + + .. doctest:: + + >>> class A: + ... pass + >>> a = A() + >>> a.abcde = 1 + >>> del a.abcdf # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + AttributeError: 'A' object has no attribute 'abcdf'. Did you mean: 'abcde'? + + (Contributed by Nikita Sobolev and Pranjal Prajapati in :gh:`136588`.) New modules From ae0da23c9219952bfb71ae39afc6949e6ed8fa04 Mon Sep 17 00:00:00 2001 From: Pranjal095 Date: Sun, 13 Jul 2025 11:49:35 +0530 Subject: [PATCH 16/17] Make requested change --- Doc/whatsnew/3.15.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index b1376d5a806134..9172e76d675bb7 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -175,7 +175,7 @@ Other language changes * Several error messages incorrectly using the term "argument" have been corrected. (Contributed by Stan Ulbrych in :gh:`133382`.) -* The interpreter now provides helpful suggestions +* The interpreter now provides a suggestion when :func:`delattr` fails due to a missing attribute. When an attribute name that closely resembles an existing attribute is used, the interpreter will suggest the correct attribute From 6f7e03ad1be755cb6d6f90cc8c40d979a882a866 Mon Sep 17 00:00:00 2001 From: Pranjal095 Date: Mon, 14 Jul 2025 21:47:11 +0530 Subject: [PATCH 17/17] Make requested changes --- Doc/whatsnew/3.15.rst | 4 ++-- Lib/test/test_traceback.py | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 9172e76d675bb7..477229ecd1eaed 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -175,8 +175,8 @@ Other language changes * Several error messages incorrectly using the term "argument" have been corrected. (Contributed by Stan Ulbrych in :gh:`133382`.) -* The interpreter now provides a suggestion - when :func:`delattr` fails due to a missing attribute. +* The interpreter now provides a suggestion tries to provide a suggestion when + :func:`delattr` fails due to a missing attribute. When an attribute name that closely resembles an existing attribute is used, the interpreter will suggest the correct attribute name in the error message. For example: diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 12b5930330f22d..831292026e9669 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4701,8 +4701,8 @@ class PurePythonGetattrSuggestionFormattingTests( unittest.TestCase, ): """ - Same set of tests (for attribute access) as above using the pure Python implementation of - traceback printing in traceback.py. + Same set of tests (for attribute access) as above using the pure Python + implementation of traceback printing in traceback.py. """ @@ -4712,8 +4712,8 @@ class PurePythonDelattrSuggestionFormattingTests( unittest.TestCase, ): """ - Same set of tests (for attribute deletion) as above using the pure Python implementation of - traceback printing in traceback.py. + Same set of tests (for attribute deletion) as above using the pure Python + implementation of traceback printing in traceback.py. """ @@ -4724,7 +4724,8 @@ class CPythonGetattrSuggestionFormattingTests( unittest.TestCase, ): """ - Same set of tests (for attribute access) as above but with Python's internal traceback printing. + Same set of tests (for attribute access) as above but with Python's + internal traceback printing. """ @@ -4735,7 +4736,8 @@ class CPythonDelattrSuggestionFormattingTests( unittest.TestCase, ): """ - Same set of tests (for attribute deletion) as above but with Python's internal traceback printing. + Same set of tests (for attribute deletion) as above but with Python's + internal traceback printing. """ class MiscTest(unittest.TestCase):