diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 63ef5f84301794..73b6aa82238628 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -379,6 +379,9 @@ Improved error messages .. code-block:: python + from dataclasses import dataclass + from math import pi + @dataclass class Circle: radius: float diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 2fbc2a041269f4..0d30da2768d09b 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4401,17 +4401,23 @@ def __init__(self): def test_getattr_nested_with_property(self): # Test that descriptors (including properties) are suggested in nested attributes class Inner: + def __init__(self): + self.access_counter = 0 @property def computed(self): + self.access_counter += 1 return 42 class Outer: def __init__(self): self.inner = Inner() - actual = self.get_suggestion(Outer(), 'computed') - # Descriptors should not be suggested to avoid executing arbitrary code + obj = Outer() + actual = self.get_suggestion(obj, 'computed') + # Descriptors should be suggested self.assertIn("inner.computed", actual) + # Should not increment the access counter + self.assertEqual(obj.inner.access_counter, 0) 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 4e809acb7a01bb..bf128f75dc97b8 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -14,6 +14,7 @@ import io import importlib.util import pathlib +import inspect import _colorize from contextlib import suppress @@ -1670,30 +1671,27 @@ 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. Skips lazy imports to avoid triggering module loading. """ # 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__'): - continue # Skip descriptors to avoid executing arbitrary code + with suppress(AttributeError): + attr_obj = inspect.getattr_static(obj, attr_name) + + try: + inspect.getattr_static(attr_obj, '__get__') + continue # Descriptor, skip it as we can't access its contents safely + except AttributeError: + pass # Skip lazy imports to avoid triggering module loading if _is_lazy_import(obj, attr_name): continue - # 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) + inspect.getattr_static(attr_obj, wrong_name) - if hasattr(attr_obj, wrong_name): - return f"{attr_name}.{wrong_name}" + return f"{attr_name}.{wrong_name}" return None diff --git a/Misc/NEWS.d/next/Library/2026-03-02-13-35-14.gh-issue-145413.-x5Hpk.rst b/Misc/NEWS.d/next/Library/2026-03-02-13-35-14.gh-issue-145413.-x5Hpk.rst new file mode 100644 index 00000000000000..6b29579a328f9a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-02-13-35-14.gh-issue-145413.-x5Hpk.rst @@ -0,0 +1 @@ +Fixed nested properties being executed in error message suggestions