gh-130425: Add "Did you mean [...]" suggestions for del obj.attr (GH-136588)
Co-authored-by: sobolevn <mail@sobolevn.me> Co-authored-by: Ned Batchelder <ned@nedbatchelder.com> Co-authored-by: Tomas R. <tomas.roun8@gmail.com> Co-authored-by: Petr Viktorin <encukou@gmail.com>
This commit is contained in:
committed by
GitHub
parent
4519b8acb5
commit
f27af8ba8e
@@ -239,6 +239,25 @@ Other language changes
|
||||
* Several error messages incorrectly using the term "argument" have been corrected.
|
||||
(Contributed by Stan Ulbrych in :gh:`133382`.)
|
||||
|
||||
* The interpreter now 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:
|
||||
|
||||
.. 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`.)
|
||||
|
||||
* Unraisable exceptions are now highlighted with color by default. This can be
|
||||
controlled by :ref:`environment variables <using-on-controlling-color>`.
|
||||
(Contributed by Peter Bierma in :gh:`134170`.)
|
||||
|
||||
@@ -4064,11 +4064,13 @@ class TestTracebackException_ExceptionGroups(unittest.TestCase):
|
||||
global_for_suggestions = None
|
||||
|
||||
|
||||
class SuggestionFormattingTestBase:
|
||||
class SuggestionFormattingTestMixin:
|
||||
attr_function = getattr
|
||||
|
||||
def get_suggestion(self, obj, attr_name=None):
|
||||
if attr_name is not None:
|
||||
def callable():
|
||||
getattr(obj, attr_name)
|
||||
self.attr_function(obj, attr_name)
|
||||
else:
|
||||
callable = obj
|
||||
|
||||
@@ -4077,7 +4079,9 @@ class SuggestionFormattingTestBase:
|
||||
)
|
||||
return result_lines[0]
|
||||
|
||||
def test_getattr_suggestions(self):
|
||||
|
||||
class BaseSuggestionTests(SuggestionFormattingTestMixin):
|
||||
def test_suggestions(self):
|
||||
class Substitution:
|
||||
noise = more_noise = a = bc = None
|
||||
blech = None
|
||||
@@ -4120,7 +4124,7 @@ class SuggestionFormattingTestBase:
|
||||
actual = self.get_suggestion(cls(), 'bluch')
|
||||
self.assertIn(suggestion, actual)
|
||||
|
||||
def test_getattr_suggestions_underscored(self):
|
||||
def test_suggestions_underscored(self):
|
||||
class A:
|
||||
bluch = None
|
||||
|
||||
@@ -4128,10 +4132,11 @@ class SuggestionFormattingTestBase:
|
||||
self.assertIn("'bluch'", self.get_suggestion(A(), '_luch'))
|
||||
self.assertIn("'bluch'", self.get_suggestion(A(), '_bluch'))
|
||||
|
||||
attr_function = self.attr_function
|
||||
class B:
|
||||
_bluch = None
|
||||
def method(self, name):
|
||||
getattr(self, name)
|
||||
attr_function(self, name)
|
||||
|
||||
self.assertIn("'_bluch'", self.get_suggestion(B(), '_blach'))
|
||||
self.assertIn("'_bluch'", self.get_suggestion(B(), '_luch'))
|
||||
@@ -4141,20 +4146,21 @@ class SuggestionFormattingTestBase:
|
||||
self.assertIn("'_bluch'", self.get_suggestion(partial(B().method, '_luch')))
|
||||
self.assertIn("'_bluch'", self.get_suggestion(partial(B().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')
|
||||
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
|
||||
|
||||
for name in ("b", "v", "m", "py"):
|
||||
with self.subTest(name=name):
|
||||
actual = self.get_suggestion(MyClass, name)
|
||||
actual = self.get_suggestion(MyClass(), name)
|
||||
self.assertNotIn("Did you mean", actual)
|
||||
self.assertNotIn("'vvv", actual)
|
||||
self.assertNotIn("'mom'", actual)
|
||||
@@ -4162,7 +4168,7 @@ class SuggestionFormattingTestBase:
|
||||
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
|
||||
@@ -4173,7 +4179,16 @@ class SuggestionFormattingTestBase:
|
||||
actual = self.get_suggestion(A(), 'bluch')
|
||||
self.assertNotIn("blech", actual)
|
||||
|
||||
def test_getattr_suggestions_no_args(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 GetattrSuggestionTests(BaseSuggestionTests):
|
||||
def test_suggestions_no_args(self):
|
||||
class A:
|
||||
blech = None
|
||||
def __getattr__(self, attr):
|
||||
@@ -4190,7 +4205,7 @@ class SuggestionFormattingTestBase:
|
||||
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
|
||||
@@ -4214,13 +4229,12 @@ class SuggestionFormattingTestBase:
|
||||
actual = self.get_suggestion(cls(), 'bluch')
|
||||
self.assertIn("blech", actual)
|
||||
|
||||
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)
|
||||
|
||||
class DelattrSuggestionTests(BaseSuggestionTests):
|
||||
attr_function = delattr
|
||||
|
||||
|
||||
class SuggestionFormattingTestBase(SuggestionFormattingTestMixin):
|
||||
def test_attribute_error_with_failing_dict(self):
|
||||
class T:
|
||||
bluch = 1
|
||||
@@ -4876,6 +4890,51 @@ 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):
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
Add ``"Did you mean: 'attr'?"`` suggestion when using ``del obj.attr`` if ``attr``
|
||||
does not exist.
|
||||
@@ -6983,6 +6983,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);
|
||||
(void)_PyObject_SetAttributeErrorContext(obj, name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user