Skip to content

Commit 2480363

Browse files
committed
ENG-8507: rebind MutableProxy when it is linked to an old _self_state reference
Avoids an issue where MutableProxy references are saved in the state, then when accessing them again, they reference the wrong StateProxy. So even though the code enters `async with self`, the StateProxy that is marked mutable is NOT the StateProxy associated with the value. With this change, anytime a MutableProxy is accessed through a StateProxy, its state reference is reset to the current StateProxy before returning it.
1 parent bd1f7c6 commit 2480363

File tree

2 files changed

+52
-2
lines changed

2 files changed

+52
-2
lines changed

reflex/istate/proxy.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,8 +508,13 @@ def _wrap_recursive(self, value: Any) -> Any:
508508
# When called from dataclasses internal code, return the unwrapped value
509509
if self._is_called_from_dataclasses_internal():
510510
return value
511-
# Recursively wrap mutable types, but do not re-wrap MutableProxy instances.
512-
if is_mutable_type(type(value)) and not isinstance(value, MutableProxy):
511+
# If we already have a proxy, make sure the state reference is up to date and return it.
512+
if isinstance(value, MutableProxy):
513+
if value._self_state is not self._self_state:
514+
value._self_state = self._self_state
515+
return value
516+
# Recursively wrap mutable types.
517+
if is_mutable_type(type(value)):
513518
base_cls = globals()[self.__base_proxy__]
514519
return base_cls(
515520
wrapped=value,

tests/units/test_state.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4386,3 +4386,48 @@ async def fetch_data_state(self) -> None:
43864386
state = await mock_app.state_manager.get_state(_substate_key(token, OtherState))
43874387
other_state = await state.get_state(OtherState)
43884388
await other_state.fetch_data_state() # Should not raise exception.
4389+
4390+
4391+
class MutableProxyState(BaseState):
4392+
"""A test state with a MutableProxy var."""
4393+
4394+
data: dict[str, list[int]] = {"a": [1], "b": [2]}
4395+
4396+
4397+
@pytest.mark.asyncio
4398+
async def test_rebind_mutable_proxy(mock_app: rx.App, token: str) -> None:
4399+
"""Test that previously bound MutableProxy instances can be rebound correctly."""
4400+
mock_app.state_manager.state = mock_app._state = MutableProxyState
4401+
state = await mock_app.state_manager.get_state(
4402+
_substate_key(token, MutableProxyState)
4403+
)
4404+
state.router = RouterData.from_router_data({
4405+
"query": {},
4406+
"token": token,
4407+
"sid": "test_sid",
4408+
})
4409+
state_proxy = StateProxy(state)
4410+
assert isinstance(state_proxy.data, MutableProxy)
4411+
async with state_proxy:
4412+
state_proxy.data["a"] = state_proxy.data["b"]
4413+
assert state_proxy.data["a"] is not state_proxy.data["b"]
4414+
assert state_proxy.data["a"].__wrapped__ is state_proxy.data["b"].__wrapped__
4415+
4416+
new_state_proxy = StateProxy(state)
4417+
assert state_proxy is not new_state_proxy
4418+
assert new_state_proxy.data["a"]._self_state is new_state_proxy
4419+
assert state_proxy.data["a"]._self_state is state_proxy
4420+
4421+
async with state_proxy:
4422+
state_proxy.data["a"].append(3)
4423+
4424+
state = await mock_app.state_manager.get_state(
4425+
_substate_key(token, MutableProxyState)
4426+
)
4427+
assert state.data["a"] == [2, 3]
4428+
if isinstance(mock_app.state_manager, StateManagerRedis):
4429+
# In redis mode, the object identity does not persist across async with self calls.
4430+
assert state.data["b"] == [2]
4431+
else:
4432+
# In disk/memory mode, the fact that data["b"] was mutated via data["a"] persists.
4433+
assert state.data["b"] == [2, 3]

0 commit comments

Comments
 (0)