add module prefix to generated state names (#3214)

* add module prefix to state names

* fix state names in test_app

* update state names in test_state

* fix state names in test_var

* fix state name in test_component

* fix state names in test_format

* fix state names in test_foreach

* fix state names in test_cond

* fix state names in test_datatable

* fix state names in test_colors

* fix state names in test_script

* fix state names in test_match

* fix state name in event1 fixture

* fix pyright and darglint

* fix state names in state_tree

* fix state names in redis only test

* fix state names in test_client_storage

* fix state name in js template

* add `get_state_name` and `get_full_state_name` helpers for `AppHarness`

* fix state names in test_dynamic_routes

* use new state name helpers in test_client_storage

* fix state names in test_event_actions

* fix state names in test_event_chain

* fix state names in test_upload

* fix state name in test_login_flow

* fix state names in test_input

* fix state names in test_form_submit

* ruff

* validate state module names

* wtf is going on here?

* remove comments leftover from refactoring

* adjust new test_add_style_embedded_vars

* fix state name in state.js

* fix integration/test_client_state.py

new SessionStorage feature was added with more full state names that need to be formatted in

* fix pre-commit issues in test_client_storage.py

* adjust test_computed_vars

* adjust safe-guards

* fix redis tests with new exception state

---------

Co-authored-by: Masen Furer <m_github@0x26.net>
This commit is contained in:
benedikt-bartscher 2024-07-11 20:13:57 +02:00 committed by GitHub
parent d378e4a70c
commit 3039b54a75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 479 additions and 413 deletions

View File

@ -229,7 +229,8 @@ async def test_client_side_state(
local_storage: Local storage helper. local_storage: Local storage helper.
session_storage: Session storage helper. session_storage: Session storage helper.
""" """
assert client_side.app_instance is not None app = client_side.app_instance
assert app is not None
assert client_side.frontend_url is not None assert client_side.frontend_url is not None
def poll_for_token(): def poll_for_token():
@ -333,29 +334,37 @@ async def test_client_side_state(
set_sub_sub("l1s", "l1s value") set_sub_sub("l1s", "l1s value")
set_sub_sub("s1s", "s1s value") set_sub_sub("s1s", "s1s value")
state_name = client_side.get_full_state_name(["_client_side_state"])
sub_state_name = client_side.get_full_state_name(
["_client_side_state", "_client_side_sub_state"]
)
sub_sub_state_name = client_side.get_full_state_name(
["_client_side_state", "_client_side_sub_state", "_client_side_sub_sub_state"]
)
exp_cookies = { exp_cookies = {
"state.client_side_state.client_side_sub_state.c1": { f"{sub_state_name}.c1": {
"domain": "localhost", "domain": "localhost",
"httpOnly": False, "httpOnly": False,
"name": "state.client_side_state.client_side_sub_state.c1", "name": f"{sub_state_name}.c1",
"path": "/", "path": "/",
"sameSite": "Lax", "sameSite": "Lax",
"secure": False, "secure": False,
"value": "c1%20value", "value": "c1%20value",
}, },
"state.client_side_state.client_side_sub_state.c2": { f"{sub_state_name}.c2": {
"domain": "localhost", "domain": "localhost",
"httpOnly": False, "httpOnly": False,
"name": "state.client_side_state.client_side_sub_state.c2", "name": f"{sub_state_name}.c2",
"path": "/", "path": "/",
"sameSite": "Lax", "sameSite": "Lax",
"secure": False, "secure": False,
"value": "c2%20value", "value": "c2%20value",
}, },
"state.client_side_state.client_side_sub_state.c4": { f"{sub_state_name}.c4": {
"domain": "localhost", "domain": "localhost",
"httpOnly": False, "httpOnly": False,
"name": "state.client_side_state.client_side_sub_state.c4", "name": f"{sub_state_name}.c4",
"path": "/", "path": "/",
"sameSite": "Strict", "sameSite": "Strict",
"secure": False, "secure": False,
@ -370,19 +379,19 @@ async def test_client_side_state(
"secure": False, "secure": False,
"value": "c6%20value", "value": "c6%20value",
}, },
"state.client_side_state.client_side_sub_state.c7": { f"{sub_state_name}.c7": {
"domain": "localhost", "domain": "localhost",
"httpOnly": False, "httpOnly": False,
"name": "state.client_side_state.client_side_sub_state.c7", "name": f"{sub_state_name}.c7",
"path": "/", "path": "/",
"sameSite": "Lax", "sameSite": "Lax",
"secure": False, "secure": False,
"value": "c7%20value", "value": "c7%20value",
}, },
"state.client_side_state.client_side_sub_state.client_side_sub_sub_state.c1s": { f"{sub_sub_state_name}.c1s": {
"domain": "localhost", "domain": "localhost",
"httpOnly": False, "httpOnly": False,
"name": "state.client_side_state.client_side_sub_state.client_side_sub_sub_state.c1s", "name": f"{sub_sub_state_name}.c1s",
"path": "/", "path": "/",
"sameSite": "Lax", "sameSite": "Lax",
"secure": False, "secure": False,
@ -400,18 +409,13 @@ async def test_client_side_state(
# Test cookie with expiry by itself to avoid timing flakiness # Test cookie with expiry by itself to avoid timing flakiness
set_sub("c3", "c3 value") set_sub("c3", "c3 value")
AppHarness._poll_for( AppHarness._poll_for(lambda: f"{sub_state_name}.c3" in cookie_info_map(driver))
lambda: "state.client_side_state.client_side_sub_state.c3" c3_cookie = cookie_info_map(driver)[f"{sub_state_name}.c3"]
in cookie_info_map(driver)
)
c3_cookie = cookie_info_map(driver)[
"state.client_side_state.client_side_sub_state.c3"
]
assert c3_cookie.pop("expiry") is not None assert c3_cookie.pop("expiry") is not None
assert c3_cookie == { assert c3_cookie == {
"domain": "localhost", "domain": "localhost",
"httpOnly": False, "httpOnly": False,
"name": "state.client_side_state.client_side_sub_state.c3", "name": f"{sub_state_name}.c3",
"path": "/", "path": "/",
"sameSite": "Lax", "sameSite": "Lax",
"secure": False, "secure": False,
@ -420,52 +424,24 @@ async def test_client_side_state(
time.sleep(2) # wait for c3 to expire time.sleep(2) # wait for c3 to expire
if not isinstance(driver, Firefox): if not isinstance(driver, Firefox):
# Note: Firefox does not remove expired cookies Bug 576347 # Note: Firefox does not remove expired cookies Bug 576347
assert ( assert f"{sub_state_name}.c3" not in cookie_info_map(driver)
"state.client_side_state.client_side_sub_state.c3"
not in cookie_info_map(driver)
)
local_storage_items = local_storage.items() local_storage_items = local_storage.items()
local_storage_items.pop("chakra-ui-color-mode", None) local_storage_items.pop("chakra-ui-color-mode", None)
local_storage_items.pop("last_compiled_time", None) local_storage_items.pop("last_compiled_time", None)
assert ( assert local_storage_items.pop(f"{sub_state_name}.l1") == "l1 value"
local_storage_items.pop("state.client_side_state.client_side_sub_state.l1") assert local_storage_items.pop(f"{sub_state_name}.l2") == "l2 value"
== "l1 value"
)
assert (
local_storage_items.pop("state.client_side_state.client_side_sub_state.l2")
== "l2 value"
)
assert local_storage_items.pop("l3") == "l3 value" assert local_storage_items.pop("l3") == "l3 value"
assert ( assert local_storage_items.pop(f"{sub_state_name}.l4") == "l4 value"
local_storage_items.pop("state.client_side_state.client_side_sub_state.l4") assert local_storage_items.pop(f"{sub_sub_state_name}.l1s") == "l1s value"
== "l4 value"
)
assert (
local_storage_items.pop(
"state.client_side_state.client_side_sub_state.client_side_sub_sub_state.l1s"
)
== "l1s value"
)
assert not local_storage_items assert not local_storage_items
session_storage_items = session_storage.items() session_storage_items = session_storage.items()
session_storage_items.pop("token", None) session_storage_items.pop("token", None)
assert ( assert session_storage_items.pop(f"{sub_state_name}.s1") == "s1 value"
session_storage_items.pop("state.client_side_state.client_side_sub_state.s1") assert session_storage_items.pop(f"{sub_state_name}.s2") == "s2 value"
== "s1 value"
)
assert (
session_storage_items.pop("state.client_side_state.client_side_sub_state.s2")
== "s2 value"
)
assert session_storage_items.pop("s3") == "s3 value" assert session_storage_items.pop("s3") == "s3 value"
assert ( assert session_storage_items.pop(f"{sub_sub_state_name}.s1s") == "s1s value"
session_storage_items.pop(
"state.client_side_state.client_side_sub_state.client_side_sub_sub_state.s1s"
)
== "s1s value"
)
assert not session_storage_items assert not session_storage_items
assert c1.text == "c1 value" assert c1.text == "c1 value"
@ -528,7 +504,7 @@ async def test_client_side_state(
assert s1s.text == "s1s value" assert s1s.text == "s1s value"
# reset the backend state to force refresh from client storage # reset the backend state to force refresh from client storage
async with client_side.modify_state(f"{token}_state.client_side_state") as state: async with client_side.modify_state(f"{token}_{state_name}") as state:
state.reset() state.reset()
driver.refresh() driver.refresh()
@ -576,16 +552,11 @@ async def test_client_side_state(
assert s1s.text == "s1s value" assert s1s.text == "s1s value"
# make sure c5 cookie shows up on the `/foo` route # make sure c5 cookie shows up on the `/foo` route
AppHarness._poll_for( AppHarness._poll_for(lambda: f"{sub_state_name}.c5" in cookie_info_map(driver))
lambda: "state.client_side_state.client_side_sub_state.c5" assert cookie_info_map(driver)[f"{sub_state_name}.c5"] == {
in cookie_info_map(driver)
)
assert cookie_info_map(driver)[
"state.client_side_state.client_side_sub_state.c5"
] == {
"domain": "localhost", "domain": "localhost",
"httpOnly": False, "httpOnly": False,
"name": "state.client_side_state.client_side_sub_state.c5", "name": f"{sub_state_name}.c5",
"path": "/foo/", "path": "/foo/",
"sameSite": "Lax", "sameSite": "Lax",
"secure": False, "secure": False,

View File

@ -183,8 +183,10 @@ async def test_computed_vars(
""" """
assert computed_vars.app_instance is not None assert computed_vars.app_instance is not None
token = f"{token}_state.state" state_name = computed_vars.get_state_name("_state")
state = (await computed_vars.get_state(token)).substates["state"] full_state_name = computed_vars.get_full_state_name(["_state"])
token = f"{token}_{full_state_name}"
state = (await computed_vars.get_state(token)).substates[state_name]
assert state is not None assert state is not None
assert state.count1_backend == 0 assert state.count1_backend == 0
assert state._count1_backend == 0 assert state._count1_backend == 0
@ -236,7 +238,7 @@ async def test_computed_vars(
computed_vars.poll_for_content(depends_on_count, timeout=2, exp_not_equal="0") computed_vars.poll_for_content(depends_on_count, timeout=2, exp_not_equal="0")
== "1" == "1"
) )
state = (await computed_vars.get_state(token)).substates["state"] state = (await computed_vars.get_state(token)).substates[state_name]
assert state is not None assert state is not None
assert state.count1_backend == 1 assert state.count1_backend == 1
assert count1_backend.text == "" assert count1_backend.text == ""

View File

@ -154,18 +154,20 @@ def poll_for_order(
Returns: Returns:
An async function that polls for the order list to match the expected order. An async function that polls for the order list to match the expected order.
""" """
dynamic_state_name = dynamic_route.get_state_name("_dynamic_state")
dynamic_state_full_name = dynamic_route.get_full_state_name(["_dynamic_state"])
async def _poll_for_order(exp_order: list[str]): async def _poll_for_order(exp_order: list[str]):
async def _backend_state(): async def _backend_state():
return await dynamic_route.get_state(f"{token}_state.dynamic_state") return await dynamic_route.get_state(f"{token}_{dynamic_state_full_name}")
async def _check(): async def _check():
return (await _backend_state()).substates[ return (await _backend_state()).substates[
"dynamic_state" dynamic_state_name
].order == exp_order ].order == exp_order
await AppHarness._poll_for_async(_check) await AppHarness._poll_for_async(_check)
assert (await _backend_state()).substates["dynamic_state"].order == exp_order assert (await _backend_state()).substates[dynamic_state_name].order == exp_order
return _poll_for_order return _poll_for_order
@ -185,6 +187,7 @@ async def test_on_load_navigate(
token: The token visible in the driver browser. token: The token visible in the driver browser.
poll_for_order: function that polls for the order list to match the expected order. poll_for_order: function that polls for the order list to match the expected order.
""" """
dynamic_state_full_name = dynamic_route.get_full_state_name(["_dynamic_state"])
assert dynamic_route.app_instance is not None assert dynamic_route.app_instance is not None
is_prod = isinstance(dynamic_route, AppHarnessProd) is_prod = isinstance(dynamic_route, AppHarnessProd)
link = driver.find_element(By.ID, "link_page_next") link = driver.find_element(By.ID, "link_page_next")
@ -234,7 +237,7 @@ async def test_on_load_navigate(
driver.get(f"{driver.current_url}?foo=bar") driver.get(f"{driver.current_url}?foo=bar")
await poll_for_order(exp_order) await poll_for_order(exp_order)
assert ( assert (
await dynamic_route.get_state(f"{token}_state.dynamic_state") await dynamic_route.get_state(f"{token}_{dynamic_state_full_name}")
).router.page.params["foo"] == "bar" ).router.page.params["foo"] == "bar"
# hit a 404 and ensure we still hydrate # hit a 404 and ensure we still hydrate

View File

@ -229,20 +229,18 @@ def poll_for_order(
Returns: Returns:
An async function that polls for the order list to match the expected order. An async function that polls for the order list to match the expected order.
""" """
state_name = event_action.get_state_name("_event_action_state")
state_full_name = event_action.get_full_state_name(["_event_action_state"])
async def _poll_for_order(exp_order: list[str]): async def _poll_for_order(exp_order: list[str]):
async def _backend_state(): async def _backend_state():
return await event_action.get_state(f"{token}_state.event_action_state") return await event_action.get_state(f"{token}_{state_full_name}")
async def _check(): async def _check():
return (await _backend_state()).substates[ return (await _backend_state()).substates[state_name].order == exp_order
"event_action_state"
].order == exp_order
await AppHarness._poll_for_async(_check) await AppHarness._poll_for_async(_check)
assert (await _backend_state()).substates[ assert (await _backend_state()).substates[state_name].order == exp_order
"event_action_state"
].order == exp_order
return _poll_for_order return _poll_for_order

View File

@ -301,7 +301,8 @@ def assert_token(event_chain: AppHarness, driver: WebDriver) -> str:
token = event_chain.poll_for_value(token_input) token = event_chain.poll_for_value(token_input)
assert token is not None assert token is not None
return f"{token}_state.state" state_name = event_chain.get_full_state_name(["_state"])
return f"{token}_{state_name}"
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -400,16 +401,17 @@ async def test_event_chain_click(
exp_event_order: the expected events recorded in the State exp_event_order: the expected events recorded in the State
""" """
token = assert_token(event_chain, driver) token = assert_token(event_chain, driver)
state_name = event_chain.get_state_name("_state")
btn = driver.find_element(By.ID, button_id) btn = driver.find_element(By.ID, button_id)
btn.click() btn.click()
async def _has_all_events(): async def _has_all_events():
return len( return len(
(await event_chain.get_state(token)).substates["state"].event_order (await event_chain.get_state(token)).substates[state_name].event_order
) == len(exp_event_order) ) == len(exp_event_order)
await AppHarness._poll_for_async(_has_all_events) await AppHarness._poll_for_async(_has_all_events)
event_order = (await event_chain.get_state(token)).substates["state"].event_order event_order = (await event_chain.get_state(token)).substates[state_name].event_order
assert event_order == exp_event_order assert event_order == exp_event_order
@ -454,14 +456,15 @@ async def test_event_chain_on_load(
assert event_chain.frontend_url is not None assert event_chain.frontend_url is not None
driver.get(event_chain.frontend_url + uri) driver.get(event_chain.frontend_url + uri)
token = assert_token(event_chain, driver) token = assert_token(event_chain, driver)
state_name = event_chain.get_state_name("_state")
async def _has_all_events(): async def _has_all_events():
return len( return len(
(await event_chain.get_state(token)).substates["state"].event_order (await event_chain.get_state(token)).substates[state_name].event_order
) == len(exp_event_order) ) == len(exp_event_order)
await AppHarness._poll_for_async(_has_all_events) await AppHarness._poll_for_async(_has_all_events)
backend_state = (await event_chain.get_state(token)).substates["state"] backend_state = (await event_chain.get_state(token)).substates[state_name]
assert backend_state.event_order == exp_event_order assert backend_state.event_order == exp_event_order
assert backend_state.is_hydrated is True assert backend_state.is_hydrated is True
@ -526,6 +529,7 @@ async def test_event_chain_on_mount(
assert event_chain.frontend_url is not None assert event_chain.frontend_url is not None
driver.get(event_chain.frontend_url + uri) driver.get(event_chain.frontend_url + uri)
token = assert_token(event_chain, driver) token = assert_token(event_chain, driver)
state_name = event_chain.get_state_name("_state")
unmount_button = driver.find_element(By.ID, "unmount") unmount_button = driver.find_element(By.ID, "unmount")
assert unmount_button assert unmount_button
@ -533,11 +537,11 @@ async def test_event_chain_on_mount(
async def _has_all_events(): async def _has_all_events():
return len( return len(
(await event_chain.get_state(token)).substates["state"].event_order (await event_chain.get_state(token)).substates[state_name].event_order
) == len(exp_event_order) ) == len(exp_event_order)
await AppHarness._poll_for_async(_has_all_events) await AppHarness._poll_for_async(_has_all_events)
event_order = (await event_chain.get_state(token)).substates["state"].event_order event_order = (await event_chain.get_state(token)).substates[state_name].event_order
assert event_order == exp_event_order assert event_order == exp_event_order

View File

@ -21,6 +21,8 @@ def TestApp():
class TestAppConfig(rx.Config): class TestAppConfig(rx.Config):
"""Config for the TestApp app.""" """Config for the TestApp app."""
pass
class TestAppState(rx.State): class TestAppState(rx.State):
"""State for the TestApp app.""" """State for the TestApp app."""

View File

@ -252,10 +252,13 @@ async def test_submit(driver, form_submit: AppHarness):
submit_input = driver.find_element(By.CLASS_NAME, "rt-Button") submit_input = driver.find_element(By.CLASS_NAME, "rt-Button")
submit_input.click() submit_input.click()
state_name = form_submit.get_state_name("_form_state")
full_state_name = form_submit.get_full_state_name(["_form_state"])
async def get_form_data(): async def get_form_data():
return ( return (
(await form_submit.get_state(f"{token}_state.form_state")) (await form_submit.get_state(f"{token}_{full_state_name}"))
.substates["form_state"] .substates[state_name]
.form_data .form_data
) )

View File

@ -86,9 +86,12 @@ async def test_fully_controlled_input(fully_controlled_input: AppHarness):
token = fully_controlled_input.poll_for_value(token_input) token = fully_controlled_input.poll_for_value(token_input)
assert token assert token
state_name = fully_controlled_input.get_state_name("_state")
full_state_name = fully_controlled_input.get_full_state_name(["_state"])
async def get_state_text(): async def get_state_text():
state = await fully_controlled_input.get_state(f"{token}_state.state") state = await fully_controlled_input.get_state(f"{token}_{full_state_name}")
return state.substates["state"].text return state.substates[state_name].text
# ensure defaults are set correctly # ensure defaults are set correctly
assert ( assert (
@ -138,8 +141,10 @@ async def test_fully_controlled_input(fully_controlled_input: AppHarness):
assert fully_controlled_input.poll_for_value(plain_value_input) == "ifoonitial" assert fully_controlled_input.poll_for_value(plain_value_input) == "ifoonitial"
# clear the input on the backend # clear the input on the backend
async with fully_controlled_input.modify_state(f"{token}_state.state") as state: async with fully_controlled_input.modify_state(
state.substates["state"].text = "" f"{token}_{full_state_name}"
) as state:
state.substates[state_name].text = ""
assert await get_state_text() == "" assert await get_state_text() == ""
assert ( assert (
fully_controlled_input.poll_for_value( fully_controlled_input.poll_for_value(

View File

@ -137,6 +137,9 @@ def test_login_flow(
logout_button = driver.find_element(By.ID, "logout") logout_button = driver.find_element(By.ID, "logout")
logout_button.click() logout_button.click()
assert login_sample._poll_for(lambda: local_storage["state.state.auth_token"] == "") state_name = login_sample.get_full_state_name(["_state"])
assert login_sample._poll_for(
lambda: local_storage[f"{state_name}.auth_token"] == ""
)
with pytest.raises(NoSuchElementException): with pytest.raises(NoSuchElementException):
driver.find_element(By.ID, "auth-token") driver.find_element(By.ID, "auth-token")

View File

@ -174,7 +174,9 @@ async def test_upload_file(
# wait for the backend connection to send the token # wait for the backend connection to send the token
token = upload_file.poll_for_value(token_input) token = upload_file.poll_for_value(token_input)
assert token is not None assert token is not None
substate_token = f"{token}_state.upload_state" full_state_name = upload_file.get_full_state_name(["_upload_state"])
state_name = upload_file.get_state_name("_upload_state")
substate_token = f"{token}_{full_state_name}"
suffix = "_secondary" if secondary else "" suffix = "_secondary" if secondary else ""
@ -197,7 +199,7 @@ async def test_upload_file(
async def get_file_data(): async def get_file_data():
return ( return (
(await upload_file.get_state(substate_token)) (await upload_file.get_state(substate_token))
.substates["upload_state"] .substates[state_name]
._file_data ._file_data
) )
@ -212,8 +214,8 @@ async def test_upload_file(
state = await upload_file.get_state(substate_token) state = await upload_file.get_state(substate_token)
if secondary: if secondary:
# only the secondary form tracks progress and chain events # only the secondary form tracks progress and chain events
assert state.substates["upload_state"].event_order.count("upload_progress") == 1 assert state.substates[state_name].event_order.count("upload_progress") == 1
assert state.substates["upload_state"].event_order.count("chain_event") == 1 assert state.substates[state_name].event_order.count("chain_event") == 1
@pytest.mark.asyncio @pytest.mark.asyncio
@ -231,7 +233,9 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver):
# wait for the backend connection to send the token # wait for the backend connection to send the token
token = upload_file.poll_for_value(token_input) token = upload_file.poll_for_value(token_input)
assert token is not None assert token is not None
substate_token = f"{token}_state.upload_state" full_state_name = upload_file.get_full_state_name(["_upload_state"])
state_name = upload_file.get_state_name("_upload_state")
substate_token = f"{token}_{full_state_name}"
upload_box = driver.find_element(By.XPATH, "//input[@type='file']") upload_box = driver.find_element(By.XPATH, "//input[@type='file']")
assert upload_box assert upload_box
@ -261,7 +265,7 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver):
async def get_file_data(): async def get_file_data():
return ( return (
(await upload_file.get_state(substate_token)) (await upload_file.get_state(substate_token))
.substates["upload_state"] .substates[state_name]
._file_data ._file_data
) )
@ -343,7 +347,9 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive
# wait for the backend connection to send the token # wait for the backend connection to send the token
token = upload_file.poll_for_value(token_input) token = upload_file.poll_for_value(token_input)
assert token is not None assert token is not None
substate_token = f"{token}_state.upload_state" state_name = upload_file.get_state_name("_upload_state")
state_full_name = upload_file.get_full_state_name(["_upload_state"])
substate_token = f"{token}_{state_full_name}"
upload_box = driver.find_elements(By.XPATH, "//input[@type='file']")[1] upload_box = driver.find_elements(By.XPATH, "//input[@type='file']")[1]
upload_button = driver.find_element(By.ID, f"upload_button_secondary") upload_button = driver.find_element(By.ID, f"upload_button_secondary")
@ -362,7 +368,7 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive
# look up the backend state and assert on progress # look up the backend state and assert on progress
state = await upload_file.get_state(substate_token) state = await upload_file.get_state(substate_token)
assert state.substates["upload_state"].progress_dicts assert state.substates[state_name].progress_dicts
assert exp_name not in state.substates["upload_state"]._file_data assert exp_name not in state.substates[state_name]._file_data
target_file.unlink() target_file.unlink()

View File

@ -117,7 +117,7 @@ export const isStateful = () => {
if (event_queue.length === 0) { if (event_queue.length === 0) {
return false; return false;
} }
return event_queue.some(event => event.name.startsWith("state")); return event_queue.some(event => event.name.startsWith("reflex___state"));
} }
/** /**
@ -763,7 +763,7 @@ export const useEventLoop = (
const vars = {}; const vars = {};
vars[storage_to_state_map[e.key]] = e.newValue; vars[storage_to_state_map[e.key]] = e.newValue;
const event = Event( const event = Event(
`${state_name}.update_vars_internal_state.update_vars_internal`, `${state_name}.reflex___state____update_vars_internal_state.update_vars_internal`,
{ vars: vars } { vars: vars }
); );
addEvents([event], e); addEvents([event], e);

View File

@ -44,7 +44,7 @@ class ReflexJinjaEnvironment(Environment):
"hydrate": constants.CompileVars.HYDRATE, "hydrate": constants.CompileVars.HYDRATE,
"on_load_internal": constants.CompileVars.ON_LOAD_INTERNAL, "on_load_internal": constants.CompileVars.ON_LOAD_INTERNAL,
"update_vars_internal": constants.CompileVars.UPDATE_VARS_INTERNAL, "update_vars_internal": constants.CompileVars.UPDATE_VARS_INTERNAL,
"frontend_exception_state": constants.CompileVars.FRONTEND_EXCEPTION_STATE, "frontend_exception_state": constants.CompileVars.FRONTEND_EXCEPTION_STATE_FULL,
} }

View File

@ -62,11 +62,17 @@ class CompileVars(SimpleNamespace):
# The name of the function for converting a dict to an event. # The name of the function for converting a dict to an event.
TO_EVENT = "Event" TO_EVENT = "Event"
# The name of the internal on_load event. # The name of the internal on_load event.
ON_LOAD_INTERNAL = "on_load_internal_state.on_load_internal" ON_LOAD_INTERNAL = "reflex___state____on_load_internal_state.on_load_internal"
# The name of the internal event to update generic state vars. # The name of the internal event to update generic state vars.
UPDATE_VARS_INTERNAL = "update_vars_internal_state.update_vars_internal" UPDATE_VARS_INTERNAL = (
"reflex___state____update_vars_internal_state.update_vars_internal"
)
# The name of the frontend event exception state # The name of the frontend event exception state
FRONTEND_EXCEPTION_STATE = "state.frontend_event_exception_state" FRONTEND_EXCEPTION_STATE = "reflex___state____frontend_event_exception_state"
# The full name of the frontend exception state
FRONTEND_EXCEPTION_STATE_FULL = (
f"reflex___state____state.{FRONTEND_EXCEPTION_STATE}"
)
class PageNames(SimpleNamespace): class PageNames(SimpleNamespace):
@ -129,7 +135,7 @@ class Hooks(SimpleNamespace):
FRONTEND_ERRORS = f""" FRONTEND_ERRORS = f"""
const logFrontendError = (error, info) => {{ const logFrontendError = (error, info) => {{
if (process.env.NODE_ENV === "production") {{ if (process.env.NODE_ENV === "production") {{
addEvents([Event("{CompileVars.FRONTEND_EXCEPTION_STATE}.handle_frontend_exception", {{ addEvents([Event("{CompileVars.FRONTEND_EXCEPTION_STATE_FULL}.handle_frontend_exception", {{
stack: error.stack, stack: error.stack,
}})]) }})])
}} }}

View File

@ -426,6 +426,21 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
if isinstance(v, ComputedVar) if isinstance(v, ComputedVar)
] ]
@classmethod
def _validate_module_name(cls) -> None:
"""Check if the module name is valid.
Reflex uses ___ as state name module separator.
Raises:
NameError: If the module name is invalid.
"""
if "___" in cls.__module__:
raise NameError(
"The module name of a State class cannot contain '___'. "
"Please rename the module."
)
@classmethod @classmethod
def __init_subclass__(cls, mixin: bool = False, **kwargs): def __init_subclass__(cls, mixin: bool = False, **kwargs):
"""Do some magic for the subclass initialization. """Do some magic for the subclass initialization.
@ -445,8 +460,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
if mixin: if mixin:
return return
# Validate the module name.
cls._validate_module_name()
# Event handlers should not shadow builtin state methods. # Event handlers should not shadow builtin state methods.
cls._check_overridden_methods() cls._check_overridden_methods()
# Computed vars should not shadow builtin state props. # Computed vars should not shadow builtin state props.
cls._check_overriden_basevars() cls._check_overriden_basevars()
@ -463,20 +482,22 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
cls.inherited_backend_vars = parent_state.backend_vars cls.inherited_backend_vars = parent_state.backend_vars
# Check if another substate class with the same name has already been defined. # Check if another substate class with the same name has already been defined.
if cls.__name__ in set(c.__name__ for c in parent_state.class_subclasses): if cls.get_name() in set(
c.get_name() for c in parent_state.class_subclasses
):
if is_testing_env(): if is_testing_env():
# Clear existing subclass with same name when app is reloaded via # Clear existing subclass with same name when app is reloaded via
# utils.prerequisites.get_app(reload=True) # utils.prerequisites.get_app(reload=True)
parent_state.class_subclasses = set( parent_state.class_subclasses = set(
c c
for c in parent_state.class_subclasses for c in parent_state.class_subclasses
if c.__name__ != cls.__name__ if c.get_name() != cls.get_name()
) )
else: else:
# During normal operation, subclasses cannot have the same name, even if they are # During normal operation, subclasses cannot have the same name, even if they are
# defined in different modules. # defined in different modules.
raise StateValueError( raise StateValueError(
f"The substate class '{cls.__name__}' has been defined multiple times. " f"The substate class '{cls.get_name()}' has been defined multiple times. "
"Shadowing substate classes is not allowed." "Shadowing substate classes is not allowed."
) )
# Track this new subclass in the parent state's subclasses set. # Track this new subclass in the parent state's subclasses set.
@ -759,7 +780,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
Returns: Returns:
The name of the state. The name of the state.
""" """
return format.to_snake_case(cls.__name__) module = cls.__module__.replace(".", "___")
return format.to_snake_case(f"{module}___{cls.__name__}")
@classmethod @classmethod
@functools.lru_cache() @functools.lru_cache()

View File

@ -40,6 +40,7 @@ import reflex
import reflex.reflex import reflex.reflex
import reflex.utils.build import reflex.utils.build
import reflex.utils.exec import reflex.utils.exec
import reflex.utils.format
import reflex.utils.prerequisites import reflex.utils.prerequisites
import reflex.utils.processes import reflex.utils.processes
from reflex.state import ( from reflex.state import (
@ -177,6 +178,33 @@ class AppHarness:
app_module_path=root / app_name / f"{app_name}.py", app_module_path=root / app_name / f"{app_name}.py",
) )
def get_state_name(self, state_cls_name: str) -> str:
"""Get the state name for the given state class name.
Args:
state_cls_name: The state class name
Returns:
The state name
"""
return reflex.utils.format.to_snake_case(
f"{self.app_name}___{self.app_name}___" + state_cls_name
)
def get_full_state_name(self, path: List[str]) -> str:
"""Get the full state name for the given state class name.
Args:
path: A list of state class names
Returns:
The full state name
"""
# NOTE: using State.get_name() somehow causes trouble here
# path = [State.get_name()] + [self.get_state_name(p) for p in path]
path = ["reflex___state____state"] + [self.get_state_name(p) for p in path]
return ".".join(path)
def _get_globals_from_signature(self, func: Any) -> dict[str, Any]: def _get_globals_from_signature(self, func: Any) -> dict[str, Any]:
"""Get the globals from a function or module object. """Get the globals from a function or module object.

View File

@ -58,14 +58,14 @@ def test_script_event_handler():
) )
render_dict = component.render() render_dict = component.render()
assert ( assert (
'onReady={(_e) => addEvents([Event("ev_state.on_ready", {})], (_e), {})}' f'onReady={{(_e) => addEvents([Event("{EvState.get_full_name()}.on_ready", {{}})], (_e), {{}})}}'
in render_dict["props"] in render_dict["props"]
) )
assert ( assert (
'onLoad={(_e) => addEvents([Event("ev_state.on_load", {})], (_e), {})}' f'onLoad={{(_e) => addEvents([Event("{EvState.get_full_name()}.on_load", {{}})], (_e), {{}})}}'
in render_dict["props"] in render_dict["props"]
) )
assert ( assert (
'onError={(_e) => addEvents([Event("ev_state.on_error", {})], (_e), {})}' f'onError={{(_e) => addEvents([Event("{EvState.get_full_name()}.on_error", {{}})], (_e), {{}})}}'
in render_dict["props"] in render_dict["props"]
) )

View File

@ -14,6 +14,9 @@ class ColorState(rx.State):
shade: int = 4 shade: int = 4
color_state_name = ColorState.get_full_name().replace(".", "__")
def create_color_var(color): def create_color_var(color):
return Var.create(color) return Var.create(color)
@ -26,27 +29,27 @@ def create_color_var(color):
(create_color_var(rx.color("mint", 3, True)), "var(--mint-a3)"), (create_color_var(rx.color("mint", 3, True)), "var(--mint-a3)"),
( (
create_color_var(rx.color(ColorState.color, ColorState.shade)), # type: ignore create_color_var(rx.color(ColorState.color, ColorState.shade)), # type: ignore
"var(--${state__color_state.color}-${state__color_state.shade})", f"var(--${{{color_state_name}.color}}-${{{color_state_name}.shade}})",
), ),
( (
create_color_var(rx.color(f"{ColorState.color}", f"{ColorState.shade}")), # type: ignore create_color_var(rx.color(f"{ColorState.color}", f"{ColorState.shade}")), # type: ignore
"var(--${state__color_state.color}-${state__color_state.shade})", f"var(--${{{color_state_name}.color}}-${{{color_state_name}.shade}})",
), ),
( (
create_color_var( create_color_var(
rx.color(f"{ColorState.color_part}ato", f"{ColorState.shade}") # type: ignore rx.color(f"{ColorState.color_part}ato", f"{ColorState.shade}") # type: ignore
), ),
"var(--${state__color_state.color_part}ato-${state__color_state.shade})", f"var(--${{{color_state_name}.color_part}}ato-${{{color_state_name}.shade}})",
), ),
( (
create_color_var(f'{rx.color(ColorState.color, f"{ColorState.shade}")}'), # type: ignore create_color_var(f'{rx.color(ColorState.color, f"{ColorState.shade}")}'), # type: ignore
"var(--${state__color_state.color}-${state__color_state.shade})", f"var(--${{{color_state_name}.color}}-${{{color_state_name}.shade}})",
), ),
( (
create_color_var( create_color_var(
f'{rx.color(f"{ColorState.color}", f"{ColorState.shade}")}' # type: ignore f'{rx.color(f"{ColorState.color}", f"{ColorState.shade}")}' # type: ignore
), ),
"var(--${state__color_state.color}-${state__color_state.shade})", f"var(--${{{color_state_name}.color}}-${{{color_state_name}.shade}})",
), ),
], ],
) )
@ -68,7 +71,7 @@ def test_color(color, expected):
), ),
( (
rx.cond(True, rx.color(ColorState.color), rx.color(ColorState.color, 5)), # type: ignore rx.cond(True, rx.color(ColorState.color), rx.color(ColorState.color, 5)), # type: ignore
"{isTrue(true) ? `var(--${state__color_state.color}-7)` : `var(--${state__color_state.color}-5)`}", f"{{isTrue(true) ? `var(--${{{color_state_name}.color}}-7)` : `var(--${{{color_state_name}.color}}-5)`}}",
), ),
( (
rx.match( rx.match(
@ -79,7 +82,7 @@ def test_color(color, expected):
), ),
"{(() => { switch (JSON.stringify(`condition`)) {case JSON.stringify(`first`): return (`var(--mint-7)`);" "{(() => { switch (JSON.stringify(`condition`)) {case JSON.stringify(`first`): return (`var(--mint-7)`);"
" break;case JSON.stringify(`second`): return (`var(--tomato-5)`); break;default: " " break;case JSON.stringify(`second`): return (`var(--tomato-5)`); break;default: "
"return (`var(--${state__color_state.color}-2)`); break;};})()}", f"return (`var(--${{{color_state_name}.color}}-2)`); break;}};}})()}}",
), ),
( (
rx.match( rx.match(
@ -89,9 +92,9 @@ def test_color(color, expected):
rx.color(ColorState.color, 2), # type: ignore rx.color(ColorState.color, 2), # type: ignore
), ),
"{(() => { switch (JSON.stringify(`condition`)) {case JSON.stringify(`first`): " "{(() => { switch (JSON.stringify(`condition`)) {case JSON.stringify(`first`): "
"return (`var(--${state__color_state.color}-7)`); break;case JSON.stringify(`second`): " f"return (`var(--${{{color_state_name}.color}}-7)`); break;case JSON.stringify(`second`): "
"return (`var(--${state__color_state.color}-5)`); break;default: " f"return (`var(--${{{color_state_name}.color}}-5)`); break;default: "
"return (`var(--${state__color_state.color}-2)`); break;};})()}", f"return (`var(--${{{color_state_name}.color}}-2)`); break;}};}})()}}",
), ),
], ],
) )

View File

@ -34,7 +34,7 @@ def test_f_string_cond_interpolation():
], ],
indirect=True, indirect=True,
) )
def test_validate_cond(cond_state: Var): def test_validate_cond(cond_state: BaseState):
"""Test if cond can be a rx.Var with any values. """Test if cond can be a rx.Var with any values.
Args: Args:
@ -49,7 +49,7 @@ def test_validate_cond(cond_state: Var):
assert cond_dict["name"] == "Fragment" assert cond_dict["name"] == "Fragment"
[condition] = cond_dict["children"] [condition] = cond_dict["children"]
assert condition["cond_state"] == "isTrue(cond_state.value)" assert condition["cond_state"] == f"isTrue({cond_state.get_full_name()}.value)"
# true value # true value
true_value = condition["true_value"] true_value = condition["true_value"]

View File

@ -134,7 +134,7 @@ seen_index_vars = set()
ForEachState.colors_list, ForEachState.colors_list,
display_color, display_color,
{ {
"iterable_state": "for_each_state.colors_list", "iterable_state": f"{ForEachState.get_full_name()}.colors_list",
"iterable_type": "list", "iterable_type": "list",
}, },
), ),
@ -142,7 +142,7 @@ seen_index_vars = set()
ForEachState.colors_dict_list, ForEachState.colors_dict_list,
display_color_name, display_color_name,
{ {
"iterable_state": "for_each_state.colors_dict_list", "iterable_state": f"{ForEachState.get_full_name()}.colors_dict_list",
"iterable_type": "list", "iterable_type": "list",
}, },
), ),
@ -150,7 +150,7 @@ seen_index_vars = set()
ForEachState.colors_nested_dict_list, ForEachState.colors_nested_dict_list,
display_shade, display_shade,
{ {
"iterable_state": "for_each_state.colors_nested_dict_list", "iterable_state": f"{ForEachState.get_full_name()}.colors_nested_dict_list",
"iterable_type": "list", "iterable_type": "list",
}, },
), ),
@ -158,7 +158,7 @@ seen_index_vars = set()
ForEachState.primary_color, ForEachState.primary_color,
display_primary_colors, display_primary_colors,
{ {
"iterable_state": "for_each_state.primary_color", "iterable_state": f"{ForEachState.get_full_name()}.primary_color",
"iterable_type": "dict", "iterable_type": "dict",
}, },
), ),
@ -166,7 +166,7 @@ seen_index_vars = set()
ForEachState.color_with_shades, ForEachState.color_with_shades,
display_color_with_shades, display_color_with_shades,
{ {
"iterable_state": "for_each_state.color_with_shades", "iterable_state": f"{ForEachState.get_full_name()}.color_with_shades",
"iterable_type": "dict", "iterable_type": "dict",
}, },
), ),
@ -174,7 +174,7 @@ seen_index_vars = set()
ForEachState.nested_colors_with_shades, ForEachState.nested_colors_with_shades,
display_nested_color_with_shades, display_nested_color_with_shades,
{ {
"iterable_state": "for_each_state.nested_colors_with_shades", "iterable_state": f"{ForEachState.get_full_name()}.nested_colors_with_shades",
"iterable_type": "dict", "iterable_type": "dict",
}, },
), ),
@ -182,7 +182,7 @@ seen_index_vars = set()
ForEachState.nested_colors_with_shades, ForEachState.nested_colors_with_shades,
display_nested_color_with_shades_v2, display_nested_color_with_shades_v2,
{ {
"iterable_state": "for_each_state.nested_colors_with_shades", "iterable_state": f"{ForEachState.get_full_name()}.nested_colors_with_shades",
"iterable_type": "dict", "iterable_type": "dict",
}, },
), ),
@ -190,7 +190,7 @@ seen_index_vars = set()
ForEachState.color_tuple, ForEachState.color_tuple,
display_color_tuple, display_color_tuple,
{ {
"iterable_state": "for_each_state.color_tuple", "iterable_state": f"{ForEachState.get_full_name()}.color_tuple",
"iterable_type": "tuple", "iterable_type": "tuple",
}, },
), ),
@ -198,7 +198,7 @@ seen_index_vars = set()
ForEachState.colors_set, ForEachState.colors_set,
display_colors_set, display_colors_set,
{ {
"iterable_state": "for_each_state.colors_set", "iterable_state": f"{ForEachState.get_full_name()}.colors_set",
"iterable_type": "set", "iterable_type": "set",
}, },
), ),
@ -206,7 +206,7 @@ seen_index_vars = set()
ForEachState.nested_colors_list, ForEachState.nested_colors_list,
lambda el, i: display_nested_list_element(el, i), lambda el, i: display_nested_list_element(el, i),
{ {
"iterable_state": "for_each_state.nested_colors_list", "iterable_state": f"{ForEachState.get_full_name()}.nested_colors_list",
"iterable_type": "list", "iterable_type": "list",
}, },
), ),
@ -214,7 +214,7 @@ seen_index_vars = set()
ForEachState.color_index_tuple, ForEachState.color_index_tuple,
display_color_index_tuple, display_color_index_tuple,
{ {
"iterable_state": "for_each_state.color_index_tuple", "iterable_state": f"{ForEachState.get_full_name()}.color_index_tuple",
"iterable_type": "tuple", "iterable_type": "tuple",
}, },
), ),

View File

@ -35,7 +35,7 @@ def test_match_components():
[match_child] = match_dict["children"] [match_child] = match_dict["children"]
assert match_child["name"] == "match" assert match_child["name"] == "match"
assert str(match_child["cond"]) == "{match_state.value}" assert str(match_child["cond"]) == f"{{{MatchState.get_name()}.value}}"
match_cases = match_child["match_cases"] match_cases = match_child["match_cases"]
assert len(match_cases) == 6 assert len(match_cases) == 6
@ -72,7 +72,7 @@ def test_match_components():
assert fifth_return_value_render["name"] == "RadixThemesText" assert fifth_return_value_render["name"] == "RadixThemesText"
assert fifth_return_value_render["children"][0]["contents"] == "{`fifth value`}" assert fifth_return_value_render["children"][0]["contents"] == "{`fifth value`}"
assert match_cases[5][0]._var_name == "((match_state.num) + (1))" assert match_cases[5][0]._var_name == f"(({MatchState.get_name()}.num) + (1))"
assert match_cases[5][0]._var_type == int assert match_cases[5][0]._var_type == int
fifth_return_value_render = match_cases[5][1].render() fifth_return_value_render = match_cases[5][1].render()
assert fifth_return_value_render["name"] == "RadixThemesText" assert fifth_return_value_render["name"] == "RadixThemesText"
@ -99,11 +99,11 @@ def test_match_components():
(MatchState.string, f"{MatchState.value} - string"), (MatchState.string, f"{MatchState.value} - string"),
"default value", "default value",
), ),
"(() => { switch (JSON.stringify(match_state.value)) {case JSON.stringify(1): return (`first`); break;case JSON.stringify(2): case JSON.stringify(3): return " f"(() => {{ switch (JSON.stringify({MatchState.get_name()}.value)) {{case JSON.stringify(1): return (`first`); break;case JSON.stringify(2): case JSON.stringify(3): return "
"(`second value`); break;case JSON.stringify([1, 2]): return (`third-value`); break;case JSON.stringify(`random`): " "(`second value`); break;case JSON.stringify([1, 2]): return (`third-value`); break;case JSON.stringify(`random`): "
'return (`fourth_value`); break;case JSON.stringify({"foo": "bar"}): return (`fifth value`); ' 'return (`fourth_value`); break;case JSON.stringify({"foo": "bar"}): return (`fifth value`); '
"break;case JSON.stringify(((match_state.num) + (1))): return (`sixth value`); break;case JSON.stringify(`${match_state.value} - string`): " f"break;case JSON.stringify((({MatchState.get_name()}.num) + (1))): return (`sixth value`); break;case JSON.stringify(`${{{MatchState.get_name()}.value}} - string`): "
"return (match_state.string); break;case JSON.stringify(match_state.string): return (`${match_state.value} - string`); break;default: " f"return ({MatchState.get_name()}.string); break;case JSON.stringify({MatchState.get_name()}.string): return (`${{{MatchState.get_name()}.value}} - string`); break;default: "
"return (`default value`); break;};})()", "return (`default value`); break;};})()",
), ),
( (
@ -118,12 +118,12 @@ def test_match_components():
(MatchState.string, f"{MatchState.value} - string"), (MatchState.string, f"{MatchState.value} - string"),
MatchState.string, MatchState.string,
), ),
"(() => { switch (JSON.stringify(match_state.value)) {case JSON.stringify(1): return (`first`); break;case JSON.stringify(2): case JSON.stringify(3): return " f"(() => {{ switch (JSON.stringify({MatchState.get_name()}.value)) {{case JSON.stringify(1): return (`first`); break;case JSON.stringify(2): case JSON.stringify(3): return "
"(`second value`); break;case JSON.stringify([1, 2]): return (`third-value`); break;case JSON.stringify(`random`): " "(`second value`); break;case JSON.stringify([1, 2]): return (`third-value`); break;case JSON.stringify(`random`): "
'return (`fourth_value`); break;case JSON.stringify({"foo": "bar"}): return (`fifth value`); ' 'return (`fourth_value`); break;case JSON.stringify({"foo": "bar"}): return (`fifth value`); '
"break;case JSON.stringify(((match_state.num) + (1))): return (`sixth value`); break;case JSON.stringify(`${match_state.value} - string`): " f"break;case JSON.stringify((({MatchState.get_name()}.num) + (1))): return (`sixth value`); break;case JSON.stringify(`${{{MatchState.get_name()}.value}} - string`): "
"return (match_state.string); break;case JSON.stringify(match_state.string): return (`${match_state.value} - string`); break;default: " f"return ({MatchState.get_name()}.string); break;case JSON.stringify({MatchState.get_name()}.string): return (`${{{MatchState.get_name()}.value}} - string`); break;default: "
"return (match_state.string); break;};})()", f"return ({MatchState.get_name()}.string); break;}};}})()",
), ),
], ],
) )

View File

@ -16,14 +16,14 @@ from reflex.utils.serializers import serialize, serialize_dataframe
[["foo", "bar"], ["foo1", "bar1"]], columns=["column1", "column2"] [["foo", "bar"], ["foo1", "bar1"]], columns=["column1", "column2"]
) )
}, },
"data_table_state.data", "data",
), ),
pytest.param({"data": ["foo", "bar"]}, "data_table_state"), pytest.param({"data": ["foo", "bar"]}, ""),
pytest.param({"data": [["foo", "bar"], ["foo1", "bar1"]]}, "data_table_state"), pytest.param({"data": [["foo", "bar"], ["foo1", "bar1"]]}, ""),
], ],
indirect=["data_table_state"], indirect=["data_table_state"],
) )
def test_validate_data_table(data_table_state: rx.Var, expected): def test_validate_data_table(data_table_state: rx.State, expected):
"""Test the str/render function. """Test the str/render function.
Args: Args:
@ -40,6 +40,10 @@ def test_validate_data_table(data_table_state: rx.Var, expected):
data_table_dict = data_table_component.render() data_table_dict = data_table_component.render()
# prefix expected with state name
state_name = data_table_state.get_name()
expected = f"{state_name}.{expected}" if expected else state_name
assert data_table_dict["props"] == [ assert data_table_dict["props"] == [
f"columns={{{expected}.columns}}", f"columns={{{expected}.columns}}",
f"data={{{expected}.data}}", f"data={{{expected}.data}}",

View File

@ -825,7 +825,7 @@ def test_component_event_trigger_arbitrary_args():
assert comp.render()["props"][0] == ( assert comp.render()["props"][0] == (
"onFoo={(__e,_alpha,_bravo,_charlie) => addEvents(" "onFoo={(__e,_alpha,_bravo,_charlie) => addEvents("
'[Event("c1_state.mock_handler", {_e:__e.target.value,_bravo:_bravo["nested"],_charlie:((_charlie.custom) + (42))})], ' f'[Event("{C1State.get_full_name()}.mock_handler", {{_e:__e.target.value,_bravo:_bravo["nested"],_charlie:((_charlie.custom) + (42))}})], '
"(__e,_alpha,_bravo,_charlie), {})}" "(__e,_alpha,_bravo,_charlie), {})}"
) )
@ -2037,14 +2037,14 @@ def test_add_style_embedded_vars(test_state: BaseState):
page._add_style_recursive(Style()) page._add_style_recursive(Style())
assert ( assert (
"const test_state = useContext(StateContexts.test_state)" f"const {test_state.get_name()} = useContext(StateContexts.{test_state.get_name()})"
in page._get_all_hooks_internal() in page._get_all_hooks_internal()
) )
assert "useText" in page._get_all_hooks_internal() assert "useText" in page._get_all_hooks_internal()
assert "useParent" in page._get_all_hooks_internal() assert "useParent" in page._get_all_hooks_internal()
assert ( assert (
str(page).count( str(page).count(
'css={{"fakeParent": "parent", "color": "var(--plum-10)", "fake": "text", "margin": `${test_state.num}%`}}' f'css={{{{"fakeParent": "parent", "color": "var(--plum-10)", "fake": "text", "margin": `${{{test_state.get_name()}.num}}%`}}}}'
) )
== 1 == 1
) )

View File

@ -1,6 +1,7 @@
import pytest import pytest
from reflex.event import Event from reflex.event import Event
from reflex.state import State
def create_event(name): def create_event(name):
@ -21,4 +22,4 @@ def create_event(name):
@pytest.fixture @pytest.fixture
def event1(): def event1():
return create_event("state.hydrate") return create_event(f"{State.get_name()}.hydrate")

View File

@ -485,20 +485,12 @@ async def test_dynamic_var_event(test_state: Type[ATestState], token: str):
pytest.param( pytest.param(
[ [
( (
"list_mutation_test_state.make_friend", "make_friend",
{ {"plain_friends": ["Tommy", "another-fd"]},
"list_mutation_test_state": {
"plain_friends": ["Tommy", "another-fd"]
}
},
), ),
( (
"list_mutation_test_state.change_first_friend", "change_first_friend",
{ {"plain_friends": ["Jenny", "another-fd"]},
"list_mutation_test_state": {
"plain_friends": ["Jenny", "another-fd"]
}
},
), ),
], ],
id="append then __setitem__", id="append then __setitem__",
@ -506,12 +498,12 @@ async def test_dynamic_var_event(test_state: Type[ATestState], token: str):
pytest.param( pytest.param(
[ [
( (
"list_mutation_test_state.unfriend_first_friend", "unfriend_first_friend",
{"list_mutation_test_state": {"plain_friends": []}}, {"plain_friends": []},
), ),
( (
"list_mutation_test_state.make_friend", "make_friend",
{"list_mutation_test_state": {"plain_friends": ["another-fd"]}}, {"plain_friends": ["another-fd"]},
), ),
], ],
id="delitem then append", id="delitem then append",
@ -519,24 +511,20 @@ async def test_dynamic_var_event(test_state: Type[ATestState], token: str):
pytest.param( pytest.param(
[ [
( (
"list_mutation_test_state.make_friends_with_colleagues", "make_friends_with_colleagues",
{ {"plain_friends": ["Tommy", "Peter", "Jimmy"]},
"list_mutation_test_state": {
"plain_friends": ["Tommy", "Peter", "Jimmy"]
}
},
), ),
( (
"list_mutation_test_state.remove_tommy", "remove_tommy",
{"list_mutation_test_state": {"plain_friends": ["Peter", "Jimmy"]}}, {"plain_friends": ["Peter", "Jimmy"]},
), ),
( (
"list_mutation_test_state.remove_last_friend", "remove_last_friend",
{"list_mutation_test_state": {"plain_friends": ["Peter"]}}, {"plain_friends": ["Peter"]},
), ),
( (
"list_mutation_test_state.unfriend_all_friends", "unfriend_all_friends",
{"list_mutation_test_state": {"plain_friends": []}}, {"plain_friends": []},
), ),
], ],
id="extend, remove, pop, clear", id="extend, remove, pop, clear",
@ -544,28 +532,16 @@ async def test_dynamic_var_event(test_state: Type[ATestState], token: str):
pytest.param( pytest.param(
[ [
( (
"list_mutation_test_state.add_jimmy_to_second_group", "add_jimmy_to_second_group",
{ {"friends_in_nested_list": [["Tommy"], ["Jenny", "Jimmy"]]},
"list_mutation_test_state": {
"friends_in_nested_list": [["Tommy"], ["Jenny", "Jimmy"]]
}
},
), ),
( (
"list_mutation_test_state.remove_first_person_from_first_group", "remove_first_person_from_first_group",
{ {"friends_in_nested_list": [[], ["Jenny", "Jimmy"]]},
"list_mutation_test_state": {
"friends_in_nested_list": [[], ["Jenny", "Jimmy"]]
}
},
), ),
( (
"list_mutation_test_state.remove_first_group", "remove_first_group",
{ {"friends_in_nested_list": [["Jenny", "Jimmy"]]},
"list_mutation_test_state": {
"friends_in_nested_list": [["Jenny", "Jimmy"]]
}
},
), ),
], ],
id="nested list", id="nested list",
@ -573,24 +549,16 @@ async def test_dynamic_var_event(test_state: Type[ATestState], token: str):
pytest.param( pytest.param(
[ [
( (
"list_mutation_test_state.add_jimmy_to_tommy_friends", "add_jimmy_to_tommy_friends",
{ {"friends_in_dict": {"Tommy": ["Jenny", "Jimmy"]}},
"list_mutation_test_state": {
"friends_in_dict": {"Tommy": ["Jenny", "Jimmy"]}
}
},
), ),
( (
"list_mutation_test_state.remove_jenny_from_tommy", "remove_jenny_from_tommy",
{ {"friends_in_dict": {"Tommy": ["Jimmy"]}},
"list_mutation_test_state": {
"friends_in_dict": {"Tommy": ["Jimmy"]}
}
},
), ),
( (
"list_mutation_test_state.tommy_has_no_fds", "tommy_has_no_fds",
{"list_mutation_test_state": {"friends_in_dict": {"Tommy": []}}}, {"friends_in_dict": {"Tommy": []}},
), ),
], ],
id="list in dict", id="list in dict",
@ -614,12 +582,14 @@ async def test_list_mutation_detection__plain_list(
result = await list_mutation_state._process( result = await list_mutation_state._process(
Event( Event(
token=token, token=token,
name=event_name, name=f"{list_mutation_state.get_name()}.{event_name}",
router_data={"pathname": "/", "query": {}}, router_data={"pathname": "/", "query": {}},
payload={}, payload={},
) )
).__anext__() ).__anext__()
# prefix keys in expected_delta with the state name
expected_delta = {list_mutation_state.get_name(): expected_delta}
assert result.delta == expected_delta assert result.delta == expected_delta
@ -630,24 +600,16 @@ async def test_list_mutation_detection__plain_list(
pytest.param( pytest.param(
[ [
( (
"dict_mutation_test_state.add_age", "add_age",
{ {"details": {"name": "Tommy", "age": 20}},
"dict_mutation_test_state": {
"details": {"name": "Tommy", "age": 20}
}
},
), ),
( (
"dict_mutation_test_state.change_name", "change_name",
{ {"details": {"name": "Jenny", "age": 20}},
"dict_mutation_test_state": {
"details": {"name": "Jenny", "age": 20}
}
},
), ),
( (
"dict_mutation_test_state.remove_last_detail", "remove_last_detail",
{"dict_mutation_test_state": {"details": {"name": "Jenny"}}}, {"details": {"name": "Jenny"}},
), ),
], ],
id="update then __setitem__", id="update then __setitem__",
@ -655,12 +617,12 @@ async def test_list_mutation_detection__plain_list(
pytest.param( pytest.param(
[ [
( (
"dict_mutation_test_state.clear_details", "clear_details",
{"dict_mutation_test_state": {"details": {}}}, {"details": {}},
), ),
( (
"dict_mutation_test_state.add_age", "add_age",
{"dict_mutation_test_state": {"details": {"age": 20}}}, {"details": {"age": 20}},
), ),
], ],
id="delitem then update", id="delitem then update",
@ -668,20 +630,16 @@ async def test_list_mutation_detection__plain_list(
pytest.param( pytest.param(
[ [
( (
"dict_mutation_test_state.add_age", "add_age",
{ {"details": {"name": "Tommy", "age": 20}},
"dict_mutation_test_state": {
"details": {"name": "Tommy", "age": 20}
}
},
), ),
( (
"dict_mutation_test_state.remove_name", "remove_name",
{"dict_mutation_test_state": {"details": {"age": 20}}}, {"details": {"age": 20}},
), ),
( (
"dict_mutation_test_state.pop_out_age", "pop_out_age",
{"dict_mutation_test_state": {"details": {}}}, {"details": {}},
), ),
], ],
id="add, remove, pop", id="add, remove, pop",
@ -689,22 +647,16 @@ async def test_list_mutation_detection__plain_list(
pytest.param( pytest.param(
[ [
( (
"dict_mutation_test_state.remove_home_address", "remove_home_address",
{ {"address": [{}, {"work": "work address"}]},
"dict_mutation_test_state": {
"address": [{}, {"work": "work address"}]
}
},
), ),
( (
"dict_mutation_test_state.add_street_to_home_address", "add_street_to_home_address",
{ {
"dict_mutation_test_state": { "address": [
"address": [ {"street": "street address"},
{"street": "street address"}, {"work": "work address"},
{"work": "work address"}, ]
]
}
}, },
), ),
], ],
@ -713,34 +665,26 @@ async def test_list_mutation_detection__plain_list(
pytest.param( pytest.param(
[ [
( (
"dict_mutation_test_state.change_friend_name", "change_friend_name",
{ {
"dict_mutation_test_state": { "friend_in_nested_dict": {
"friend_in_nested_dict": { "name": "Nikhil",
"name": "Nikhil", "friend": {"name": "Tommy"},
"friend": {"name": "Tommy"},
}
} }
}, },
), ),
( (
"dict_mutation_test_state.add_friend_age", "add_friend_age",
{ {
"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},
}
} }
}, },
), ),
( (
"dict_mutation_test_state.remove_friend", "remove_friend",
{ {"friend_in_nested_dict": {"name": "Nikhil"}},
"dict_mutation_test_state": {
"friend_in_nested_dict": {"name": "Nikhil"}
}
},
), ),
], ],
id="nested dict", id="nested dict",
@ -764,12 +708,15 @@ async def test_dict_mutation_detection__plain_list(
result = await dict_mutation_state._process( result = await dict_mutation_state._process(
Event( Event(
token=token, token=token,
name=event_name, name=f"{dict_mutation_state.get_name()}.{event_name}",
router_data={"pathname": "/", "query": {}}, router_data={"pathname": "/", "query": {}},
payload={}, payload={},
) )
).__anext__() ).__anext__()
# prefix keys in expected_delta with the state name
expected_delta = {dict_mutation_state.get_name(): expected_delta}
assert result.delta == expected_delta assert result.delta == expected_delta
@ -779,12 +726,16 @@ async def test_dict_mutation_detection__plain_list(
[ [
( (
FileUploadState, FileUploadState,
{"state.file_upload_state": {"img_list": ["image1.jpg", "image2.jpg"]}}, {
FileUploadState.get_full_name(): {
"img_list": ["image1.jpg", "image2.jpg"]
}
},
), ),
( (
ChildFileUploadState, ChildFileUploadState,
{ {
"state.file_state_base1.child_file_upload_state": { ChildFileUploadState.get_full_name(): {
"img_list": ["image1.jpg", "image2.jpg"] "img_list": ["image1.jpg", "image2.jpg"]
} }
}, },
@ -792,7 +743,7 @@ async def test_dict_mutation_detection__plain_list(
( (
GrandChildFileUploadState, GrandChildFileUploadState,
{ {
"state.file_state_base1.file_state_base2.grand_child_file_upload_state": { GrandChildFileUploadState.get_full_name(): {
"img_list": ["image1.jpg", "image2.jpg"] "img_list": ["image1.jpg", "image2.jpg"]
} }
}, },
@ -1065,7 +1016,7 @@ async def test_dynamic_route_var_route_change_completed_on_load(
val=exp_val, val=exp_val,
), ),
_event( _event(
name="state.set_is_hydrated", name=f"{State.get_name()}.set_is_hydrated",
payload={"value": True}, payload={"value": True},
val=exp_val, val=exp_val,
router_data={}, router_data={},
@ -1188,7 +1139,10 @@ async def test_process_events(mocker, token: str):
app = App(state=GenState) 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=f"{GenState.get_name()}.go",
payload={"c": 5},
router_data=router_data,
) )
async for _update in process(app, event, "mock_sid", {}, "127.0.0.1"): async for _update in process(app, event, "mock_sid", {}, "127.0.0.1"):

View File

@ -217,7 +217,7 @@ def child_state(test_state) -> ChildState:
Returns: Returns:
A test child state. A test child state.
""" """
child_state = test_state.get_substate(["child_state"]) child_state = test_state.get_substate([ChildState.get_name()])
assert child_state is not None assert child_state is not None
return child_state return child_state
@ -232,7 +232,7 @@ def child_state2(test_state) -> ChildState2:
Returns: Returns:
A second test child state. A second test child state.
""" """
child_state2 = test_state.get_substate(["child_state2"]) child_state2 = test_state.get_substate([ChildState2.get_name()])
assert child_state2 is not None assert child_state2 is not None
return child_state2 return child_state2
@ -247,7 +247,7 @@ def grandchild_state(child_state) -> GrandchildState:
Returns: Returns:
A test state. A test state.
""" """
grandchild_state = child_state.get_substate(["grandchild_state"]) grandchild_state = child_state.get_substate([GrandchildState.get_name()])
assert grandchild_state is not None assert grandchild_state is not None
return grandchild_state return grandchild_state
@ -357,20 +357,20 @@ def test_computed_vars(test_state):
assert test_state.upper == "HELLO WORLD" assert test_state.upper == "HELLO WORLD"
def test_dict(test_state): def test_dict(test_state: TestState):
"""Test that the dict representation of a state is correct. """Test that the dict representation of a state is correct.
Args: Args:
test_state: A state. test_state: A state.
""" """
substates = { substates = {
"test_state", test_state.get_full_name(),
"test_state.child_state", ChildState.get_full_name(),
"test_state.child_state.grandchild_state", GrandchildState.get_full_name(),
"test_state.child_state2", ChildState2.get_full_name(),
"test_state.child_state2.grandchild_state2", GrandchildState2.get_full_name(),
"test_state.child_state3", ChildState3.get_full_name(),
"test_state.child_state3.grandchild_state3", GrandchildState3.get_full_name(),
} }
test_state_dict = test_state.dict() test_state_dict = test_state.dict()
assert set(test_state_dict) == substates assert set(test_state_dict) == substates
@ -394,22 +394,30 @@ def test_default_setters(test_state):
def test_class_indexing_with_vars(): def test_class_indexing_with_vars():
"""Test that we can index into a state var with another var.""" """Test that we can index into a state var with another var."""
prop = TestState.array[TestState.num1] prop = TestState.array[TestState.num1]
assert str(prop) == "{test_state.array.at(test_state.num1)}" assert (
str(prop) == f"{{{TestState.get_name()}.array.at({TestState.get_name()}.num1)}}"
)
prop = TestState.mapping["a"][TestState.num1] prop = TestState.mapping["a"][TestState.num1]
assert str(prop) == '{test_state.mapping["a"].at(test_state.num1)}' assert (
str(prop)
== f'{{{TestState.get_name()}.mapping["a"].at({TestState.get_name()}.num1)}}'
)
prop = TestState.mapping[TestState.map_key] prop = TestState.mapping[TestState.map_key]
assert str(prop) == "{test_state.mapping[test_state.map_key]}" assert (
str(prop)
== f"{{{TestState.get_name()}.mapping[{TestState.get_name()}.map_key]}}"
)
def test_class_attributes(): def test_class_attributes():
"""Test that we can get class attributes.""" """Test that we can get class attributes."""
prop = TestState.obj.prop1 prop = TestState.obj.prop1
assert str(prop) == "{test_state.obj.prop1}" assert str(prop) == f"{{{TestState.get_name()}.obj.prop1}}"
prop = TestState.complex[1].prop1 prop = TestState.complex[1].prop1
assert str(prop) == "{test_state.complex[1].prop1}" assert str(prop) == f"{{{TestState.get_name()}.complex[1].prop1}}"
def test_get_parent_state(): def test_get_parent_state():
@ -431,27 +439,40 @@ def test_get_substates():
def test_get_name(): def test_get_name():
"""Test getting the name of a state.""" """Test getting the name of a state."""
assert TestState.get_name() == "test_state" assert TestState.get_name() == "tests___test_state____test_state"
assert ChildState.get_name() == "child_state" assert ChildState.get_name() == "tests___test_state____child_state"
assert ChildState2.get_name() == "child_state2" assert ChildState2.get_name() == "tests___test_state____child_state2"
assert GrandchildState.get_name() == "grandchild_state" assert GrandchildState.get_name() == "tests___test_state____grandchild_state"
def test_get_full_name(): def test_get_full_name():
"""Test getting the full name.""" """Test getting the full name."""
assert TestState.get_full_name() == "test_state" assert TestState.get_full_name() == "tests___test_state____test_state"
assert ChildState.get_full_name() == "test_state.child_state" assert (
assert ChildState2.get_full_name() == "test_state.child_state2" ChildState.get_full_name()
assert GrandchildState.get_full_name() == "test_state.child_state.grandchild_state" == "tests___test_state____test_state.tests___test_state____child_state"
)
assert (
ChildState2.get_full_name()
== "tests___test_state____test_state.tests___test_state____child_state2"
)
assert (
GrandchildState.get_full_name()
== "tests___test_state____test_state.tests___test_state____child_state.tests___test_state____grandchild_state"
)
def test_get_class_substate(): def test_get_class_substate():
"""Test getting the substate of a class.""" """Test getting the substate of a class."""
assert TestState.get_class_substate(("child_state",)) == ChildState assert TestState.get_class_substate((ChildState.get_name(),)) == ChildState
assert TestState.get_class_substate(("child_state2",)) == ChildState2 assert TestState.get_class_substate((ChildState2.get_name(),)) == ChildState2
assert ChildState.get_class_substate(("grandchild_state",)) == GrandchildState
assert ( assert (
TestState.get_class_substate(("child_state", "grandchild_state")) ChildState.get_class_substate((GrandchildState.get_name(),)) == GrandchildState
)
assert (
TestState.get_class_substate(
(ChildState.get_name(), GrandchildState.get_name())
)
== GrandchildState == GrandchildState
) )
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -459,7 +480,7 @@ def test_get_class_substate():
with pytest.raises(ValueError): with pytest.raises(ValueError):
TestState.get_class_substate( TestState.get_class_substate(
( (
"child_state", ChildState.get_name(),
"invalid_child", "invalid_child",
) )
) )
@ -471,13 +492,15 @@ def test_get_class_var():
assert TestState.get_class_var(("num2",)).equals(TestState.num2) assert TestState.get_class_var(("num2",)).equals(TestState.num2)
assert ChildState.get_class_var(("value",)).equals(ChildState.value) assert ChildState.get_class_var(("value",)).equals(ChildState.value)
assert GrandchildState.get_class_var(("value2",)).equals(GrandchildState.value2) assert GrandchildState.get_class_var(("value2",)).equals(GrandchildState.value2)
assert TestState.get_class_var(("child_state", "value")).equals(ChildState.value) assert TestState.get_class_var((ChildState.get_name(), "value")).equals(
ChildState.value
)
assert TestState.get_class_var( assert TestState.get_class_var(
("child_state", "grandchild_state", "value2") (ChildState.get_name(), GrandchildState.get_name(), "value2")
).equals( ).equals(
GrandchildState.value2, GrandchildState.value2,
) )
assert ChildState.get_class_var(("grandchild_state", "value2")).equals( assert ChildState.get_class_var((GrandchildState.get_name(), "value2")).equals(
GrandchildState.value2, GrandchildState.value2,
) )
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -485,7 +508,7 @@ def test_get_class_var():
with pytest.raises(ValueError): with pytest.raises(ValueError):
TestState.get_class_var( TestState.get_class_var(
( (
"child_state", ChildState.get_name(),
"invalid_var", "invalid_var",
) )
) )
@ -513,11 +536,15 @@ def test_set_parent_and_substates(test_state, child_state, grandchild_state):
grandchild_state: A grandchild state. grandchild_state: A grandchild state.
""" """
assert len(test_state.substates) == 3 assert len(test_state.substates) == 3
assert set(test_state.substates) == {"child_state", "child_state2", "child_state3"} assert set(test_state.substates) == {
ChildState.get_name(),
ChildState2.get_name(),
ChildState3.get_name(),
}
assert child_state.parent_state == test_state assert child_state.parent_state == test_state
assert len(child_state.substates) == 1 assert len(child_state.substates) == 1
assert set(child_state.substates) == {"grandchild_state"} assert set(child_state.substates) == {GrandchildState.get_name()}
assert grandchild_state.parent_state == child_state assert grandchild_state.parent_state == child_state
assert len(grandchild_state.substates) == 0 assert len(grandchild_state.substates) == 0
@ -584,18 +611,21 @@ def test_get_substate(test_state, child_state, child_state2, grandchild_state):
child_state2: A child state. child_state2: A child state.
grandchild_state: A grandchild state. grandchild_state: A grandchild state.
""" """
assert test_state.get_substate(("child_state",)) == child_state assert test_state.get_substate((ChildState.get_name(),)) == child_state
assert test_state.get_substate(("child_state2",)) == child_state2 assert test_state.get_substate((ChildState2.get_name(),)) == child_state2
assert ( assert (
test_state.get_substate(("child_state", "grandchild_state")) == grandchild_state test_state.get_substate((ChildState.get_name(), GrandchildState.get_name()))
== grandchild_state
) )
assert child_state.get_substate(("grandchild_state",)) == grandchild_state assert child_state.get_substate((GrandchildState.get_name(),)) == grandchild_state
with pytest.raises(ValueError): with pytest.raises(ValueError):
test_state.get_substate(("invalid",)) test_state.get_substate(("invalid",))
with pytest.raises(ValueError): with pytest.raises(ValueError):
test_state.get_substate(("child_state", "invalid")) test_state.get_substate((ChildState.get_name(), "invalid"))
with pytest.raises(ValueError): with pytest.raises(ValueError):
test_state.get_substate(("child_state", "grandchild_state", "invalid")) test_state.get_substate(
(ChildState.get_name(), GrandchildState.get_name(), "invalid")
)
def test_set_dirty_var(test_state): def test_set_dirty_var(test_state):
@ -638,7 +668,7 @@ def test_set_dirty_substate(test_state, child_state, child_state2, grandchild_st
# Setting a var should mark it as dirty. # Setting a var should mark it as dirty.
child_state.value = "test" child_state.value = "test"
assert child_state.dirty_vars == {"value"} assert child_state.dirty_vars == {"value"}
assert test_state.dirty_substates == {"child_state"} assert test_state.dirty_substates == {ChildState.get_name()}
assert child_state.dirty_substates == set() assert child_state.dirty_substates == set()
# Cleaning the parent state should remove the dirty substate. # Cleaning the parent state should remove the dirty substate.
@ -648,12 +678,12 @@ def test_set_dirty_substate(test_state, child_state, child_state2, grandchild_st
# Setting a var on the grandchild should bubble up. # Setting a var on the grandchild should bubble up.
grandchild_state.value2 = "test2" grandchild_state.value2 = "test2"
assert child_state.dirty_substates == {"grandchild_state"} assert child_state.dirty_substates == {GrandchildState.get_name()}
assert test_state.dirty_substates == {"child_state"} assert test_state.dirty_substates == {ChildState.get_name()}
# Cleaning the middle state should keep the parent state dirty. # Cleaning the middle state should keep the parent state dirty.
child_state._clean() child_state._clean()
assert test_state.dirty_substates == {"child_state"} assert test_state.dirty_substates == {ChildState.get_name()}
assert child_state.dirty_substates == set() assert child_state.dirty_substates == set()
assert grandchild_state.dirty_vars == set() assert grandchild_state.dirty_vars == set()
@ -698,7 +728,11 @@ def test_reset(test_state, child_state):
assert child_state.dirty_vars == {"count", "value"} assert child_state.dirty_vars == {"count", "value"}
# The dirty substates should be reset. # The dirty substates should be reset.
assert test_state.dirty_substates == {"child_state", "child_state2", "child_state3"} assert test_state.dirty_substates == {
ChildState.get_name(),
ChildState2.get_name(),
ChildState3.get_name(),
}
@pytest.mark.asyncio @pytest.mark.asyncio
@ -719,8 +753,8 @@ async def test_process_event_simple(test_state):
# The delta should contain the changes, including computed vars. # The delta should contain the changes, including computed vars.
# assert update.delta == {"test_state": {"num1": 69, "sum": 72.14}} # assert update.delta == {"test_state": {"num1": 69, "sum": 72.14}}
assert update.delta == { assert update.delta == {
"test_state": {"num1": 69, "sum": 72.14, "upper": ""}, TestState.get_full_name(): {"num1": 69, "sum": 72.14, "upper": ""},
"test_state.child_state3.grandchild_state3": {"computed": ""}, GrandchildState3.get_full_name(): {"computed": ""},
} }
assert update.events == [] assert update.events == []
@ -738,15 +772,17 @@ async def test_process_event_substate(test_state, child_state, grandchild_state)
assert child_state.value == "" assert child_state.value == ""
assert child_state.count == 23 assert child_state.count == 23
event = Event( event = Event(
token="t", name="child_state.change_both", payload={"value": "hi", "count": 12} token="t",
name=f"{ChildState.get_name()}.change_both",
payload={"value": "hi", "count": 12},
) )
update = await test_state._process(event).__anext__() update = await test_state._process(event).__anext__()
assert child_state.value == "HI" assert child_state.value == "HI"
assert child_state.count == 24 assert child_state.count == 24
assert update.delta == { assert update.delta == {
"test_state": {"sum": 3.14, "upper": ""}, TestState.get_full_name(): {"sum": 3.14, "upper": ""},
"test_state.child_state": {"value": "HI", "count": 24}, ChildState.get_full_name(): {"value": "HI", "count": 24},
"test_state.child_state3.grandchild_state3": {"computed": ""}, GrandchildState3.get_full_name(): {"computed": ""},
} }
test_state._clean() test_state._clean()
@ -754,15 +790,15 @@ async def test_process_event_substate(test_state, child_state, grandchild_state)
assert grandchild_state.value2 == "" assert grandchild_state.value2 == ""
event = Event( event = Event(
token="t", token="t",
name="child_state.grandchild_state.set_value2", name=f"{GrandchildState.get_full_name()}.set_value2",
payload={"value": "new"}, payload={"value": "new"},
) )
update = await test_state._process(event).__anext__() update = await test_state._process(event).__anext__()
assert grandchild_state.value2 == "new" assert grandchild_state.value2 == "new"
assert update.delta == { assert update.delta == {
"test_state": {"sum": 3.14, "upper": ""}, TestState.get_full_name(): {"sum": 3.14, "upper": ""},
"test_state.child_state.grandchild_state": {"value2": "new"}, GrandchildState.get_full_name(): {"value2": "new"},
"test_state.child_state3.grandchild_state3": {"computed": ""}, GrandchildState3.get_full_name(): {"computed": ""},
} }
@ -786,7 +822,7 @@ async def test_process_event_generator():
else: else:
assert gen_state.value == count assert gen_state.value == count
assert update.delta == { assert update.delta == {
"gen_state": {"value": count}, GenState.get_full_name(): {"value": count},
} }
assert not update.final assert not update.final
@ -1955,7 +1991,7 @@ async def test_background_task_no_block(mock_app: rx.App, token: str):
mock_app, mock_app,
Event( Event(
token=token, token=token,
name=f"{BackgroundTaskState.get_name()}.background_task", name=f"{BackgroundTaskState.get_full_name()}.background_task",
router_data=router_data, router_data=router_data,
payload={}, payload={},
), ),
@ -1975,7 +2011,7 @@ async def test_background_task_no_block(mock_app: rx.App, token: str):
mock_app, mock_app,
Event( Event(
token=token, token=token,
name=f"{BackgroundTaskState.get_name()}.other", name=f"{BackgroundTaskState.get_full_name()}.other",
router_data=router_data, router_data=router_data,
payload={}, payload={},
), ),
@ -1986,7 +2022,7 @@ async def test_background_task_no_block(mock_app: rx.App, token: str):
# other task returns delta # other task returns delta
assert update == StateUpdate( assert update == StateUpdate(
delta={ delta={
BackgroundTaskState.get_name(): { BackgroundTaskState.get_full_name(): {
"order": [ "order": [
"background_task:start", "background_task:start",
"other", "other",
@ -2022,10 +2058,13 @@ async def test_background_task_no_block(mock_app: rx.App, token: str):
emit_mock = mock_app.event_namespace.emit emit_mock = mock_app.event_namespace.emit
first_ws_message = json.loads(emit_mock.mock_calls[0].args[1]) first_ws_message = json.loads(emit_mock.mock_calls[0].args[1])
assert first_ws_message["delta"]["background_task_state"].pop("router") is not None assert (
first_ws_message["delta"][BackgroundTaskState.get_full_name()].pop("router")
is not None
)
assert first_ws_message == { assert first_ws_message == {
"delta": { "delta": {
"background_task_state": { BackgroundTaskState.get_full_name(): {
"order": ["background_task:start"], "order": ["background_task:start"],
"computed_order": ["background_task:start"], "computed_order": ["background_task:start"],
} }
@ -2036,14 +2075,16 @@ async def test_background_task_no_block(mock_app: rx.App, token: str):
for call in emit_mock.mock_calls[1:5]: for call in emit_mock.mock_calls[1:5]:
assert json.loads(call.args[1]) == { assert json.loads(call.args[1]) == {
"delta": { "delta": {
"background_task_state": {"computed_order": ["background_task:start"]} BackgroundTaskState.get_full_name(): {
"computed_order": ["background_task:start"],
}
}, },
"events": [], "events": [],
"final": True, "final": True,
} }
assert json.loads(emit_mock.mock_calls[-2].args[1]) == { assert json.loads(emit_mock.mock_calls[-2].args[1]) == {
"delta": { "delta": {
"background_task_state": { BackgroundTaskState.get_full_name(): {
"order": exp_order, "order": exp_order,
"computed_order": exp_order, "computed_order": exp_order,
"dict_list": {}, "dict_list": {},
@ -2054,7 +2095,7 @@ async def test_background_task_no_block(mock_app: rx.App, token: str):
} }
assert json.loads(emit_mock.mock_calls[-1].args[1]) == { assert json.loads(emit_mock.mock_calls[-1].args[1]) == {
"delta": { "delta": {
"background_task_state": { BackgroundTaskState.get_full_name(): {
"computed_order": exp_order, "computed_order": exp_order,
}, },
}, },
@ -2683,7 +2724,7 @@ async def test_preprocess(app_module_mock, token, test_state, expected, mocker):
assert isinstance(update, StateUpdate) assert isinstance(update, StateUpdate)
updates.append(update) updates.append(update)
assert len(updates) == 1 assert len(updates) == 1
assert updates[0].delta["state"].pop("router") is not None assert updates[0].delta[State.get_name()].pop("router") is not None
assert updates[0].delta == exp_is_hydrated(state, False) assert updates[0].delta == exp_is_hydrated(state, False)
events = updates[0].events events = updates[0].events
@ -2727,7 +2768,7 @@ async def test_preprocess_multiple_load_events(app_module_mock, token, mocker):
assert isinstance(update, StateUpdate) assert isinstance(update, StateUpdate)
updates.append(update) updates.append(update)
assert len(updates) == 1 assert len(updates) == 1
assert updates[0].delta["state"].pop("router") is not None assert updates[0].delta[State.get_name()].pop("router") is not None
assert updates[0].delta == exp_is_hydrated(state, False) assert updates[0].delta == exp_is_hydrated(state, False)
events = updates[0].events events = updates[0].events
@ -2759,22 +2800,27 @@ async def test_get_state(mock_app: rx.App, token: str):
if isinstance(mock_app.state_manager, StateManagerMemory): if isinstance(mock_app.state_manager, StateManagerMemory):
# All substates are available # All substates are available
assert tuple(sorted(test_state.substates)) == ( assert tuple(sorted(test_state.substates)) == (
"child_state", ChildState.get_name(),
"child_state2", ChildState2.get_name(),
"child_state3", ChildState3.get_name(),
) )
else: else:
# Sibling states are only populated if they have computed vars # Sibling states are only populated if they have computed vars
assert tuple(sorted(test_state.substates)) == ("child_state2", "child_state3") assert tuple(sorted(test_state.substates)) == (
ChildState2.get_name(),
ChildState3.get_name(),
)
# Because ChildState3 has a computed var, it is always dirty, and always populated. # Because ChildState3 has a computed var, it is always dirty, and always populated.
assert ( assert (
test_state.substates["child_state3"].substates["grandchild_state3"].computed test_state.substates[ChildState3.get_name()]
.substates[GrandchildState3.get_name()]
.computed
== "" == ""
) )
# Get the child_state2 directly. # Get the child_state2 directly.
child_state2_direct = test_state.get_substate(["child_state2"]) child_state2_direct = test_state.get_substate([ChildState2.get_name()])
child_state2_get_state = await test_state.get_state(ChildState2) child_state2_get_state = await test_state.get_state(ChildState2)
# These should be the same object. # These should be the same object.
assert child_state2_direct is child_state2_get_state assert child_state2_direct is child_state2_get_state
@ -2785,19 +2831,21 @@ async def test_get_state(mock_app: rx.App, token: str):
# Now the original root should have all substates populated. # Now the original root should have all substates populated.
assert tuple(sorted(test_state.substates)) == ( assert tuple(sorted(test_state.substates)) == (
"child_state", ChildState.get_name(),
"child_state2", ChildState2.get_name(),
"child_state3", ChildState3.get_name(),
) )
# ChildState should be retrievable # ChildState should be retrievable
child_state_direct = test_state.get_substate(["child_state"]) child_state_direct = test_state.get_substate([ChildState.get_name()])
child_state_get_state = await test_state.get_state(ChildState) child_state_get_state = await test_state.get_state(ChildState)
# These should be the same object. # These should be the same object.
assert child_state_direct is child_state_get_state assert child_state_direct is child_state_get_state
# GrandchildState instance should be the same as the one retrieved from the child_state2. # GrandchildState instance should be the same as the one retrieved from the child_state2.
assert grandchild_state is child_state_direct.get_substate(["grandchild_state"]) assert grandchild_state is child_state_direct.get_substate(
[GrandchildState.get_name()]
)
grandchild_state.value2 = "set_value" grandchild_state.value2 = "set_value"
assert test_state.get_delta() == { assert test_state.get_delta() == {
@ -2824,21 +2872,21 @@ async def test_get_state(mock_app: rx.App, token: str):
test_state._clean() test_state._clean()
# All substates are available # All substates are available
assert tuple(sorted(new_test_state.substates)) == ( assert tuple(sorted(new_test_state.substates)) == (
"child_state", ChildState.get_name(),
"child_state2", ChildState2.get_name(),
"child_state3", ChildState3.get_name(),
) )
else: else:
# With redis, we get a whole new instance # With redis, we get a whole new instance
assert new_test_state is not test_state assert new_test_state is not test_state
# Sibling states are only populated if they have computed vars # Sibling states are only populated if they have computed vars
assert tuple(sorted(new_test_state.substates)) == ( assert tuple(sorted(new_test_state.substates)) == (
"child_state2", ChildState2.get_name(),
"child_state3", ChildState3.get_name(),
) )
# Set a value on child_state2, should update cached var in grandchild_state2 # Set a value on child_state2, should update cached var in grandchild_state2
child_state2 = new_test_state.get_substate(("child_state2",)) child_state2 = new_test_state.get_substate((ChildState2.get_name(),))
child_state2.value = "set_c2_value" child_state2.value = "set_c2_value"
assert new_test_state.get_delta() == { assert new_test_state.get_delta() == {
@ -2929,8 +2977,8 @@ async def test_get_state_from_sibling_not_cached(mock_app: rx.App, token: str):
if isinstance(mock_app.state_manager, StateManagerRedis): if isinstance(mock_app.state_manager, StateManagerRedis):
# When redis is used, only states with computed vars are pre-fetched. # When redis is used, only states with computed vars are pre-fetched.
assert "child2" not in root.substates assert Child2.get_name() not in root.substates
assert "child3" in root.substates # (due to @rx.var) assert Child3.get_name() in root.substates # (due to @rx.var)
# Get the unconnected sibling state, which will be used to `get_state` other instances. # Get the unconnected sibling state, which will be used to `get_state` other instances.
child = root.get_substate(Child.get_full_name().split(".")) child = root.get_substate(Child.get_full_name().split("."))

View File

@ -237,7 +237,13 @@ def state_manager_redis(app_module_mock) -> Generator[StateManager, None, None]:
[ [
( (
Root, Root,
["tree_a", "tree_b", "tree_c", "tree_d", "tree_e"], [
TreeA.get_name(),
TreeB.get_name(),
TreeC.get_name(),
TreeD.get_name(),
TreeE.get_name(),
],
[ [
TreeA.get_full_name(), TreeA.get_full_name(),
SubA_A.get_full_name(), SubA_A.get_full_name(),
@ -261,7 +267,7 @@ def state_manager_redis(app_module_mock) -> Generator[StateManager, None, None]:
), ),
( (
TreeA, TreeA,
("tree_a", "tree_d", "tree_e"), (TreeA.get_name(), TreeD.get_name(), TreeE.get_name()),
[ [
TreeA.get_full_name(), TreeA.get_full_name(),
SubA_A.get_full_name(), SubA_A.get_full_name(),
@ -276,7 +282,7 @@ def state_manager_redis(app_module_mock) -> Generator[StateManager, None, None]:
), ),
( (
SubA_A_A_A, SubA_A_A_A,
["tree_a", "tree_d", "tree_e"], [TreeA.get_name(), TreeD.get_name(), TreeE.get_name()],
[ [
TreeA.get_full_name(), TreeA.get_full_name(),
SubA_A.get_full_name(), SubA_A.get_full_name(),
@ -288,7 +294,7 @@ def state_manager_redis(app_module_mock) -> Generator[StateManager, None, None]:
), ),
( (
TreeB, TreeB,
["tree_b", "tree_d", "tree_e"], [TreeB.get_name(), TreeD.get_name(), TreeE.get_name()],
[ [
TreeB.get_full_name(), TreeB.get_full_name(),
SubB_A.get_full_name(), SubB_A.get_full_name(),
@ -300,7 +306,7 @@ def state_manager_redis(app_module_mock) -> Generator[StateManager, None, None]:
), ),
( (
SubB_B, SubB_B,
["tree_b", "tree_d", "tree_e"], [TreeB.get_name(), TreeD.get_name(), TreeE.get_name()],
[ [
TreeB.get_full_name(), TreeB.get_full_name(),
SubB_B.get_full_name(), SubB_B.get_full_name(),
@ -309,7 +315,7 @@ def state_manager_redis(app_module_mock) -> Generator[StateManager, None, None]:
), ),
( (
SubB_C_A, SubB_C_A,
["tree_b", "tree_d", "tree_e"], [TreeB.get_name(), TreeD.get_name(), TreeE.get_name()],
[ [
TreeB.get_full_name(), TreeB.get_full_name(),
SubB_C.get_full_name(), SubB_C.get_full_name(),
@ -319,7 +325,7 @@ def state_manager_redis(app_module_mock) -> Generator[StateManager, None, None]:
), ),
( (
TreeC, TreeC,
["tree_c", "tree_d", "tree_e"], [TreeC.get_name(), TreeD.get_name(), TreeE.get_name()],
[ [
TreeC.get_full_name(), TreeC.get_full_name(),
SubC_A.get_full_name(), SubC_A.get_full_name(),
@ -328,14 +334,14 @@ def state_manager_redis(app_module_mock) -> Generator[StateManager, None, None]:
), ),
( (
TreeD, TreeD,
["tree_d", "tree_e"], [TreeD.get_name(), TreeE.get_name()],
[ [
*ALWAYS_COMPUTED_DICT_KEYS, *ALWAYS_COMPUTED_DICT_KEYS,
], ],
), ),
( (
TreeE, TreeE,
["tree_d", "tree_e"], [TreeE.get_name(), TreeD.get_name()],
[ [
# Extra siblings of computed var included now. # Extra siblings of computed var included now.
SubE_A_A_A_B.get_full_name(), SubE_A_A_A_B.get_full_name(),

View File

@ -739,64 +739,55 @@ def test_var_unsupported_indexing_dicts(var, index):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"fixture,full_name", "fixture",
[ [
("ParentState", "parent_state.var_without_annotation"), "ParentState",
("ChildState", "parent_state__child_state.var_without_annotation"), "ChildState",
( "GrandChildState",
"GrandChildState", "StateWithAnyVar",
"parent_state__child_state__grand_child_state.var_without_annotation",
),
("StateWithAnyVar", "state_with_any_var.var_without_annotation"),
], ],
) )
def test_computed_var_without_annotation_error(request, fixture, full_name): def test_computed_var_without_annotation_error(request, fixture):
"""Test that a type error is thrown when an attribute of a computed var is """Test that a type error is thrown when an attribute of a computed var is
accessed without annotating the computed var. accessed without annotating the computed var.
Args: Args:
request: Fixture Request. request: Fixture Request.
fixture: The state fixture. fixture: The state fixture.
full_name: The full name of the state var.
""" """
with pytest.raises(TypeError) as err: with pytest.raises(TypeError) as err:
state = request.getfixturevalue(fixture) state = request.getfixturevalue(fixture)
state.var_without_annotation.foo state.var_without_annotation.foo
assert ( full_name = state.var_without_annotation._var_full_name
err.value.args[0] assert (
== f"You must provide an annotation for the state var `{full_name}`. Annotation cannot be `typing.Any`" err.value.args[0]
) == f"You must provide an annotation for the state var `{full_name}`. Annotation cannot be `typing.Any`"
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"fixture,full_name", "fixture",
[ [
( "StateWithCorrectVarAnnotation",
"StateWithCorrectVarAnnotation", "StateWithWrongVarAnnotation",
"state_with_correct_var_annotation.var_with_annotation",
),
(
"StateWithWrongVarAnnotation",
"state_with_wrong_var_annotation.var_with_annotation",
),
], ],
) )
def test_computed_var_with_annotation_error(request, fixture, full_name): def test_computed_var_with_annotation_error(request, fixture):
"""Test that an Attribute error is thrown when a non-existent attribute of an annotated computed var is """Test that an Attribute error is thrown when a non-existent attribute of an annotated computed var is
accessed or when the wrong annotation is provided to a computed var. accessed or when the wrong annotation is provided to a computed var.
Args: Args:
request: Fixture Request. request: Fixture Request.
fixture: The state fixture. fixture: The state fixture.
full_name: The full name of the state var.
""" """
with pytest.raises(AttributeError) as err: with pytest.raises(AttributeError) as err:
state = request.getfixturevalue(fixture) state = request.getfixturevalue(fixture)
state.var_with_annotation.foo state.var_with_annotation.foo
assert ( full_name = state.var_with_annotation._var_full_name
err.value.args[0] assert (
== f"The State var `{full_name}` has no attribute 'foo' or may have been annotated wrongly." err.value.args[0]
) == f"The State var `{full_name}` has no attribute 'foo' or may have been annotated wrongly."
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -1402,12 +1393,15 @@ def test_invalid_var_operations(operand1_var: Var, operand2_var, operators: List
(Var.create(1), "1"), (Var.create(1), "1"),
(Var.create([1, 2, 3]), "[1, 2, 3]"), (Var.create([1, 2, 3]), "[1, 2, 3]"),
(Var.create({"foo": "bar"}), '{"foo": "bar"}'), (Var.create({"foo": "bar"}), '{"foo": "bar"}'),
(Var.create(ATestState.value, _var_is_string=True), "a_test_state.value"), (
Var.create(ATestState.value, _var_is_string=True),
f"{ATestState.get_full_name()}.value",
),
( (
Var.create(f"{ATestState.value} string", _var_is_string=True), Var.create(f"{ATestState.value} string", _var_is_string=True),
"`${a_test_state.value} string`", f"`${{{ATestState.get_full_name()}.value}} string`",
), ),
(Var.create(ATestState.dict_val), "a_test_state.dict_val"), (Var.create(ATestState.dict_val), f"{ATestState.get_full_name()}.dict_val"),
], ],
) )
def test_var_name_unwrapped(var, expected): def test_var_name_unwrapped(var, expected):

View File

@ -432,8 +432,8 @@ def test_format_cond(
], ],
Var.create("yellow", _var_is_string=True), Var.create("yellow", _var_is_string=True),
"(() => { switch (JSON.stringify(state__state.value)) {case JSON.stringify(1): return (`red`); break;case JSON.stringify(2): case JSON.stringify(3): " "(() => { switch (JSON.stringify(state__state.value)) {case JSON.stringify(1): return (`red`); break;case JSON.stringify(2): case JSON.stringify(3): "
"return (`blue`); break;case JSON.stringify(test_state.mapping): return " f"return (`blue`); break;case JSON.stringify({TestState.get_full_name()}.mapping): return "
"(test_state.num1); break;case JSON.stringify(`${test_state.map_key}-key`): return (`return-key`);" f"({TestState.get_full_name()}.num1); break;case JSON.stringify(`${{{TestState.get_full_name()}.map_key}}-key`): return (`return-key`);"
" break;default: return (`yellow`); break;};})()", " break;default: return (`yellow`); break;};})()",
) )
], ],
@ -585,11 +585,14 @@ def test_get_handler_parts(input, output):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"input,output", "input,output",
[ [
(TestState.do_something, "test_state.do_something"), (TestState.do_something, f"{TestState.get_full_name()}.do_something"),
(ChildState.change_both, "test_state.child_state.change_both"), (
ChildState.change_both,
f"{ChildState.get_full_name()}.change_both",
),
( (
GrandchildState.do_nothing, GrandchildState.do_nothing,
"test_state.child_state.grandchild_state.do_nothing", f"{GrandchildState.get_full_name()}.do_nothing",
), ),
], ],
) )