Skip to content

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

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

Closed
wants to merge 10 commits into from
Prev Previous commit
Next Next commit
Refactored getattr and delattr tests
  • Loading branch information
Pranjal095 committed Jul 12, 2025
commit 04254addb93c336306195e2c785837ec4b644c27
193 changes: 77 additions & 116 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -4074,62 +4074,121 @@ 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":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't quite like the current design :(
It would be even more complex, when we add setattr tests (btw, I think that they should also be added in this PR).

Can we instead create a base class out of existing testcase? And then create three subclasses which would look like so:

class GetattrSuggestionsTests(BaseSuggestionsTests, unittest.TestCase):
    def get_suggestion(self, obj, name):
        # get suggestions using `getattr`

class SetattrSuggestionsTests(BaseSuggestionsTests, unittest.TestCase):
    def get_suggestion(self, obj, name):
        # get suggestions using `setattr`

# the same for `delattr`

Copy link
Author

@Pranjal095 Pranjal095 Mar 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll first refactor the tests as per your suggestion, and keep that as a separate commit.
I was unsure of adding setattr suggestion tests to this PR because:

  1. The concerning issue was raised for adding delattr suggestions alone.
  2. There are multiple ways to simulate an AttributeError for setattr (custom __slots__, @property etc.), and I felt it would be better to discuss the appropriate testing mechanism in a separate PR.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have made the requested changes as per the refactor request. Kindly review the PR again.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, just a gentle reminder to review the PR changes whenever convenient:) @sobolevn @picnixz

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)
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_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
# for suggestions.
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
Expand Down Expand Up @@ -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
Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy