Skip to content

Commit b459a7c

Browse files
fix: Preserve object identity when pickling MutableProxy (#6097)
* fix: Preserve object identity when pickling MutableProxy * thanks greptile, it wasn't a dataclass once
1 parent 5add4ba commit b459a7c

File tree

3 files changed

+50
-12
lines changed

3 files changed

+50
-12
lines changed

reflex/istate/proxy.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from reflex.state import BaseState, StateUpdate
2727

2828
T_STATE = TypeVar("T_STATE", bound="BaseState")
29+
T = TypeVar("T")
2930

3031

3132
class StateProxy(wrapt.ObjectProxy):
@@ -671,19 +672,23 @@ def __deepcopy__(self, memo: dict[int, Any] | None = None) -> Any:
671672
return copy.deepcopy(self.__wrapped__, memo=memo)
672673

673674
def __reduce_ex__(self, protocol_version: SupportsIndex):
674-
"""Get the state for redis serialization.
675+
"""Serialize the wrapped object for pickle, stripping off the proxy.
675676
676-
This method is called by cloudpickle to serialize the object.
677-
678-
It explicitly serializes the wrapped object, stripping off the mutable proxy.
677+
Returns a function that reconstructs to the wrapped object directly,
678+
ensuring pickle's memo system correctly tracks object identity.
679679
680680
Args:
681681
protocol_version: The protocol version.
682682
683683
Returns:
684-
Tuple of (wrapped class, empty args, class __getstate__)
684+
Tuple that reconstructs to the wrapped object.
685685
"""
686-
return self.__wrapped__.__reduce_ex__(protocol_version)
686+
return (_unwrap_for_pickle, (self.__wrapped__,))
687+
688+
689+
def _unwrap_for_pickle(obj: T) -> T:
690+
"""Return the object unchanged. Used by MutableProxy.__reduce_ex__."""
691+
return obj
687692

688693

689694
@serializer

tests/units/istate/test_proxy.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Tests for MutableProxy pickle behavior."""
2+
3+
import dataclasses
4+
import pickle
5+
6+
import reflex as rx
7+
from reflex.istate.proxy import MutableProxy
8+
9+
10+
@dataclasses.dataclass
11+
class Item:
12+
"""Simple picklable object for testing."""
13+
14+
id: int
15+
16+
17+
class ProxyTestState(rx.State):
18+
"""Test state with a list field."""
19+
20+
items: list[Item] = []
21+
22+
23+
def test_mutable_proxy_pickle_preserves_object_identity():
24+
"""Test that same object referenced directly and via proxy maintains identity."""
25+
state = ProxyTestState()
26+
obj = Item(1)
27+
28+
data = {
29+
"direct": [obj],
30+
"proxied": [MutableProxy(obj, state, "items")],
31+
}
32+
33+
unpickled = pickle.loads(pickle.dumps(data))
34+
35+
assert unpickled["direct"][0].id == 1
36+
assert unpickled["proxied"][0].id == 1
37+
assert unpickled["direct"][0] is unpickled["proxied"][0]

tests/units/test_state.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4468,9 +4468,5 @@ async def test_rebind_mutable_proxy(mock_app: rx.App, token: str) -> None:
44684468
) as state:
44694469
assert isinstance(state, MutableProxyState)
44704470
assert state.data["a"] == [2, 3]
4471-
if isinstance(mock_app.state_manager, StateManagerRedis):
4472-
# In redis mode, the object identity does not persist across async with self calls.
4473-
assert state.data["b"] == [2]
4474-
else:
4475-
# In disk/memory mode, the fact that data["b"] was mutated via data["a"] persists.
4476-
assert state.data["b"] == [2, 3]
4471+
# Object identity persists across serialization, so data["b"] is also mutated.
4472+
assert state.data["b"] == [2, 3]

0 commit comments

Comments
 (0)