diff --git a/benchmarks/test_benchmark_compile_pages.py b/benchmarks/test_benchmark_compile_pages.py index 149fc6130..6cf39f60c 100644 --- a/benchmarks/test_benchmark_compile_pages.py +++ b/benchmarks/test_benchmark_compile_pages.py @@ -46,10 +46,26 @@ def render_multiple_pages(app, num: int): class State(rx.State): """The app state.""" - position: str - college: str - age: Tuple[int, int] = (18, 50) - salary: Tuple[int, int] = (0, 25000000) + position: rx.Field[str] + college: rx.Field[str] + age: rx.Field[Tuple[int, int]] = rx.field((18, 50)) + salary: rx.Field[Tuple[int, int]] = rx.field((0, 25000000)) + + @rx.event + def set_position(self, value: str): + self.position = value + + @rx.event + def set_college(self, value: str): + self.college = value + + @rx.event + def set_age(self, value: list[int]): + self.age = (value[0], value[1]) + + @rx.event + def set_salary(self, value: list[int]): + self.salary = (value[0], value[1]) comp1 = rx.center( rx.theme_panel(), @@ -74,13 +90,13 @@ def render_multiple_pages(app, num: int): rx.select( ["C", "PF", "SF", "PG", "SG"], placeholder="Select a position. (All)", - on_change=State.set_position, # pyright: ignore [reportAttributeAccessIssue] + on_change=State.set_position, size="3", ), rx.select( college, placeholder="Select a college. (All)", - on_change=State.set_college, # pyright: ignore [reportAttributeAccessIssue] + on_change=State.set_college, size="3", ), ), @@ -95,7 +111,7 @@ def render_multiple_pages(app, num: int): default_value=[18, 50], min=18, max=50, - on_value_commit=State.set_age, # pyright: ignore [reportAttributeAccessIssue] + on_value_commit=State.set_age, ), align_items="left", width="100%", @@ -110,7 +126,7 @@ def render_multiple_pages(app, num: int): default_value=[0, 25000000], min=0, max=25000000, - on_value_commit=State.set_salary, # pyright: ignore [reportAttributeAccessIssue] + on_value_commit=State.set_salary, ), align_items="left", width="100%", diff --git a/reflex/app.py b/reflex/app.py index d290b8f49..67d4f5b91 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -591,7 +591,9 @@ class App(MiddlewareMixin, LifespanMixin): Returns: The generated component. """ - return component if isinstance(component, Component) else component() + from reflex.compiler.compiler import into_component + + return into_component(component) def add_page( self, diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index b4bfcdf4b..667a477e8 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import datetime from pathlib import Path -from typing import TYPE_CHECKING, Callable, Dict, Iterable, Optional, Tuple, Type, Union +from typing import TYPE_CHECKING, Dict, Iterable, Optional, Sequence, Tuple, Type, Union from reflex import constants from reflex.compiler import templates, utils @@ -545,30 +545,47 @@ def purge_web_pages_dir(): if TYPE_CHECKING: - from reflex.app import UnevaluatedPage - - COMPONENT_TYPE = Union[Component, Var, Tuple[Union[Component, Var], ...]] - COMPONENT_TYPE_OR_CALLABLE = Union[COMPONENT_TYPE, Callable[[], COMPONENT_TYPE]] + from reflex.app import ComponentCallable, UnevaluatedPage -def componentify_unevaluated( - possible_component: COMPONENT_TYPE_OR_CALLABLE, -) -> Component: - """Convert a possible component to a component. +def _into_component_once(component: Component | ComponentCallable) -> Component | None: + """Convert a component to a Component. Args: - possible_component: The possible component to convert. + component: The component to convert. Returns: - The component. + The converted component. """ - if isinstance(possible_component, Var): - return Fragment.create(possible_component) - if isinstance(possible_component, tuple): - return Fragment.create(*possible_component) - if isinstance(possible_component, Component): - return possible_component - return componentify_unevaluated(possible_component()) + if isinstance(component, Component): + return component + if isinstance(component, (Var, int, float, str)): + return Fragment.create(component) + if isinstance(component, Sequence): + return Fragment.create(*component) + return None + + +def into_component(component: Component | ComponentCallable) -> Component: + """Convert a component to a Component. + + Args: + component: The component to convert. + + Returns: + The converted component. + + Raises: + TypeError: If the component is not a Component. + """ + if (converted := _into_component_once(component)) is not None: + return converted + if ( + callable(component) + and (converted := _into_component_once(component())) is not None + ): + return converted + raise TypeError(f"Expected a Component, got {type(component)}") def compile_unevaluated_page( @@ -591,7 +608,7 @@ def compile_unevaluated_page( The compiled component and whether state should be enabled. """ # Generate the component if it is a callable. - component = componentify_unevaluated(page.component) + component = into_component(page.component) component._add_style_recursive(style or {}, theme) @@ -696,7 +713,7 @@ class ExecutorSafeFunctions: The route, compiled component, and compiled page. """ component, enable_state = compile_unevaluated_page( - route, cls.UNCOMPILED_PAGES[route] + route, cls.UNCOMPILED_PAGES[route], cls.STATE, style, theme ) return route, component, compile_page(route, component, cls.STATE) diff --git a/reflex/state.py b/reflex/state.py index c2d5a84ea..ff1a4b2ca 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1742,6 +1742,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): Yields: StateUpdate object + + Raises: + ValueError: If a string value is received for an int or float type and cannot be converted. """ from reflex.utils import telemetry @@ -1779,12 +1782,25 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): hinted_args, (Base, BaseModelV1, BaseModelV2) ): payload[arg] = hinted_args(**value) - if isinstance(value, list) and (hinted_args is set or hinted_args is Set): + elif isinstance(value, list) and (hinted_args is set or hinted_args is Set): payload[arg] = set(value) - if isinstance(value, list) and ( + elif isinstance(value, list) and ( hinted_args is tuple or hinted_args is Tuple ): payload[arg] = tuple(value) + elif isinstance(value, str) and ( + hinted_args is int or hinted_args is float + ): + try: + payload[arg] = hinted_args(value) + except ValueError: + raise ValueError( + f"Received a string value ({value}) for {arg} but expected a {hinted_args}" + ) from None + else: + console.warn( + f"Received a string value ({value}) for {arg} but expected a {hinted_args}. A simple conversion was successful." + ) # Wrap the function in a try/except block. try: @@ -2459,7 +2475,7 @@ class ComponentState(State, mixin=True): Returns: A new instance of the Component with an independent copy of the State. """ - from reflex.compiler.compiler import componentify_unevaluated + from reflex.compiler.compiler import into_component cls._per_component_state_instance_count += 1 state_cls_name = f"{cls.__name__}_n{cls._per_component_state_instance_count}" @@ -2472,7 +2488,7 @@ class ComponentState(State, mixin=True): # Save a reference to the dynamic state for pickle/unpickle. setattr(reflex.istate.dynamic, state_cls_name, component_state) component = component_state.get_component(*children, **props) - component = componentify_unevaluated(component) + component = into_component(component) component.State = component_state return component diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 49ffaeb98..c4cd910ae 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -1050,7 +1050,7 @@ class Var(Generic[VAR_TYPE]): """ actual_name = self._var_field_name - def setter(state: BaseState, value: Any): + def setter(state: Any, value: Any): """Get the setter for the var. Args: @@ -1068,6 +1068,8 @@ class Var(Generic[VAR_TYPE]): else: setattr(state, actual_name, value) + setter.__annotations__["value"] = self._var_type + setter.__qualname__ = self._get_setter_name() return setter diff --git a/tests/integration/test_background_task.py b/tests/integration/test_background_task.py index f312f8122..91a1b5ae1 100644 --- a/tests/integration/test_background_task.py +++ b/tests/integration/test_background_task.py @@ -20,7 +20,11 @@ def BackgroundTask(): class State(rx.State): counter: int = 0 _task_id: int = 0 - iterations: int = 10 + iterations: rx.Field[int] = rx.field(10) + + @rx.event + def set_iterations(self, value: str): + self.iterations = int(value) @rx.event(background=True) async def handle_event(self): @@ -125,8 +129,8 @@ def BackgroundTask(): rx.input( id="iterations", placeholder="Iterations", - value=State.iterations.to_string(), # pyright: ignore [reportAttributeAccessIssue] - on_change=State.set_iterations, # pyright: ignore [reportAttributeAccessIssue] + value=State.iterations.to_string(), + on_change=State.set_iterations, ), rx.button( "Delayed Increment",