Skip to content

gh-130425: Add "Did you mean [...]" suggestions for del obj.attr #136588

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ee4081f
gh-130425: Add "Did you mean" suggestion for `del obj.attr`
sobolevn Feb 21, 2025
cf9a052
Update NEWS
sobolevn Feb 22, 2025
c369876
gh-130428: Add tests for delattr suggestions
Pranjal095 Feb 22, 2025
04254ad
Refactored getattr and delattr tests
Pranjal095 Feb 22, 2025
57deaf3
Added else branches to handle unrecognized operations
Pranjal095 Feb 22, 2025
bbae851
Refactor getattr and setattr suggestion tests
Pranjal095 Mar 4, 2025
8ba26c4
docs: -I also implies -P (#131539)
nedbat Mar 28, 2025
b1210dd
GH-134236: make regen-all (GH-134237)
sobolevn May 19, 2025
60e6243
GH-131798: Optimize away isinstance calls in the JIT (GH-134369)
tomasr8 May 22, 2025
0ecb6d8
Refactor getattr and delattr suggestion tests
Pranjal095 Jul 12, 2025
9a4e53b
Fix linting issue
Pranjal095 Jul 12, 2025
d1e38c2
Add requested changes
Pranjal095 Jul 12, 2025
9a361aa
Merge remote-tracking branch 'upstream/main' into delattr-suggestions-re
Pranjal095 Jul 12, 2025
fb654a4
Add requested changes
Pranjal095 Jul 12, 2025
023ad72
Add requested changes
Pranjal095 Jul 12, 2025
0b9b2e4
Fix doc indentation issue
Pranjal095 Jul 12, 2025
ae0da23
Make requested change
Pranjal095 Jul 13, 2025
6f7e03a
Make requested changes
Pranjal095 Jul 14, 2025
6488adb
Add requested changes
Pranjal095 Aug 2, 2025
e008ea4
Merge branch 'main' into delattr-suggestions-re
Pranjal095 Aug 5, 2025
68ea8f1
Parametrize getattr/setattr in get_suggestion
encukou Aug 19, 2025
30d8043
Run test_suggestions_for_same_name for delattr too
encukou Aug 19, 2025
8c9c597
Merge pull request #1 from encukou/delattr-suggestions-re
Pranjal095 Aug 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,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`.)
Expand Down
93 changes: 76 additions & 17 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -4021,11 +4021,13 @@ def test_dont_swallow_subexceptions_of_falsey_exceptiongroup(self):
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

Expand All @@ -4034,7 +4036,9 @@ def callable():
)
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
Expand Down Expand Up @@ -4077,18 +4081,19 @@ class CaseChangeOverSubstitution:
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

self.assertIn("'bluch'", self.get_suggestion(A(), 'blach'))
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'))
Expand All @@ -4098,28 +4103,29 @@ def method(self, name):
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)
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
Expand All @@ -4130,7 +4136,16 @@ class A:
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):
Expand All @@ -4147,7 +4162,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 +4186,12 @@ def __getattr__(self, attr):
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
Expand Down Expand Up @@ -4655,6 +4669,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):
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 @@ -6982,6 +6982,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;
}

Expand Down
Loading