Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add hybrid_property #3806

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

benedikt-bartscher
Copy link
Contributor

@benedikt-bartscher benedikt-bartscher commented Aug 17, 2024

Reflex currently renders @propertys on States like this: <property object at 0x723b334e5940>.
This PR adds a 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.

example

Note: full_name and has_last_name are not rx.vars and are not sent over the wire (websocket). They are instead computed from first_name and last_name separately in frontend (js) and backend (python).

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>`."""
        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."""
        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."""
        return "yes" if self.last_name else "no"

    @has_last_name.var
    def has_last_name(cls) -> Var[str]:
        """The frontend implementation for has_last_name"""
        return rx.cond(cls.last_name, "yes", "no")

    def handle_stuff(self) -> None:
        """A simple event handler which showcases backend-level access to the hybrid properties."""
        print(f"python_full_name: {self.python_full_name}")
        print(f"full_name: {self.full_name}")
        print(f"has_last_name: {self.has_last_name}")


def index() -> rx.Component:
    return rx.vstack(
        rx.text(f"python_full_name: {State.python_full_name}"),
        rx.text(f"full_name: {State.full_name}"),
        rx.text(f"has_last_name: {State.has_last_name}"),
        rx.input(
            value=State.last_name,
            on_change=State.set_last_name,  # type: ignore
        ),
        rx.button("Handle Stuff", on_click=State.handle_stuff),
    )


app = rx.App()
app.add_page(index)

@benedikt-bartscher benedikt-bartscher marked this pull request as ready for review August 17, 2024 16:52
@benedikt-bartscher
Copy link
Contributor Author

We could implement the same for ComputedVars with a small patch.

diff --git a/reflex/vars.py b/reflex/vars.py
index 8f1f0186..c502d814 100644
--- a/reflex/vars.py
+++ b/reflex/vars.py
@@ -2154,10 +2154,16 @@ class BaseVar(Var):
         return setter
 
 
+VAR_CALLABLE = Callable[[Any], Var]
+
+
 @dataclasses.dataclass(init=False, eq=False)
 class ComputedVar(Var, property):
     """A field with computed getters."""
 
+    # The optional var function for the property.
+    _var: VAR_CALLABLE | None = None
+
     # Whether to track dependencies and cache computed values
     _cache: bool = dataclasses.field(default=False)
 
@@ -2322,6 +2328,18 @@ class ComputedVar(Var, property):
             return True
         return datetime.datetime.now() - last_updated > self._update_interval
 
+    def var(self, func: VAR_CALLABLE) -> 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
+
     def __get__(self, instance: BaseState | None, owner):
         """Get the ComputedVar value.
 
@@ -2334,6 +2352,9 @@ class ComputedVar(Var, property):
         Returns:
             The value of the var for the given instance.
         """
+        if instance is None and self._var is not None:
+            return self._var(owner, value=self)
+
         if instance is None or not self._cache:
             return super().__get__(instance, owner)
 
@@ -2546,8 +2567,6 @@ def computed_var(
 # Partial function of computed_var with cache=True
 cached_var = functools.partial(computed_var, cache=True, _deprecated_cached_var=True)
 
-VAR_CALLABLE = Callable[[Any], Var]
-
 
 class HybridProperty(property):
     """A hybrid property that can also be used in frontend/as var."""

@benedikt-bartscher
Copy link
Contributor Author

I can move this to experimental if needed

benedikt-bartscher and others added 5 commits September 5, 2024 19:48
Merge remote-tracking branch 'upstream/main' into hybrid-properties
Merge remote-tracking branch 'upstream/main' into hybrid-properties
@Lendemor
Copy link
Collaborator

Lendemor commented Oct 25, 2024

I think we can expose this under experimental yes, since it doesn't seems like it impact any existing feature.
Probably need some changes due to recent changes to vars.

Marking as Draft until resynced with main and the changes needed are done.

@Lendemor Lendemor marked this pull request as draft October 25, 2024 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants