Merge e64783bdcc
into 6848915883
This commit is contained in:
commit
731f1d3141
@ -10,6 +10,7 @@ from reflex.components.sonner.toast import toast as toast
|
|||||||
from ..utils.console import warn
|
from ..utils.console import warn
|
||||||
from . import hooks as hooks
|
from . import hooks as hooks
|
||||||
from .client_state import ClientStateVar as ClientStateVar
|
from .client_state import ClientStateVar as ClientStateVar
|
||||||
|
from .hybrid_property import hybrid_property as hybrid_property
|
||||||
from .layout import layout as layout
|
from .layout import layout as layout
|
||||||
from .misc import run_in_thread as run_in_thread
|
from .misc import run_in_thread as run_in_thread
|
||||||
|
|
||||||
@ -77,4 +78,5 @@ _x = ExperimentalNamespace(
|
|||||||
PropsBase=PropsBase,
|
PropsBase=PropsBase,
|
||||||
run_in_thread=run_in_thread,
|
run_in_thread=run_in_thread,
|
||||||
code_block=code_block,
|
code_block=code_block,
|
||||||
|
hybrid_property=hybrid_property,
|
||||||
)
|
)
|
||||||
|
49
reflex/experimental/hybrid_property.py
Normal file
49
reflex/experimental/hybrid_property.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
"""hybrid_property decorator which functions like a normal python property but additionally allows (class-level) access from the frontend. You can use the same code for frontend and backend, or implement 2 different methods."""
|
||||||
|
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from reflex.utils.types import Self, override
|
||||||
|
from reflex.vars.base import Var
|
||||||
|
|
||||||
|
|
||||||
|
class HybridProperty(property):
|
||||||
|
"""A hybrid property that can also be used in frontend/as var."""
|
||||||
|
|
||||||
|
# The optional var function for the property.
|
||||||
|
_var: Callable[[Any], Var] | None = None
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __get__(self, instance: Any, owner: type | None = None, /) -> Any:
|
||||||
|
"""Get the value of the property. If the property is not bound to an instance return a frontend Var.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance: The instance of the class accessing this property.
|
||||||
|
owner: The class that this descriptor is attached to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The value of the property or a frontend Var.
|
||||||
|
"""
|
||||||
|
if instance is not None:
|
||||||
|
return super().__get__(instance, owner)
|
||||||
|
if self._var is not None:
|
||||||
|
# Call custom var function if set
|
||||||
|
return self._var(owner)
|
||||||
|
else:
|
||||||
|
# Call the property getter function if no custom var function is set
|
||||||
|
assert self.fget is not None
|
||||||
|
return self.fget(owner)
|
||||||
|
|
||||||
|
def var(self, func: Callable[[Any], Var]) -> Self:
|
||||||
|
"""Set the (optional) var function for the property.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func: The var function to set.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The property instance with the var function set.
|
||||||
|
"""
|
||||||
|
self._var = func
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
hybrid_property = HybridProperty
|
@ -67,6 +67,11 @@ else:
|
|||||||
return func
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 11):
|
||||||
|
from typing import Self as Self
|
||||||
|
else:
|
||||||
|
from typing_extensions import Self as Self
|
||||||
|
|
||||||
# Potential GenericAlias types for isinstance checks.
|
# Potential GenericAlias types for isinstance checks.
|
||||||
GenericAliasTypes = [_GenericAlias]
|
GenericAliasTypes = [_GenericAlias]
|
||||||
|
|
||||||
|
194
tests/integration/test_hybrid_properties.py
Normal file
194
tests/integration/test_hybrid_properties.py
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
"""Test hybrid properties."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
|
||||||
|
from reflex.testing import DEFAULT_TIMEOUT, AppHarness, WebDriver
|
||||||
|
|
||||||
|
|
||||||
|
def HybridProperties():
|
||||||
|
"""Test app for hybrid properties."""
|
||||||
|
import reflex as rx
|
||||||
|
from reflex.experimental import hybrid_property
|
||||||
|
from reflex.vars import Var
|
||||||
|
|
||||||
|
class State(rx.State):
|
||||||
|
first_name: str = "John"
|
||||||
|
last_name: str = "Doe"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def python_full_name(self) -> str:
|
||||||
|
"""A normal python property to showcase the current behavior. This renders to smth like `<property object at 0x723b334e5940>`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The full name of the person.
|
||||||
|
"""
|
||||||
|
return f"{self.first_name} {self.last_name}"
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def full_name(self) -> str:
|
||||||
|
"""A simple hybrid property which uses the same code for both frontend and backend.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The full name of the person.
|
||||||
|
"""
|
||||||
|
return f"{self.first_name} {self.last_name}"
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def has_last_name(self) -> str:
|
||||||
|
"""A more complex hybrid property which uses different code for frontend and backend.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: "yes" if the person has a last name, "no" otherwise.
|
||||||
|
"""
|
||||||
|
return "yes" if self.last_name else "no"
|
||||||
|
|
||||||
|
@has_last_name.var
|
||||||
|
def has_last_name(cls) -> Var[str]:
|
||||||
|
"""The frontend code for the `has_last_name` hybrid property.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Var[str]: The value of the hybrid property.
|
||||||
|
"""
|
||||||
|
return rx.cond(cls.last_name, "yes", "no")
|
||||||
|
|
||||||
|
def index() -> rx.Component:
|
||||||
|
return rx.center(
|
||||||
|
rx.vstack(
|
||||||
|
rx.input(
|
||||||
|
id="token",
|
||||||
|
value=State.router.session.client_token,
|
||||||
|
is_read_only=True,
|
||||||
|
),
|
||||||
|
rx.text(
|
||||||
|
f"python_full_name: {State.python_full_name}", id="python_full_name"
|
||||||
|
),
|
||||||
|
rx.text(f"full_name: {State.full_name}", id="full_name"),
|
||||||
|
rx.text(f"has_last_name: {State.has_last_name}", id="has_last_name"),
|
||||||
|
rx.input(
|
||||||
|
value=State.last_name,
|
||||||
|
on_change=State.setvar("last_name"),
|
||||||
|
id="set_last_name",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
app = rx.App()
|
||||||
|
app.add_page(index)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def hybrid_properties(
|
||||||
|
tmp_path_factory: pytest.TempPathFactory,
|
||||||
|
) -> Generator[AppHarness, None, None]:
|
||||||
|
"""Start HybridProperties app at tmp_path via AppHarness.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tmp_path_factory: pytest tmp_path_factory fixture
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
running AppHarness instance
|
||||||
|
"""
|
||||||
|
with AppHarness.create(
|
||||||
|
root=tmp_path_factory.mktemp(f"hybrid_properties"),
|
||||||
|
app_source=HybridProperties, # type: ignore
|
||||||
|
) as harness:
|
||||||
|
yield harness
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def driver(hybrid_properties: AppHarness) -> Generator[WebDriver, None, None]:
|
||||||
|
"""Get an instance of the browser open to the hybrid_properties app.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hybrid_properties: harness for HybridProperties app
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
WebDriver instance.
|
||||||
|
"""
|
||||||
|
assert hybrid_properties.app_instance is not None, "app is not running"
|
||||||
|
driver = hybrid_properties.frontend()
|
||||||
|
try:
|
||||||
|
yield driver
|
||||||
|
finally:
|
||||||
|
driver.quit()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def token(hybrid_properties: AppHarness, driver: WebDriver) -> str:
|
||||||
|
"""Get a function that returns the active token.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hybrid_properties: harness for HybridProperties app.
|
||||||
|
driver: WebDriver instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The token for the connected client
|
||||||
|
"""
|
||||||
|
assert hybrid_properties.app_instance is not None
|
||||||
|
token_input = driver.find_element(By.ID, "token")
|
||||||
|
assert token_input
|
||||||
|
|
||||||
|
# wait for the backend connection to send the token
|
||||||
|
token = hybrid_properties.poll_for_value(token_input, timeout=DEFAULT_TIMEOUT * 2)
|
||||||
|
assert token is not None
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_hybrid_properties(
|
||||||
|
hybrid_properties: AppHarness,
|
||||||
|
driver: WebDriver,
|
||||||
|
token: str,
|
||||||
|
):
|
||||||
|
"""Test that hybrid properties are working as expected.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hybrid_properties: harness for HybridProperties app.
|
||||||
|
driver: WebDriver instance.
|
||||||
|
token: The token for the connected client.
|
||||||
|
"""
|
||||||
|
assert hybrid_properties.app_instance is not None
|
||||||
|
|
||||||
|
state_name = hybrid_properties.get_state_name("_state")
|
||||||
|
full_state_name = hybrid_properties.get_full_state_name(["_state"])
|
||||||
|
token = f"{token}_{full_state_name}"
|
||||||
|
|
||||||
|
state = (await hybrid_properties.get_state(token)).substates[state_name]
|
||||||
|
assert state is not None
|
||||||
|
assert state.full_name == "John Doe"
|
||||||
|
assert state.has_last_name == "yes"
|
||||||
|
|
||||||
|
full_name = driver.find_element(By.ID, "full_name")
|
||||||
|
assert full_name.text == "full_name: John Doe"
|
||||||
|
|
||||||
|
python_full_name = driver.find_element(By.ID, "python_full_name")
|
||||||
|
assert "<property object at 0x" in python_full_name.text
|
||||||
|
|
||||||
|
has_last_name = driver.find_element(By.ID, "has_last_name")
|
||||||
|
assert has_last_name.text == "has_last_name: yes"
|
||||||
|
|
||||||
|
set_last_name = driver.find_element(By.ID, "set_last_name")
|
||||||
|
# clear the input
|
||||||
|
set_last_name.send_keys(Keys.CONTROL + "a")
|
||||||
|
set_last_name.send_keys(Keys.DELETE)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
hybrid_properties.poll_for_content(
|
||||||
|
has_last_name, exp_not_equal="has_last_name: yes"
|
||||||
|
)
|
||||||
|
== "has_last_name: no"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert full_name.text == "full_name: John"
|
||||||
|
|
||||||
|
state = (await hybrid_properties.get_state(token)).substates[state_name]
|
||||||
|
assert state is not None
|
||||||
|
assert state.full_name == "John "
|
||||||
|
assert state.has_last_name == "no"
|
Loading…
Reference in New Issue
Block a user