Skip to content

Commit 3beb1a6

Browse files
authored
ENG-8507: rebind MutableProxy when it is linked to an old _self_state reference (#6048)
* 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. * fix test for oplock enabled
1 parent e9662f4 commit 3beb1a6

File tree

2 files changed

+55
-2
lines changed

2 files changed

+55
-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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4386,3 +4386,51 @@ 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+
async with mock_app.state_manager.modify_state(
4402+
_substate_key(token, MutableProxyState)
4403+
) as state:
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+
# Flush any oplock.
4417+
await mock_app.state_manager.close()
4418+
4419+
new_state_proxy = StateProxy(state)
4420+
assert state_proxy is not new_state_proxy
4421+
assert new_state_proxy.data["a"]._self_state is new_state_proxy
4422+
assert state_proxy.data["a"]._self_state is state_proxy
4423+
4424+
async with state_proxy:
4425+
state_proxy.data["a"].append(3)
4426+
4427+
async with mock_app.state_manager.modify_state(
4428+
_substate_key(token, MutableProxyState)
4429+
) as state:
4430+
assert state.data["a"] == [2, 3]
4431+
if isinstance(mock_app.state_manager, StateManagerRedis):
4432+
# In redis mode, the object identity does not persist across async with self calls.
4433+
assert state.data["b"] == [2]
4434+
else:
4435+
# In disk/memory mode, the fact that data["b"] was mutated via data["a"] persists.
4436+
assert state.data["b"] == [2, 3]

0 commit comments

Comments
 (0)