bpo-38530: Offer suggestions on AttributeError (#16856)

When printing AttributeError, PyErr_Display will offer suggestions of similar 
attribute names in the object that the exception was raised from:

>>> collections.namedtoplo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'collections' has no attribute 'namedtoplo'. Did you mean: namedtuple?
This commit is contained in:
Pablo Galindo 2021-04-14 02:36:07 +01:00 committed by GitHub
parent 3bc694d5f3
commit 37494b441a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 472 additions and 17 deletions

View file

@ -1414,6 +1414,165 @@ class ExceptionTests(unittest.TestCase):
gc_collect()
class AttributeErrorTests(unittest.TestCase):
def test_attributes(self):
# Setting 'attr' should not be a problem.
exc = AttributeError('Ouch!')
self.assertIsNone(exc.name)
self.assertIsNone(exc.obj)
sentinel = object()
exc = AttributeError('Ouch', name='carry', obj=sentinel)
self.assertEqual(exc.name, 'carry')
self.assertIs(exc.obj, sentinel)
def test_getattr_has_name_and_obj(self):
class A:
blech = None
obj = A()
try:
obj.bluch
except AttributeError as exc:
self.assertEqual("bluch", exc.name)
self.assertEqual(obj, exc.obj)
def test_getattr_has_name_and_obj_for_method(self):
class A:
def blech(self):
return
obj = A()
try:
obj.bluch()
except AttributeError as exc:
self.assertEqual("bluch", exc.name)
self.assertEqual(obj, exc.obj)
def test_getattr_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
for cls, suggestion in [(Substitution, "blech?"),
(Elimination, "blch?"),
(Addition, "bluchin?"),
(EliminationOverAddition, "bluc?"),
(SubstitutionOverElimination, "blach?"),
(SubstitutionOverAddition, "blach?")]:
try:
cls().bluch
except AttributeError as exc:
with support.captured_stderr() as err:
sys.__excepthook__(*sys.exc_info())
self.assertIn(suggestion, err.getvalue())
def test_getattr_suggestions_do_not_trigger_for_long_attributes(self):
class A:
blech = None
try:
A().somethingverywrong
except AttributeError as exc:
with support.captured_stderr() as err:
sys.__excepthook__(*sys.exc_info())
self.assertNotIn("blech", err.getvalue())
def test_getattr_suggestions_do_not_trigger_for_big_dicts(self):
class A:
blech = None
# A class with a very big __dict__ will not be consider
# for suggestions.
for index in range(101):
setattr(A, f"index_{index}", None)
try:
A().bluch
except AttributeError as exc:
with support.captured_stderr() as err:
sys.__excepthook__(*sys.exc_info())
self.assertNotIn("blech", err.getvalue())
def test_getattr_suggestions_no_args(self):
class A:
blech = None
def __getattr__(self, attr):
raise AttributeError()
try:
A().bluch
except AttributeError as exc:
with support.captured_stderr() as err:
sys.__excepthook__(*sys.exc_info())
self.assertIn("blech", err.getvalue())
class A:
blech = None
def __getattr__(self, attr):
raise AttributeError
try:
A().bluch
except AttributeError as exc:
with support.captured_stderr() as err:
sys.__excepthook__(*sys.exc_info())
self.assertIn("blech", err.getvalue())
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 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]:
try:
cls().bluch
except AttributeError as exc:
with support.captured_stderr() as err:
sys.__excepthook__(*sys.exc_info())
self.assertIn("blech", err.getvalue())
class ImportErrorTests(unittest.TestCase):
def test_attributes(self):