From 6647cd5e8a69635774952983c5ffae8975ec1ac7 Mon Sep 17 00:00:00 2001 From: Denis Laxalde Date: Tue, 5 Mar 2024 15:36:32 +0100 Subject: [PATCH] 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 --- changelog.d/1253.change.md | 2 ++ src/attr/_make.py | 8 ++++++-- tests/test_slots.py | 39 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 changelog.d/1253.change.md diff --git a/changelog.d/1253.change.md b/changelog.d/1253.change.md new file mode 100644 index 000000000..6c81f9839 --- /dev/null +++ b/changelog.d/1253.change.md @@ -0,0 +1,2 @@ +Preserve `AttributeError` raised by properties of slotted classes with +`functools.cached_properties`. diff --git a/src/attr/_make.py b/src/attr/_make.py index 09c2fe2f5..e7616ae7d 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -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)", ] diff --git a/tests/test_slots.py b/tests/test_slots.py index c1332f2d3..78215ea18 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -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(): """