From 43934319512c37c8491942688203f7e494a6c7be Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Fri, 20 Mar 2026 00:19:06 +0000 Subject: [PATCH] gh-146171: Fix nested AttributeError suggestions --- Lib/test/test_traceback.py | 10 ++++++---- Lib/traceback.py | 16 ++++++++++------ ...6-03-20-00-28-00.gh-issue-146171.P5Jk2R7v.rst | 2 ++ 3 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-03-20-00-28-00.gh-issue-146171.P5Jk2R7v.rst diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 14a08995bf127c..7124e49b22e361 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4420,19 +4420,21 @@ def __init__(self): self.assertNotIn("inner.foo", actual) def test_getattr_nested_with_property(self): - # Test that descriptors (including properties) are suggested in nested attributes + # Property suggestions should not execute the property getter. class Inner: @property def computed(self): - return 42 + return missing_name class Outer: def __init__(self): self.inner = Inner() actual = self.get_suggestion(Outer(), 'computed') - # Descriptors should not be suggested to avoid executing arbitrary code - self.assertIn("inner.computed", actual) + self.assertIn( + "Did you mean '.inner.computed' instead of '.computed'", + actual, + ) def test_getattr_nested_no_suggestion_for_deep_nesting(self): # Test that deeply nested attributes (2+ levels) are not suggested diff --git a/Lib/traceback.py b/Lib/traceback.py index 956cab49131990..56a72ce7f5b293 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1670,16 +1670,20 @@ def _check_for_nested_attribute(obj, wrong_name, attrs): Returns the first nested attribute suggestion found, or None. Limited to checking 20 attributes. - Only considers non-descriptor attributes to avoid executing arbitrary code. + Only considers non-descriptor outer attributes to avoid executing + arbitrary code. Checks nested attributes statically so descriptors such + as properties can still be suggested without invoking them. Skips lazy imports to avoid triggering module loading. """ + from inspect import getattr_static + # Check for nested attributes (only one level deep) attrs_to_check = [x for x in attrs if not x.startswith('_')][:20] # Limit number of attributes to check for attr_name in attrs_to_check: with suppress(Exception): # Check if attr_name is a descriptor - if so, skip it - attr_from_class = getattr(type(obj), attr_name, None) - if attr_from_class is not None and hasattr(attr_from_class, '__get__'): + attr_from_class = getattr_static(type(obj), attr_name, _sentinel) + if attr_from_class is not _sentinel and hasattr(attr_from_class, '__get__'): continue # Skip descriptors to avoid executing arbitrary code # Skip lazy imports to avoid triggering module loading @@ -1689,10 +1693,10 @@ def _check_for_nested_attribute(obj, wrong_name, attrs): # Safe to get the attribute since it's not a descriptor attr_obj = getattr(obj, attr_name) - # Check if the nested attribute exists and is not a descriptor - nested_attr_from_class = getattr(type(attr_obj), wrong_name, None) + if _is_lazy_import(attr_obj, wrong_name): + continue - if hasattr(attr_obj, wrong_name): + if getattr_static(attr_obj, wrong_name, _sentinel) is not _sentinel: return f"{attr_name}.{wrong_name}" return None diff --git a/Misc/NEWS.d/next/Library/2026-03-20-00-28-00.gh-issue-146171.P5Jk2R7v.rst b/Misc/NEWS.d/next/Library/2026-03-20-00-28-00.gh-issue-146171.P5Jk2R7v.rst new file mode 100644 index 00000000000000..9514085bd3d27b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-20-00-28-00.gh-issue-146171.P5Jk2R7v.rst @@ -0,0 +1,2 @@ +Nested :exc:`AttributeError` suggestions now include property-backed +attributes on nested objects without executing the property getter.