Skip to content

Commit

Permalink
Preserve AttributeError in slotted classes with cached_property
Browse files Browse the repository at this point in the history
In slotted classes' generated __getattr__(), we try __getattribute__()
before __getattr__(), if available, and eventually let AttributeError
propagate. This matches better with the behaviour described in Python's
documentation "Customizing attribute access":

  https://docs.python.org/3/reference/datamodel.html#customizing-attribute-access

Fix #1230
  • Loading branch information
dlax committed Mar 5, 2024
1 parent b9084fa commit 6647cd5
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 2 deletions.
2 changes: 2 additions & 0 deletions changelog.d/1253.change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Preserve `AttributeError` raised by properties of slotted classes with
`functools.cached_properties`.
8 changes: 6 additions & 2 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,8 +625,12 @@ def _make_cached_property_getattr(
else:
lines.extend(
[
" if hasattr(super(), '__getattr__'):",
" return super().__getattr__(item)",
" try:",
" return super().__getattribute__(item)",
" except AttributeError:",
" if not hasattr(super(), '__getattr__'):",
" raise",
" return super().__getattr__(item)",
" original_error = f\"'{self.__class__.__name__}' object has no attribute '{item}'\"",
" raise AttributeError(original_error)",
]
Expand Down
39 changes: 39 additions & 0 deletions tests/test_slots.py
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,45 @@ def f(self):
a.z


@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_property_raising_attributeerror():
"""
Ensures AttributeError raised by a property is preserved by __getattr__()
implementation.
Regression test for issue https://github.com/python-attrs/attrs/issues/1230
"""

@attr.s(slots=True)
class A:
x = attr.ib()

@functools.cached_property
def f(self):
return self.p

@property
def p(self):
raise AttributeError("I am a property")

@functools.cached_property
def g(self):
return self.q

@property
def q(self):
return 2

a = A(1)
with pytest.raises(AttributeError, match=r"^I am a property$"):
a.p
with pytest.raises(AttributeError, match=r"^I am a property$"):
a.f

assert a.g == 2
assert a.q == 2


@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_property_with_getattr_calls_getattr_for_missing_attributes():
"""
Expand Down

0 comments on commit 6647cd5

Please sign in to comment.