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 20 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
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
Prev Previous commit
Next Next commit
Add requested changes
  • Loading branch information
Pranjal095 committed Jul 12, 2025
commit d1e38c2913faa42229e477fae7becd1ed13bc653
58 changes: 29 additions & 29 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -4020,7 +4020,8 @@ def test_dont_swallow_subexceptions_of_falsey_exceptiongroup(self):

global_for_suggestions = None

class SuggestionFormattingTestBaseParent:

class SuggestionFormattingTestMixin:
def get_suggestion(self, obj, attr_name=None):
if attr_name is not None:
def callable():
Expand All @@ -4033,10 +4034,12 @@ def callable():
)
return result_lines[0]

class BaseSuggestionTests(SuggestionFormattingTestBaseParent):

class BaseSuggestionTests(SuggestionFormattingTestMixin):
"""
Subclasses need to implement the get_suggestion method.
"""

def test_suggestions(self):
class Substitution:
noise = more_noise = a = bc = None
Expand Down Expand Up @@ -4077,50 +4080,40 @@ class CaseChangeOverSubstitution:
(EliminationOverAddition, "'bluc'?"),
(CaseChangeOverSubstitution, "'BLuch'?"),
]:
obj = cls()
actual = self.get_suggestion(obj, 'bluch')
actual = self.get_suggestion(cls(), 'bluch')
self.assertIn(suggestion, actual)

def test_suggestions_underscored(self):
class A:
bluch = None

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'))
self.assertIn("'bluch'", self.get_suggestion(A(), 'blach'))
self.assertIn("'bluch'", self.get_suggestion(A(), '_luch'))
self.assertIn("'bluch'", self.get_suggestion(A(), '_bluch'))

class B:
_bluch = None
def method(self, name):
getattr(self, name)

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'))

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')))
self.assertIn("'_bluch'", self.get_suggestion(B(), '_blach'))
self.assertIn("'_bluch'", self.get_suggestion(B(), '_luch'))
self.assertNotIn("'_bluch'", self.get_suggestion(B(), 'bluch'))

def test_do_not_trigger_for_long_attributes(self):
class A:
blech = None

obj = A()
actual = self.get_suggestion(obj, 'somethingverywrong')
actual = self.get_suggestion(A(), 'somethingverywrong')
self.assertNotIn("blech", actual)

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(obj, name)
actual = self.get_suggestion(MyClass(), name)
self.assertNotIn("Did you mean", actual)
self.assertNotIn("'vvv", actual)
self.assertNotIn("'mom'", actual)
Expand All @@ -4136,10 +4129,10 @@ class A:
for index in range(2000):
setattr(A, f"index_{index}", None)

obj = A()
actual = self.get_suggestion(obj, 'bluch')
actual = self.get_suggestion(A(), 'bluch')
self.assertNotIn("blech", actual)


class GetattrSuggestionTests(BaseSuggestionTests):
def get_suggestion(self, obj, attr_name=None):
if attr_name is not None:
Expand All @@ -4153,11 +4146,6 @@ def callable():
)
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
Expand Down Expand Up @@ -4206,6 +4194,17 @@ def __dir__(self):
actual = self.get_suggestion(A(), 'blech')
self.assertNotIn("Did you mean", actual)

def test_suggestions_with_method_call(self):
class B:
_bluch = None
def method(self, name):
getattr(self, name)

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')))


class DelattrSuggestionTests(BaseSuggestionTests):
def get_suggestion(self, obj, attr_name):
def callable():
Expand All @@ -4216,7 +4215,8 @@ def callable():
)
return result_lines[0]

class SuggestionFormattingTestBase(SuggestionFormattingTestBaseParent):

class SuggestionFormattingTestBase(SuggestionFormattingTestMixin):
def test_attribute_error_with_failing_dict(self):
class T:
bluch = 1
Expand Down