Skip to content

Commit 4597228

Browse files
Pin copy.copy and copy.deepcopy guards on dbapi reject classes
The existing pickle-guard test pins ``pickle.dumps`` rejection on Connection / Cursor / AsyncConnection / AsyncCursor — but copy.copy and copy.deepcopy are independent surfaces that route through __reduce_ex__ → __reduce__ as well. Today they pass through to the same TypeError because the rejection lives on __reduce__, but a future regression that adds a __copy__ / __deepcopy__ hook would silently produce a broken duplicate of a class that holds live transports / asyncio locks. Pin both copy paths explicitly. Mirrors the SA-adapter and client- layer pin shape established by prior cycles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fff2faf commit 4597228

1 file changed

Lines changed: 31 additions & 0 deletions

File tree

tests/test_pickle_guard.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,34 @@ def test_connection_error_message_names_actionable_alternative(self) -> None:
4747
with pytest.raises(TypeError) as excinfo:
4848
pickle.dumps(conn)
4949
assert "consumer process" in str(excinfo.value)
50+
51+
52+
class TestCopyGuard:
53+
"""Sibling pin for ``copy.copy`` / ``copy.deepcopy``.
54+
55+
``copy.copy(c)`` and ``copy.deepcopy(c)`` route through
56+
``__reduce_ex__`` → ``__reduce__`` (the same path pickle uses), so
57+
a class that rejects pickle also rejects copy. Pin both copy paths
58+
explicitly — without these, a regression that tightens
59+
``__reduce__`` to refuse pickle but accepts copy via a separate
60+
``__copy__`` / ``__deepcopy__`` hook would silently succeed and
61+
produce a broken duplicate of a class that holds live transports
62+
/ asyncio locks. Mirrors the SA-adapter and client-layer pin
63+
shape established by prior cycles.
64+
"""
65+
66+
@pytest.mark.parametrize("cls", [Connection, Cursor, AsyncConnection, AsyncCursor])
67+
def test_copy_copy_refuses_with_clear_error(self, cls: type) -> None:
68+
import copy
69+
70+
instance = cls.__new__(cls) # type: ignore[call-overload]
71+
with pytest.raises(TypeError, match=f"cannot pickle '{cls.__name__}'"):
72+
copy.copy(instance)
73+
74+
@pytest.mark.parametrize("cls", [Connection, Cursor, AsyncConnection, AsyncCursor])
75+
def test_copy_deepcopy_refuses_with_clear_error(self, cls: type) -> None:
76+
import copy
77+
78+
instance = cls.__new__(cls) # type: ignore[call-overload]
79+
with pytest.raises(TypeError, match=f"cannot pickle '{cls.__name__}'"):
80+
copy.deepcopy(instance)

0 commit comments

Comments
 (0)