Skip to content

Commit 4722202

Browse files
picnixzambv
andauthored
gh-139933: correctly suggest attributes for classes with a custom __dir__ (GH-139950)
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
1 parent 7a1da45 commit 4722202

File tree

3 files changed

+39
-14
lines changed

3 files changed

+39
-14
lines changed

Lib/test/test_traceback.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4213,6 +4213,27 @@ def method(self, name):
42134213
self.assertIn("'._bluch'", self.get_suggestion(partial(B().method, '_luch')))
42144214
self.assertIn("'._bluch'", self.get_suggestion(partial(B().method, 'bluch')))
42154215

4216+
def test_suggestions_with_custom___dir__(self):
4217+
class M(type):
4218+
def __dir__(cls):
4219+
return [None, "fox"]
4220+
4221+
class C0:
4222+
def __dir__(self):
4223+
return [..., "bluch"]
4224+
4225+
class C1(C0, metaclass=M):
4226+
pass
4227+
4228+
self.assertNotIn("'.bluch'", self.get_suggestion(C0, "blach"))
4229+
self.assertIn("'.bluch'", self.get_suggestion(C0(), "blach"))
4230+
4231+
self.assertIn("'.fox'", self.get_suggestion(C1, "foo"))
4232+
self.assertNotIn("'.fox'", self.get_suggestion(C1(), "foo"))
4233+
4234+
self.assertNotIn("'.bluch'", self.get_suggestion(C1, "blach"))
4235+
self.assertIn("'.bluch'", self.get_suggestion(C1(), "blach"))
4236+
42164237

42174238
def test_do_not_trigger_for_long_attributes(self):
42184239
class A:

Lib/traceback.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,6 +1698,19 @@ def _check_for_nested_attribute(obj, wrong_name, attrs):
16981698
return None
16991699

17001700

1701+
def _get_safe___dir__(obj):
1702+
# Use obj.__dir__() to avoid a TypeError when calling dir(obj).
1703+
# See gh-131001 and gh-139933.
1704+
# Also filters out lazy imports to avoid triggering module loading.
1705+
try:
1706+
d = obj.__dir__()
1707+
except TypeError: # when obj is a class
1708+
d = type(obj).__dir__(obj)
1709+
return sorted(
1710+
x for x in d if isinstance(x, str) and not _is_lazy_import(obj, x)
1711+
)
1712+
1713+
17011714
def _compute_suggestion_error(exc_value, tb, wrong_name):
17021715
if wrong_name is None or not isinstance(wrong_name, str):
17031716
return None
@@ -1711,13 +1724,7 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
17111724
if isinstance(exc_value, AttributeError):
17121725
obj = exc_value.obj
17131726
try:
1714-
try:
1715-
d = dir(obj)
1716-
except TypeError: # Attributes are unsortable, e.g. int and str
1717-
d = list(obj.__class__.__dict__.keys()) + list(obj.__dict__.keys())
1718-
d = sorted([x for x in d if isinstance(x, str)])
1719-
# Filter out lazy imports to avoid triggering module loading
1720-
d = [x for x in d if not _is_lazy_import(obj, x)]
1727+
d = _get_safe___dir__(obj)
17211728
hide_underscored = (wrong_name[:1] != '_')
17221729
if hide_underscored and tb is not None:
17231730
while tb.tb_next is not None:
@@ -1744,13 +1751,7 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
17441751
elif isinstance(exc_value, ImportError):
17451752
try:
17461753
mod = __import__(exc_value.name)
1747-
try:
1748-
d = dir(mod)
1749-
except TypeError: # Attributes are unsortable, e.g. int and str
1750-
d = list(mod.__dict__.keys())
1751-
d = sorted([x for x in d if isinstance(x, str)])
1752-
# Filter out lazy imports to avoid triggering module loading
1753-
d = [x for x in d if not _is_lazy_import(mod, x)]
1754+
d = _get_safe___dir__(mod)
17541755
if wrong_name[:1] != '_':
17551756
d = [x for x in d if x[:1] != '_']
17561757
except Exception:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Improve :exc:`AttributeError` suggestions for classes with a custom
2+
:meth:`~object.__dir__` method returning a list of unsortable values.
3+
Patch by Bénédikt Tran.

0 commit comments

Comments
 (0)