From c985f418a80f6ae6a40b6e36c87bfca6f669ea0f Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 2 Jul 2024 21:47:09 +0300 Subject: [PATCH 1/9] add immutable var class --- reflex/experimental/__init__.py | 1 + reflex/experimental/vars/__init__.py | 0 reflex/experimental/vars/base.py | 73 ++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 reflex/experimental/vars/__init__.py create mode 100644 reflex/experimental/vars/base.py diff --git a/reflex/experimental/__init__.py b/reflex/experimental/__init__.py index f0eca0c845..c04ce023da 100644 --- a/reflex/experimental/__init__.py +++ b/reflex/experimental/__init__.py @@ -42,6 +42,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..e69de29bb2 diff --git a/reflex/experimental/vars/base.py b/reflex/experimental/vars/base.py new file mode 100644 index 0000000000..41afad5f42 --- /dev/null +++ b/reflex/experimental/vars/base.py @@ -0,0 +1,73 @@ +import dataclasses +import sys +from typing import Any, Literal, Optional, Type, Union, get_args, get_origin +from reflex.utils import types +from reflex.vars import Var, VarData + + +@dataclasses.dataclass( + eq=False, + frozen=True, + **{"slots": True} if sys.version_info >= (3, 10) else {}, +) +class ImmutableVar(Var): + # The name of the var. + _var_name: str = dataclasses.field() + + # The type of the var. + _var_type: Type = dataclasses.field(default=Any) + + # Whether this is a local javascript variable. + _var_is_local: bool = dataclasses.field(default=False) + + # Whether the var is a string literal. + _var_is_string: bool = dataclasses.field(default=False) + + # _var_full_name should be prefixed with _var_state + _var_full_name_needs_state_prefix: bool = dataclasses.field(default=False) + + # Extra metadata associated with the Var + _var_data: Optional[VarData] = dataclasses.field(default=None) + + def get_default_value(self) -> Any: + """Get the default value of the var. + + Returns: + The default value of the var. + + Raises: + ImportError: If the var is a dataframe and pandas is not installed. + """ + if types.is_optional(self._var_type): + return None + + type_ = ( + get_origin(self._var_type) + if types.is_generic_alias(self._var_type) + else self._var_type + ) + if type_ is Literal: + args = get_args(self._var_type) + return args[0] if args else None + if issubclass(type_, str): + return "" + if issubclass(type_, types.get_args(Union[int, float])): + return 0 + if issubclass(type_, bool): + return False + if issubclass(type_, list): + return [] + if issubclass(type_, dict): + return {} + if issubclass(type_, tuple): + return () + if types.is_dataframe(type_): + try: + import pandas as pd + + return pd.DataFrame() + except ImportError as e: + raise ImportError( + "Please install pandas to use dataframes in your app." + ) from e + return set() if issubclass(type_, set) else None From 20407a484af12c96807ed4bd1d859f7860ce85c1 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 2 Jul 2024 21:57:32 +0300 Subject: [PATCH 2/9] add missing docs --- reflex/experimental/vars/__init__.py | 1 + reflex/experimental/vars/base.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/reflex/experimental/vars/__init__.py b/reflex/experimental/vars/__init__.py index e69de29bb2..0111751359 100644 --- a/reflex/experimental/vars/__init__.py +++ b/reflex/experimental/vars/__init__.py @@ -0,0 +1 @@ +"""Experimental Immutable-Based Var System.""" diff --git a/reflex/experimental/vars/base.py b/reflex/experimental/vars/base.py index 41afad5f42..747bc7cc7c 100644 --- a/reflex/experimental/vars/base.py +++ b/reflex/experimental/vars/base.py @@ -1,6 +1,9 @@ +"""Collection of base classes.""" + import dataclasses import sys from typing import Any, Literal, Optional, Type, Union, get_args, get_origin + from reflex.utils import types from reflex.vars import Var, VarData @@ -11,6 +14,8 @@ **{"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() From 10a6a58422f150e01c65be196588ec843bb904e6 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 2 Jul 2024 22:02:04 +0300 Subject: [PATCH 3/9] override _replace --- reflex/experimental/vars/base.py | 60 +++++++++++--------------------- 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/reflex/experimental/vars/base.py b/reflex/experimental/vars/base.py index 747bc7cc7c..e2592f4c0e 100644 --- a/reflex/experimental/vars/base.py +++ b/reflex/experimental/vars/base.py @@ -2,7 +2,7 @@ import dataclasses import sys -from typing import Any, Literal, Optional, Type, Union, get_args, get_origin +from typing import Any, Literal, Optional, Self, Type, Union, get_args, get_origin from reflex.utils import types from reflex.vars import Var, VarData @@ -34,45 +34,27 @@ class ImmutableVar(Var): # Extra metadata associated with the Var _var_data: Optional[VarData] = dataclasses.field(default=None) - def get_default_value(self) -> Any: - """Get the default value of the var. + def _replace(self, merge_var_data=None, **kwargs: Any) -> Self: + """Make a copy of this Var with updated fields. - Returns: - The default value of the var. + Args: + merge_var_data: VarData to merge into the existing VarData. + **kwargs: Var fields to update. - Raises: - ImportError: If the var is a dataframe and pandas is not installed. + Returns: + A new ImmutableVar with the updated fields overwriting the corresponding fields in this Var. """ - if types.is_optional(self._var_type): - return None - - type_ = ( - get_origin(self._var_type) - if types.is_generic_alias(self._var_type) - else self._var_type + field_values = dict( + _var_name=kwargs.pop("_var_name", self._var_name), + _var_type=kwargs.pop("_var_type", self._var_type), + _var_is_local=kwargs.pop("_var_is_local", self._var_is_local), + _var_is_string=kwargs.pop("_var_is_string", self._var_is_string), + _var_full_name_needs_state_prefix=kwargs.pop( + "_var_full_name_needs_state_prefix", + self._var_full_name_needs_state_prefix, + ), + _var_data=VarData.merge( + kwargs.get("_var_data", self._var_data), merge_var_data + ), ) - if type_ is Literal: - args = get_args(self._var_type) - return args[0] if args else None - if issubclass(type_, str): - return "" - if issubclass(type_, types.get_args(Union[int, float])): - return 0 - if issubclass(type_, bool): - return False - if issubclass(type_, list): - return [] - if issubclass(type_, dict): - return {} - if issubclass(type_, tuple): - return () - if types.is_dataframe(type_): - try: - import pandas as pd - - return pd.DataFrame() - except ImportError as e: - raise ImportError( - "Please install pandas to use dataframes in your app." - ) from e - return set() if issubclass(type_, set) else None + return ImmutableVar(**field_values) From f0be8a692432b39cc16e0c0f0acd123c21b10ce8 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 2 Jul 2024 22:08:09 +0300 Subject: [PATCH 4/9] fix type imports --- reflex/experimental/vars/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/reflex/experimental/vars/base.py b/reflex/experimental/vars/base.py index e2592f4c0e..4ed804bdbb 100644 --- a/reflex/experimental/vars/base.py +++ b/reflex/experimental/vars/base.py @@ -2,9 +2,8 @@ import dataclasses import sys -from typing import Any, Literal, Optional, Self, Type, Union, get_args, get_origin +from typing import Any, Optional, Self, Type -from reflex.utils import types from reflex.vars import Var, VarData From fa8203b13cf314dccfa927a8724fc5f47eef4518 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 2 Jul 2024 22:16:02 +0300 Subject: [PATCH 5/9] override create as well --- reflex/experimental/vars/base.py | 78 +++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/reflex/experimental/vars/base.py b/reflex/experimental/vars/base.py index 4ed804bdbb..1a752d0b4d 100644 --- a/reflex/experimental/vars/base.py +++ b/reflex/experimental/vars/base.py @@ -4,7 +4,9 @@ import sys from typing import Any, Optional, Self, Type -from reflex.vars import Var, VarData +from reflex.utils import console, serializers, types +from reflex.utils.exceptions import VarTypeError +from reflex.vars import Var, VarData, _extract_var_data @dataclasses.dataclass( @@ -57,3 +59,77 @@ def _replace(self, merge_var_data=None, **kwargs: Any) -> Self: ), ) return ImmutableVar(**field_values) + + @classmethod + def create( + cls, + value: Any, + _var_is_local: bool = True, + _var_is_string: bool | None = None, + _var_data: Optional[VarData] = 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. + _var_is_string: Whether the var is a string literal. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The var. + + Raises: + VarTypeError: If the value is JSON-unserializable. + """ + 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 ( + serialized_type is not None + and _var_is_string is None + and issubclass(serialized_type, str) + ): + _var_is_string = 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) + + if _var_is_string is None and type_ is str: + console.deprecate( + feature_name=f"Creating a Var ({value}) from a string without specifying _var_is_string", + reason=( + "Specify _var_is_string=False to create a Var that is not a string literal. " + "In the future, creating a Var from a string will be treated as a string literal " + "by default." + ), + deprecation_version="0.5.4", + removal_version="0.6.0", + ) + + return ImmutableVar( + _var_name=name, + _var_type=type_, + _var_is_local=_var_is_local, + _var_is_string=_var_is_string if _var_is_string is not None else False, + _var_data=_var_data, + ) From 03da500754e0bf231660dfda5dbb783ce50db843 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 2 Jul 2024 23:40:19 +0300 Subject: [PATCH 6/9] remove deprecated properties and arguments --- reflex/experimental/vars/base.py | 75 ++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/reflex/experimental/vars/base.py b/reflex/experimental/vars/base.py index 1a752d0b4d..3304821ab3 100644 --- a/reflex/experimental/vars/base.py +++ b/reflex/experimental/vars/base.py @@ -1,5 +1,7 @@ """Collection of base classes.""" +from __future__ import annotations + import dataclasses import sys from typing import Any, Optional, Self, Type @@ -23,17 +25,35 @@ class ImmutableVar(Var): # The type of the var. _var_type: Type = dataclasses.field(default=Any) - # Whether this is a local javascript variable. - _var_is_local: bool = dataclasses.field(default=False) + # Extra metadata associated with the Var + _var_data: Optional[VarData] = dataclasses.field(default=None) - # Whether the var is a string literal. - _var_is_string: bool = dataclasses.field(default=False) + @property + def _var_is_local(self) -> bool: + """Whether this is a local javascript variable. - # _var_full_name should be prefixed with _var_state - _var_full_name_needs_state_prefix: bool = dataclasses.field(default=False) + Returns: + False + """ + return False - # Extra metadata associated with the Var - _var_data: Optional[VarData] = dataclasses.field(default=None) + @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) -> Self: """Make a copy of this Var with updated fields. @@ -44,7 +64,25 @@ def _replace(self, merge_var_data=None, **kwargs: Any) -> Self: 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", None) is not None: + raise TypeError( + "The _var_is_local argument is not supported for ImmutableVar." + ) + + if kwargs.get("_var_is_string", None) is not None: + raise TypeError( + "The _var_is_string argument is not supported for ImmutableVar." + ) + + if kwargs.get("_var_full_name_needs_state_prefix", None) is not None: + 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), @@ -64,16 +102,16 @@ def _replace(self, merge_var_data=None, **kwargs: Any) -> Self: def create( cls, value: Any, - _var_is_local: bool = True, + _var_is_local: bool | None = None, _var_is_string: bool | None = None, - _var_data: Optional[VarData] = 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. - _var_is_string: Whether the var is a string literal. + _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: @@ -81,7 +119,18 @@ def create( 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. @@ -129,7 +178,5 @@ def create( return ImmutableVar( _var_name=name, _var_type=type_, - _var_is_local=_var_is_local, - _var_is_string=_var_is_string if _var_is_string is not None else False, _var_data=_var_data, ) From 852c473f22725049aa992ffb0a611c821431cb65 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 3 Jul 2024 01:30:26 +0300 Subject: [PATCH 7/9] remove unused code in ImmutableVar --- reflex/experimental/vars/base.py | 34 +++++--------------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/reflex/experimental/vars/base.py b/reflex/experimental/vars/base.py index 3304821ab3..8b714d91c3 100644 --- a/reflex/experimental/vars/base.py +++ b/reflex/experimental/vars/base.py @@ -6,7 +6,7 @@ import sys from typing import Any, Optional, Self, Type -from reflex.utils import console, serializers, types +from reflex.utils import serializers, types from reflex.utils.exceptions import VarTypeError from reflex.vars import Var, VarData, _extract_var_data @@ -68,17 +68,17 @@ def _replace(self, merge_var_data=None, **kwargs: Any) -> Self: 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", None) 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", None) is not None: + 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", None) is not None: + 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." ) @@ -86,12 +86,6 @@ def _replace(self, merge_var_data=None, **kwargs: Any) -> Self: field_values = dict( _var_name=kwargs.pop("_var_name", self._var_name), _var_type=kwargs.pop("_var_type", self._var_type), - _var_is_local=kwargs.pop("_var_is_local", self._var_is_local), - _var_is_string=kwargs.pop("_var_is_string", self._var_is_string), - _var_full_name_needs_state_prefix=kwargs.pop( - "_var_full_name_needs_state_prefix", - self._var_full_name_needs_state_prefix, - ), _var_data=VarData.merge( kwargs.get("_var_data", self._var_data), merge_var_data ), @@ -150,31 +144,13 @@ def create( if type_ in types.JSONType: name = value else: - name, serialized_type = serializers.serialize(value, get_type=True) - if ( - serialized_type is not None - and _var_is_string is None - and issubclass(serialized_type, str) - ): - _var_is_string = True + 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) - if _var_is_string is None and type_ is str: - console.deprecate( - feature_name=f"Creating a Var ({value}) from a string without specifying _var_is_string", - reason=( - "Specify _var_is_string=False to create a Var that is not a string literal. " - "In the future, creating a Var from a string will be treated as a string literal " - "by default." - ), - deprecation_version="0.5.4", - removal_version="0.6.0", - ) - return ImmutableVar( _var_name=name, _var_type=type_, From a0fb6254ecc70ba39e246e4fd0c5179f3f7f8600 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 3 Jul 2024 20:48:20 +0300 Subject: [PATCH 8/9] fix namespace issue --- reflex/experimental/__init__.py | 1 + reflex/experimental/vars/__init__.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/reflex/experimental/__init__.py b/reflex/experimental/__init__.py index c04ce023da..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 diff --git a/reflex/experimental/vars/__init__.py b/reflex/experimental/vars/__init__.py index 0111751359..5f99f22929 100644 --- a/reflex/experimental/vars/__init__.py +++ b/reflex/experimental/vars/__init__.py @@ -1 +1,3 @@ """Experimental Immutable-Based Var System.""" + +from .base import ImmutableVar as ImmutableVar From e80592e67919d461bc8d5c806f483168ad447b7b Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 3 Jul 2024 20:53:13 +0300 Subject: [PATCH 9/9] no Self in 3.8 --- reflex/experimental/vars/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reflex/experimental/vars/base.py b/reflex/experimental/vars/base.py index 8b714d91c3..52bce91614 100644 --- a/reflex/experimental/vars/base.py +++ b/reflex/experimental/vars/base.py @@ -4,7 +4,7 @@ import dataclasses import sys -from typing import Any, Optional, Self, Type +from typing import Any, Optional, Type from reflex.utils import serializers, types from reflex.utils.exceptions import VarTypeError @@ -55,7 +55,7 @@ def _var_full_name_needs_state_prefix(self) -> bool: """ return False - def _replace(self, merge_var_data=None, **kwargs: Any) -> Self: + def _replace(self, merge_var_data=None, **kwargs: Any): """Make a copy of this Var with updated fields. Args: