Preserve AttributeError in slotted classes with cached_property (#1253)

* 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 https://github.com/python-attrs/attrs/issues/1230

* Update changelog.d/1253.change.md

---------

Co-authored-by: Hynek Schlawack <hs@ox.cx>
This commit is contained in:
Denis Laxalde 2024-04-02 06:57:25 +02:00 committed by GitHub
parent 82a14627fd
commit 88e2896ca9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 46 additions and 2 deletions

View File

@ -0,0 +1 @@
Preserve `AttributeError` raised by properties of slotted classes with `functools.cached_properties`.

View File

@ -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)",
]

View File

@ -806,6 +806,45 @@ def test_slots_cached_property_with_empty_getattr_raises_attribute_error_of_requ
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():
"""