From 2665658a7d61c10d4e034d3cf68543c1f3a7320e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 11 Mar 2026 16:11:38 +0100 Subject: [PATCH] [3.13] gh-139933: correctly suggest attributes for classes with a custom `__dir__` (GH-139950) (GH-145827) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 4722202a1a81974089801e6173d269836b6a074f) (cherry picked from commit 0a80015ac26d60bff54f57ce9f73ffc5386249b1) Co-authored-by: Łukasz Langa Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_traceback.py | 21 ++++++++++++++++++ Lib/traceback.py | 22 ++++++++++--------- ...-10-11-11-50-59.gh-issue-139933.05MHlx.rst | 3 +++ 3 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-10-11-11-50-59.gh-issue-139933.05MHlx.rst diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 915055fdc743f8..fec6750afa9d2f 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4113,6 +4113,27 @@ 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_with_custom___dir__(self): + class M(type): + def __dir__(cls): + return [None, "fox"] + + class C0: + def __dir__(self): + return [..., "bluch"] + + class C1(C0, metaclass=M): + pass + + self.assertNotIn("'bluch'", self.get_suggestion(C0, "blach")) + self.assertIn("'bluch'", self.get_suggestion(C0(), "blach")) + + self.assertIn("'fox'", self.get_suggestion(C1, "foo")) + self.assertNotIn("'fox'", self.get_suggestion(C1(), "foo")) + + self.assertNotIn("'bluch'", self.get_suggestion(C1, "blach")) + self.assertIn("'bluch'", self.get_suggestion(C1(), "blach")) + def test_getattr_suggestions_do_not_trigger_for_long_attributes(self): class A: blech = None diff --git a/Lib/traceback.py b/Lib/traceback.py index 572a3177cb0e14..b412954bd53286 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1484,17 +1484,23 @@ def _substitution_cost(ch_a, ch_b): return _MOVE_COST +def _get_safe___dir__(obj): + # Use obj.__dir__() to avoid a TypeError when calling dir(obj). + # See gh-131001 and gh-139933. + try: + d = obj.__dir__() + except TypeError: # when obj is a class + d = type(obj).__dir__(obj) + return sorted(x for x in d if isinstance(x, str)) + + def _compute_suggestion_error(exc_value, tb, wrong_name): if wrong_name is None or not isinstance(wrong_name, str): return None if isinstance(exc_value, AttributeError): obj = exc_value.obj try: - try: - d = dir(obj) - except TypeError: # Attributes are unsortable, e.g. int and str - d = list(obj.__class__.__dict__.keys()) + list(obj.__dict__.keys()) - d = sorted([x for x in d if isinstance(x, str)]) + d = _get_safe___dir__(obj) hide_underscored = (wrong_name[:1] != '_') if hide_underscored and tb is not None: while tb.tb_next is not None: @@ -1509,11 +1515,7 @@ def _compute_suggestion_error(exc_value, tb, wrong_name): elif isinstance(exc_value, ImportError): try: mod = __import__(exc_value.name) - try: - d = dir(mod) - except TypeError: # Attributes are unsortable, e.g. int and str - d = list(mod.__dict__.keys()) - d = sorted([x for x in d if isinstance(x, str)]) + d = _get_safe___dir__(mod) if wrong_name[:1] != '_': d = [x for x in d if x[:1] != '_'] except Exception: diff --git a/Misc/NEWS.d/next/Library/2025-10-11-11-50-59.gh-issue-139933.05MHlx.rst b/Misc/NEWS.d/next/Library/2025-10-11-11-50-59.gh-issue-139933.05MHlx.rst new file mode 100644 index 00000000000000..d76f0873d77265 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-11-11-50-59.gh-issue-139933.05MHlx.rst @@ -0,0 +1,3 @@ +Improve :exc:`AttributeError` suggestions for classes with a custom +:meth:`~object.__dir__` method returning a list of unsortable values. +Patch by Bénédikt Tran.