|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
import io
|
|
import io
|
|
import os.path
|
|
import os.path
|
|
import sys
|
|
import sys
|
|
|
|
+import uuid
|
|
from typing import List, Tuple, Type
|
|
from typing import List, Tuple, Type
|
|
|
|
|
|
if sys.version_info.major >= 3 and sys.version_info.minor > 7:
|
|
if sys.version_info.major >= 3 and sys.version_info.minor > 7:
|
|
@@ -30,11 +31,18 @@ from reflex.components import Box, Component, Cond, Fragment, Text
|
|
from reflex.event import Event, get_hydrate_event
|
|
from reflex.event import Event, get_hydrate_event
|
|
from reflex.middleware import HydrateMiddleware
|
|
from reflex.middleware import HydrateMiddleware
|
|
from reflex.model import Model
|
|
from reflex.model import Model
|
|
-from reflex.state import State, StateUpdate
|
|
|
|
|
|
+from reflex.state import State, StateManagerRedis, StateUpdate
|
|
from reflex.style import Style
|
|
from reflex.style import Style
|
|
from reflex.utils import format
|
|
from reflex.utils import format
|
|
from reflex.vars import ComputedVar
|
|
from reflex.vars import ComputedVar
|
|
|
|
|
|
|
|
+from .states import (
|
|
|
|
+ ChildFileUploadState,
|
|
|
|
+ FileUploadState,
|
|
|
|
+ GenState,
|
|
|
|
+ GrandChildFileUploadState,
|
|
|
|
+)
|
|
|
|
+
|
|
|
|
|
|
@pytest.fixture
|
|
@pytest.fixture
|
|
def index_page():
|
|
def index_page():
|
|
@@ -64,6 +72,12 @@ def about_page():
|
|
return about
|
|
return about
|
|
|
|
|
|
|
|
|
|
|
|
+class ATestState(State):
|
|
|
|
+ """A simple state for testing."""
|
|
|
|
+
|
|
|
|
+ var: int
|
|
|
|
+
|
|
|
|
+
|
|
@pytest.fixture()
|
|
@pytest.fixture()
|
|
def test_state() -> Type[State]:
|
|
def test_state() -> Type[State]:
|
|
"""A default state.
|
|
"""A default state.
|
|
@@ -71,11 +85,7 @@ def test_state() -> Type[State]:
|
|
Returns:
|
|
Returns:
|
|
A default state.
|
|
A default state.
|
|
"""
|
|
"""
|
|
-
|
|
|
|
- class TestState(State):
|
|
|
|
- var: int
|
|
|
|
-
|
|
|
|
- return TestState
|
|
|
|
|
|
+ return ATestState
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
@pytest.fixture()
|
|
@@ -313,23 +323,28 @@ def test_initialize_admin_dashboard_with_view_overrides(test_model):
|
|
assert app.admin_dash.view_overrides[test_model] == TestModelView
|
|
assert app.admin_dash.view_overrides[test_model] == TestModelView
|
|
|
|
|
|
|
|
|
|
-def test_initialize_with_state(test_state):
|
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
+async def test_initialize_with_state(test_state: Type[ATestState], token: str):
|
|
"""Test setting the state of an app.
|
|
"""Test setting the state of an app.
|
|
|
|
|
|
Args:
|
|
Args:
|
|
test_state: The default state.
|
|
test_state: The default state.
|
|
|
|
+ token: a Token.
|
|
"""
|
|
"""
|
|
app = App(state=test_state)
|
|
app = App(state=test_state)
|
|
assert app.state == test_state
|
|
assert app.state == test_state
|
|
|
|
|
|
# Get a state for a given token.
|
|
# Get a state for a given token.
|
|
- token = "token"
|
|
|
|
- state = app.state_manager.get_state(token)
|
|
|
|
|
|
+ state = await app.state_manager.get_state(token)
|
|
assert isinstance(state, test_state)
|
|
assert isinstance(state, test_state)
|
|
assert state.var == 0 # type: ignore
|
|
assert state.var == 0 # type: ignore
|
|
|
|
|
|
|
|
+ if isinstance(app.state_manager, StateManagerRedis):
|
|
|
|
+ await app.state_manager.redis.close()
|
|
|
|
+
|
|
|
|
|
|
-def test_set_and_get_state(test_state):
|
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
+async def test_set_and_get_state(test_state):
|
|
"""Test setting and getting the state of an app with different tokens.
|
|
"""Test setting and getting the state of an app with different tokens.
|
|
|
|
|
|
Args:
|
|
Args:
|
|
@@ -338,47 +353,51 @@ def test_set_and_get_state(test_state):
|
|
app = App(state=test_state)
|
|
app = App(state=test_state)
|
|
|
|
|
|
# Create two tokens.
|
|
# Create two tokens.
|
|
- token1 = "token1"
|
|
|
|
- token2 = "token2"
|
|
|
|
|
|
+ token1 = str(uuid.uuid4())
|
|
|
|
+ token2 = str(uuid.uuid4())
|
|
|
|
|
|
# Get the default state for each token.
|
|
# Get the default state for each token.
|
|
- state1 = app.state_manager.get_state(token1)
|
|
|
|
- state2 = app.state_manager.get_state(token2)
|
|
|
|
|
|
+ state1 = await app.state_manager.get_state(token1)
|
|
|
|
+ state2 = await app.state_manager.get_state(token2)
|
|
assert state1.var == 0 # type: ignore
|
|
assert state1.var == 0 # type: ignore
|
|
assert state2.var == 0 # type: ignore
|
|
assert state2.var == 0 # type: ignore
|
|
|
|
|
|
# Set the vars to different values.
|
|
# Set the vars to different values.
|
|
state1.var = 1
|
|
state1.var = 1
|
|
state2.var = 2
|
|
state2.var = 2
|
|
- app.state_manager.set_state(token1, state1)
|
|
|
|
- app.state_manager.set_state(token2, state2)
|
|
|
|
|
|
+ await app.state_manager.set_state(token1, state1)
|
|
|
|
+ await app.state_manager.set_state(token2, state2)
|
|
|
|
|
|
# Get the states again and check the values.
|
|
# Get the states again and check the values.
|
|
- state1 = app.state_manager.get_state(token1)
|
|
|
|
- state2 = app.state_manager.get_state(token2)
|
|
|
|
|
|
+ state1 = await app.state_manager.get_state(token1)
|
|
|
|
+ state2 = await app.state_manager.get_state(token2)
|
|
assert state1.var == 1 # type: ignore
|
|
assert state1.var == 1 # type: ignore
|
|
assert state2.var == 2 # type: ignore
|
|
assert state2.var == 2 # type: ignore
|
|
|
|
|
|
|
|
+ if isinstance(app.state_manager, StateManagerRedis):
|
|
|
|
+ await app.state_manager.redis.close()
|
|
|
|
+
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
-async def test_dynamic_var_event(test_state):
|
|
|
|
|
|
+async def test_dynamic_var_event(test_state: Type[ATestState], token: str):
|
|
"""Test that the default handler of a dynamic generated var
|
|
"""Test that the default handler of a dynamic generated var
|
|
works as expected.
|
|
works as expected.
|
|
|
|
|
|
Args:
|
|
Args:
|
|
test_state: State Fixture.
|
|
test_state: State Fixture.
|
|
|
|
+ token: a Token.
|
|
"""
|
|
"""
|
|
- test_state = test_state()
|
|
|
|
- test_state.add_var("int_val", int, 0)
|
|
|
|
- result = await test_state._process(
|
|
|
|
|
|
+ state = test_state() # type: ignore
|
|
|
|
+ state.add_var("int_val", int, 0)
|
|
|
|
+ result = await state._process(
|
|
Event(
|
|
Event(
|
|
- token="fake-token",
|
|
|
|
- name="test_state.set_int_val",
|
|
|
|
|
|
+ token=token,
|
|
|
|
+ name=f"{test_state.get_name()}.set_int_val",
|
|
router_data={"pathname": "/", "query": {}},
|
|
router_data={"pathname": "/", "query": {}},
|
|
payload={"value": 50},
|
|
payload={"value": 50},
|
|
)
|
|
)
|
|
).__anext__()
|
|
).__anext__()
|
|
- assert result.delta == {"test_state": {"int_val": 50}}
|
|
|
|
|
|
+ assert result.delta == {test_state.get_name(): {"int_val": 50}}
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
@@ -388,12 +407,20 @@ async def test_dynamic_var_event(test_state):
|
|
pytest.param(
|
|
pytest.param(
|
|
[
|
|
[
|
|
(
|
|
(
|
|
- "test_state.make_friend",
|
|
|
|
- {"test_state": {"plain_friends": ["Tommy", "another-fd"]}},
|
|
|
|
|
|
+ "list_mutation_test_state.make_friend",
|
|
|
|
+ {
|
|
|
|
+ "list_mutation_test_state": {
|
|
|
|
+ "plain_friends": ["Tommy", "another-fd"]
|
|
|
|
+ }
|
|
|
|
+ },
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.change_first_friend",
|
|
|
|
- {"test_state": {"plain_friends": ["Jenny", "another-fd"]}},
|
|
|
|
|
|
+ "list_mutation_test_state.change_first_friend",
|
|
|
|
+ {
|
|
|
|
+ "list_mutation_test_state": {
|
|
|
|
+ "plain_friends": ["Jenny", "another-fd"]
|
|
|
|
+ }
|
|
|
|
+ },
|
|
),
|
|
),
|
|
],
|
|
],
|
|
id="append then __setitem__",
|
|
id="append then __setitem__",
|
|
@@ -401,12 +428,12 @@ async def test_dynamic_var_event(test_state):
|
|
pytest.param(
|
|
pytest.param(
|
|
[
|
|
[
|
|
(
|
|
(
|
|
- "test_state.unfriend_first_friend",
|
|
|
|
- {"test_state": {"plain_friends": []}},
|
|
|
|
|
|
+ "list_mutation_test_state.unfriend_first_friend",
|
|
|
|
+ {"list_mutation_test_state": {"plain_friends": []}},
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.make_friend",
|
|
|
|
- {"test_state": {"plain_friends": ["another-fd"]}},
|
|
|
|
|
|
+ "list_mutation_test_state.make_friend",
|
|
|
|
+ {"list_mutation_test_state": {"plain_friends": ["another-fd"]}},
|
|
),
|
|
),
|
|
],
|
|
],
|
|
id="delitem then append",
|
|
id="delitem then append",
|
|
@@ -414,20 +441,24 @@ async def test_dynamic_var_event(test_state):
|
|
pytest.param(
|
|
pytest.param(
|
|
[
|
|
[
|
|
(
|
|
(
|
|
- "test_state.make_friends_with_colleagues",
|
|
|
|
- {"test_state": {"plain_friends": ["Tommy", "Peter", "Jimmy"]}},
|
|
|
|
|
|
+ "list_mutation_test_state.make_friends_with_colleagues",
|
|
|
|
+ {
|
|
|
|
+ "list_mutation_test_state": {
|
|
|
|
+ "plain_friends": ["Tommy", "Peter", "Jimmy"]
|
|
|
|
+ }
|
|
|
|
+ },
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.remove_tommy",
|
|
|
|
- {"test_state": {"plain_friends": ["Peter", "Jimmy"]}},
|
|
|
|
|
|
+ "list_mutation_test_state.remove_tommy",
|
|
|
|
+ {"list_mutation_test_state": {"plain_friends": ["Peter", "Jimmy"]}},
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.remove_last_friend",
|
|
|
|
- {"test_state": {"plain_friends": ["Peter"]}},
|
|
|
|
|
|
+ "list_mutation_test_state.remove_last_friend",
|
|
|
|
+ {"list_mutation_test_state": {"plain_friends": ["Peter"]}},
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.unfriend_all_friends",
|
|
|
|
- {"test_state": {"plain_friends": []}},
|
|
|
|
|
|
+ "list_mutation_test_state.unfriend_all_friends",
|
|
|
|
+ {"list_mutation_test_state": {"plain_friends": []}},
|
|
),
|
|
),
|
|
],
|
|
],
|
|
id="extend, remove, pop, clear",
|
|
id="extend, remove, pop, clear",
|
|
@@ -435,24 +466,28 @@ async def test_dynamic_var_event(test_state):
|
|
pytest.param(
|
|
pytest.param(
|
|
[
|
|
[
|
|
(
|
|
(
|
|
- "test_state.add_jimmy_to_second_group",
|
|
|
|
|
|
+ "list_mutation_test_state.add_jimmy_to_second_group",
|
|
{
|
|
{
|
|
- "test_state": {
|
|
|
|
|
|
+ "list_mutation_test_state": {
|
|
"friends_in_nested_list": [["Tommy"], ["Jenny", "Jimmy"]]
|
|
"friends_in_nested_list": [["Tommy"], ["Jenny", "Jimmy"]]
|
|
}
|
|
}
|
|
},
|
|
},
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.remove_first_person_from_first_group",
|
|
|
|
|
|
+ "list_mutation_test_state.remove_first_person_from_first_group",
|
|
{
|
|
{
|
|
- "test_state": {
|
|
|
|
|
|
+ "list_mutation_test_state": {
|
|
"friends_in_nested_list": [[], ["Jenny", "Jimmy"]]
|
|
"friends_in_nested_list": [[], ["Jenny", "Jimmy"]]
|
|
}
|
|
}
|
|
},
|
|
},
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.remove_first_group",
|
|
|
|
- {"test_state": {"friends_in_nested_list": [["Jenny", "Jimmy"]]}},
|
|
|
|
|
|
+ "list_mutation_test_state.remove_first_group",
|
|
|
|
+ {
|
|
|
|
+ "list_mutation_test_state": {
|
|
|
|
+ "friends_in_nested_list": [["Jenny", "Jimmy"]]
|
|
|
|
+ }
|
|
|
|
+ },
|
|
),
|
|
),
|
|
],
|
|
],
|
|
id="nested list",
|
|
id="nested list",
|
|
@@ -460,16 +495,24 @@ async def test_dynamic_var_event(test_state):
|
|
pytest.param(
|
|
pytest.param(
|
|
[
|
|
[
|
|
(
|
|
(
|
|
- "test_state.add_jimmy_to_tommy_friends",
|
|
|
|
- {"test_state": {"friends_in_dict": {"Tommy": ["Jenny", "Jimmy"]}}},
|
|
|
|
|
|
+ "list_mutation_test_state.add_jimmy_to_tommy_friends",
|
|
|
|
+ {
|
|
|
|
+ "list_mutation_test_state": {
|
|
|
|
+ "friends_in_dict": {"Tommy": ["Jenny", "Jimmy"]}
|
|
|
|
+ }
|
|
|
|
+ },
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.remove_jenny_from_tommy",
|
|
|
|
- {"test_state": {"friends_in_dict": {"Tommy": ["Jimmy"]}}},
|
|
|
|
|
|
+ "list_mutation_test_state.remove_jenny_from_tommy",
|
|
|
|
+ {
|
|
|
|
+ "list_mutation_test_state": {
|
|
|
|
+ "friends_in_dict": {"Tommy": ["Jimmy"]}
|
|
|
|
+ }
|
|
|
|
+ },
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.tommy_has_no_fds",
|
|
|
|
- {"test_state": {"friends_in_dict": {"Tommy": []}}},
|
|
|
|
|
|
+ "list_mutation_test_state.tommy_has_no_fds",
|
|
|
|
+ {"list_mutation_test_state": {"friends_in_dict": {"Tommy": []}}},
|
|
),
|
|
),
|
|
],
|
|
],
|
|
id="list in dict",
|
|
id="list in dict",
|
|
@@ -477,7 +520,9 @@ async def test_dynamic_var_event(test_state):
|
|
],
|
|
],
|
|
)
|
|
)
|
|
async def test_list_mutation_detection__plain_list(
|
|
async def test_list_mutation_detection__plain_list(
|
|
- event_tuples: List[Tuple[str, List[str]]], list_mutation_state: State
|
|
|
|
|
|
+ event_tuples: List[Tuple[str, List[str]]],
|
|
|
|
+ list_mutation_state: State,
|
|
|
|
+ token: str,
|
|
):
|
|
):
|
|
"""Test list mutation detection
|
|
"""Test list mutation detection
|
|
when reassignment is not explicitly included in the logic.
|
|
when reassignment is not explicitly included in the logic.
|
|
@@ -485,11 +530,12 @@ async def test_list_mutation_detection__plain_list(
|
|
Args:
|
|
Args:
|
|
event_tuples: From parametrization.
|
|
event_tuples: From parametrization.
|
|
list_mutation_state: A state with list mutation features.
|
|
list_mutation_state: A state with list mutation features.
|
|
|
|
+ token: a Token.
|
|
"""
|
|
"""
|
|
for event_name, expected_delta in event_tuples:
|
|
for event_name, expected_delta in event_tuples:
|
|
result = await list_mutation_state._process(
|
|
result = await list_mutation_state._process(
|
|
Event(
|
|
Event(
|
|
- token="fake-token",
|
|
|
|
|
|
+ token=token,
|
|
name=event_name,
|
|
name=event_name,
|
|
router_data={"pathname": "/", "query": {}},
|
|
router_data={"pathname": "/", "query": {}},
|
|
payload={},
|
|
payload={},
|
|
@@ -506,16 +552,24 @@ async def test_list_mutation_detection__plain_list(
|
|
pytest.param(
|
|
pytest.param(
|
|
[
|
|
[
|
|
(
|
|
(
|
|
- "test_state.add_age",
|
|
|
|
- {"test_state": {"details": {"name": "Tommy", "age": 20}}},
|
|
|
|
|
|
+ "dict_mutation_test_state.add_age",
|
|
|
|
+ {
|
|
|
|
+ "dict_mutation_test_state": {
|
|
|
|
+ "details": {"name": "Tommy", "age": 20}
|
|
|
|
+ }
|
|
|
|
+ },
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.change_name",
|
|
|
|
- {"test_state": {"details": {"name": "Jenny", "age": 20}}},
|
|
|
|
|
|
+ "dict_mutation_test_state.change_name",
|
|
|
|
+ {
|
|
|
|
+ "dict_mutation_test_state": {
|
|
|
|
+ "details": {"name": "Jenny", "age": 20}
|
|
|
|
+ }
|
|
|
|
+ },
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.remove_last_detail",
|
|
|
|
- {"test_state": {"details": {"name": "Jenny"}}},
|
|
|
|
|
|
+ "dict_mutation_test_state.remove_last_detail",
|
|
|
|
+ {"dict_mutation_test_state": {"details": {"name": "Jenny"}}},
|
|
),
|
|
),
|
|
],
|
|
],
|
|
id="update then __setitem__",
|
|
id="update then __setitem__",
|
|
@@ -523,12 +577,12 @@ async def test_list_mutation_detection__plain_list(
|
|
pytest.param(
|
|
pytest.param(
|
|
[
|
|
[
|
|
(
|
|
(
|
|
- "test_state.clear_details",
|
|
|
|
- {"test_state": {"details": {}}},
|
|
|
|
|
|
+ "dict_mutation_test_state.clear_details",
|
|
|
|
+ {"dict_mutation_test_state": {"details": {}}},
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.add_age",
|
|
|
|
- {"test_state": {"details": {"age": 20}}},
|
|
|
|
|
|
+ "dict_mutation_test_state.add_age",
|
|
|
|
+ {"dict_mutation_test_state": {"details": {"age": 20}}},
|
|
),
|
|
),
|
|
],
|
|
],
|
|
id="delitem then update",
|
|
id="delitem then update",
|
|
@@ -536,16 +590,20 @@ async def test_list_mutation_detection__plain_list(
|
|
pytest.param(
|
|
pytest.param(
|
|
[
|
|
[
|
|
(
|
|
(
|
|
- "test_state.add_age",
|
|
|
|
- {"test_state": {"details": {"name": "Tommy", "age": 20}}},
|
|
|
|
|
|
+ "dict_mutation_test_state.add_age",
|
|
|
|
+ {
|
|
|
|
+ "dict_mutation_test_state": {
|
|
|
|
+ "details": {"name": "Tommy", "age": 20}
|
|
|
|
+ }
|
|
|
|
+ },
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.remove_name",
|
|
|
|
- {"test_state": {"details": {"age": 20}}},
|
|
|
|
|
|
+ "dict_mutation_test_state.remove_name",
|
|
|
|
+ {"dict_mutation_test_state": {"details": {"age": 20}}},
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.pop_out_age",
|
|
|
|
- {"test_state": {"details": {}}},
|
|
|
|
|
|
+ "dict_mutation_test_state.pop_out_age",
|
|
|
|
+ {"dict_mutation_test_state": {"details": {}}},
|
|
),
|
|
),
|
|
],
|
|
],
|
|
id="add, remove, pop",
|
|
id="add, remove, pop",
|
|
@@ -553,13 +611,17 @@ async def test_list_mutation_detection__plain_list(
|
|
pytest.param(
|
|
pytest.param(
|
|
[
|
|
[
|
|
(
|
|
(
|
|
- "test_state.remove_home_address",
|
|
|
|
- {"test_state": {"address": [{}, {"work": "work address"}]}},
|
|
|
|
|
|
+ "dict_mutation_test_state.remove_home_address",
|
|
|
|
+ {
|
|
|
|
+ "dict_mutation_test_state": {
|
|
|
|
+ "address": [{}, {"work": "work address"}]
|
|
|
|
+ }
|
|
|
|
+ },
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.add_street_to_home_address",
|
|
|
|
|
|
+ "dict_mutation_test_state.add_street_to_home_address",
|
|
{
|
|
{
|
|
- "test_state": {
|
|
|
|
|
|
+ "dict_mutation_test_state": {
|
|
"address": [
|
|
"address": [
|
|
{"street": "street address"},
|
|
{"street": "street address"},
|
|
{"work": "work address"},
|
|
{"work": "work address"},
|
|
@@ -573,9 +635,9 @@ async def test_list_mutation_detection__plain_list(
|
|
pytest.param(
|
|
pytest.param(
|
|
[
|
|
[
|
|
(
|
|
(
|
|
- "test_state.change_friend_name",
|
|
|
|
|
|
+ "dict_mutation_test_state.change_friend_name",
|
|
{
|
|
{
|
|
- "test_state": {
|
|
|
|
|
|
+ "dict_mutation_test_state": {
|
|
"friend_in_nested_dict": {
|
|
"friend_in_nested_dict": {
|
|
"name": "Nikhil",
|
|
"name": "Nikhil",
|
|
"friend": {"name": "Tommy"},
|
|
"friend": {"name": "Tommy"},
|
|
@@ -584,9 +646,9 @@ async def test_list_mutation_detection__plain_list(
|
|
},
|
|
},
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.add_friend_age",
|
|
|
|
|
|
+ "dict_mutation_test_state.add_friend_age",
|
|
{
|
|
{
|
|
- "test_state": {
|
|
|
|
|
|
+ "dict_mutation_test_state": {
|
|
"friend_in_nested_dict": {
|
|
"friend_in_nested_dict": {
|
|
"name": "Nikhil",
|
|
"name": "Nikhil",
|
|
"friend": {"name": "Tommy", "age": 30},
|
|
"friend": {"name": "Tommy", "age": 30},
|
|
@@ -595,8 +657,12 @@ async def test_list_mutation_detection__plain_list(
|
|
},
|
|
},
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "test_state.remove_friend",
|
|
|
|
- {"test_state": {"friend_in_nested_dict": {"name": "Nikhil"}}},
|
|
|
|
|
|
+ "dict_mutation_test_state.remove_friend",
|
|
|
|
+ {
|
|
|
|
+ "dict_mutation_test_state": {
|
|
|
|
+ "friend_in_nested_dict": {"name": "Nikhil"}
|
|
|
|
+ }
|
|
|
|
+ },
|
|
),
|
|
),
|
|
],
|
|
],
|
|
id="nested dict",
|
|
id="nested dict",
|
|
@@ -604,7 +670,9 @@ async def test_list_mutation_detection__plain_list(
|
|
],
|
|
],
|
|
)
|
|
)
|
|
async def test_dict_mutation_detection__plain_list(
|
|
async def test_dict_mutation_detection__plain_list(
|
|
- event_tuples: List[Tuple[str, List[str]]], dict_mutation_state: State
|
|
|
|
|
|
+ event_tuples: List[Tuple[str, List[str]]],
|
|
|
|
+ dict_mutation_state: State,
|
|
|
|
+ token: str,
|
|
):
|
|
):
|
|
"""Test dict mutation detection
|
|
"""Test dict mutation detection
|
|
when reassignment is not explicitly included in the logic.
|
|
when reassignment is not explicitly included in the logic.
|
|
@@ -612,11 +680,12 @@ async def test_dict_mutation_detection__plain_list(
|
|
Args:
|
|
Args:
|
|
event_tuples: From parametrization.
|
|
event_tuples: From parametrization.
|
|
dict_mutation_state: A state with dict mutation features.
|
|
dict_mutation_state: A state with dict mutation features.
|
|
|
|
+ token: a Token.
|
|
"""
|
|
"""
|
|
for event_name, expected_delta in event_tuples:
|
|
for event_name, expected_delta in event_tuples:
|
|
result = await dict_mutation_state._process(
|
|
result = await dict_mutation_state._process(
|
|
Event(
|
|
Event(
|
|
- token="fake-token",
|
|
|
|
|
|
+ token=token,
|
|
name=event_name,
|
|
name=event_name,
|
|
router_data={"pathname": "/", "query": {}},
|
|
router_data={"pathname": "/", "query": {}},
|
|
payload={},
|
|
payload={},
|
|
@@ -628,41 +697,43 @@ async def test_dict_mutation_detection__plain_list(
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
@pytest.mark.parametrize(
|
|
- "fixture, delta",
|
|
|
|
|
|
+ ("state", "delta"),
|
|
[
|
|
[
|
|
(
|
|
(
|
|
- "upload_state",
|
|
|
|
|
|
+ FileUploadState,
|
|
{"file_upload_state": {"img_list": ["image1.jpg", "image2.jpg"]}},
|
|
{"file_upload_state": {"img_list": ["image1.jpg", "image2.jpg"]}},
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "upload_sub_state",
|
|
|
|
|
|
+ ChildFileUploadState,
|
|
{
|
|
{
|
|
- "file_state.file_upload_state": {
|
|
|
|
|
|
+ "file_state_base1.child_file_upload_state": {
|
|
"img_list": ["image1.jpg", "image2.jpg"]
|
|
"img_list": ["image1.jpg", "image2.jpg"]
|
|
}
|
|
}
|
|
},
|
|
},
|
|
),
|
|
),
|
|
(
|
|
(
|
|
- "upload_grand_sub_state",
|
|
|
|
|
|
+ GrandChildFileUploadState,
|
|
{
|
|
{
|
|
- "base_file_state.file_sub_state.file_upload_state": {
|
|
|
|
|
|
+ "file_state_base1.file_state_base2.grand_child_file_upload_state": {
|
|
"img_list": ["image1.jpg", "image2.jpg"]
|
|
"img_list": ["image1.jpg", "image2.jpg"]
|
|
}
|
|
}
|
|
},
|
|
},
|
|
),
|
|
),
|
|
],
|
|
],
|
|
)
|
|
)
|
|
-async def test_upload_file(fixture, request, delta):
|
|
|
|
|
|
+async def test_upload_file(tmp_path, state, delta, token: str):
|
|
"""Test that file upload works correctly.
|
|
"""Test that file upload works correctly.
|
|
|
|
|
|
Args:
|
|
Args:
|
|
- fixture: The state.
|
|
|
|
- request: Fixture request.
|
|
|
|
|
|
+ tmp_path: Temporary path.
|
|
|
|
+ state: The state class.
|
|
delta: Expected delta
|
|
delta: Expected delta
|
|
|
|
+ token: a Token.
|
|
"""
|
|
"""
|
|
- app = App(state=request.getfixturevalue(fixture))
|
|
|
|
|
|
+ state._tmp_path = tmp_path
|
|
|
|
+ app = App(state=state)
|
|
app.event_namespace.emit = AsyncMock() # type: ignore
|
|
app.event_namespace.emit = AsyncMock() # type: ignore
|
|
- current_state = app.state_manager.get_state("token")
|
|
|
|
|
|
+ current_state = await app.state_manager.get_state(token)
|
|
data = b"This is binary data"
|
|
data = b"This is binary data"
|
|
|
|
|
|
# Create a binary IO object and write data to it
|
|
# Create a binary IO object and write data to it
|
|
@@ -670,11 +741,11 @@ async def test_upload_file(fixture, request, delta):
|
|
bio.write(data)
|
|
bio.write(data)
|
|
|
|
|
|
file1 = UploadFile(
|
|
file1 = UploadFile(
|
|
- filename="token:file_upload_state.multi_handle_upload:True:image1.jpg",
|
|
|
|
|
|
+ filename=f"{token}:{state.get_name()}.multi_handle_upload:True:image1.jpg",
|
|
file=bio,
|
|
file=bio,
|
|
)
|
|
)
|
|
file2 = UploadFile(
|
|
file2 = UploadFile(
|
|
- filename="token:file_upload_state.multi_handle_upload:True:image2.jpg",
|
|
|
|
|
|
+ filename=f"{token}:{state.get_name()}.multi_handle_upload:True:image2.jpg",
|
|
file=bio,
|
|
file=bio,
|
|
)
|
|
)
|
|
upload_fn = upload(app)
|
|
upload_fn = upload(app)
|
|
@@ -684,22 +755,27 @@ async def test_upload_file(fixture, request, delta):
|
|
app.event_namespace.emit.assert_called_with( # type: ignore
|
|
app.event_namespace.emit.assert_called_with( # type: ignore
|
|
"event", state_update.json(), to=current_state.get_sid()
|
|
"event", state_update.json(), to=current_state.get_sid()
|
|
)
|
|
)
|
|
- assert app.state_manager.get_state("token").dict()["img_list"] == [
|
|
|
|
|
|
+ assert (await app.state_manager.get_state(token)).dict()["img_list"] == [
|
|
"image1.jpg",
|
|
"image1.jpg",
|
|
"image2.jpg",
|
|
"image2.jpg",
|
|
]
|
|
]
|
|
|
|
|
|
|
|
+ if isinstance(app.state_manager, StateManagerRedis):
|
|
|
|
+ await app.state_manager.redis.close()
|
|
|
|
+
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
@pytest.mark.parametrize(
|
|
- "fixture", ["upload_state", "upload_sub_state", "upload_grand_sub_state"]
|
|
|
|
|
|
+ "state",
|
|
|
|
+ [FileUploadState, ChildFileUploadState, GrandChildFileUploadState],
|
|
)
|
|
)
|
|
-async def test_upload_file_without_annotation(fixture, request):
|
|
|
|
|
|
+async def test_upload_file_without_annotation(state, tmp_path, token):
|
|
"""Test that an error is thrown when there's no param annotated with rx.UploadFile or List[UploadFile].
|
|
"""Test that an error is thrown when there's no param annotated with rx.UploadFile or List[UploadFile].
|
|
|
|
|
|
Args:
|
|
Args:
|
|
- fixture: The state.
|
|
|
|
- request: Fixture request.
|
|
|
|
|
|
+ state: The state class.
|
|
|
|
+ tmp_path: Temporary path.
|
|
|
|
+ token: a Token.
|
|
"""
|
|
"""
|
|
data = b"This is binary data"
|
|
data = b"This is binary data"
|
|
|
|
|
|
@@ -707,14 +783,15 @@ async def test_upload_file_without_annotation(fixture, request):
|
|
bio = io.BytesIO()
|
|
bio = io.BytesIO()
|
|
bio.write(data)
|
|
bio.write(data)
|
|
|
|
|
|
- app = App(state=request.getfixturevalue(fixture))
|
|
|
|
|
|
+ state._tmp_path = tmp_path
|
|
|
|
+ app = App(state=state)
|
|
|
|
|
|
file1 = UploadFile(
|
|
file1 = UploadFile(
|
|
- filename="token:file_upload_state.handle_upload2:True:image1.jpg",
|
|
|
|
|
|
+ filename=f"{token}:{state.get_name()}.handle_upload2:True:image1.jpg",
|
|
file=bio,
|
|
file=bio,
|
|
)
|
|
)
|
|
file2 = UploadFile(
|
|
file2 = UploadFile(
|
|
- filename="token:file_upload_state.handle_upload2:True:image2.jpg",
|
|
|
|
|
|
+ filename=f"{token}:{state.get_name()}.handle_upload2:True:image2.jpg",
|
|
file=bio,
|
|
file=bio,
|
|
)
|
|
)
|
|
fn = upload(app)
|
|
fn = upload(app)
|
|
@@ -722,9 +799,12 @@ async def test_upload_file_without_annotation(fixture, request):
|
|
await fn([file1, file2])
|
|
await fn([file1, file2])
|
|
assert (
|
|
assert (
|
|
err.value.args[0]
|
|
err.value.args[0]
|
|
- == "`file_upload_state.handle_upload2` handler should have a parameter annotated as List[rx.UploadFile]"
|
|
|
|
|
|
+ == f"`{state.get_name()}.handle_upload2` handler should have a parameter annotated as List[rx.UploadFile]"
|
|
)
|
|
)
|
|
|
|
|
|
|
|
+ if isinstance(app.state_manager, StateManagerRedis):
|
|
|
|
+ await app.state_manager.redis.close()
|
|
|
|
+
|
|
|
|
|
|
class DynamicState(State):
|
|
class DynamicState(State):
|
|
"""State class for testing dynamic route var.
|
|
"""State class for testing dynamic route var.
|
|
@@ -768,6 +848,7 @@ class DynamicState(State):
|
|
async def test_dynamic_route_var_route_change_completed_on_load(
|
|
async def test_dynamic_route_var_route_change_completed_on_load(
|
|
index_page,
|
|
index_page,
|
|
windows_platform: bool,
|
|
windows_platform: bool,
|
|
|
|
+ token: str,
|
|
):
|
|
):
|
|
"""Create app with dynamic route var, and simulate navigation.
|
|
"""Create app with dynamic route var, and simulate navigation.
|
|
|
|
|
|
@@ -777,6 +858,7 @@ async def test_dynamic_route_var_route_change_completed_on_load(
|
|
Args:
|
|
Args:
|
|
index_page: The index page.
|
|
index_page: The index page.
|
|
windows_platform: Whether the system is windows.
|
|
windows_platform: Whether the system is windows.
|
|
|
|
+ token: a Token.
|
|
"""
|
|
"""
|
|
arg_name = "dynamic"
|
|
arg_name = "dynamic"
|
|
route = f"/test/[{arg_name}]"
|
|
route = f"/test/[{arg_name}]"
|
|
@@ -792,10 +874,9 @@ async def test_dynamic_route_var_route_change_completed_on_load(
|
|
}
|
|
}
|
|
assert constants.ROUTER_DATA in app.state().computed_var_dependencies
|
|
assert constants.ROUTER_DATA in app.state().computed_var_dependencies
|
|
|
|
|
|
- token = "mock_token"
|
|
|
|
sid = "mock_sid"
|
|
sid = "mock_sid"
|
|
client_ip = "127.0.0.1"
|
|
client_ip = "127.0.0.1"
|
|
- state = app.state_manager.get_state(token)
|
|
|
|
|
|
+ state = await app.state_manager.get_state(token)
|
|
assert state.dynamic == ""
|
|
assert state.dynamic == ""
|
|
exp_vals = ["foo", "foobar", "baz"]
|
|
exp_vals = ["foo", "foobar", "baz"]
|
|
|
|
|
|
@@ -817,6 +898,7 @@ async def test_dynamic_route_var_route_change_completed_on_load(
|
|
**kwargs,
|
|
**kwargs,
|
|
)
|
|
)
|
|
|
|
|
|
|
|
+ prev_exp_val = ""
|
|
for exp_index, exp_val in enumerate(exp_vals):
|
|
for exp_index, exp_val in enumerate(exp_vals):
|
|
hydrate_event = _event(name=get_hydrate_event(state), val=exp_val)
|
|
hydrate_event = _event(name=get_hydrate_event(state), val=exp_val)
|
|
exp_router_data = {
|
|
exp_router_data = {
|
|
@@ -826,13 +908,14 @@ async def test_dynamic_route_var_route_change_completed_on_load(
|
|
"token": token,
|
|
"token": token,
|
|
**hydrate_event.router_data,
|
|
**hydrate_event.router_data,
|
|
}
|
|
}
|
|
- update = await process(
|
|
|
|
|
|
+ process_coro = process(
|
|
app,
|
|
app,
|
|
event=hydrate_event,
|
|
event=hydrate_event,
|
|
sid=sid,
|
|
sid=sid,
|
|
headers={},
|
|
headers={},
|
|
client_ip=client_ip,
|
|
client_ip=client_ip,
|
|
- ).__anext__() # type: ignore
|
|
|
|
|
|
+ )
|
|
|
|
+ update = await process_coro.__anext__() # type: ignore
|
|
|
|
|
|
# route change triggers: [full state dict, call on_load events, call set_is_hydrated(True)]
|
|
# route change triggers: [full state dict, call on_load events, call set_is_hydrated(True)]
|
|
assert update == StateUpdate(
|
|
assert update == StateUpdate(
|
|
@@ -860,14 +943,27 @@ async def test_dynamic_route_var_route_change_completed_on_load(
|
|
),
|
|
),
|
|
],
|
|
],
|
|
)
|
|
)
|
|
|
|
+ if isinstance(app.state_manager, StateManagerRedis):
|
|
|
|
+ # When redis is used, the state is not updated until the processing is complete
|
|
|
|
+ state = await app.state_manager.get_state(token)
|
|
|
|
+ assert state.dynamic == prev_exp_val
|
|
|
|
+
|
|
|
|
+ # complete the processing
|
|
|
|
+ with pytest.raises(StopAsyncIteration):
|
|
|
|
+ await process_coro.__anext__() # type: ignore
|
|
|
|
+
|
|
|
|
+ # check that router data was written to the state_manager store
|
|
|
|
+ state = await app.state_manager.get_state(token)
|
|
assert state.dynamic == exp_val
|
|
assert state.dynamic == exp_val
|
|
- on_load_update = await process(
|
|
|
|
|
|
+
|
|
|
|
+ process_coro = process(
|
|
app,
|
|
app,
|
|
event=_dynamic_state_event(name="on_load", val=exp_val),
|
|
event=_dynamic_state_event(name="on_load", val=exp_val),
|
|
sid=sid,
|
|
sid=sid,
|
|
headers={},
|
|
headers={},
|
|
client_ip=client_ip,
|
|
client_ip=client_ip,
|
|
- ).__anext__() # type: ignore
|
|
|
|
|
|
+ )
|
|
|
|
+ on_load_update = await process_coro.__anext__() # type: ignore
|
|
assert on_load_update == StateUpdate(
|
|
assert on_load_update == StateUpdate(
|
|
delta={
|
|
delta={
|
|
state.get_name(): {
|
|
state.get_name(): {
|
|
@@ -879,7 +975,10 @@ async def test_dynamic_route_var_route_change_completed_on_load(
|
|
},
|
|
},
|
|
events=[],
|
|
events=[],
|
|
)
|
|
)
|
|
- on_set_is_hydrated_update = await process(
|
|
|
|
|
|
+ # complete the processing
|
|
|
|
+ with pytest.raises(StopAsyncIteration):
|
|
|
|
+ await process_coro.__anext__() # type: ignore
|
|
|
|
+ process_coro = process(
|
|
app,
|
|
app,
|
|
event=_dynamic_state_event(
|
|
event=_dynamic_state_event(
|
|
name="set_is_hydrated", payload={"value": True}, val=exp_val
|
|
name="set_is_hydrated", payload={"value": True}, val=exp_val
|
|
@@ -887,7 +986,8 @@ async def test_dynamic_route_var_route_change_completed_on_load(
|
|
sid=sid,
|
|
sid=sid,
|
|
headers={},
|
|
headers={},
|
|
client_ip=client_ip,
|
|
client_ip=client_ip,
|
|
- ).__anext__() # type: ignore
|
|
|
|
|
|
+ )
|
|
|
|
+ on_set_is_hydrated_update = await process_coro.__anext__() # type: ignore
|
|
assert on_set_is_hydrated_update == StateUpdate(
|
|
assert on_set_is_hydrated_update == StateUpdate(
|
|
delta={
|
|
delta={
|
|
state.get_name(): {
|
|
state.get_name(): {
|
|
@@ -899,15 +999,19 @@ async def test_dynamic_route_var_route_change_completed_on_load(
|
|
},
|
|
},
|
|
events=[],
|
|
events=[],
|
|
)
|
|
)
|
|
|
|
+ # complete the processing
|
|
|
|
+ with pytest.raises(StopAsyncIteration):
|
|
|
|
+ await process_coro.__anext__() # type: ignore
|
|
|
|
|
|
# a simple state update event should NOT trigger on_load or route var side effects
|
|
# a simple state update event should NOT trigger on_load or route var side effects
|
|
- update = await process(
|
|
|
|
|
|
+ process_coro = process(
|
|
app,
|
|
app,
|
|
event=_dynamic_state_event(name="on_counter", val=exp_val),
|
|
event=_dynamic_state_event(name="on_counter", val=exp_val),
|
|
sid=sid,
|
|
sid=sid,
|
|
headers={},
|
|
headers={},
|
|
client_ip=client_ip,
|
|
client_ip=client_ip,
|
|
- ).__anext__() # type: ignore
|
|
|
|
|
|
+ )
|
|
|
|
+ update = await process_coro.__anext__() # type: ignore
|
|
assert update == StateUpdate(
|
|
assert update == StateUpdate(
|
|
delta={
|
|
delta={
|
|
state.get_name(): {
|
|
state.get_name(): {
|
|
@@ -919,42 +1023,54 @@ async def test_dynamic_route_var_route_change_completed_on_load(
|
|
},
|
|
},
|
|
events=[],
|
|
events=[],
|
|
)
|
|
)
|
|
|
|
+ # complete the processing
|
|
|
|
+ with pytest.raises(StopAsyncIteration):
|
|
|
|
+ await process_coro.__anext__() # type: ignore
|
|
|
|
+
|
|
|
|
+ prev_exp_val = exp_val
|
|
|
|
+ state = await app.state_manager.get_state(token)
|
|
assert state.loaded == len(exp_vals)
|
|
assert state.loaded == len(exp_vals)
|
|
assert state.counter == len(exp_vals)
|
|
assert state.counter == len(exp_vals)
|
|
# print(f"Expected {exp_vals} rendering side effects, got {state.side_effect_counter}")
|
|
# print(f"Expected {exp_vals} rendering side effects, got {state.side_effect_counter}")
|
|
# assert state.side_effect_counter == len(exp_vals)
|
|
# assert state.side_effect_counter == len(exp_vals)
|
|
|
|
|
|
|
|
+ if isinstance(app.state_manager, StateManagerRedis):
|
|
|
|
+ await app.state_manager.redis.close()
|
|
|
|
+
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
-async def test_process_events(gen_state, mocker):
|
|
|
|
|
|
+async def test_process_events(mocker, token: str):
|
|
"""Test that an event is processed properly and that it is postprocessed
|
|
"""Test that an event is processed properly and that it is postprocessed
|
|
n+1 times. Also check that the processing flag of the last stateupdate is set to
|
|
n+1 times. Also check that the processing flag of the last stateupdate is set to
|
|
False.
|
|
False.
|
|
|
|
|
|
Args:
|
|
Args:
|
|
- gen_state: The state.
|
|
|
|
mocker: mocker object.
|
|
mocker: mocker object.
|
|
|
|
+ token: a Token.
|
|
"""
|
|
"""
|
|
router_data = {
|
|
router_data = {
|
|
"pathname": "/",
|
|
"pathname": "/",
|
|
"query": {},
|
|
"query": {},
|
|
- "token": "mock_token",
|
|
|
|
|
|
+ "token": token,
|
|
"sid": "mock_sid",
|
|
"sid": "mock_sid",
|
|
"headers": {},
|
|
"headers": {},
|
|
"ip": "127.0.0.1",
|
|
"ip": "127.0.0.1",
|
|
}
|
|
}
|
|
- app = App(state=gen_state)
|
|
|
|
|
|
+ app = App(state=GenState)
|
|
mocker.patch.object(app, "postprocess", AsyncMock())
|
|
mocker.patch.object(app, "postprocess", AsyncMock())
|
|
event = Event(
|
|
event = Event(
|
|
- token="token", name="gen_state.go", payload={"c": 5}, router_data=router_data
|
|
|
|
|
|
+ token=token, name="gen_state.go", payload={"c": 5}, router_data=router_data
|
|
)
|
|
)
|
|
|
|
|
|
async for _update in process(app, event, "mock_sid", {}, "127.0.0.1"): # type: ignore
|
|
async for _update in process(app, event, "mock_sid", {}, "127.0.0.1"): # type: ignore
|
|
pass
|
|
pass
|
|
|
|
|
|
- assert app.state_manager.get_state("token").value == 5
|
|
|
|
|
|
+ assert (await app.state_manager.get_state(token)).value == 5
|
|
assert app.postprocess.call_count == 6
|
|
assert app.postprocess.call_count == 6
|
|
|
|
|
|
|
|
+ if isinstance(app.state_manager, StateManagerRedis):
|
|
|
|
+ await app.state_manager.redis.close()
|
|
|
|
+
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
@pytest.mark.parametrize(
|
|
("state", "overlay_component", "exp_page_child"),
|
|
("state", "overlay_component", "exp_page_child"),
|