|
1 | 1 | """Exceptions for dqlite client.""" |
2 | 2 |
|
3 | | -from typing import ClassVar |
| 3 | +from typing import Any, ClassVar |
4 | 4 |
|
5 | 5 | from dqlitewire.exceptions import ProtocolError as _WireProtocolError |
6 | 6 |
|
@@ -40,28 +40,46 @@ def __init__(self, *args: object, raw_message: str | None = None) -> None: |
40 | 40 | super().__init__(*args) |
41 | 41 | self.raw_message = raw_message |
42 | 42 |
|
| 43 | + def __getstate__(self) -> dict[str, object]: |
| 44 | + """State capture for pickle. |
| 45 | +
|
| 46 | + Default ``Exception.__reduce__`` returns ``(cls, self.args)`` |
| 47 | + which reconstructs via ``cls(*args)``. That loses every |
| 48 | + field set on the instance after ``Exception.__init__`` — |
| 49 | + most notably ``raw_message`` (set on the ``DqliteError`` |
| 50 | + base) and the ``code`` carried by ``DqliteConnectionError``. |
| 51 | + The carriers exist precisely so the wire-level signal |
| 52 | + survives cross-process pickling (``ProcessPoolExecutor``, |
| 53 | + ``multiprocessing.Queue``, Celery task results, SA's |
| 54 | + multiprocess pool). |
| 55 | +
|
| 56 | + Pair with :meth:`__setstate__` so the explicit |
| 57 | + ``__getstate__`` / ``__setstate__`` shape replaces the |
| 58 | + previous ``__reduce__``-with-dict-overlay. The new shape |
| 59 | + makes the captured state visible in stack traces (no longer |
| 60 | + hidden behind ``self.__dict__.copy()``) and tolerates |
| 61 | + future subclass refactors that add slot-based attributes |
| 62 | + (those slot values can be added to the dict explicitly in |
| 63 | + the subclass override rather than silently lost behind |
| 64 | + ``__dict__``). |
| 65 | + """ |
| 66 | + return self.__dict__.copy() |
| 67 | + |
| 68 | + def __setstate__(self, state: dict[str, Any] | None) -> None: |
| 69 | + if state: |
| 70 | + self.__dict__.update(state) |
| 71 | + |
43 | 72 | def __reduce__( |
44 | 73 | self, |
45 | 74 | ) -> tuple[type["DqliteError"], tuple[object, ...], dict[str, object]]: |
46 | | - # Default ``Exception.__reduce__`` returns ``(cls, self.args)``, |
47 | | - # which reconstructs via ``cls(*args)``. That loses every field |
48 | | - # set on the instance after ``Exception.__init__`` — most |
49 | | - # notably ``raw_message`` (set on the ``DqliteError`` base) and |
50 | | - # the ``code`` carried by ``DqliteConnectionError``. The |
51 | | - # carriers were added in cycle 27 (XP2 / XP3) precisely so the |
52 | | - # wire-level signal would survive cross-process pickling |
53 | | - # (``ProcessPoolExecutor``, ``multiprocessing.Queue``, Celery |
54 | | - # task results, SA's multiprocess pool); without overriding |
55 | | - # ``__reduce__`` the round-trip silently drops them. |
56 | | - # |
57 | | - # Return the 3-tuple ``(callable, args, state)`` form so pickle |
58 | | - # reconstructs via ``cls(*args)`` then applies state via |
59 | | - # ``self.__dict__.update(state)`` — preserving every attribute |
60 | | - # we set on the instance (``raw_message`` on the base, ``code`` |
61 | | - # on subclasses, the truncated ``message`` on |
62 | | - # ``OperationalError``). All subclasses of ``DqliteError`` |
63 | | - # inherit this discipline automatically. |
64 | | - return (self.__class__, self.args, self.__dict__.copy()) |
| 75 | + # 3-tuple form: pickle reconstructs via ``cls(*args)`` then |
| 76 | + # invokes ``__setstate__`` with the dict from |
| 77 | + # ``__getstate__``. Subclasses inherit this discipline |
| 78 | + # automatically — every subclass we ship today carries its |
| 79 | + # extra fields on ``__dict__`` (no ``__slots__``), so the |
| 80 | + # default ``__getstate__`` returning ``self.__dict__.copy()`` |
| 81 | + # captures them. |
| 82 | + return (self.__class__, self.args, self.__getstate__()) |
65 | 83 |
|
66 | 84 |
|
67 | 85 | class DqliteConnectionError(DqliteError): |
|
0 commit comments