Repository: python-attrs/attrs

Issue:
AttributeError propagation is broken in cached_property with slots=True 
Let's start with a regular class:
```python
class A:

    @cached_property
    def x(self):
        return self.y

    @property
    def y(self):
        raise AttributeError("Message")
        
a = A()
print(a.x)  # AttributeError: Message
```
Just as expected. Now let's use attrs with `__dict__`:
```python
@define(slots=False)
class A:
    ...

print(a.x)  # AttributeError: Message
```
So far so good. But:
```python
@define  # slots=True by default
class A:
    ...

print(a.x)  # AttributeError: 'A' object has no attribute 'y'
```

This made me spend quite some time wondering where did my attribute go :)


Discussion:
I'm a little confused about what's going on here – are we somehow rewriting `AttributError`s?

ping @diabolo-dan 😇
Yeah, as I recall there's some rewriting there to avoid a confusing `__getattr__` attribute error.  

I think there's a try/except that could be rewritten as an `if hasatr`, which would probably resolve this.  There are some performance implications to the change, but it's probably a reasonable tradeoff for optimising the more common case. 

I won't be able to look at it this week, as I'm on holiday, but might be able to take a look when I'm back next week. 
Thanks, and enjoy your holiday!



Patch:
diff --git a/changelog.d/1253.change.md b/changelog.d/1253.change.md
new file mode 100644
index 000000000..3c32d8356
--- /dev/null
+++ b/changelog.d/1253.change.md
@@ -0,0 +1,1 @@
+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 0114582aa..d3bfb440f 100644
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -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)",
             ]


Test patch:
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():
     """