Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
adfd79b
add hybrid_property
benedikt-bartscher Aug 17, 2024
acdd88e
Merge remote-tracking branch 'upstream/main' into hybrid-properties
benedikt-bartscher Aug 19, 2024
f4f44f8
Merge remote-tracking branch 'upstream/main' into hybrid-properties
benedikt-bartscher Aug 22, 2024
2ff47e1
Merge remote-tracking branch 'upstream/main' into hybrid-properties
benedikt-bartscher Aug 23, 2024
58399e5
Merge remote-tracking branch 'origin/main' into hybrid-properties
benedikt-bartscher Aug 29, 2024
dbec527
fix merge conflicts
benedikt-bartscher Sep 5, 2024
0f38cd6
remove reference to computed var
adhami3310 Sep 10, 2024
f4a45fd
fix conflicts
benedikt-bartscher Sep 10, 2024
fc58c38
Merge remote-tracking branch 'upstream/fix-reference-to-old-computed-…
benedikt-bartscher Sep 10, 2024
7d3071e
actually fix conflicts
benedikt-bartscher Sep 10, 2024
30ded81
use other callable
benedikt-bartscher Sep 10, 2024
a541107
Merge remote-tracking branch 'upstream/main' into hybrid-properties
benedikt-bartscher Sep 11, 2024
fdd30a6
better Self typing for old python versions
benedikt-bartscher Sep 11, 2024
c852767
Merge remote-tracking branch 'upstream/main' into hybrid-properties
benedikt-bartscher Nov 1, 2024
1b3c82d
move hybrid_property to experimental namespace
benedikt-bartscher Nov 20, 2024
e64783b
Merge remote-tracking branch 'upstream/main' into hybrid-properties
benedikt-bartscher Nov 20, 2024
0594a8a
Merge remote-tracking branch 'upstream/main' into hybrid-properties
benedikt-bartscher Aug 10, 2025
8290c86
oups
benedikt-bartscher Aug 10, 2025
5fc942e
fix: adjust test for hybrid properties to upstream changes
benedikt-bartscher Aug 10, 2025
69d57af
Update reflex/experimental/hybrid_property.py
benedikt-bartscher Aug 10, 2025
34e67ed
ruffing
benedikt-bartscher Aug 10, 2025
0ac6e3e
fix darglint, document exception for hybrid_property
benedikt-bartscher Aug 14, 2025
21fa0bc
Merge remote-tracking branch 'upstream/main' into hybrid-properties
benedikt-bartscher Aug 14, 2025
1e76998
Merge remote-tracking branch 'upstream' into hybrid-properties
benedikt-bartscher Jan 27, 2026
cf67b87
fix pyright
benedikt-bartscher Jan 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions reflex/experimental/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from . import hooks as hooks
from .client_state import ClientStateVar as ClientStateVar
from .hybrid_property import hybrid_property as hybrid_property


class ExperimentalNamespace(SimpleNamespace):
Expand Down Expand Up @@ -58,4 +59,5 @@ def register_component_warning(component_name: str):
client_state=ClientStateVar.create,
hooks=hooks,
code_block=code_block,
hybrid_property=hybrid_property,
)
49 changes: 49 additions & 0 deletions reflex/experimental/hybrid_property.py
Original file line number Diff line number Diff line change
@@ -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 collections.abc import Callable
from typing import Any

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)
# 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
195 changes: 195 additions & 0 deletions tests/integration/test_hybrid_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
"""Test hybrid properties."""

from __future__ import annotations

from collections.abc 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.el.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.el.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("hybrid_properties"),
app_source=HybridProperties,
) 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 = AppHarness.poll_for_or_raise_timeout(
lambda: driver.find_element(By.ID, "token")
)

# 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}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Token is being reassigned here, shadowing the fixture parameter. Consider using a different variable name like state_token to avoid confusion.


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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The assertion expects 'John ' (with trailing space) but this seems inconsistent with the frontend assertion on line 190 that expects 'John'. This could indicate a bug in the hybrid property implementation or test logic.


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