From 9056fbc48ef1228f3fb6fdde7fc98d6a5d1d0cd9 Mon Sep 17 00:00:00 2001 From: Hiumee Date: Mon, 2 Mar 2026 13:19:21 +0200 Subject: [PATCH 1/3] Fix traceback: Nested attribute suggestions execute property code --- Doc/whatsnew/3.15.rst | 2 ++ Lib/test/test_traceback.py | 10 +++++-- Lib/traceback.py | 26 +++++++++---------- ...-03-02-13-35-14.gh-issue-145413.-x5Hpk.rst | 1 + 4 files changed, 23 insertions(+), 16 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-03-02-13-35-14.gh-issue-145413.-x5Hpk.rst diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 63ef5f84301794..273612b8ef2c85 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -378,6 +378,8 @@ Improved error messages name, the error message will suggest accessing it via that inner attribute: .. code-block:: python + from dataclasses import dataclass + from math import pi @dataclass class Circle: 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..3be59f5a078f74 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 From 17294d3aab1f6fdd5ba760cb0ffc1d075e99e328 Mon Sep 17 00:00:00 2001 From: Hiumee Date: Mon, 2 Mar 2026 13:42:45 +0200 Subject: [PATCH 2/3] Fix indent --- Lib/traceback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 3be59f5a078f74..bf128f75dc97b8 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1678,13 +1678,13 @@ def _check_for_nested_attribute(obj, wrong_name, attrs): for attr_name in attrs_to_check: 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 From cdb37871d43a56cfd03760ddbcb3fde27fedc719 Mon Sep 17 00:00:00 2001 From: Hiumee Date: Mon, 2 Mar 2026 13:46:34 +0200 Subject: [PATCH 3/3] Fix docs --- Doc/whatsnew/3.15.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 273612b8ef2c85..73b6aa82238628 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -378,6 +378,7 @@ Improved error messages name, the error message will suggest accessing it via that inner attribute: .. code-block:: python + from dataclasses import dataclass from math import pi