Repository: twisted/twisted

Issue:
Regression since 2024.3.0: CopiedFailure relies on parents being settable
My PR #12109 caused a regression; in my defense due to lack of test coverage.

When I run Tahoe-LAFS tests with latest Twisted:

```python
          File "/home/itamarst/devel/tahoe-lafs/.tox/py312/lib/python3.12/site-packages/foolscap/banana.py", line 625, in dataReceived
            self.handleData(chunk)
          File "/home/itamarst/devel/tahoe-lafs/.tox/py312/lib/python3.12/site-packages/foolscap/banana.py", line 856, in handleData
            self.handleClose(count)
          File "/home/itamarst/devel/tahoe-lafs/.tox/py312/lib/python3.12/site-packages/foolscap/banana.py", line 1055, in handleClose
            obj, ready_deferred = child.receiveClose()
          File "/home/itamarst/devel/tahoe-lafs/.tox/py312/lib/python3.12/site-packages/foolscap/copyable.py", line 196, in receiveClose
            obj = self.factory(self.d)
          File "/home/itamarst/devel/tahoe-lafs/.tox/py312/lib/python3.12/site-packages/foolscap/copyable.py", line 301, in _RemoteCopyFactory
            obj.setCopyableState(state)
          File "/home/itamarst/devel/tahoe-lafs/.tox/py312/lib/python3.12/site-packages/foolscap/call.py", line 884, in setCopyableState
            self.parents = [six.ensure_str(p) for p in state['parents']]
        builtins.AttributeError: property 'parents' of 'CopiedFailure' object has no setter
```


Discussion:

Patch:
diff --git a/src/twisted/newsfragments/12279.bugfix b/src/twisted/newsfragments/12279.bugfix
new file mode 100644
index 0000000000..8b6afeabdf
--- /dev/null
+++ b/src/twisted/newsfragments/12279.bugfix
@@ -0,0 +1,1 @@
+Fixed unreleased regression caused by PR 12109.
\ No newline at end of file
diff --git a/src/twisted/python/failure.py b/src/twisted/python/failure.py
index 39712ecf5c..d253ffad74 100644
--- a/src/twisted/python/failure.py
+++ b/src/twisted/python/failure.py
@@ -429,6 +429,10 @@ class Failure(BaseException):
             self._parents = [self.type]
         return self._parents
 
+    @parents.setter
+    def parents(self, parents):
+        self._parents = parents
+
     def _extrapolate(self, otherFailure):
         """
         Extrapolate from one failure into another, copying its stack frames.


Test patch:
diff --git a/src/twisted/test/test_failure.py b/src/twisted/test/test_failure.py
index 9a3daae306..e280d14810 100644
--- a/src/twisted/test/test_failure.py
+++ b/src/twisted/test/test_failure.py
@@ -568,6 +568,17 @@ class FailureTests(SynchronousTestCase):
         f = failure.Failure(ComparableException("hello"))
         self.assertEqual(f.__getstate__()["parents"], f.parents)
 
+    def test_settableParents(self) -> None:
+        """
+        C{Failure.parents} can be set, both before and after pickling.
+
+        This is used by Foolscap.
+        """
+        original_failure = failure.Failure(ComparableException("hello"))
+        original_failure.parents = original_failure.parents[:]
+        failure2 = pickle.loads(pickle.dumps(original_failure))
+        failure2.parents = failure2.parents[:]
+
 
 class BrokenStr(Exception):
     """