diff --git a/integration/test_var_operations.py b/integration/test_var_operations.py index 5cf644ca2..5898e4e55 100644 --- a/integration/test_var_operations.py +++ b/integration/test_var_operations.py @@ -15,6 +15,8 @@ def VarOperations(): from typing import Dict, List import reflex as rx + from reflex.ivars.base import LiteralVar + from reflex.ivars.sequence import ArrayVar class VarOperationState(rx.State): int_var1: int = 10 @@ -29,8 +31,8 @@ def VarOperations(): str_var2: str = "second" str_var3: str = "ThIrD" str_var4: str = "a long string" - dict1: Dict = {1: 2} - dict2: Dict = {3: 4} + dict1: Dict[int, int] = {1: 2} + dict2: Dict[int, int] = {3: 4} html_str: str = "
hello
" app = rx.App(state=rx.State) @@ -547,29 +549,29 @@ def VarOperations(): "second", query=[VarOperationState.str_var2], ), - rx.text(rx.Var.range(2, 5).join(","), id="list_join_range1"), - rx.text(rx.Var.range(2, 10, 2).join(","), id="list_join_range2"), - rx.text(rx.Var.range(5, 0, -1).join(","), id="list_join_range3"), - rx.text(rx.Var.range(0, 3).join(","), id="list_join_range4"), + rx.text(ArrayVar.range(2, 5).join(","), id="list_join_range1"), + rx.text(ArrayVar.range(2, 10, 2).join(","), id="list_join_range2"), + rx.text(ArrayVar.range(5, 0, -1).join(","), id="list_join_range3"), + rx.text(ArrayVar.range(0, 3).join(","), id="list_join_range4"), rx.box( rx.foreach( - rx.Var.range(0, 2), + ArrayVar.range(0, 2), lambda x: rx.text(VarOperationState.list1[x], as_="p"), ), id="foreach_list_arg", ), rx.box( rx.foreach( - rx.Var.range(0, 2), + ArrayVar.range(0, 2), lambda x, ix: rx.text(VarOperationState.list1[ix], as_="p"), ), id="foreach_list_ix", ), rx.box( rx.foreach( - rx.Var.create_safe(list(range(0, 3))).to(List[int]), + LiteralVar.create(list(range(0, 3))).to(ArrayVar, List[int]), lambda x: rx.foreach( - rx.Var.range(x), + ArrayVar.range(x), lambda y: rx.text(VarOperationState.list1[y], as_="p"), ), ), @@ -783,6 +785,7 @@ def test_var_operations(driver, var_operations: AppHarness): ] for tag, expected in tests: + print(tag) assert driver.find_element(By.ID, tag).text == expected # Highlight component with var query (does not plumb ID) diff --git a/reflex/__init__.py b/reflex/__init__.py index faf3e087e..9364f02a2 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -338,6 +338,7 @@ _SUBMODULES: set[str] = { "testing", "utils", "vars", + "ivars", "config", "compiler", } diff --git a/reflex/__init__.pyi b/reflex/__init__.pyi index 8fe42cfb3..94103a1d0 100644 --- a/reflex/__init__.pyi +++ b/reflex/__init__.pyi @@ -12,6 +12,7 @@ from . import compiler as compiler from . import components as components from . import config as config from . import event as event +from . import ivars as ivars from . import model as model from . import style as style from . import testing as testing diff --git a/reflex/app.py b/reflex/app.py index 7e40a95bf..90c29fefa 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -527,9 +527,10 @@ class App(MiddlewareMixin, LifespanMixin, Base): self._enable_state() else: for var in component._get_vars(include_children=True): - if not var._var_data: + var_data = var._get_all_var_data() + if not var_data: continue - if not var._var_data.state: + if not var_data.state: continue self._enable_state() break diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 4345e244f..525f75cfb 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -17,6 +17,7 @@ from reflex.components.component import ( StatefulComponent, ) from reflex.config import get_config +from reflex.ivars.base import ImmutableVar from reflex.state import BaseState from reflex.style import SYSTEM_COLOR_MODE from reflex.utils.exec import is_prod_mode @@ -80,7 +81,7 @@ def _compile_contexts(state: Optional[Type[BaseState]], theme: Component | None) The compiled context file. """ appearance = getattr(theme, "appearance", None) - if appearance is None or Var.create_safe(appearance)._var_name == "inherit": + if appearance is None or str(ImmutableVar.create_safe(appearance)) == "inherit": appearance = SYSTEM_COLOR_MODE last_compiled_time = str(datetime.now()) diff --git a/reflex/components/base/app_wrap.py b/reflex/components/base/app_wrap.py index 76bf2d99b..7d6a085b4 100644 --- a/reflex/components/base/app_wrap.py +++ b/reflex/components/base/app_wrap.py @@ -2,7 +2,7 @@ from reflex.components.base.fragment import Fragment from reflex.components.component import Component -from reflex.vars import Var +from reflex.ivars.base import ImmutableVar class AppWrap(Fragment): @@ -15,6 +15,4 @@ class AppWrap(Fragment): Returns: A new AppWrap component containing {children}. """ - return super().create( - Var.create("{children}", _var_is_local=False, _var_is_string=False) - ) + return super().create(ImmutableVar.create("children")) diff --git a/reflex/components/base/bare.py b/reflex/components/base/bare.py index 0de7307db..8cc83b83e 100644 --- a/reflex/components/base/bare.py +++ b/reflex/components/base/bare.py @@ -7,13 +7,14 @@ from typing import Any, Iterator from reflex.components.component import Component from reflex.components.tags import Tag from reflex.components.tags.tagless import Tagless +from reflex.ivars.base import ImmutableVar from reflex.vars import Var class Bare(Component): """A component with no tag.""" - contents: Var[str] + contents: Var[Any] @classmethod def create(cls, contents: Any) -> Component: @@ -25,6 +26,8 @@ class Bare(Component): Returns: The component. """ + if isinstance(contents, ImmutableVar): + return cls(contents=contents) if isinstance(contents, Var) and contents._var_data: contents = contents.to(str) else: @@ -32,6 +35,8 @@ class Bare(Component): return cls(contents=contents) # type: ignore def _render(self) -> Tag: + if isinstance(self.contents, ImmutableVar): + return Tagless(contents=f"{{{str(self.contents)}}}") return Tagless(contents=str(self.contents)) def _get_vars(self, include_children: bool = False) -> Iterator[Var]: diff --git a/reflex/components/base/error_boundary.py b/reflex/components/base/error_boundary.py index e90f0ed63..058976498 100644 --- a/reflex/components/base/error_boundary.py +++ b/reflex/components/base/error_boundary.py @@ -9,6 +9,8 @@ from reflex.components.component import Component from reflex.components.el import div, p from reflex.constants import Hooks, Imports from reflex.event import EventChain, EventHandler +from reflex.ivars.base import ImmutableVar +from reflex.ivars.function import FunctionVar from reflex.utils.imports import ImportVar from reflex.vars import Var @@ -20,14 +22,14 @@ class ErrorBoundary(Component): tag = "ErrorBoundary" # Fired when the boundary catches an error. - on_error: EventHandler[lambda error, info: [error, info]] = Var.create_safe( # type: ignore - "logFrontendError", _var_is_string=False, _var_is_local=False - ).to(EventChain) + on_error: EventHandler[lambda error, info: [error, info]] = ImmutableVar( # type: ignore + "logFrontendError" + ).to(FunctionVar, EventChain) # Rendered instead of the children when an error is caught. - Fallback_component: Var[Component] = Var.create_safe( - "Fallback", _var_is_string=False, _var_is_local=False - ).to(Component) + Fallback_component: Var[Component] = ImmutableVar.create_safe("Fallback")._replace( + _var_type=Component + ) def add_imports(self) -> dict[str, list[ImportVar]]: """Add imports for the component. @@ -56,7 +58,7 @@ class ErrorBoundary(Component): fallback_container = div( p("Ooops...Unknown Reflex error has occured:"), p( - Var.create("error.message", _var_is_local=False, _var_is_string=False), + ImmutableVar.create("error.message"), color="red", ), p("Please contact the support."), diff --git a/reflex/components/base/script.py b/reflex/components/base/script.py index bef2f036b..7b0596966 100644 --- a/reflex/components/base/script.py +++ b/reflex/components/base/script.py @@ -9,6 +9,7 @@ from typing import Literal from reflex.components.component import Component from reflex.event import EventHandler +from reflex.ivars.base import LiteralVar from reflex.vars import Var @@ -31,7 +32,7 @@ class Script(Component): # When the script will execute: afterInteractive (defer-like behavior) | beforeInteractive | lazyOnload (async-like behavior) strategy: Var[Literal["afterInteractive", "beforeInteractive", "lazyOnload"]] = ( - Var.create_safe("afterInteractive", _var_is_string=True) + LiteralVar.create("afterInteractive") ) # Triggered when the script is loading diff --git a/reflex/components/chakra/base.py b/reflex/components/chakra/base.py index 0b6b97b61..bd55a9415 100644 --- a/reflex/components/chakra/base.py +++ b/reflex/components/chakra/base.py @@ -6,6 +6,7 @@ from functools import lru_cache from typing import List, Literal from reflex.components.component import Component +from reflex.ivars.base import ImmutableVar from reflex.utils.imports import ImportDict, ImportVar from reflex.vars import Var @@ -66,9 +67,7 @@ class ChakraProvider(ChakraComponent): A new ChakraProvider component. """ return super().create( - theme=Var.create( - "extendTheme(theme)", _var_is_local=False, _var_is_string=False - ), + theme=ImmutableVar.create("extendTheme(theme)"), ) def add_imports(self) -> ImportDict: diff --git a/reflex/components/chakra/forms/checkbox.py b/reflex/components/chakra/forms/checkbox.py index 7b5ed7b50..bf38b8efe 100644 --- a/reflex/components/chakra/forms/checkbox.py +++ b/reflex/components/chakra/forms/checkbox.py @@ -8,6 +8,7 @@ from reflex.components.chakra import ( LiteralTagSize, ) from reflex.event import EventHandler +from reflex.ivars.base import LiteralVar from reflex.vars import Var @@ -50,7 +51,7 @@ class Checkbox(ChakraComponent): name: Var[str] # The value of the input field when checked (use is_checked prop for a bool) - value: Var[str] = Var.create("true", _var_is_string=True) # type: ignore + value: Var[str] = LiteralVar.create("true") # The spacing between the checkbox and its label text (0.5rem) spacing: Var[str] diff --git a/reflex/components/chakra/media/image.py b/reflex/components/chakra/media/image.py index b68fe8e6a..6a4f59711 100644 --- a/reflex/components/chakra/media/image.py +++ b/reflex/components/chakra/media/image.py @@ -7,6 +7,7 @@ from typing import Any, Optional from reflex.components.chakra import ChakraComponent, LiteralImageLoading from reflex.components.component import Component from reflex.event import EventHandler +from reflex.ivars.base import LiteralVar from reflex.vars import Var @@ -70,5 +71,5 @@ class Image(ChakraComponent): """ src = props.get("src", None) if src is not None and not isinstance(src, (Var)): - props["src"] = Var.create(value=src, _var_is_string=True) + props["src"] = LiteralVar.create(value=src) return super().create(*children, **props) diff --git a/reflex/components/component.py b/reflex/components/component.py index bb3e9053f..159c6e6ca 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -43,11 +43,12 @@ from reflex.event import ( call_event_handler, get_handler_args, ) +from reflex.ivars.base import ImmutableVar, LiteralVar from reflex.style import Style, format_as_emotion from reflex.utils import console, format, imports, types from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict, parse_imports from reflex.utils.serializers import serializer -from reflex.vars import BaseVar, Var, VarData +from reflex.vars import BaseVar, ImmutableVarData, Var, VarData class BaseComponent(Base, ABC): @@ -320,9 +321,8 @@ class Component(BaseComponent, ABC): # Set default values for any props. if types._issubclass(field.type_, Var): field.required = False - field.default = Var.create( - field.default, _var_is_string=isinstance(field.default, str) - ) + if field.default is not None: + field.default = LiteralVar.create(field.default) elif types._issubclass(field.type_, EventHandler): field.required = False @@ -351,10 +351,7 @@ class Component(BaseComponent, ABC): "id": kwargs.get("id"), "children": children, **{ - prop: Var.create( - kwargs[prop], - _var_is_string=False if isinstance(kwargs[prop], str) else None, - ) + prop: LiteralVar.create(kwargs[prop]) for prop in self.get_initial_props() if prop in kwargs }, @@ -401,10 +398,10 @@ class Component(BaseComponent, ABC): passed_types = None try: # Try to create a var from the value. - kwargs[key] = Var.create( - value, - _var_is_string=False if isinstance(value, str) else None, - ) + if isinstance(value, Var): + kwargs[key] = value + else: + kwargs[key] = LiteralVar.create(value) # Check that the var type is not None. if kwargs[key] is None: @@ -448,7 +445,6 @@ class Component(BaseComponent, ABC): raise TypeError( f"Invalid var passed for prop {type(self).__name__}.{key}, expected type {expected_type}, got value {value_name} of type {passed_types or passed_type}." ) - # Check if the key is an event trigger. if key in component_specific_triggers: # Temporarily disable full control for event triggers. @@ -692,9 +688,7 @@ class Component(BaseComponent, ABC): # Add ref to element if `id` is not None. ref = self.get_ref() if ref is not None: - props["ref"] = Var.create( - ref, _var_is_local=False, _var_is_string=False - ) + props["ref"] = ImmutableVar.create(ref) else: props = props.copy() @@ -809,7 +803,7 @@ class Component(BaseComponent, ABC): else ( Fragment.create(*child) if isinstance(child, tuple) - else Bare.create(contents=Var.create(child, _var_is_string=True)) + else Bare.create(contents=LiteralVar.create(child)) ) ) for child in children @@ -936,7 +930,12 @@ class Component(BaseComponent, ABC): """ if isinstance(self.style, Var): return {"css": self.style} - return {"css": Var.create(format_as_emotion(self.style))} + emotion_style = format_as_emotion(self.style) + return ( + {"css": LiteralVar.create(emotion_style)} + if emotion_style is not None + else {} + ) def render(self) -> Dict: """Render the component. @@ -1091,10 +1090,10 @@ class Component(BaseComponent, ABC): # Style keeps track of its own VarData instance, so embed in a temp Var that is yielded. if isinstance(self.style, dict) and self.style or isinstance(self.style, Var): vars.append( - BaseVar( + ImmutableVar( _var_name="style", _var_type=str, - _var_data=self.style._var_data, + _var_data=ImmutableVarData.merge(self.style._var_data), ) ) @@ -1113,10 +1112,8 @@ class Component(BaseComponent, ABC): vars.append(comp_prop) elif isinstance(comp_prop, str): # Collapse VarData encoded in f-strings. - var = Var.create_safe( - comp_prop, _var_is_string=isinstance(comp_prop, str) - ) - if var._var_data is not None: + var = LiteralVar.create(comp_prop) + if var._get_all_var_data() is not None: vars.append(var) # Get Vars associated with children. @@ -1358,8 +1355,9 @@ class Component(BaseComponent, ABC): event_imports = Imports.EVENTS if self.event_triggers else {} # Collect imports from Vars used directly by this component. + var_datas = [var._get_all_var_data() for var in self._get_vars()] var_imports = [ - var._var_data.imports for var in self._get_vars() if var._var_data + var_data.imports for var_data in var_datas if var_data is not None ] added_import_dicts: list[ParsedImportDict] = [] @@ -1427,7 +1425,7 @@ class Component(BaseComponent, ABC): """ ref = self.get_ref() if ref is not None: - return f"const {ref} = useRef(null); {str(Var.create_safe(ref, _var_is_string=False).as_ref())} = {ref};" + return f"const {ref} = useRef(null); {str(ImmutableVar.create_safe(ref).as_ref())} = {ref};" def _get_vars_hooks(self) -> dict[str, None]: """Get the hooks required by vars referenced in this component. @@ -1437,8 +1435,13 @@ class Component(BaseComponent, ABC): """ vars_hooks = {} for var in self._get_vars(): - if var._var_data: - vars_hooks.update(var._var_data.hooks) + var_data = var._get_all_var_data() + if var_data is not None: + vars_hooks.update( + var_data.hooks + if isinstance(var_data.hooks, dict) + else {k: None for k in var_data.hooks} + ) return vars_hooks def _get_events_hooks(self) -> dict[str, None]: @@ -1487,11 +1490,12 @@ class Component(BaseComponent, ABC): def extract_var_hooks(hook: Var): _imports = {} - if hook._var_data is not None: - for sub_hook in hook._var_data.hooks: + var_data = VarData.merge(hook._get_all_var_data()) + if var_data is not None: + for sub_hook in var_data.hooks: code[sub_hook] = {} - if hook._var_data.imports: - _imports = hook._var_data.imports + if var_data.imports: + _imports = var_data.imports if str(hook) in code: code[str(hook)] = imports.merge_imports(code[str(hook)], _imports) else: @@ -1505,6 +1509,7 @@ class Component(BaseComponent, ABC): extract_var_hooks(hook) else: code[hook] = {} + return code def _get_hooks(self) -> str | None: @@ -1561,7 +1566,7 @@ class Component(BaseComponent, ABC): The ref name. """ # do not create a ref if the id is dynamic or unspecified - if self.id is None or isinstance(self.id, BaseVar): + if self.id is None or isinstance(self.id, (BaseVar, ImmutableVar)): return None return format.format_ref(self.id) @@ -1707,7 +1712,7 @@ class CustomComponent(Component): # Handle subclasses of Base. if isinstance(value, Base): - base_value = Var.create(value) + base_value = LiteralVar.create(value) # Track hooks and imports associated with Component instances. if base_value is not None and isinstance(value, Component): @@ -1721,7 +1726,7 @@ class CustomComponent(Component): else: value = base_value else: - value = Var.create(value, _var_is_string=isinstance(value, str)) + value = LiteralVar.create(value) # Set the prop. self.props[format.to_camel_case(key)] = value @@ -1800,19 +1805,19 @@ class CustomComponent(Component): """ return super()._render(props=self.props) - def get_prop_vars(self) -> List[BaseVar]: + def get_prop_vars(self) -> List[ImmutableVar]: """Get the prop vars. Returns: The prop vars. """ return [ - BaseVar( + ImmutableVar( _var_name=name, _var_type=( prop._var_type if types._isinstance(prop, Var) else type(prop) ), - ) + ).guess_type() for name, prop in self.props.items() ] @@ -1981,7 +1986,7 @@ class StatefulComponent(BaseComponent): if not should_memoize: # Determine if any Vars have associated data. for prop_var in component._get_vars(): - if prop_var._var_data: + if prop_var._get_all_var_data(): should_memoize = True break @@ -1996,7 +2001,7 @@ class StatefulComponent(BaseComponent): should_memoize = True break child = cls._child_var(child) - if isinstance(child, Var) and child._var_data: + if isinstance(child, Var) and child._get_all_var_data(): should_memoize = True break @@ -2187,12 +2192,12 @@ class StatefulComponent(BaseComponent): # Calculate Var dependencies accessed by the handler for useCallback dep array. var_deps = ["addEvents", "Event"] for arg in event_args: - if arg._var_data is None: + if arg._get_all_var_data() is None: continue - for hook in arg._var_data.hooks: + for hook in arg._get_all_var_data().hooks: var_deps.extend(cls._get_hook_deps(hook)) memo_var_data = VarData.merge( - *[var._var_data for var in event_args], + *[var._get_all_var_data() for var in event_args], VarData( imports={"react": [ImportVar(tag="useCallback")]}, ), @@ -2200,7 +2205,7 @@ class StatefulComponent(BaseComponent): # Store the memoized function name and hook code for this event trigger. trigger_memo[event_trigger] = ( - Var.create_safe(memo_name, _var_is_string=False)._replace( + ImmutableVar.create_safe(memo_name)._replace( _var_type=EventChain, merge_var_data=memo_var_data ), f"const {memo_name} = useCallback({rendered_chain}, [{', '.join(var_deps)}])", diff --git a/reflex/components/core/banner.py b/reflex/components/core/banner.py index c6b46696c..acdab19c5 100644 --- a/reflex/components/core/banner.py +++ b/reflex/components/core/banner.py @@ -19,47 +19,42 @@ from reflex.components.radix.themes.typography.text import Text from reflex.components.sonner.toast import Toaster, ToastProps from reflex.constants import Dirs, Hooks, Imports from reflex.constants.compiler import CompileVars +from reflex.ivars.base import ImmutableVar, LiteralVar +from reflex.ivars.function import FunctionStringVar +from reflex.ivars.number import BooleanVar +from reflex.ivars.sequence import LiteralArrayVar from reflex.utils.imports import ImportDict, ImportVar -from reflex.utils.serializers import serialize -from reflex.vars import Var, VarData +from reflex.vars import ImmutableVarData, Var, VarData connect_error_var_data: VarData = VarData( # type: ignore imports=Imports.EVENTS, hooks={Hooks.EVENTS: None}, ) -connect_errors: Var = Var.create_safe( +connect_errors: Var = ImmutableVar.create_safe( value=CompileVars.CONNECT_ERROR, - _var_is_local=True, - _var_is_string=False, _var_data=connect_error_var_data, ) -connection_error: Var = Var.create_safe( - value="(connectErrors.length > 0) ? connectErrors[connectErrors.length - 1].message : ''", - _var_is_local=False, - _var_is_string=False, +connection_error: Var = ImmutableVar.create_safe( + value="((connectErrors.length > 0) ? connectErrors[connectErrors.length - 1].message : '')", _var_data=connect_error_var_data, ) -connection_errors_count: Var = Var.create_safe( +connection_errors_count: Var = ImmutableVar.create_safe( value="connectErrors.length", - _var_is_string=False, - _var_is_local=False, _var_data=connect_error_var_data, ) -has_connection_errors: Var = Var.create_safe( - value="connectErrors.length > 0", - _var_is_string=False, +has_connection_errors: Var = ImmutableVar.create_safe( + value="(connectErrors.length > 0)", _var_data=connect_error_var_data, -).to(bool) +).to(BooleanVar) -has_too_many_connection_errors: Var = Var.create_safe( - value="connectErrors.length >= 2", - _var_is_string=False, +has_too_many_connection_errors: Var = ImmutableVar.create_safe( + value="(connectErrors.length >= 2)", _var_data=connect_error_var_data, -).to(bool) +).to(BooleanVar) class WebsocketTargetURL(Bare): @@ -77,13 +72,21 @@ class WebsocketTargetURL(Bare): } @classmethod - def create(cls) -> Component: + def create(cls) -> ImmutableVar: """Create a websocket target URL component. Returns: The websocket target URL component. """ - return super().create(contents="{getBackendURL(env.EVENT).href}") + return ImmutableVar( + _var_name="getBackendURL(env.EVENT).href", + _var_data=ImmutableVarData( + imports={ + "/env.json": [ImportVar(tag="env", is_default=True)], + f"/{Dirs.STATE_PATH}": [ImportVar(tag="getBackendURL")], + }, + ), + ) def default_connection_error() -> list[str | Var | Component]: @@ -112,24 +115,34 @@ class ConnectionToaster(Toaster): toast_id = "websocket-error" target_url = WebsocketTargetURL.create() props = ToastProps( # type: ignore - description=Var.create( - f"`Check if server is reachable at ${target_url}`", - _var_is_string=False, - _var_is_local=False, + description=LiteralVar.create( + f"Check if server is reachable at {target_url}", ), close_button=True, duration=120000, id=toast_id, ) - hook = Var.create_safe( - f""" -const toast_props = {serialize(props)}; -const [userDismissed, setUserDismissed] = useState(false); -useEffect(() => {{ - if ({has_too_many_connection_errors}) {{ + + individual_hooks = [ + f"const toast_props = {str(LiteralVar.create(props))};", + f"const [userDismissed, setUserDismissed] = useState(false);", + FunctionStringVar( + "useEffect", + _var_data=VarData( + imports={ + "react": ["useEffect", "useState"], + **dict(target_url._get_all_var_data().imports), # type: ignore + } + ), + ).call( + # TODO: This breaks the assumption that Vars are JS expressions + ImmutableVar.create_safe( + f""" +() => {{ + if ({str(has_too_many_connection_errors)}) {{ if (!userDismissed) {{ toast.error( - `Cannot connect to server: {connection_error}.`, + `Cannot connect to server: ${{{connection_error}}}.`, {{...toast_props, onDismiss: () => setUserDismissed(true)}}, ) }} @@ -137,20 +150,16 @@ useEffect(() => {{ toast.dismiss("{toast_id}"); setUserDismissed(false); // after reconnection reset dismissed state }} -}}, [{connect_errors}]);""", - _var_is_string=False, - ) - imports: ImportDict = { - "react": ["useEffect", "useState"], - **target_url._get_imports(), # type: ignore - } - hook._var_data = VarData.merge( - connect_errors._var_data, - VarData(imports=imports), - ) +}} +""" + ), + LiteralArrayVar([connect_errors]), + ), + ] + return [ Hooks.EVENTS, - hook, + *individual_hooks, ] @classmethod @@ -240,6 +249,7 @@ class WifiOffPulse(Icon): Returns: The icon component with default props applied. """ + pulse_var = ImmutableVar.create("pulse") return super().create( "wifi_off", color=props.pop("color", "crimson"), @@ -248,7 +258,7 @@ class WifiOffPulse(Icon): position=props.pop("position", "fixed"), bottom=props.pop("botton", "33px"), right=props.pop("right", "33px"), - animation=Var.create(f"${{pulse}} 1s infinite", _var_is_string=True), + animation=LiteralVar.create(f"{pulse_var} 1s infinite"), **props, ) diff --git a/reflex/components/core/cond.py b/reflex/components/core/cond.py index 5d9ab7544..0f1376d3c 100644 --- a/reflex/components/core/cond.py +++ b/reflex/components/core/cond.py @@ -2,17 +2,17 @@ from __future__ import annotations -from typing import Any, Dict, Optional, Union, overload +from typing import Any, Dict, Optional, overload from reflex.components.base.fragment import Fragment from reflex.components.component import BaseComponent, Component, MemoizationLeaf from reflex.components.tags import CondTag, Tag from reflex.constants import Dirs -from reflex.constants.colors import Color +from reflex.ivars.base import ImmutableVar, LiteralVar +from reflex.ivars.number import TernaryOperator from reflex.style import LIGHT_COLOR_MODE, resolved_color_mode -from reflex.utils import format from reflex.utils.imports import ImportDict, ImportVar -from reflex.vars import BaseVar, Var, VarData +from reflex.vars import Var, VarData _IS_TRUE_IMPORT: ImportDict = { f"/{Dirs.STATE_PATH}": [ImportVar(tag="isTrue")], @@ -118,10 +118,10 @@ def cond(condition: Any, c1: Component) -> Component: ... @overload -def cond(condition: Any, c1: Any, c2: Any) -> BaseVar: ... +def cond(condition: Any, c1: Any, c2: Any) -> ImmutableVar: ... -def cond(condition: Any, c1: Any, c2: Any = None): +def cond(condition: Any, c1: Any, c2: Any = None) -> Component | ImmutableVar: """Create a conditional component or Prop. Args: @@ -135,14 +135,8 @@ def cond(condition: Any, c1: Any, c2: Any = None): Raises: ValueError: If the arguments are invalid. """ - var_datas: list[VarData | None] = [ - VarData( # type: ignore - imports=_IS_TRUE_IMPORT, - ), - ] - # Convert the condition to a Var. - cond_var = Var.create(condition) + cond_var = LiteralVar.create(condition) assert cond_var is not None, "The condition must be set." # If the first component is a component, create a Cond component. @@ -151,8 +145,6 @@ def cond(condition: Any, c1: Any, c2: Any = None): c2, BaseComponent ), "Both arguments must be components." return Cond.create(cond_var, c1, c2) - if isinstance(c1, Var): - var_datas.append(c1._var_data) # Otherwise, create a conditional Var. # Check that the second argument is valid. @@ -160,37 +152,20 @@ def cond(condition: Any, c1: Any, c2: Any = None): raise ValueError("Both arguments must be props.") if c2 is None: raise ValueError("For conditional vars, the second argument must be set.") - if isinstance(c2, Var): - var_datas.append(c2._var_data) def create_var(cond_part): - return Var.create_safe( - cond_part, - _var_is_string=isinstance(cond_part, (str, Color)), - ) + return LiteralVar.create_safe(cond_part) # convert the truth and false cond parts into vars so the _var_data can be obtained. c1 = create_var(c1) c2 = create_var(c2) - var_datas.extend([c1._var_data, c2._var_data]) - - c1_type = c1._var_type if isinstance(c1, Var) else type(c1) - c2_type = c2._var_type if isinstance(c2, Var) else type(c2) - - var_type = c1_type if c1_type == c2_type else Union[c1_type, c2_type] # Create the conditional var. - return cond_var._replace( - _var_name=format.format_cond( - cond=cond_var._var_full_name, - true_value=c1, - false_value=c2, - is_prop=True, - ), - _var_type=var_type, - _var_is_local=False, - _var_full_name_needs_state_prefix=False, - merge_var_data=VarData.merge(*var_datas), + return TernaryOperator( + condition=cond_var, + if_true=c1, + if_false=c2, + _var_data=VarData(imports=_IS_TRUE_IMPORT), ) @@ -205,7 +180,7 @@ def color_mode_cond(light: Any, dark: Any = None) -> Var | Component: The conditional component or prop. """ return cond( - resolved_color_mode == Var.create(LIGHT_COLOR_MODE, _var_is_string=True), + resolved_color_mode == LiteralVar.create(LIGHT_COLOR_MODE), light, dark, ) diff --git a/reflex/components/core/foreach.py b/reflex/components/core/foreach.py index 44e0685dd..327680a2b 100644 --- a/reflex/components/core/foreach.py +++ b/reflex/components/core/foreach.py @@ -9,6 +9,7 @@ from reflex.components.base.fragment import Fragment from reflex.components.component import Component from reflex.components.tags import IterTag from reflex.constants import MemoizationMode +from reflex.ivars.base import ImmutableVar from reflex.state import ComponentState from reflex.utils import console from reflex.vars import Var @@ -61,7 +62,7 @@ class Foreach(Component): deprecation_version="0.5.0", removal_version="0.6.0", ) - iterable = Var.create_safe(iterable, _var_is_string=False) + iterable = ImmutableVar.create_safe(iterable) if iterable._var_type == Any: raise ForeachVarError( f"Could not foreach over var `{iterable._var_full_name}` of type Any. " diff --git a/reflex/components/core/match.py b/reflex/components/core/match.py index e85739605..a88757263 100644 --- a/reflex/components/core/match.py +++ b/reflex/components/core/match.py @@ -5,8 +5,8 @@ from typing import Any, Dict, List, Optional, Tuple, Union from reflex.components.base import Fragment from reflex.components.component import BaseComponent, Component, MemoizationLeaf -from reflex.components.core.colors import Color from reflex.components.tags import MatchTag, Tag +from reflex.ivars.base import LiteralVar from reflex.style import Style from reflex.utils import format, types from reflex.utils.exceptions import MatchTypeError @@ -68,7 +68,7 @@ class Match(MemoizationLeaf): Raises: ValueError: If the condition is not provided. """ - match_cond_var = Var.create(cond, _var_is_string=isinstance(cond, str)) + match_cond_var = LiteralVar.create(cond) if match_cond_var is None: raise ValueError("The condition must be set") @@ -118,12 +118,11 @@ class Match(MemoizationLeaf): The case element Var. """ _var_data = case_element._var_data if isinstance(case_element, Style) else None # type: ignore - case_element = Var.create( - case_element, - _var_is_string=isinstance(case_element, (str, Color)), - ) + case_element = LiteralVar.create(case_element) if _var_data is not None: - case_element._var_data = VarData.merge(case_element._var_data, _var_data) # type: ignore + case_element._var_data = VarData.merge( + case_element._get_all_var_data(), _var_data + ) # type: ignore return case_element @classmethod diff --git a/reflex/components/datadisplay/code.py b/reflex/components/datadisplay/code.py index 47cba3736..56ea1fd9a 100644 --- a/reflex/components/datadisplay/code.py +++ b/reflex/components/datadisplay/code.py @@ -12,6 +12,7 @@ from reflex.components.radix.themes.components.button import Button from reflex.components.radix.themes.layout.box import Box from reflex.constants.colors import Color from reflex.event import set_clipboard +from reflex.ivars.base import ImmutableVar, LiteralVar from reflex.style import Style from reflex.utils import format from reflex.utils.imports import ImportDict, ImportVar @@ -484,7 +485,7 @@ class CodeBlock(Component): if children: props["code"] = children[0] if not isinstance(props["code"], Var): - props["code"] = Var.create(props["code"], _var_is_string=True) + props["code"] = LiteralVar.create(props["code"]) # Create the component. code_block = super().create( @@ -505,10 +506,8 @@ class CodeBlock(Component): out = super()._render() predicate, qmark, value = self.theme._var_name.partition("?") out.add_props( - style=Var.create( + style=ImmutableVar.create( format.to_camel_case(f"{predicate}{qmark}{value.replace('`', '')}"), - _var_is_local=False, - _var_is_string=False, ) ).remove_props("theme", "code") if self.code is not None: diff --git a/reflex/components/datadisplay/dataeditor.py b/reflex/components/datadisplay/dataeditor.py index c6d9c1981..f3296e0b7 100644 --- a/reflex/components/datadisplay/dataeditor.py +++ b/reflex/components/datadisplay/dataeditor.py @@ -9,6 +9,7 @@ from reflex.base import Base from reflex.components.component import Component, NoSSRComponent from reflex.components.literals import LiteralRowMarker from reflex.event import EventHandler +from reflex.ivars.base import ImmutableVar from reflex.utils import console, format, types from reflex.utils.imports import ImportDict, ImportVar from reflex.utils.serializers import serializer @@ -293,9 +294,7 @@ class DataEditor(NoSSRComponent): # Define the name of the getData callback associated with this component and assign to get_cell_content. data_callback = f"getData_{editor_id}" - self.get_cell_content = Var.create( - data_callback, _var_is_local=False, _var_is_string=False - ) # type: ignore + self.get_cell_content = ImmutableVar.create(data_callback) # type: ignore code = [f"function {data_callback}([col, row])" "{"] diff --git a/reflex/components/el/elements/forms.py b/reflex/components/el/elements/forms.py index cdad212aa..5c78816a2 100644 --- a/reflex/components/el/elements/forms.py +++ b/reflex/components/el/elements/forms.py @@ -11,13 +11,14 @@ from reflex.components.el.element import Element from reflex.components.tags.tag import Tag from reflex.constants import Dirs, EventTriggers from reflex.event import EventChain, EventHandler +from reflex.ivars.base import ImmutableVar from reflex.utils.format import format_event_chain from reflex.utils.imports import ImportDict from reflex.vars import BaseVar, Var from .base import BaseHTML -FORM_DATA = Var.create("form_data", _var_is_string=False) +FORM_DATA = ImmutableVar.create("form_data") HANDLE_SUBMIT_JS_JINJA2 = Environment().from_string( """ const handleSubmit_{{ handle_submit_unique_name }} = useCallback((ev) => { diff --git a/reflex/components/markdown/markdown.py b/reflex/components/markdown/markdown.py index e62c85af3..536b43930 100644 --- a/reflex/components/markdown/markdown.py +++ b/reflex/components/markdown/markdown.py @@ -17,6 +17,7 @@ from reflex.components.radix.themes.typography.heading import Heading from reflex.components.radix.themes.typography.link import Link from reflex.components.radix.themes.typography.text import Text from reflex.components.tags.tag import Tag +from reflex.ivars.base import LiteralVar from reflex.utils import types from reflex.utils.imports import ImportDict, ImportVar from reflex.vars import Var @@ -287,7 +288,7 @@ class Markdown(Component): function {self._get_component_map_name()} () {{ {formatted_hooks} return ( - {str(Var.create(self.format_component_map()))} + {str(LiteralVar.create(self.format_component_map()))} ) }} """ diff --git a/reflex/components/next/image.py b/reflex/components/next/image.py index 108d17452..ed9e534fb 100644 --- a/reflex/components/next/image.py +++ b/reflex/components/next/image.py @@ -3,6 +3,7 @@ from typing import Any, Literal, Optional, Union from reflex.event import EventHandler +from reflex.ivars.base import LiteralVar from reflex.utils import types from reflex.vars import Var @@ -104,6 +105,6 @@ class Image(NextComponent): src = props.get("src", None) if src is not None and not isinstance(src, (Var)): - props["src"] = Var.create(value=src, _var_is_string=True) + props["src"] = LiteralVar.create(src) return super().create(*children, **props) diff --git a/reflex/components/radix/primitives/accordion.py b/reflex/components/radix/primitives/accordion.py index b93bbf284..7c6ab3c63 100644 --- a/reflex/components/radix/primitives/accordion.py +++ b/reflex/components/radix/primitives/accordion.py @@ -11,6 +11,7 @@ from reflex.components.lucide.icon import Icon from reflex.components.radix.primitives.base import RadixPrimitiveComponent from reflex.components.radix.themes.base import LiteralAccentColor, LiteralRadius from reflex.event import EventHandler +from reflex.ivars.base import LiteralVar from reflex.style import Style from reflex.vars import Var, get_uuid_string_var @@ -464,14 +465,12 @@ to { Returns: The style of the component. """ - slideDown = Var.create( + slideDown = LiteralVar.create( f"${{slideDown}} var(--animation-duration) var(--animation-easing)", - _var_is_string=True, ) - slideUp = Var.create( + slideUp = LiteralVar.create( f"${{slideUp}} var(--animation-duration) var(--animation-easing)", - _var_is_string=True, ) return { diff --git a/reflex/components/radix/themes/base.py b/reflex/components/radix/themes/base.py index e0e05cc81..8ec509ab8 100644 --- a/reflex/components/radix/themes/base.py +++ b/reflex/components/radix/themes/base.py @@ -7,6 +7,7 @@ from typing import Any, Dict, Literal from reflex.components import Component from reflex.components.tags import Tag from reflex.config import get_config +from reflex.ivars.base import ImmutableVar from reflex.utils.imports import ImportDict, ImportVar from reflex.vars import Var @@ -230,10 +231,8 @@ class Theme(RadixThemesComponent): def _render(self, props: dict[str, Any] | None = None) -> Tag: tag = super()._render(props) tag.add_props( - css=Var.create( - "{{...theme.styles.global[':root'], ...theme.styles.global.body}}", - _var_is_local=False, - _var_is_string=False, + css=ImmutableVar.create( + f"{{...theme.styles.global[':root'], ...theme.styles.global.body}}" ), ) return tag diff --git a/reflex/components/radix/themes/color_mode.py b/reflex/components/radix/themes/color_mode.py index 8243a822a..f0ef477cc 100644 --- a/reflex/components/radix/themes/color_mode.py +++ b/reflex/components/radix/themes/color_mode.py @@ -17,7 +17,6 @@ rx.text( from __future__ import annotations -import dataclasses from typing import Literal, get_args from reflex.components.component import BaseComponent @@ -25,6 +24,7 @@ from reflex.components.core.cond import Cond, color_mode_cond, cond from reflex.components.lucide.icon import Icon from reflex.components.radix.themes.components.dropdown_menu import dropdown_menu from reflex.components.radix.themes.components.switch import Switch +from reflex.ivars.base import ImmutableVar from reflex.style import ( LIGHT_COLOR_MODE, color_mode, @@ -33,7 +33,7 @@ from reflex.style import ( toggle_color_mode, ) from reflex.utils import console -from reflex.vars import BaseVar, Var +from reflex.vars import Var from .components.icon_button import IconButton @@ -195,7 +195,7 @@ class ColorModeSwitch(Switch): ) -class ColorModeNamespace(BaseVar): +class ColorModeNamespace(ImmutableVar): """Namespace for color mode components.""" icon = staticmethod(ColorModeIcon.create) @@ -204,5 +204,7 @@ class ColorModeNamespace(BaseVar): color_mode = color_mode_var_and_namespace = ColorModeNamespace( - **dataclasses.asdict(color_mode) + _var_name=color_mode._var_name, + _var_type=color_mode._var_type, + _var_data=color_mode._var_data, ) diff --git a/reflex/components/radix/themes/components/radio_group.py b/reflex/components/radix/themes/components/radio_group.py index 20ccbd5b5..c159742d7 100644 --- a/reflex/components/radix/themes/components/radio_group.py +++ b/reflex/components/radix/themes/components/radio_group.py @@ -10,6 +10,8 @@ from reflex.components.core.breakpoints import Responsive from reflex.components.radix.themes.layout.flex import Flex from reflex.components.radix.themes.typography.text import Text from reflex.event import EventHandler +from reflex.ivars.base import ImmutableVar, LiteralVar +from reflex.ivars.function import JSON_STRINGIFY from reflex.vars import Var from ..base import ( @@ -147,28 +149,24 @@ class HighLevelRadioGroup(RadixThemesComponent): color_scheme = props.pop("color_scheme", None) default_value = props.pop("default_value", "") - default_value = Var.create(default_value, _var_is_string=True) + default_value = LiteralVar.create(default_value) # convert only non-strings to json(JSON.stringify) so quotes are not rendered # for string literal types. if isinstance(default_value, str) or ( isinstance(default_value, Var) and default_value._var_type is str ): - default_value = Var.create(default_value, _var_is_string=True) # type: ignore + default_value = LiteralVar.create(default_value) # type: ignore else: - default_value = ( - Var.create(default_value, _var_is_string=False) - .to_string() # type: ignore - ._replace(_var_is_local=False) - ) + default_value = JSON_STRINGIFY.call(ImmutableVar.create(default_value)) def radio_group_item(value: str | Var) -> Component: item_value = Var.create(value, _var_is_string=False) # type: ignore item_value = rx.cond( item_value._type() == str, # type: ignore item_value, - item_value.to_string()._replace(_var_is_local=False), # type: ignore - )._replace(_var_type=str) + JSON_STRINGIFY.call(item_value), # type: ignore + ) return Text.create( Flex.create( diff --git a/reflex/components/tags/iter_tag.py b/reflex/components/tags/iter_tag.py index 95a496b71..0828f8b10 100644 --- a/reflex/components/tags/iter_tag.py +++ b/reflex/components/tags/iter_tag.py @@ -6,7 +6,8 @@ import inspect from typing import TYPE_CHECKING, Any, Callable, List, Tuple, Type, Union, get_args from reflex.components.tags.tag import Tag -from reflex.vars import BaseVar, Var +from reflex.ivars.base import ImmutableVar +from reflex.vars import Var if TYPE_CHECKING: from reflex.components.component import Component @@ -53,10 +54,10 @@ class IterTag(Tag): Returns: The index var. """ - return BaseVar( + return ImmutableVar( _var_name=self.index_var_name, _var_type=int, - ) + ).guess_type() def get_arg_var(self) -> Var: """Get the arg var for the tag (with curly braces). @@ -66,10 +67,10 @@ class IterTag(Tag): Returns: The arg var. """ - return BaseVar( + return ImmutableVar( _var_name=self.arg_var_name, _var_type=self.get_iterable_var_type(), - ) + ).guess_type() def get_index_var_arg(self) -> Var: """Get the index var for the tag (without curly braces). @@ -79,11 +80,10 @@ class IterTag(Tag): Returns: The index var. """ - return BaseVar( + return ImmutableVar( _var_name=self.index_var_name, _var_type=int, - _var_is_local=True, - ) + ).guess_type() def get_arg_var_arg(self) -> Var: """Get the arg var for the tag (without curly braces). @@ -93,11 +93,10 @@ class IterTag(Tag): Returns: The arg var. """ - return BaseVar( + return ImmutableVar( _var_name=self.arg_var_name, _var_type=self.get_iterable_var_type(), - _var_is_local=True, - ) + ).guess_type() def render_component(self) -> Component: """Render the component. diff --git a/reflex/components/tags/tag.py b/reflex/components/tags/tag.py index bacea6a41..5a26a164f 100644 --- a/reflex/components/tags/tag.py +++ b/reflex/components/tags/tag.py @@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Union from reflex.base import Base from reflex.event import EventChain +from reflex.ivars.base import ImmutableVar, LiteralVar from reflex.utils import format, types from reflex.vars import Var @@ -41,7 +42,7 @@ class Tag(Base): # Convert any props to vars. if "props" in kwargs: kwargs["props"] = { - name: Var.create(value, _var_is_string=False) + name: ImmutableVar.create(value) for name, value in kwargs["props"].items() } super().__init__(*args, **kwargs) @@ -63,14 +64,12 @@ class Tag(Base): Returns: The tag with the props added. """ - from reflex.components.core.colors import Color - self.props.update( { - format.to_camel_case(name, allow_hyphens=True): prop - if types._isinstance(prop, Union[EventChain, dict]) - else Var.create( - prop, _var_is_string=isinstance(prop, Color) + format.to_camel_case(name, allow_hyphens=True): ( + prop + if types._isinstance(prop, Union[EventChain, dict]) + else LiteralVar.create(prop) ) # rx.color is always a string for name, prop in kwargs.items() if self.is_valid_prop(prop) diff --git a/reflex/event.py b/reflex/event.py index 7e98ade38..1c43334a1 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -18,9 +18,12 @@ from typing import ( from reflex import constants from reflex.base import Base +from reflex.ivars.base import ImmutableVar, LiteralVar +from reflex.ivars.function import FunctionStringVar, FunctionVar +from reflex.ivars.object import ObjectVar from reflex.utils import format from reflex.utils.types import ArgsSpec -from reflex.vars import BaseVar, Var +from reflex.vars import ImmutableVarData, Var try: from typing import Annotated @@ -186,7 +189,7 @@ class EventHandler(EventActionsMixin): # Get the function args. fn_args = inspect.getfullargspec(self.fn).args[1:] - fn_args = (Var.create_safe(arg, _var_is_string=False) for arg in fn_args) + fn_args = (ImmutableVar.create_safe(arg) for arg in fn_args) # Construct the payload. values = [] @@ -197,7 +200,7 @@ class EventHandler(EventActionsMixin): # Otherwise, convert to JSON. try: - values.append(Var.create(arg, _var_is_string=isinstance(arg, str))) + values.append(LiteralVar.create(arg)) except TypeError as e: raise EventHandlerTypeError( f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}." @@ -264,13 +267,13 @@ class EventSpec(EventActionsMixin): # Get the remaining unfilled function args. fn_args = inspect.getfullargspec(self.handler.fn).args[1 + len(self.args) :] - fn_args = (Var.create_safe(arg, _var_is_string=False) for arg in fn_args) + fn_args = (ImmutableVar.create_safe(arg) for arg in fn_args) # Construct the payload. values = [] for arg in args: try: - values.append(Var.create(arg, _var_is_string=isinstance(arg, str))) + values.append(LiteralVar.create(arg)) except TypeError as e: raise EventHandlerTypeError( f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}." @@ -388,15 +391,16 @@ class FileUpload(Base): upload_id = self.upload_id or DEFAULT_UPLOAD_ID spec_args = [ ( - Var.create_safe("files", _var_is_string=False), - Var.create_safe( - f"filesById[{Var.create_safe(upload_id, _var_is_string=True)._var_name_unwrapped}]", - _var_is_string=False, - )._replace(_var_data=upload_files_context_var_data), + ImmutableVar.create_safe("files"), + ImmutableVar( + _var_name="filesById", + _var_type=dict[str, Any], + _var_data=ImmutableVarData.merge(upload_files_context_var_data), + ).to(ObjectVar)[LiteralVar.create_safe(upload_id)], ), ( - Var.create_safe("upload_id", _var_is_string=False), - Var.create_safe(upload_id, _var_is_string=True), + ImmutableVar.create_safe("upload_id"), + LiteralVar.create_safe(upload_id), ), ] if self.on_upload_progress is not None: @@ -424,11 +428,10 @@ class FileUpload(Base): formatted_chain = str(format.format_prop(on_upload_progress_chain)) spec_args.append( ( - Var.create_safe("on_upload_progress", _var_is_string=False), - BaseVar( - _var_name=formatted_chain.strip("{}"), - _var_type=EventChain, - ), + ImmutableVar.create_safe("on_upload_progress"), + FunctionStringVar( + formatted_chain.strip("{}"), + ).to(FunctionVar, EventChain), ), ) return EventSpec( @@ -465,8 +468,8 @@ def server_side(name: str, sig: inspect.Signature, **kwargs) -> EventSpec: handler=EventHandler(fn=fn), args=tuple( ( - Var.create_safe(k, _var_is_string=False), - Var.create_safe(v, _var_is_string=isinstance(v, str)), + ImmutableVar.create_safe(k), + LiteralVar.create(v), ) for k, v in kwargs.items() ), @@ -542,7 +545,7 @@ def set_focus(ref: str) -> EventSpec: return server_side( "_set_focus", get_fn_signature(set_focus), - ref=Var.create_safe(format.format_ref(ref), _var_is_string=True), + ref=ImmutableVar.create_safe(format.format_ref(ref)), ) @@ -573,7 +576,7 @@ def set_value(ref: str, value: Any) -> EventSpec: return server_side( "_set_value", get_fn_signature(set_value), - ref=Var.create_safe(format.format_ref(ref), _var_is_string=True), + ref=ImmutableVar.create_safe(format.format_ref(ref)), value=value, ) @@ -757,11 +760,13 @@ def _callback_arg_spec(eval_result): def call_script( javascript_code: str | Var[str], - callback: EventSpec - | EventHandler - | Callable - | List[EventSpec | EventHandler | Callable] - | None = None, + callback: ( + EventSpec + | EventHandler + | Callable + | List[EventSpec | EventHandler | Callable] + | None + ) = None, ) -> EventSpec: """Create an event handler that executes arbitrary javascript code. @@ -865,10 +870,8 @@ def parse_args_spec(arg_spec: ArgsSpec): annotations = get_type_hints(arg_spec) return arg_spec( *[ - BaseVar( - _var_name=f"_{l_arg}", - _var_type=annotations.get(l_arg, FrontendEvent), - _var_is_local=True, + ImmutableVar(f"_{l_arg}").to( + ObjectVar, annotations.get(l_arg, FrontendEvent) ) for l_arg in spec.args ] diff --git a/reflex/experimental/__init__.py b/reflex/experimental/__init__.py index 060f7e7f1..f0eca0c84 100644 --- a/reflex/experimental/__init__.py +++ b/reflex/experimental/__init__.py @@ -8,7 +8,6 @@ from reflex.components.sonner.toast import toast as toast 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 @@ -43,7 +42,6 @@ _x = ExperimentalNamespace( 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/ivars/__init__.py similarity index 100% rename from reflex/experimental/vars/__init__.py rename to reflex/ivars/__init__.py diff --git a/reflex/experimental/vars/base.py b/reflex/ivars/base.py similarity index 54% rename from reflex/experimental/vars/base.py rename to reflex/ivars/base.py index 7da1e6537..dd12dcb07 100644 --- a/reflex/experimental/vars/base.py +++ b/reflex/ivars/base.py @@ -6,6 +6,7 @@ import dataclasses import functools import inspect import sys +import traceback from typing import ( TYPE_CHECKING, Any, @@ -13,12 +14,14 @@ from typing import ( Dict, Generic, List, + Literal, Optional, Set, Tuple, Type, TypeVar, Union, + get_args, overload, ) @@ -26,7 +29,7 @@ from typing_extensions import ParamSpec, get_origin from reflex import constants from reflex.base import Base -from reflex.utils import serializers, types +from reflex.utils import console, imports, serializers, types from reflex.utils.exceptions import VarTypeError from reflex.vars import ( ImmutableVarData, @@ -44,9 +47,16 @@ if TYPE_CHECKING: NumberVar, ToBooleanVarOperation, ToNumberVarOperation, + EqualOperation, + GreaterThanOperation, + GreaterThanOrEqualOperation, + LessThanOperation, + LessThanOrEqualOperation, ) from .object import ObjectVar, ToObjectOperation from .sequence import ArrayVar, StringVar, ToArrayOperation, ToStringOperation + from reflex.state import BaseState + VAR_TYPE = TypeVar("VAR_TYPE") @@ -376,10 +386,10 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): from .function import FunctionVar, ToFunctionOperation if issubclass(output, FunctionVar): - if fixed_type is not None and not issubclass(fixed_type, Callable): - raise TypeError( - f"Unsupported type {var_type} for FunctionVar. Must be Callable." - ) + # if fixed_type is not None and not issubclass(fixed_type, Callable): + # raise TypeError( + # f"Unsupported type {var_type} for FunctionVar. Must be Callable." + # ) return ToFunctionOperation(self, var_type or Callable) return output( @@ -405,6 +415,9 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): fixed_type = var_type if inspect.isclass(var_type) else get_origin(var_type) + if fixed_type is Union: + return self + if issubclass(fixed_type, (int, float)): return self.to(NumberVar, var_type) if issubclass(fixed_type, dict): @@ -417,6 +430,276 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): return self.to(ObjectVar, var_type) return self + 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 + + def get_setter_name(self, include_state: bool = True) -> str: + """Get the name of the var's generated setter function. + + Args: + include_state: Whether to include the state name in the setter name. + + Returns: + The name of the setter function. + """ + setter = constants.SETTER_PREFIX + self._var_name + if self._var_data is None: + return setter + if not include_state or self._var_data.state == "": + return setter + print("get_setter_name", self._var_data.state, setter) + return ".".join((self._var_data.state, setter)) + + def get_setter(self) -> Callable[[BaseState, Any], None]: + """Get the var's setter function. + + Returns: + A function that that creates a setter for the var. + """ + + def setter(state: BaseState, value: Any): + """Get the setter for the var. + + Args: + state: The state within which we add the setter function. + value: The value to set. + """ + if self._var_type in [int, float]: + try: + value = self._var_type(value) + setattr(state, self._var_name, value) + except ValueError: + console.debug( + f"{type(state).__name__}.{self._var_name}: Failed conversion of {value} to '{self._var_type.__name__}'. Value not set.", + ) + else: + setattr(state, self._var_name, value) + + setter.__qualname__ = self.get_setter_name() + + return setter + + def __eq__(self, other: Var | Any) -> BooleanVar: + """ + Check if the current variable is equal to the given variable. + + Args: + other (Var | Any): The variable to compare with. + + Returns: + BooleanVar: A BooleanVar object representing the result of the equality check. + """ + from .number import EqualOperation + + return EqualOperation(self, other) + + def __ne__(self, other: Var | Any) -> BooleanVar: + """ + Check if the current object is not equal to the given object. + + Parameters: + other (Var | Any): The object to compare with. + + Returns: + BooleanVar: A BooleanVar object representing the result of the comparison. + """ + from .number import EqualOperation + + return ~EqualOperation(self, other) + + def __gt__(self, other: Var | Any) -> BooleanVar: + """ + Compare the current instance with another variable and return a BooleanVar representing the result of the greater than operation. + + Args: + other (Var | Any): The variable to compare with. + + Returns: + BooleanVar: A BooleanVar representing the result of the greater than operation. + """ + from .number import GreaterThanOperation + + return GreaterThanOperation(self, other) + + def __ge__(self, other: Var | Any) -> BooleanVar: + """ + Check if the value of this variable is greater than or equal to the value of another variable or object. + + Args: + other (Var | Any): The variable or object to compare with. + + Returns: + BooleanVar: A BooleanVar object representing the result of the comparison. + """ + from .number import GreaterThanOrEqualOperation + + return GreaterThanOrEqualOperation(self, other) + + def __lt__(self, other: Var | Any) -> BooleanVar: + """ + Compare the current instance with another variable using the less than (<) operator. + + Args: + other: The variable to compare with. + + Returns: + A `BooleanVar` object representing the result of the comparison. + """ + from .number import LessThanOperation + + return LessThanOperation(self, other) + + def __le__(self, other: Var | Any) -> BooleanVar: + """ + Compare if the current instance is less than or equal to the given value. + + Args: + other: The value to compare with. + + Returns: + A BooleanVar object representing the result of the comparison. + """ + from .number import LessThanOrEqualOperation + + return LessThanOrEqualOperation(self, other) + + def bool(self) -> BooleanVar: + """Convert the var to a boolean. + + Returns: + The boolean var. + """ + from .number import ToBooleanVarOperation + + return ToBooleanVarOperation(self) + + def __and__(self, other: Var | Any) -> ImmutableVar: + """Perform a logical AND operation on the current instance and another variable. + + Args: + other: The variable to perform the logical AND operation with. + + Returns: + A `BooleanVar` object representing the result of the logical AND operation. + """ + + return AndOperation(self, other) + + def __rand__(self, other: Var | Any) -> ImmutableVar: + """Perform a logical AND operation on the current instance and another variable. + + Args: + other: The variable to perform the logical AND operation with. + + Returns: + A `BooleanVar` object representing the result of the logical AND operation. + """ + + return AndOperation(other, self) + + def __or__(self, other: Var | Any) -> ImmutableVar: + """Perform a logical OR operation on the current instance and another variable. + + Args: + other: The variable to perform the logical OR operation with. + + Returns: + A `BooleanVar` object representing the result of the logical OR operation. + """ + + return OrOperation(self, other) + + def __ror__(self, other: Var | Any) -> ImmutableVar: + """Perform a logical OR operation on the current instance and another variable. + + Args: + other: The variable to perform the logical OR operation with. + + Returns: + A `BooleanVar` object representing the result of the logical OR operation. + """ + + return OrOperation(other, self) + + def __invert__(self) -> BooleanVar: + """Perform a logical NOT operation on the current instance. + + Returns: + A `BooleanVar` object representing the result of the logical NOT operation. + """ + from .number import BooleanNotOperation + + return BooleanNotOperation(self.bool()) + + def to_string(self) -> ImmutableVar: + """Convert the var to a string. + + Returns: + The string var. + """ + from .function import JSON_STRINGIFY + + return JSON_STRINGIFY.call(self) + + def as_ref(self) -> ImmutableVar: + """Get a reference to the var. + + Returns: + The reference to the var. + """ + + from .object import ObjectVar + + refs = ImmutableVar( + _var_name="refs", + _var_data=ImmutableVarData( + imports={ + f"/{constants.Dirs.STATE_PATH}": [imports.ImportVar(tag="refs")] + } + ), + ).to(ObjectVar) + return refs[self] + OUTPUT = TypeVar("OUTPUT", bound=ImmutableVar) @@ -457,6 +740,9 @@ class LiteralVar(ImmutableVar): value.dict(), _var_type=type(value), _var_data=_var_data ) + if isinstance(value, dict): + return LiteralObjectVar(value, _var_data=_var_data) + from .number import LiteralBooleanVar, LiteralNumberVar from .sequence import LiteralArrayVar, LiteralStringVar @@ -467,7 +753,6 @@ class LiteralVar(ImmutableVar): int: LiteralNumberVar, float: LiteralNumberVar, bool: LiteralBooleanVar, - dict: LiteralObjectVar, list: LiteralArrayVar, tuple: LiteralArrayVar, set: LiteralArrayVar, @@ -581,3 +866,165 @@ def figure_out_type(value: Any) -> Type: unionize(*(figure_out_type(v) for v in value.values())), ] return type(value) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + **{"slots": True} if sys.version_info >= (3, 10) else {}, +) +class AndOperation(ImmutableVar): + """Class for the logical AND operation.""" + + # The first var. + _var1: Var = dataclasses.field(default_factory=lambda: LiteralVar.create(None)) + + # The second var. + _var2: Var = dataclasses.field(default_factory=lambda: LiteralVar.create(None)) + + def __init__( + self, var1: Var | Any, var2: Var | Any, _var_data: VarData | None = None + ): + """Initialize the AndOperation. + + Args: + var1: The first var. + var2: The second var. + _var_data: Additional hooks and imports associated with the Var. + """ + super(type(self), self).__init__( + _var_name="", + _var_type=Union[var1._var_type, var2._var_type], + _var_data=ImmutableVarData.merge(_var_data), + ) + object.__setattr__( + self, "_var1", var1 if isinstance(var1, Var) else LiteralVar.create(var1) + ) + object.__setattr__( + self, "_var2", var2 if isinstance(var2, Var) else LiteralVar.create(var2) + ) + object.__delattr__(self, "_var_name") + + @functools.cached_property + def _cached_var_name(self) -> str: + """Get the cached var name. + + Returns: + The cached var name. + """ + return f"({str(self._var1)} && {str(self._var2)})" + + def __getattr__(self, name: str) -> Any: + """Get an attribute of the var. + + Args: + name: The name of the attribute. + + Returns: + The attribute. + """ + if name == "_var_name": + return self._cached_var_name + return getattr(super(type(self), self), name) + + @functools.cached_property + def _cached_get_all_var_data(self) -> ImmutableVarData | None: + """Get the cached VarData. + + Returns: + The cached VarData. + """ + return ImmutableVarData.merge( + self._var1._get_all_var_data(), + self._var2._get_all_var_data(), + self._var_data, + ) + + def _get_all_var_data(self) -> ImmutableVarData | None: + """Get all VarData associated with the Var. + + Returns: + The VarData of the components and all of its children. + """ + return self._cached_get_all_var_data + + +@dataclasses.dataclass( + eq=False, + frozen=True, + **{"slots": True} if sys.version_info >= (3, 10) else {}, +) +class OrOperation(ImmutableVar): + """Class for the logical OR operation.""" + + # The first var. + _var1: Var = dataclasses.field(default_factory=lambda: LiteralVar.create(None)) + + # The second var. + _var2: Var = dataclasses.field(default_factory=lambda: LiteralVar.create(None)) + + def __init__( + self, var1: Var | Any, var2: Var | Any, _var_data: VarData | None = None + ): + """Initialize the OrOperation. + + Args: + var1: The first var. + var2: The second var. + _var_data: Additional hooks and imports associated with the Var. + """ + super(type(self), self).__init__( + _var_name="", + _var_type=Union[var1._var_type, var2._var_type], + _var_data=ImmutableVarData.merge(_var_data), + ) + object.__setattr__( + self, "_var1", var1 if isinstance(var1, Var) else LiteralVar.create(var1) + ) + object.__setattr__( + self, "_var2", var2 if isinstance(var2, Var) else LiteralVar.create(var2) + ) + object.__delattr__(self, "_var_name") + + @functools.cached_property + def _cached_var_name(self) -> str: + """Get the cached var name. + + Returns: + The cached var name. + """ + return f"({str(self._var1)} || {str(self._var2)})" + + def __getattr__(self, name: str) -> Any: + """Get an attribute of the var. + + Args: + name: The name of the attribute. + + Returns: + The attribute. + """ + if name == "_var_name": + return self._cached_var_name + return getattr(super(type(self), self), name) + + @functools.cached_property + def _cached_get_all_var_data(self) -> ImmutableVarData | None: + """Get the cached VarData. + + Returns: + The cached VarData. + """ + return ImmutableVarData.merge( + self._var1._get_all_var_data(), + self._var2._get_all_var_data(), + self._var_data, + ) + + def _get_all_var_data(self) -> ImmutableVarData | None: + """Get all VarData associated with the Var. + + Returns: + The VarData of the components and all of its children. + """ + return self._cached_get_all_var_data diff --git a/reflex/experimental/vars/function.py b/reflex/ivars/function.py similarity index 98% rename from reflex/experimental/vars/function.py rename to reflex/ivars/function.py index 4514a482d..c20c90772 100644 --- a/reflex/experimental/vars/function.py +++ b/reflex/ivars/function.py @@ -7,7 +7,7 @@ import sys from functools import cached_property from typing import Any, Callable, Optional, Tuple, Type, Union -from reflex.experimental.vars.base import ImmutableVar, LiteralVar +from .base import ImmutableVar, LiteralVar from reflex.vars import ImmutableVarData, Var, VarData @@ -288,3 +288,6 @@ class ToFunctionOperation(FunctionVar): The VarData of the components and all of its children. """ return self._cached_get_all_var_data + + +JSON_STRINGIFY = FunctionStringVar("JSON.stringify") diff --git a/reflex/experimental/vars/number.py b/reflex/ivars/number.py similarity index 88% rename from reflex/experimental/vars/number.py rename to reflex/ivars/number.py index 6bd3a7ff7..0e3424a33 100644 --- a/reflex/experimental/vars/number.py +++ b/reflex/ivars/number.py @@ -8,7 +8,9 @@ import sys from functools import cached_property from typing import Any, Union -from reflex.experimental.vars.base import ( +from reflex.utils.types import GenericType + +from .base import ( ImmutableVar, LiteralVar, ) @@ -188,54 +190,6 @@ class NumberVar(ImmutableVar[Union[int, float]]): """ return NumberNegateOperation(self) - def __and__(self, other: number_types | boolean_types) -> BooleanAndOperation: - """Boolean AND two numbers. - - Args: - other: The other number. - - Returns: - The boolean AND operation. - """ - boolified_other = other.bool() if isinstance(other, Var) else bool(other) - return BooleanAndOperation(self.bool(), boolified_other) - - def __rand__(self, other: number_types | boolean_types) -> BooleanAndOperation: - """Boolean AND two numbers. - - Args: - other: The other number. - - Returns: - The boolean AND operation. - """ - boolified_other = other.bool() if isinstance(other, Var) else bool(other) - return BooleanAndOperation(boolified_other, self.bool()) - - def __or__(self, other: number_types | boolean_types) -> BooleanOrOperation: - """Boolean OR two numbers. - - Args: - other: The other number. - - Returns: - The boolean OR operation. - """ - boolified_other = other.bool() if isinstance(other, Var) else bool(other) - return BooleanOrOperation(self.bool(), boolified_other) - - def __ror__(self, other: number_types | boolean_types) -> BooleanOrOperation: - """Boolean OR two numbers. - - Args: - other: The other number. - - Returns: - The boolean OR operation. - """ - boolified_other = other.bool() if isinstance(other, Var) else bool(other) - return BooleanOrOperation(boolified_other, self.bool()) - def __invert__(self) -> BooleanNotOperation: """Boolean NOT the number. @@ -284,7 +238,7 @@ class NumberVar(ImmutableVar[Union[int, float]]): """ return NumberTruncOperation(self) - def __lt__(self, other: number_types | boolean_types) -> LessThanOperation: + def __lt__(self, other: Any) -> LessThanOperation: """Less than comparison. Args: @@ -293,9 +247,11 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The result of the comparison. """ - return LessThanOperation(self, +other) + if isinstance(other, (NumberVar, BooleanVar, int, float, bool)): + return LessThanOperation(self, +other) + return LessThanOperation(self, other) - def __le__(self, other: number_types | boolean_types) -> LessThanOrEqualOperation: + def __le__(self, other: Any) -> LessThanOrEqualOperation: """Less than or equal comparison. Args: @@ -304,9 +260,11 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The result of the comparison. """ - return LessThanOrEqualOperation(self, +other) + if isinstance(other, (NumberVar, BooleanVar, int, float, bool)): + return LessThanOrEqualOperation(self, +other) + return LessThanOrEqualOperation(self, other) - def __eq__(self, other: number_types | boolean_types) -> EqualOperation: + def __eq__(self, other: Any) -> EqualOperation: """Equal comparison. Args: @@ -315,9 +273,11 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The result of the comparison. """ - return EqualOperation(self, +other) + if isinstance(other, (NumberVar, BooleanVar, int, float, bool)): + return EqualOperation(self, +other) + return EqualOperation(self, other) - def __ne__(self, other: number_types | boolean_types) -> NotEqualOperation: + def __ne__(self, other: Any) -> NotEqualOperation: """Not equal comparison. Args: @@ -326,9 +286,11 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The result of the comparison. """ - return NotEqualOperation(self, +other) + if isinstance(other, (NumberVar, BooleanVar, int, float, bool)): + return NotEqualOperation(self, +other) + return NotEqualOperation(self, other) - def __gt__(self, other: number_types | boolean_types) -> GreaterThanOperation: + def __gt__(self, other: Any) -> GreaterThanOperation: """Greater than comparison. Args: @@ -337,11 +299,11 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The result of the comparison. """ - return GreaterThanOperation(self, +other) + if isinstance(other, (NumberVar, BooleanVar, int, float, bool)): + return GreaterThanOperation(self, +other) + return GreaterThanOperation(self, other) - def __ge__( - self, other: number_types | boolean_types - ) -> GreaterThanOrEqualOperation: + def __ge__(self, other: Any) -> GreaterThanOrEqualOperation: """Greater than or equal comparison. Args: @@ -350,7 +312,9 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The result of the comparison. """ - return GreaterThanOrEqualOperation(self, +other) + if isinstance(other, (NumberVar, BooleanVar, int, float, bool)): + return GreaterThanOrEqualOperation(self, +other) + return GreaterThanOrEqualOperation(self, other) def bool(self) -> NotEqualOperation: """Boolean conversion. @@ -696,50 +660,6 @@ class NumberTruncOperation(UnaryNumberOperation): class BooleanVar(ImmutableVar[bool]): """Base class for immutable boolean vars.""" - def __and__(self, other: bool) -> BooleanAndOperation: - """AND two booleans. - - Args: - other: The other boolean. - - Returns: - The boolean AND operation. - """ - return BooleanAndOperation(self, other) - - def __rand__(self, other: bool) -> BooleanAndOperation: - """AND two booleans. - - Args: - other: The other boolean. - - Returns: - The boolean AND operation. - """ - return BooleanAndOperation(other, self) - - def __or__(self, other: bool) -> BooleanOrOperation: - """OR two booleans. - - Args: - other: The other boolean. - - Returns: - The boolean OR operation. - """ - return BooleanOrOperation(self, other) - - def __ror__(self, other: bool) -> BooleanOrOperation: - """OR two booleans. - - Args: - other: The other boolean. - - Returns: - The boolean OR operation. - """ - return BooleanOrOperation(other, self) - def __invert__(self) -> BooleanNotOperation: """NOT the boolean. @@ -913,16 +833,16 @@ class BooleanToIntOperation(NumberVar): frozen=True, **{"slots": True} if sys.version_info >= (3, 10) else {}, ) -class NumberComparisonOperation(BooleanVar): +class ComparisonOperation(BooleanVar): """Base class for immutable boolean vars that are the result of a comparison operation.""" - a: number_types = dataclasses.field(default=0) - b: number_types = dataclasses.field(default=0) + a: Var = dataclasses.field(default_factory=lambda: LiteralBooleanVar(True)) + b: Var = dataclasses.field(default_factory=lambda: LiteralBooleanVar(True)) def __init__( self, - a: number_types, - b: number_types, + a: Var | Any, + b: Var | Any, _var_data: VarData | None = None, ): """Initialize the comparison operation var. @@ -932,13 +852,13 @@ class NumberComparisonOperation(BooleanVar): b: The second value. _var_data: Additional hooks and imports associated with the Var. """ - super(NumberComparisonOperation, self).__init__( + super(ComparisonOperation, self).__init__( _var_name="", _var_type=bool, _var_data=ImmutableVarData.merge(_var_data), ) - object.__setattr__(self, "a", a) - object.__setattr__(self, "b", b) + object.__setattr__(self, "a", a if isinstance(a, Var) else LiteralVar.create(a)) + object.__setattr__(self, "b", b if isinstance(b, Var) else LiteralVar.create(b)) object.__delattr__(self, "_var_name") @cached_property @@ -961,7 +881,7 @@ class NumberComparisonOperation(BooleanVar): """ if name == "_var_name": return self._cached_var_name - getattr(super(NumberComparisonOperation, self), name) + getattr(super(ComparisonOperation, self), name) @cached_property def _cached_get_all_var_data(self) -> ImmutableVarData | None: @@ -980,7 +900,7 @@ class NumberComparisonOperation(BooleanVar): return self._cached_get_all_var_data -class GreaterThanOperation(NumberComparisonOperation): +class GreaterThanOperation(ComparisonOperation): """Base class for immutable boolean vars that are the result of a greater than operation.""" @cached_property @@ -995,7 +915,7 @@ class GreaterThanOperation(NumberComparisonOperation): return f"({str(first_value)} > {str(second_value)})" -class GreaterThanOrEqualOperation(NumberComparisonOperation): +class GreaterThanOrEqualOperation(ComparisonOperation): """Base class for immutable boolean vars that are the result of a greater than or equal operation.""" @cached_property @@ -1010,7 +930,7 @@ class GreaterThanOrEqualOperation(NumberComparisonOperation): return f"({str(first_value)} >= {str(second_value)})" -class LessThanOperation(NumberComparisonOperation): +class LessThanOperation(ComparisonOperation): """Base class for immutable boolean vars that are the result of a less than operation.""" @cached_property @@ -1025,7 +945,7 @@ class LessThanOperation(NumberComparisonOperation): return f"({str(first_value)} < {str(second_value)})" -class LessThanOrEqualOperation(NumberComparisonOperation): +class LessThanOrEqualOperation(ComparisonOperation): """Base class for immutable boolean vars that are the result of a less than or equal operation.""" @cached_property @@ -1040,7 +960,7 @@ class LessThanOrEqualOperation(NumberComparisonOperation): return f"({str(first_value)} <= {str(second_value)})" -class EqualOperation(NumberComparisonOperation): +class EqualOperation(ComparisonOperation): """Base class for immutable boolean vars that are the result of an equal operation.""" @cached_property @@ -1052,10 +972,10 @@ class EqualOperation(NumberComparisonOperation): """ first_value = self.a if isinstance(self.a, Var) else LiteralVar.create(self.a) second_value = self.b if isinstance(self.b, Var) else LiteralVar.create(self.b) - return f"({str(first_value)} == {str(second_value)})" + return f"({str(first_value)} === {str(second_value)})" -class NotEqualOperation(NumberComparisonOperation): +class NotEqualOperation(ComparisonOperation): """Base class for immutable boolean vars that are the result of a not equal operation.""" @cached_property @@ -1139,36 +1059,6 @@ class LogicalOperation(BooleanVar): return self._cached_get_all_var_data -class BooleanAndOperation(LogicalOperation): - """Base class for immutable boolean vars that are the result of a logical AND operation.""" - - @cached_property - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - first_value = self.a if isinstance(self.a, Var) else LiteralVar.create(self.a) - second_value = self.b if isinstance(self.b, Var) else LiteralVar.create(self.b) - return f"({str(first_value)} && {str(second_value)})" - - -class BooleanOrOperation(LogicalOperation): - """Base class for immutable boolean vars that are the result of a logical OR operation.""" - - @cached_property - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - first_value = self.a if isinstance(self.a, Var) else LiteralVar.create(self.a) - second_value = self.b if isinstance(self.b, Var) else LiteralVar.create(self.b) - return f"({str(first_value)} || {str(second_value)})" - - class BooleanNotOperation(BooleanVar): """Base class for immutable boolean vars that are the result of a logical NOT operation.""" @@ -1428,7 +1318,7 @@ class ToBooleanVarOperation(BooleanVar): Returns: The name of the var. """ - return str(self._original_value) + return f"Boolean({str(self._original_value)})" def __getattr__(self, name: str) -> Any: """Get an attribute of the var. @@ -1456,3 +1346,84 @@ class ToBooleanVarOperation(BooleanVar): def _get_all_var_data(self) -> ImmutableVarData | None: return self._cached_get_all_var_data + + +class TernaryOperator(ImmutableVar): + """Base class for immutable vars that are the result of a ternary operation.""" + + condition: Var = dataclasses.field(default_factory=lambda: LiteralBooleanVar(False)) + if_true: Var = dataclasses.field(default_factory=lambda: LiteralNumberVar(0)) + if_false: Var = dataclasses.field(default_factory=lambda: LiteralNumberVar(0)) + + def __init__( + self, + condition: Var | Any, + if_true: Var | Any, + if_false: Var | Any, + _var_type: GenericType | None = None, + _var_data: VarData | None = None, + ): + """Initialize the ternary operation var. + + Args: + condition: The condition. + if_true: The value if the condition is true. + if_false: The value if the condition is false. + _var_data: Additional hooks and imports associated with the Var. + """ + condition = ( + condition if isinstance(condition, Var) else LiteralVar.create(condition) + ) + if_true = if_true if isinstance(if_true, Var) else LiteralVar.create(if_true) + if_false = ( + if_false if isinstance(if_false, Var) else LiteralVar.create(if_false) + ) + + super(TernaryOperator, self).__init__( + _var_name="", + _var_type=_var_type or Union[if_true._var_type, if_false._var_type], + _var_data=ImmutableVarData.merge(_var_data), + ) + object.__setattr__(self, "condition", condition) + object.__setattr__(self, "if_true", if_true) + object.__setattr__(self, "if_false", if_false) + object.__delattr__(self, "_var_name") + + @cached_property + def _cached_var_name(self) -> str: + """The name of the var. + + Returns: + The name of the var. + """ + return f"({str(self.condition)} ? {str(self.if_true)} : {str(self.if_false)})" + + def __getattr__(self, name: str) -> Any: + """Get an attribute of the var. + + Args: + name: The name of the attribute. + + Returns: + The attribute value. + """ + if name == "_var_name": + return self._cached_var_name + getattr(super(TernaryOperator, self), name) + + @cached_property + def _cached_get_all_var_data(self) -> ImmutableVarData | None: + """Get all VarData associated with the Var. + + Returns: + The VarData of the components and all of its children. + """ + return ImmutableVarData.merge( + self.condition._get_all_var_data(), + self.if_true._get_all_var_data(), + self.if_false._get_all_var_data(), + self._var_data, + ) + + def _get_all_var_data(self) -> ImmutableVarData | None: + return self._cached_get_all_var_data diff --git a/reflex/experimental/vars/object.py b/reflex/ivars/object.py similarity index 87% rename from reflex/experimental/vars/object.py rename to reflex/ivars/object.py index a227f0d7c..aebc23e1e 100644 --- a/reflex/experimental/vars/object.py +++ b/reflex/ivars/object.py @@ -22,13 +22,15 @@ from typing import ( from typing_extensions import get_origin -from reflex.experimental.vars.base import ( +from reflex.utils import console + +from .base import ( ImmutableVar, LiteralVar, figure_out_type, ) -from reflex.experimental.vars.number import NumberVar -from reflex.experimental.vars.sequence import ArrayVar, StringVar +from .number import BooleanVar, NumberVar +from .sequence import ArrayVar, StringVar from reflex.utils.exceptions import VarAttributeError from reflex.utils.types import GenericType, get_attribute_access_type from reflex.vars import ImmutableVarData, Var, VarData @@ -46,23 +48,13 @@ OTHER_KEY_TYPE = TypeVar("OTHER_KEY_TYPE") class ObjectVar(ImmutableVar[OBJECT_TYPE]): """Base class for immutable object vars.""" - @overload - def _key_type(self: ObjectVar[Dict[KEY_TYPE, VALUE_TYPE]]) -> KEY_TYPE: ... - - @overload - def _key_type(self) -> Type: ... - def _key_type(self) -> Type: """Get the type of the keys of the object. Returns: The type of the keys of the object. """ - fixed_type = ( - self._var_type if isclass(self._var_type) else get_origin(self._var_type) - ) - args = get_args(self._var_type) if issubclass(fixed_type, dict) else () - return args[0] if args else Any + return str @overload def _value_type(self: ObjectVar[Dict[KEY_TYPE, VALUE_TYPE]]) -> VALUE_TYPE: ... @@ -82,15 +74,7 @@ class ObjectVar(ImmutableVar[OBJECT_TYPE]): args = get_args(self._var_type) if issubclass(fixed_type, dict) else () return args[1] if args else Any - @overload - def keys( - self: ObjectVar[Dict[KEY_TYPE, VALUE_TYPE]], - ) -> ArrayVar[List[KEY_TYPE]]: ... - - @overload - def keys(self) -> ArrayVar: ... - - def keys(self) -> ArrayVar: + def keys(self) -> ArrayVar[List[str]]: """Get the keys of the object. Returns: @@ -117,7 +101,7 @@ class ObjectVar(ImmutableVar[OBJECT_TYPE]): @overload def entries( self: ObjectVar[Dict[KEY_TYPE, VALUE_TYPE]], - ) -> ArrayVar[List[Tuple[KEY_TYPE, VALUE_TYPE]]]: ... + ) -> ArrayVar[List[Tuple[str, VALUE_TYPE]]]: ... @overload def entries(self) -> ArrayVar: ... @@ -258,6 +242,8 @@ class ObjectVar(ImmutableVar[OBJECT_TYPE]): Returns: The attribute of the var. """ + if name.startswith("__") and name.endswith("__"): + return getattr(super(type(self), self), name) fixed_type = ( self._var_type if isclass(self._var_type) else get_origin(self._var_type) ) @@ -272,6 +258,17 @@ class ObjectVar(ImmutableVar[OBJECT_TYPE]): else: return ObjectItemOperation(self, name).guess_type() + def contains(self, key: Var | Any) -> BooleanVar: + """Check if the object contains a key. + + Args: + key: The key to check. + + Returns: + The result of the check. + """ + return ObjectHasOwnProperty(self, key) + @dataclasses.dataclass( eq=False, @@ -369,12 +366,12 @@ class LiteralObjectVar(LiteralVar, ObjectVar[OBJECT_TYPE]): return ImmutableVarData.merge( *[ value._get_all_var_data() - for key, value in self._var_value + for value in self._var_value.values() if isinstance(value, Var) ], *[ key._get_all_var_data() - for key, value in self._var_value + for key in self._var_value.keys() if isinstance(key, Var) ], self._var_data, @@ -802,3 +799,81 @@ class ToObjectOperation(ObjectVar): The VarData of the components and all of its children. """ return self._cached_get_all_var_data + + +@dataclasses.dataclass( + eq=False, + frozen=True, + **{"slots": True} if sys.version_info >= (3, 10) else {}, +) +class ObjectHasOwnProperty(BooleanVar): + """Operation to check if an object has a property.""" + + value: ObjectVar = dataclasses.field(default_factory=lambda: LiteralObjectVar({})) + key: Var | Any = dataclasses.field(default_factory=lambda: LiteralVar.create(None)) + + def __init__( + self, + value: ObjectVar, + key: Var | Any, + _var_data: VarData | None = None, + ): + """Initialize the object has own property operation. + + Args: + value: The value of the operation. + key: The key to check. + _var_data: Additional hooks and imports associated with the operation. + """ + super(ObjectHasOwnProperty, self).__init__( + _var_name="", + _var_data=ImmutableVarData.merge(_var_data), + ) + object.__setattr__(self, "value", value) + object.__setattr__( + self, "key", key if isinstance(key, Var) else LiteralVar.create(key) + ) + object.__delattr__(self, "_var_name") + + @cached_property + def _cached_var_name(self) -> str: + """The name of the operation. + + Returns: + The name of the operation. + """ + return f"{str(self.value)}.hasOwnProperty({str(self.key)})" + + def __getattr__(self, name): + """Get an attribute of the operation. + + Args: + name: The name of the attribute. + + Returns: + The attribute of the operation. + """ + if name == "_var_name": + return self._cached_var_name + return super(type(self), self).__getattr__(name) + + @cached_property + def _cached_get_all_var_data(self) -> ImmutableVarData | None: + """Get all VarData associated with the operation. + + Returns: + The VarData of the components and all of its children. + """ + return ImmutableVarData.merge( + self.value._get_all_var_data(), + self.key._get_all_var_data(), + self._var_data, + ) + + def _get_all_var_data(self) -> ImmutableVarData | None: + """Wrapper method for cached property. + + Returns: + The VarData of the components and all of its children. + """ + return self._cached_get_all_var_data diff --git a/reflex/experimental/vars/sequence.py b/reflex/ivars/sequence.py similarity index 88% rename from reflex/experimental/vars/sequence.py rename to reflex/ivars/sequence.py index f622159a6..5c34886e6 100644 --- a/reflex/experimental/vars/sequence.py +++ b/reflex/ivars/sequence.py @@ -27,13 +27,13 @@ from typing_extensions import get_origin from reflex import constants from reflex.constants.base import REFLEX_VAR_OPENING_TAG -from reflex.experimental.vars.base import ( +from .base import ( ImmutableVar, LiteralVar, figure_out_type, unionize, ) -from reflex.experimental.vars.number import ( +from .number import ( BooleanVar, LiteralNumberVar, NotEqualOperation, @@ -71,27 +71,29 @@ class StringVar(ImmutableVar[str]): """ return ConcatVarOperation(other, self) - def __mul__(self, other: int) -> ConcatVarOperation: - """Concatenate two strings. + def __mul__(self, other: NumberVar | int) -> StringVar: + """ + Multiply the sequence by a number or an integer. Args: - other: The other string. + other (NumberVar | int): The number or integer to multiply the sequence by. Returns: - The string concatenation operation. + StringVar: The resulting sequence after multiplication. """ - return ConcatVarOperation(*[self for _ in range(other)]) + return (self.split() * other).join() - def __rmul__(self, other: int) -> ConcatVarOperation: - """Concatenate two strings. + def __rmul__(self, other: NumberVar | int) -> StringVar: + """ + Multiply the sequence by a number or an integer. Args: - other: The other string. + other (NumberVar | int): The number or integer to multiply the sequence by. Returns: - The string concatenation operation. + StringVar: The resulting sequence after multiplication. """ - return ConcatVarOperation(*[self for _ in range(other)]) + return (self.split() * other).join() @overload def __getitem__(self, i: slice) -> ArrayJoinOperation: ... @@ -596,10 +598,17 @@ class LiteralStringVar(LiteralVar, StringVar): var_data.interpolations = [ (realstart, realstart + string_length) ] + var_content = value[end : (end + string_length)] + if ( + var_content[0] == "{" + and var_content[-1] == "}" + and strings_and_vals + and strings_and_vals[-1][-1] == "$" + ): + strings_and_vals[-1] = strings_and_vals[-1][:-1] + var_content = "(" + var_content[1:-1] + ")" strings_and_vals.append( - ImmutableVar.create_safe( - value[end : (end + string_length)], _var_data=var_data - ) + ImmutableVar.create_safe(var_content, _var_data=var_data) ) value = value[(end + string_length) :] @@ -728,8 +737,6 @@ VALUE_TYPE = TypeVar("VALUE_TYPE") class ArrayVar(ImmutableVar[ARRAY_VAR_TYPE]): """Base class for immutable array vars.""" - from reflex.experimental.vars.sequence import StringVar - def join(self, sep: StringVar | str = "") -> ArrayJoinOperation: """Join the elements of the array. @@ -739,7 +746,6 @@ class ArrayVar(ImmutableVar[ARRAY_VAR_TYPE]): Returns: The joined elements. """ - from reflex.experimental.vars.sequence import ArrayJoinOperation return ArrayJoinOperation(self, sep) @@ -751,6 +757,18 @@ class ArrayVar(ImmutableVar[ARRAY_VAR_TYPE]): """ return ArrayReverseOperation(self) + def __add__(self, other: ArrayVar[ARRAY_VAR_TYPE]) -> ArrayConcatOperation: + """ + Concatenate two arrays. + + Parameters: + other (ArrayVar[ARRAY_VAR_TYPE]): The other array to concatenate. + + Returns: + ArrayConcatOperation: The concatenation of the two arrays. + """ + return ArrayConcatOperation(self, other) + @overload def __getitem__(self, i: slice) -> ArrayVar[ARRAY_VAR_TYPE]: ... @@ -915,6 +933,30 @@ class ArrayVar(ImmutableVar[ARRAY_VAR_TYPE]): """ return ArrayContainsOperation(self, other) + def __mul__(self, other: NumberVar | int) -> ArrayVar[ARRAY_VAR_TYPE]: + """ + Multiply the sequence by a number or integer. + + Parameters: + other (NumberVar | int): The number or integer to multiply the sequence by. + + Returns: + ArrayVar[ARRAY_VAR_TYPE]: The result of multiplying the sequence by the given number or integer. + """ + return ArrayRepeatOperation(self, other) + + def __rmul__(self, other: NumberVar | int) -> ArrayVar[ARRAY_VAR_TYPE]: + """ + Multiply the sequence by a number or integer. + + Parameters: + other (NumberVar | int): The number or integer to multiply the sequence by. + + Returns: + ArrayVar[ARRAY_VAR_TYPE]: The result of multiplying the sequence by the given number or integer. + """ + return ArrayRepeatOperation(self, other) + LIST_ELEMENT = TypeVar("LIST_ELEMENT") @@ -1296,7 +1338,7 @@ class ArrayReverseOperation(ArrayToArrayOperation): Returns: The name of the var. """ - return f"{str(self.a)}.reverse()" + return f"{str(self.a)}.slice().reverse()" @dataclasses.dataclass( @@ -1762,3 +1804,140 @@ class ToArrayOperation(ArrayVar): def _get_all_var_data(self) -> ImmutableVarData | None: return self._cached_get_all_var_data + + +@dataclasses.dataclass( + eq=False, + frozen=True, + **{"slots": True} if sys.version_info >= (3, 10) else {}, +) +class ArrayRepeatOperation(ArrayVar): + """Base class for immutable array vars that are the result of an array repeat operation.""" + + a: ArrayVar = dataclasses.field(default_factory=lambda: LiteralArrayVar([])) + n: NumberVar = dataclasses.field(default_factory=lambda: LiteralNumberVar(0)) + + def __init__( + self, a: ArrayVar, n: NumberVar | int, _var_data: VarData | None = None + ): + """Initialize the array repeat operation var. + + Args: + a: The array. + n: The number of times to repeat the array. + _var_data: Additional hooks and imports associated with the Var. + """ + super(ArrayRepeatOperation, self).__init__( + _var_name="", + _var_type=a._var_type, + _var_data=ImmutableVarData.merge(_var_data), + ) + object.__setattr__(self, "a", a) + object.__setattr__( + self, + "n", + n if isinstance(n, Var) else LiteralNumberVar(n), + ) + object.__delattr__(self, "_var_name") + + @cached_property + def _cached_var_name(self) -> str: + """The name of the var. + + Returns: + The name of the var. + """ + return f"Array.from({{ length: {str(self.n)} }}).flatMap(() => {str(self.a)})" + + def __getattr__(self, name: str) -> Any: + """Get an attribute of the var. + + Args: + name: The name of the attribute. + + Returns: + The attribute value. + """ + if name == "_var_name": + return self._cached_var_name + getattr(super(ArrayRepeatOperation, self), name) + + @cached_property + def _cached_get_all_var_data(self) -> ImmutableVarData | None: + """Get all VarData associated with the Var. + + Returns: + The VarData of the components and all of its children. + """ + return ImmutableVarData.merge( + self.a._get_all_var_data(), self.n._get_all_var_data(), self._var_data + ) + + def _get_all_var_data(self) -> ImmutableVarData | None: + return self._cached_get_all_var_data + + +@dataclasses.dataclass( + eq=False, + frozen=True, + **{"slots": True} if sys.version_info >= (3, 10) else {}, +) +class ArrayConcatOperation(ArrayVar): + """Base class for immutable array vars that are the result of an array concat operation.""" + + a: ArrayVar = dataclasses.field(default_factory=lambda: LiteralArrayVar([])) + b: ArrayVar = dataclasses.field(default_factory=lambda: LiteralArrayVar([])) + + def __init__(self, a: ArrayVar, b: ArrayVar, _var_data: VarData | None = None): + """Initialize the array concat operation var. + + Args: + a: The first array. + b: The second array. + _var_data: Additional hooks and imports associated with the Var. + """ + # TODO: Figure out how to merge the types of a and b + super(ArrayConcatOperation, self).__init__( + _var_name="", + _var_type=List[ARRAY_VAR_TYPE], + _var_data=ImmutableVarData.merge(_var_data), + ) + object.__setattr__(self, "a", a) + object.__setattr__(self, "b", b) + object.__delattr__(self, "_var_name") + + @cached_property + def _cached_var_name(self) -> str: + """The name of the var. + + Returns: + The name of the var. + """ + return f"[...{str(self.a)}, ...{str(self.b)}]" + + def __getattr__(self, name: str) -> Any: + """Get an attribute of the var. + + Args: + name: The name of the attribute. + + Returns: + The attribute value. + """ + if name == "_var_name": + return self._cached_var_name + getattr(super(ArrayConcatOperation, self), name) + + @cached_property + def _cached_get_all_var_data(self) -> ImmutableVarData | None: + """Get all VarData associated with the Var. + + Returns: + The VarData of the components and all of its children. + """ + return ImmutableVarData.merge( + self.a._get_all_var_data(), self.b._get_all_var_data(), self._var_data + ) + + def _get_all_var_data(self) -> ImmutableVarData | None: + return self._cached_get_all_var_data diff --git a/reflex/state.py b/reflex/state.py index e29336042..2d90902d4 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -32,6 +32,7 @@ import dill from sqlalchemy.orm import DeclarativeBase from reflex.config import get_config +from reflex.ivars.base import ImmutableVar try: import pydantic.v1 as pydantic @@ -55,7 +56,12 @@ from reflex.utils import console, format, prerequisites, types from reflex.utils.exceptions import ImmutableStateError, LockExpiredError from reflex.utils.exec import is_testing_env from reflex.utils.serializers import SerializedType, serialize, serializer -from reflex.vars import BaseVar, ComputedVar, Var, computed_var +from reflex.vars import ( + ComputedVar, + ImmutableVarData, + Var, + computed_var, +) if TYPE_CHECKING: from reflex.components.component import Component @@ -298,7 +304,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): vars: ClassVar[Dict[str, Var]] = {} # The base vars of the class. - base_vars: ClassVar[Dict[str, BaseVar]] = {} + base_vars: ClassVar[Dict[str, ImmutableVar]] = {} # The computed vars of the class. computed_vars: ClassVar[Dict[str, ComputedVar]] = {} @@ -520,9 +526,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): # Set the base and computed vars. cls.base_vars = { - f.name: BaseVar(_var_name=f.name, _var_type=f.outer_type_)._var_set_state( - cls - ) + f.name: ImmutableVar( + _var_name=format.format_state_name(cls.get_full_name()) + "." + f.name, + _var_type=f.outer_type_, + _var_data=ImmutableVarData.from_state(cls), + ).guess_type() for f in cls.get_fields().values() if f.name not in cls.get_skip_vars() } @@ -846,7 +854,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): return getattr(substate, name) @classmethod - def _init_var(cls, prop: BaseVar): + def _init_var(cls, prop: ImmutableVar): """Initialize a variable. Args: @@ -889,7 +897,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): ) # create the variable based on name and type - var = BaseVar(_var_name=name, _var_type=type_) + var = ImmutableVar(_var_name=name, _var_type=type_).guess_type() var._var_set_state(cls) # add the pydantic field dynamically (must be done before _init_var) @@ -909,13 +917,18 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): cls._init_var_dependency_dicts() @classmethod - def _set_var(cls, prop: BaseVar): + def _set_var(cls, prop: ImmutableVar): """Set the var as a class member. Args: prop: The var instance to set. """ - setattr(cls, prop._var_name, prop) + acutal_var_name = ( + prop._var_name + if "." not in prop._var_name + else prop._var_name.split(".")[-1] + ) + setattr(cls, acutal_var_name, prop) @classmethod def _create_event_handler(cls, fn): @@ -935,7 +948,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): cls.setvar = cls.event_handlers["setvar"] = EventHandlerSetVar(state_cls=cls) @classmethod - def _create_setter(cls, prop: BaseVar): + def _create_setter(cls, prop: ImmutableVar): """Create a setter for the var. Args: @@ -948,14 +961,17 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): setattr(cls, setter_name, event_handler) @classmethod - def _set_default_value(cls, prop: BaseVar): + def _set_default_value(cls, prop: ImmutableVar): """Set the default value for the var. Args: prop: The var to set the default value for. """ # Get the pydantic field for the var. - field = cls.get_fields()[prop._var_name] + if "." in prop._var_name: + field = cls.get_fields()[prop._var_name.split(".")[-1]] + else: + field = cls.get_fields()[prop._var_name] if field.required: default_value = prop.get_default_value() if default_value is not None: diff --git a/reflex/style.py b/reflex/style.py index 69e93ed39..a4aeecdc3 100644 --- a/reflex/style.py +++ b/reflex/style.py @@ -7,9 +7,11 @@ from typing import Any, Literal, Tuple, Type from reflex import constants from reflex.components.core.breakpoints import Breakpoints, breakpoints_values from reflex.event import EventChain +from reflex.ivars.base import ImmutableVar, LiteralVar +from reflex.ivars.function import FunctionVar from reflex.utils import format from reflex.utils.imports import ImportVar -from reflex.vars import BaseVar, CallableVar, Var, VarData +from reflex.vars import ImmutableVarData, Var, VarData VarData.update_forward_refs() # Ensure all type definitions are resolved @@ -25,7 +27,7 @@ color_mode_imports = { } -def _color_mode_var(_var_name: str, _var_type: Type = str) -> BaseVar: +def _color_mode_var(_var_name: str, _var_type: Type = str) -> ImmutableVar: """Create a Var that destructs the _var_name from ColorModeContext. Args: @@ -33,24 +35,22 @@ def _color_mode_var(_var_name: str, _var_type: Type = str) -> BaseVar: _var_type: The type of the Var. Returns: - The BaseVar for accessing _var_name from ColorModeContext. + The Var that resolves to the color mode. """ - return BaseVar( + return ImmutableVar( _var_name=_var_name, _var_type=_var_type, - _var_is_local=False, - _var_is_string=False, - _var_data=VarData( + _var_data=ImmutableVarData( imports=color_mode_imports, hooks={f"const {{ {_var_name} }} = useContext(ColorModeContext)": None}, ), - ) + ).guess_type() -@CallableVar +# @CallableVar def set_color_mode( new_color_mode: LiteralColorMode | Var[LiteralColorMode] | None = None, -) -> BaseVar[EventChain]: +) -> Var[EventChain]: """Create an EventChain Var that sets the color mode to a specific value. Note: `set_color_mode` is not a real event and cannot be triggered from a @@ -70,11 +70,14 @@ def set_color_mode( return base_setter if not isinstance(new_color_mode, Var): - new_color_mode = Var.create_safe(new_color_mode, _var_is_string=True) - return base_setter._replace( - _var_name=f"() => {base_setter._var_name}({new_color_mode._var_name_unwrapped})", - merge_var_data=new_color_mode._var_data, - ) + new_color_mode = LiteralVar.create(new_color_mode) + + return ImmutableVar( + f"() => {str(base_setter)}({str(new_color_mode)})", + _var_data=ImmutableVarData.merge( + base_setter._get_all_var_data(), new_color_mode._get_all_var_data() + ), + ).to(FunctionVar, EventChain) # Var resolves to the current color mode for the app ("light", "dark" or "system") @@ -111,7 +114,9 @@ def media_query(breakpoint_expr: str): return f"@media screen and (min-width: {breakpoint_expr})" -def convert_item(style_item: str | Var) -> tuple[str, VarData | None]: +def convert_item( + style_item: str | Var, +) -> tuple[str, VarData | ImmutableVarData | None]: """Format a single value in a style dictionary. Args: @@ -122,13 +127,13 @@ def convert_item(style_item: str | Var) -> tuple[str, VarData | None]: """ if isinstance(style_item, Var): # If the value is a Var, extract the var_data and cast as str. - return str(style_item), style_item._var_data + return str(style_item), style_item._get_all_var_data() # Otherwise, convert to Var to collapse VarData encoded in f-string. - new_var = Var.create(style_item, _var_is_string=False) + new_var = ImmutableVar.create(style_item) if new_var is not None and new_var._var_data: # The wrapped backtick is used to identify the Var for interpolation. - return f"`{str(new_var)}`", new_var._var_data + return f"`{str(new_var)}`", new_var._get_all_var_data() return style_item, None @@ -175,7 +180,11 @@ def convert(style_dict): for key, value in style_dict.items(): keys = format_style_key(key) - if isinstance(value, dict): + if isinstance(value, Var): + return_val = value + new_var_data = value._get_all_var_data() + update_out_dict(return_val, keys) + elif isinstance(value, dict): # Recursively format nested style dictionaries. return_val, new_var_data = convert(value) update_out_dict(return_val, keys) @@ -254,7 +263,7 @@ class Style(dict): value: The value to set. """ # Create a Var to collapse VarData encoded in f-string. - _var = Var.create(value, _var_is_string=False) + _var = ImmutableVar.create(value) if _var is not None: # Carry the imports/hooks when setting a Var as a value. self._var_data = VarData.merge(self._var_data, _var._var_data) diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 59bbbd91c..01b7cb712 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -9,6 +9,8 @@ import re from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union from reflex import constants +from reflex.ivars.base import ImmutableVar +from reflex.ivars.function import FunctionVar from reflex.utils import exceptions, types from reflex.vars import BaseVar, Var @@ -483,8 +485,14 @@ def format_props(*single_props, **key_value_props) -> list[str]: The formatted props list. """ # Format all the props. + from reflex.ivars.base import ImmutableVar + return [ - f"{name}={format_prop(prop)}" + ( + f"{name}={{{format_prop(prop)}}}" + if isinstance(prop, ImmutableVar) + else f"{name}={format_prop(prop)}" + ) for name, prop in sorted(key_value_props.items()) if prop is not None ] + [str(prop) for prop in single_props] @@ -613,11 +621,13 @@ def format_event_chain( def format_queue_events( - events: EventSpec - | EventHandler - | Callable - | List[EventSpec | EventHandler | Callable] - | None = None, + events: ( + EventSpec + | EventHandler + | Callable + | List[EventSpec | EventHandler | Callable] + | None + ) = None, args_spec: Optional[ArgsSpec] = None, ) -> Var[EventChain]: """Format a list of event handler / event spec as a javascript callback. @@ -647,9 +657,7 @@ def format_queue_events( ) if not events: - return Var.create_safe( - "() => null", _var_is_string=False, _var_is_local=False - ).to(EventChain) + return ImmutableVar("(() => null)").to(FunctionVar, EventChain) # If no spec is provided, the function will take no arguments. def _default_args_spec(): @@ -682,12 +690,10 @@ def format_queue_events( # Return the final code snippet, expecting queueEvents, processEvent, and socket to be in scope. # Typically this snippet will _only_ run from within an rx.call_script eval context. - return Var.create_safe( + return ImmutableVar( f"{arg_def} => {{queueEvents([{','.join(payloads)}], {constants.CompileVars.SOCKET}); " f"processEvent({constants.CompileVars.SOCKET})}}", - _var_is_string=False, - _var_is_local=False, - ).to(EventChain) + ).to(FunctionVar, EventChain) def format_query_params(router_data: dict[str, Any]) -> dict[str, str]: @@ -939,6 +945,6 @@ def format_data_editor_cell(cell: Any): The formatted cell. """ return { - "kind": Var.create(value="GridCellKind.Text", _var_is_string=False), + "kind": ImmutableVar.create("GridCellKind.Text"), "data": cell, } diff --git a/reflex/vars.py b/reflex/vars.py index ffaf16455..23f2fe6a7 100644 --- a/reflex/vars.py +++ b/reflex/vars.py @@ -345,6 +345,33 @@ class ImmutableVarData: == imports.collapse_imports(other.imports) ) + @classmethod + def from_state(cls, state: Type[BaseState] | str) -> ImmutableVarData: + """Set the state of the var. + + Args: + state: The state to set or the full name of the state. + + Returns: + The var with the set state. + """ + from reflex.utils import format + + state_name = state if isinstance(state, str) else state.get_full_name() + new_var_data = ImmutableVarData( + state=state_name, + hooks={ + "const {0} = useContext(StateContexts.{0})".format( + format.format_state_name(state_name) + ): None + }, + imports={ + f"/{constants.Dirs.CONTEXTS_PATH}": [ImportVar(tag="StateContexts")], + "react": [ImportVar(tag="useContext")], + }, + ) + return new_var_data + def _decode_var_immutable(value: str) -> tuple[ImmutableVarData | None, str]: """Decode the state name from a formatted var. @@ -800,11 +827,19 @@ class Var: """ from reflex.utils import format - out = ( - self._var_full_name - if self._var_is_local - else format.wrap(self._var_full_name, "{") - ) + if self._var_is_local: + console.deprecate( + feature_name="Local Vars", + reason=( + "Setting _var_is_local to True does not have any effect anymore. " + "Use the new ImmutableVar instead." + ), + deprecation_version="0.5.9", + removal_version="0.6.0", + ) + out = self._var_full_name + else: + out = format.wrap(self._var_full_name, "{") if self._var_is_string: out = format.format_string(out) return out diff --git a/reflex/vars.pyi b/reflex/vars.pyi index 47d433374..69041d563 100644 --- a/reflex/vars.pyi +++ b/reflex/vars.pyi @@ -63,6 +63,8 @@ class ImmutableVarData: def merge( cls, *others: ImmutableVarData | VarData | None ) -> ImmutableVarData | None: ... + @classmethod + def from_state(cls, state: Type[BaseState] | str) -> ImmutableVarData: ... def _decode_var_immutable(value: str) -> tuple[ImmutableVarData, str]: ... @@ -150,7 +152,7 @@ class Var: @property def _var_full_name(self) -> str: ... def _var_set_state(self, state: Type[BaseState] | str) -> Any: ... - def _get_all_var_data(self) -> VarData: ... + def _get_all_var_data(self) -> VarData | ImmutableVarData: ... def json(self) -> str: ... @dataclass(eq=False) diff --git a/tests/test_var.py b/tests/test_var.py index 5c67d9924..3269706d0 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -8,19 +8,19 @@ from pandas import DataFrame from reflex.base import Base from reflex.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG -from reflex.experimental.vars.base import ( +from reflex.ivars.base import ( ImmutableVar, LiteralVar, var_operation, ) -from reflex.experimental.vars.function import ArgsFunctionOperation, FunctionStringVar -from reflex.experimental.vars.number import ( +from reflex.ivars.function import ArgsFunctionOperation, FunctionStringVar +from reflex.ivars.number import ( LiteralBooleanVar, LiteralNumberVar, NumberVar, ) -from reflex.experimental.vars.object import LiteralObjectVar -from reflex.experimental.vars.sequence import ( +from reflex.ivars.object import LiteralObjectVar +from reflex.ivars.sequence import ( ArrayVar, ConcatVarOperation, LiteralArrayVar,