Skip to content

Commit

Permalink
Preserve AttributeError in slotted classes with cached_property (#1253)
Browse files Browse the repository at this point in the history
* Preserve AttributeError in slotted classes with cached_property

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

* Update changelog.d/1253.change.md

---------

Co-authored-by: Hynek Schlawack <hs@ox.cx>
  • Loading branch information
dlax and hynek committed Apr 2, 2024
1 parent 82a1462 commit 88e2896
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 2 deletions.
1 change: 1 addition & 0 deletions changelog.d/1253.change.md
@@ -0,0 +1 @@
Preserve `AttributeError` raised by properties of slotted classes with `functools.cached_properties`.
8 changes: 6 additions & 2 deletions src/attr/_make.py
Expand Up @@ -619,8 +619,12 @@ def _make_cached_property_getattr(cached_properties, original_getattr, cls):
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
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 88e2896

Please sign in to comment.