diff --git a/reflex/experimental/__init__.py b/reflex/experimental/__init__.py index f0eca0c845..060f7e7f15 100644 --- a/reflex/experimental/__init__.py +++ b/reflex/experimental/__init__.py @@ -8,6 +8,7 @@ from ..utils.console import warn from . import hooks as hooks +from . import vars as vars from .assets import asset as asset from .client_state import ClientStateVar as ClientStateVar from .layout import layout as layout @@ -42,6 +43,7 @@ def toast(self): asset=asset, client_state=ClientStateVar.create, hooks=hooks, + vars=vars, layout=layout, progress=progress, PropsBase=PropsBase, diff --git a/reflex/experimental/vars/__init__.py b/reflex/experimental/vars/__init__.py new file mode 100644 index 0000000000..5f99f22929 --- /dev/null +++ b/reflex/experimental/vars/__init__.py @@ -0,0 +1,3 @@ +"""Experimental Immutable-Based Var System.""" + +from .base import ImmutableVar as ImmutableVar diff --git a/reflex/experimental/vars/base.py b/reflex/experimental/vars/base.py new file mode 100644 index 0000000000..52bce91614 --- /dev/null +++ b/reflex/experimental/vars/base.py @@ -0,0 +1,158 @@ +"""Collection of base classes.""" + +from __future__ import annotations + +import dataclasses +import sys +from typing import Any, Optional, Type + +from reflex.utils import serializers, types +from reflex.utils.exceptions import VarTypeError +from reflex.vars import Var, VarData, _extract_var_data + + +@dataclasses.dataclass( + eq=False, + frozen=True, + **{"slots": True} if sys.version_info >= (3, 10) else {}, +) +class ImmutableVar(Var): + """Base class for immutable vars.""" + + # The name of the var. + _var_name: str = dataclasses.field() + + # The type of the var. + _var_type: Type = dataclasses.field(default=Any) + + # Extra metadata associated with the Var + _var_data: Optional[VarData] = dataclasses.field(default=None) + + @property + def _var_is_local(self) -> bool: + """Whether this is a local javascript variable. + + Returns: + False + """ + return False + + @property + def _var_is_string(self) -> bool: + """Whether the var is a string literal. + + Returns: + False + """ + return False + + @property + def _var_full_name_needs_state_prefix(self) -> bool: + """Whether the full name of the var needs a _var_state prefix. + + Returns: + False + """ + return False + + def _replace(self, merge_var_data=None, **kwargs: Any): + """Make a copy of this Var with updated fields. + + Args: + merge_var_data: VarData to merge into the existing VarData. + **kwargs: Var fields to update. + + Returns: + A new ImmutableVar with the updated fields overwriting the corresponding fields in this Var. + + Raises: + TypeError: If _var_is_local, _var_is_string, or _var_full_name_needs_state_prefix is not None. + """ + if kwargs.get("_var_is_local", False) is not False: + raise TypeError( + "The _var_is_local argument is not supported for ImmutableVar." + ) + + if kwargs.get("_var_is_string", False) is not False: + raise TypeError( + "The _var_is_string argument is not supported for ImmutableVar." + ) + + if kwargs.get("_var_full_name_needs_state_prefix", False) is not False: + raise TypeError( + "The _var_full_name_needs_state_prefix argument is not supported for ImmutableVar." + ) + + field_values = dict( + _var_name=kwargs.pop("_var_name", self._var_name), + _var_type=kwargs.pop("_var_type", self._var_type), + _var_data=VarData.merge( + kwargs.get("_var_data", self._var_data), merge_var_data + ), + ) + return ImmutableVar(**field_values) + + @classmethod + def create( + cls, + value: Any, + _var_is_local: bool | None = None, + _var_is_string: bool | None = None, + _var_data: VarData | None = None, + ) -> Var | None: + """Create a var from a value. + + Args: + value: The value to create the var from. + _var_is_local: Whether the var is local. Deprecated. + _var_is_string: Whether the var is a string literal. Deprecated. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The var. + + Raises: + VarTypeError: If the value is JSON-unserializable. + TypeError: If _var_is_local or _var_is_string is not None. + """ + if _var_is_local is not None: + raise TypeError( + "The _var_is_local argument is not supported for ImmutableVar." + ) + + if _var_is_string is not None: + raise TypeError( + "The _var_is_string argument is not supported for ImmutableVar." + ) + + from reflex.utils import format + + # Check for none values. + if value is None: + return None + + # If the value is already a var, do nothing. + if isinstance(value, Var): + return value + + # Try to pull the imports and hooks from contained values. + if not isinstance(value, str): + _var_data = VarData.merge(*_extract_var_data(value), _var_data) + + # Try to serialize the value. + type_ = type(value) + if type_ in types.JSONType: + name = value + else: + name, _serialized_type = serializers.serialize(value, get_type=True) + if name is None: + raise VarTypeError( + f"No JSON serializer found for var {value} of type {type_}." + ) + name = name if isinstance(name, str) else format.json_dumps(name) + + return ImmutableVar( + _var_name=name, + _var_type=type_, + _var_data=_var_data, + )