Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/changes/newsfragments/5518.improved
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Errors raised inside a ``@property`` getter on a subclass of
:class:`~qcodes.utils.DelegateAttributes` (such as :class:`~qcodes.instrument.Instrument`)
are now surfaced with their original traceback, instead of being masked by a
generic ``AttributeError: ... object and its delegates have no attribute ...``
message. The underlying cause of the failure is now visible in the traceback,
making misbehaving properties much easier to debug.
12 changes: 12 additions & 0 deletions src/qcodes/utils/attribute_helpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import inspect
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any, ClassVar

Expand Down Expand Up @@ -38,6 +39,17 @@ class DelegateAttributes:
"""

def __getattr__(self, key: str) -> Any:
# If ``key`` names a data descriptor (e.g. ``@property``) on the class,
# the only way Python reaches ``__getattr__`` is because the descriptor's
# own ``__get__`` raised ``AttributeError``. Re-invoke the descriptor so
# the underlying error surfaces with its original traceback, instead of
# being masked by the generic "has no attribute" error below.
descriptor = inspect.getattr_static(type(self), key, None)
if descriptor is not None and hasattr(descriptor, "__get__") and (
hasattr(descriptor, "__set__") or hasattr(descriptor, "__delete__")
):
return descriptor.__get__(self, type(self))
Comment on lines 41 to +51
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DelegateAttributes.__getattr__ is on a hot path for instruments (e.g., resolving parameters/functions/submodules). Adding inspect.getattr_static on every delegated attribute lookup may introduce measurable overhead. Consider a cheaper descriptor lookup (e.g., walking type(self).__mro__ and checking __dict__ directly) and/or caching per-class descriptor results to avoid repeated introspection.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@officialasishkumar I think this is a real concern. Would it be possible to rewrite the code such that we only use inspect as a fall back if the attr cannot be found by lookup in the delegate attrs?


if key in self.omit_delegate_attrs:
raise AttributeError(
f"'{self.__class__.__name__}' does not delegate attribute {key}"
Expand Down
55 changes: 55 additions & 0 deletions tests/helpers/test_delegate_attribues.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,58 @@ class ToBoth(DelegateAttributes):
# all appropriate items are in dir() exactly once
for attr in ["rock", "paper", "scissors", "year", "water"]:
assert dir(tb).count(attr) == 1


def test_faulty_property_surfaces_original_attribute_error() -> None:
"""A ``@property`` whose getter raises ``AttributeError`` should surface
the *inner* error instead of being masked by the generic
``DelegateAttributes.__getattr__`` fallback.
"""

class WithFaultyProperty(DelegateAttributes):
@property
def prop(self) -> int:
# ``missing`` does not exist; accessing it raises AttributeError.
# Before this fix, Python fell through to ``__getattr__`` which
# reported "no attribute 'prop'" and hid the real cause.
return self.missing # type: ignore[attr-defined]

obj = WithFaultyProperty()
with pytest.raises(AttributeError, match="missing"):
obj.prop


def test_faulty_property_preserves_inner_traceback() -> None:
"""The traceback of the surfaced error must include the line inside the
property's getter that actually raised, so downstream debugging is
possible.
"""

class WithFaultyProperty(DelegateAttributes):
@property
def prop(self) -> int:
raise AttributeError("specific underlying failure")

obj = WithFaultyProperty()
with pytest.raises(AttributeError, match="specific underlying failure"):
obj.prop


Comment on lines +216 to +219
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_faulty_property_preserves_inner_traceback claims to validate that the traceback includes the line inside the property getter, but the test currently only matches the exception message. Please assert on excinfo.traceback (e.g., that a frame/function corresponding to the property getter is present, or that the failing line is included) so this test actually guards the intended behavior.

Suggested change
with pytest.raises(AttributeError, match="specific underlying failure"):
obj.prop
with pytest.raises(
AttributeError, match="specific underlying failure"
) as excinfo:
obj.prop
assert any(entry.name == "prop" for entry in excinfo.traceback)

Copilot uses AI. Check for mistakes.
def test_working_property_still_returns_value() -> None:
"""Re-invocation of a descriptor from ``__getattr__`` must never happen for
a well-behaved ``@property`` — normal attribute lookup should resolve the
value directly without touching ``__getattr__`` at all.
"""

call_count = 0

class WithWorkingProperty(DelegateAttributes):
@property
def prop(self) -> int:
nonlocal call_count
call_count += 1
return 42

obj = WithWorkingProperty()
assert obj.prop == 42
assert call_count == 1
Loading