Skip to content

Commit 216426b

Browse files
Pickle ServerFailure round-trip via separate (code, message) args
Previously ServerFailure stored only a pre-formatted string in Exception.args, so pickle/copy.deepcopy reconstruction called the class with a single positional argument against the 2-required __init__ and raised TypeError. Pass code and message as separate super-args and override __str__ to preserve the "[code] message" display format. Mirrors the same fix already applied to client.OperationalError.
1 parent c02c33c commit 216426b

2 files changed

Lines changed: 41 additions & 1 deletion

File tree

src/dqlitewire/exceptions.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,17 @@ class ServerFailure(ProtocolError):
8484
message: Human-readable error message from the server.
8585
"""
8686

87+
code: int
88+
message: str
89+
8790
def __init__(self, code: int, message: str) -> None:
88-
super().__init__(f"[{code}] {message}")
8991
self.code = code
9092
self.message = message
93+
# Pass ``code`` and ``message`` through as separate args so
94+
# ``self.args == (code, message)``; otherwise ``pickle`` /
95+
# ``copy.deepcopy`` reconstruct via ``ServerFailure(*args)``
96+
# with a single positional argument and raise ``TypeError``.
97+
super().__init__(code, message)
98+
99+
def __str__(self) -> str:
100+
return f"[{self.code}] {self.message}"

tests/test_exceptions.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,36 @@ def test_str_includes_code_and_message(self) -> None:
7373
assert "5" in s
7474
assert "database is locked" in s
7575

76+
def test_pickle_round_trip(self) -> None:
77+
"""ServerFailure must survive ``pickle.dumps`` + ``pickle.loads``.
78+
79+
Workers that propagate exceptions back to a parent process
80+
(multiprocessing, concurrent.futures.ProcessPoolExecutor, Celery)
81+
unpickle by calling ``ServerFailure(*args)`` on the pickled args
82+
tuple. If args hold a single pre-formatted string, unpickle calls
83+
``ServerFailure('[5] db locked')`` against a 2-arg ``__init__``
84+
and raises ``TypeError``. Mirror the fix that landed for
85+
``client.OperationalError``.
86+
"""
87+
import pickle
88+
89+
original = ServerFailure(code=5, message="database is locked")
90+
restored = pickle.loads(pickle.dumps(original))
91+
assert isinstance(restored, ServerFailure)
92+
assert restored.code == 5
93+
assert restored.message == "database is locked"
94+
assert str(restored) == str(original)
95+
96+
def test_copy_deepcopy_round_trip(self) -> None:
97+
"""``copy.deepcopy`` uses the same reduce path as pickle."""
98+
import copy
99+
100+
original = ServerFailure(code=10, message="no free pages")
101+
restored = copy.deepcopy(original)
102+
assert isinstance(restored, ServerFailure)
103+
assert restored.code == 10
104+
assert restored.message == "no free pages"
105+
76106

77107
class TestRaiseSiteSubclasses:
78108
"""Each raise site in codec.py / buffer.py must raise a specific

0 commit comments

Comments
 (0)