Skip to content
132 changes: 107 additions & 25 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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
Expand Down Expand Up @@ -4074,63 +4077,88 @@ 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)
self.assertNotIn("'id'", actual)
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
# for suggestions.
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):
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add ``"Did you mean: 'attr'?"`` suggestion when using ``del obj.attr`` if ``attr``
does not exist.
1 change: 1 addition & 0 deletions Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading