Skip to content

Commit 7bff07f

Browse files
Correct DqliteConnection.__reduce__ docstring on actual class state
The TypeError diagnostic and rationale comment named "a WeakSet of registered cursors" — but DqliteConnection has no WeakSet, no ``_cursors`` attribute, no cursor tracking at all (verified by ``vars(conn)`` enumeration). The empirical "post-connect cryptic AttributeError('Can't pickle local object 'WeakSet.__init__.<locals>._remove'')" claim in the docstring and test docstring was also empirically wrong — pre-fix, bypassing the guard, ``DqliteConnection`` actually pickled silently and produced a corrupt duplicate (the same failure mode ConnectionPool's reject correctly described). Replace both pieces of misleading text with the actual class state (live socket / loop-bound asyncio.Lock / loop-bound wire protocol) and the actual pre-fix failure mode (silent corrupt duplicate via default __reduce__). The runtime ``TypeError`` message and the rationale comment now match what ``vars(conn)`` actually shows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 18cee40 commit 7bff07f

2 files changed

Lines changed: 16 additions & 14 deletions

File tree

src/dqliteclient/connection.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -950,18 +950,19 @@ def __repr__(self) -> str:
950950
return f"<DqliteConnection address={self._address!r} database={self._database!r} {state}>"
951951

952952
def __reduce__(self) -> NoReturn:
953-
# ``DqliteConnection`` holds a live socket, an event-loop-bound
954-
# asyncio.Lock, and a WeakSet of registered cursors — none of
955-
# which survive serialization. Surface a clear driver-level
956-
# TypeError instead of leaking the underlying ``cannot pickle
957-
# '_thread.lock'`` (or, worse, the cryptic
958-
# ``Can't pickle local object 'WeakSet.__init__.<locals>._remove'``
959-
# post-connect). Symmetric with the dbapi-layer guards on
960-
# Connection / Cursor.
953+
# ``DqliteConnection`` holds a live socket (writer transport),
954+
# an event-loop-bound asyncio.Lock (``_op_lock``), and a
955+
# ``DqliteProtocol`` that wraps loop-bound StreamReader /
956+
# StreamWriter — none of which survive serialization. Surface
957+
# a clear driver-level TypeError instead of leaking the
958+
# underlying ``cannot pickle '_thread.lock'`` from pickle's
959+
# default object-graph walk. Symmetric with the dbapi-layer
960+
# guards on Connection / Cursor and the wire-layer guards on
961+
# MessageEncoder / MessageDecoder / ReadBuffer / WriteBuffer.
961962
raise TypeError(
962963
f"cannot pickle {type(self).__name__!r} object — holds a "
963-
f"live socket, loop-bound asyncio.Lock, and weak cursor "
964-
f"refs; reconstruct from configuration in the target "
964+
f"live socket, loop-bound asyncio.Lock, and a wire "
965+
f"protocol; reconstruct from configuration in the target "
965966
f"process instead."
966967
)
967968

tests/test_pickle_guards.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
primitives became pickleable in 3.10+) — producing a
88
"live"-looking duplicate detached from any loop. Any use yields
99
opaque corruption.
10-
- ``DqliteConnection`` post-``connect()`` raises a cryptic
11-
``AttributeError("Can't pickle local object
12-
'WeakSet.__init__.<locals>._remove'")`` from the cursor-tracking
13-
weak set.
10+
- ``DqliteConnection`` (post- or pre-``connect()``) silently
11+
produces a corrupt duplicate via the default
12+
``Exception.__reduce__`` walk; without an explicit reject the
13+
duplicate looks "live" but holds severed loop bindings and
14+
half-state.
1415
- ``DqliteProtocol`` raises an opaque error from wrapped
1516
StreamReader / StreamWriter.
1617

0 commit comments

Comments
 (0)