Kaynağa Gözat

Merge branch 'main' into add-validation-to-function-vars

Khaleel Al-Adhami 3 ay önce
ebeveyn
işleme
a2074b9081

+ 24 - 8
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%",

+ 3 - 1
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,

+ 36 - 19
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
+    from reflex.app import ComponentCallable, UnevaluatedPage
 
-    COMPONENT_TYPE = Union[Component, Var, Tuple[Union[Component, Var], ...]]
-    COMPONENT_TYPE_OR_CALLABLE = Union[COMPONENT_TYPE, Callable[[], COMPONENT_TYPE]]
 
+def _into_component_once(component: Component | ComponentCallable) -> Component | None:
+    """Convert a component to a Component.
 
-def componentify_unevaluated(
-    possible_component: COMPONENT_TYPE_OR_CALLABLE,
-) -> Component:
-    """Convert a possible component to a component.
+    Args:
+        component: The component to convert.
+
+    Returns:
+        The converted 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:
-        possible_component: The possible component to convert.
+        component: The component to convert.
 
     Returns:
-        The component.
+        The converted component.
+
+    Raises:
+        TypeError: If the component is not a 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 (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)
 

+ 20 - 4
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
 

+ 3 - 1
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

+ 7 - 3
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",