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

This commit is contained in:
Khaleel Al-Adhami 2025-02-13 12:52:00 -08:00
commit a2074b9081
6 changed files with 94 additions and 37 deletions

View File

@ -46,10 +46,26 @@ def render_multiple_pages(app, num: int):
class State(rx.State): class State(rx.State):
"""The app state.""" """The app state."""
position: str position: rx.Field[str]
college: str college: rx.Field[str]
age: Tuple[int, int] = (18, 50) age: rx.Field[Tuple[int, int]] = rx.field((18, 50))
salary: Tuple[int, int] = (0, 25000000) 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( comp1 = rx.center(
rx.theme_panel(), rx.theme_panel(),
@ -74,13 +90,13 @@ def render_multiple_pages(app, num: int):
rx.select( rx.select(
["C", "PF", "SF", "PG", "SG"], ["C", "PF", "SF", "PG", "SG"],
placeholder="Select a position. (All)", placeholder="Select a position. (All)",
on_change=State.set_position, # pyright: ignore [reportAttributeAccessIssue] on_change=State.set_position,
size="3", size="3",
), ),
rx.select( rx.select(
college, college,
placeholder="Select a college. (All)", placeholder="Select a college. (All)",
on_change=State.set_college, # pyright: ignore [reportAttributeAccessIssue] on_change=State.set_college,
size="3", size="3",
), ),
), ),
@ -95,7 +111,7 @@ def render_multiple_pages(app, num: int):
default_value=[18, 50], default_value=[18, 50],
min=18, min=18,
max=50, max=50,
on_value_commit=State.set_age, # pyright: ignore [reportAttributeAccessIssue] on_value_commit=State.set_age,
), ),
align_items="left", align_items="left",
width="100%", width="100%",
@ -110,7 +126,7 @@ def render_multiple_pages(app, num: int):
default_value=[0, 25000000], default_value=[0, 25000000],
min=0, min=0,
max=25000000, max=25000000,
on_value_commit=State.set_salary, # pyright: ignore [reportAttributeAccessIssue] on_value_commit=State.set_salary,
), ),
align_items="left", align_items="left",
width="100%", width="100%",

View File

@ -591,7 +591,9 @@ class App(MiddlewareMixin, LifespanMixin):
Returns: Returns:
The generated component. 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( def add_page(
self, self,

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
from pathlib import Path 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 import constants
from reflex.compiler import templates, utils from reflex.compiler import templates, utils
@ -545,30 +545,47 @@ def purge_web_pages_dir():
if TYPE_CHECKING: 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 componentify_unevaluated( def _into_component_once(component: Component | ComponentCallable) -> Component | None:
possible_component: COMPONENT_TYPE_OR_CALLABLE, """Convert a component to a Component.
) -> Component:
"""Convert a possible component to a component.
Args: Args:
possible_component: The possible component to convert. component: The component to convert.
Returns: Returns:
The component. The converted component.
""" """
if isinstance(possible_component, Var): if isinstance(component, Component):
return Fragment.create(possible_component) return component
if isinstance(possible_component, tuple): if isinstance(component, (Var, int, float, str)):
return Fragment.create(*possible_component) return Fragment.create(component)
if isinstance(possible_component, Component): if isinstance(component, Sequence):
return possible_component return Fragment.create(*component)
return componentify_unevaluated(possible_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( def compile_unevaluated_page(
@ -591,7 +608,7 @@ def compile_unevaluated_page(
The compiled component and whether state should be enabled. The compiled component and whether state should be enabled.
""" """
# Generate the component if it is a callable. # 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) component._add_style_recursive(style or {}, theme)
@ -696,7 +713,7 @@ class ExecutorSafeFunctions:
The route, compiled component, and compiled page. The route, compiled component, and compiled page.
""" """
component, enable_state = compile_unevaluated_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) return route, component, compile_page(route, component, cls.STATE)

View File

@ -1742,6 +1742,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
Yields: Yields:
StateUpdate object 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 from reflex.utils import telemetry
@ -1779,12 +1782,25 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
hinted_args, (Base, BaseModelV1, BaseModelV2) hinted_args, (Base, BaseModelV1, BaseModelV2)
): ):
payload[arg] = hinted_args(**value) 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) payload[arg] = set(value)
if isinstance(value, list) and ( elif isinstance(value, list) and (
hinted_args is tuple or hinted_args is Tuple hinted_args is tuple or hinted_args is Tuple
): ):
payload[arg] = tuple(value) 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. # Wrap the function in a try/except block.
try: try:
@ -2459,7 +2475,7 @@ class ComponentState(State, mixin=True):
Returns: Returns:
A new instance of the Component with an independent copy of the State. 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 cls._per_component_state_instance_count += 1
state_cls_name = f"{cls.__name__}_n{cls._per_component_state_instance_count}" 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. # Save a reference to the dynamic state for pickle/unpickle.
setattr(reflex.istate.dynamic, state_cls_name, component_state) setattr(reflex.istate.dynamic, state_cls_name, component_state)
component = component_state.get_component(*children, **props) component = component_state.get_component(*children, **props)
component = componentify_unevaluated(component) component = into_component(component)
component.State = component_state component.State = component_state
return component return component

View File

@ -1050,7 +1050,7 @@ class Var(Generic[VAR_TYPE]):
""" """
actual_name = self._var_field_name actual_name = self._var_field_name
def setter(state: BaseState, value: Any): def setter(state: Any, value: Any):
"""Get the setter for the var. """Get the setter for the var.
Args: Args:
@ -1068,6 +1068,8 @@ class Var(Generic[VAR_TYPE]):
else: else:
setattr(state, actual_name, value) setattr(state, actual_name, value)
setter.__annotations__["value"] = self._var_type
setter.__qualname__ = self._get_setter_name() setter.__qualname__ = self._get_setter_name()
return setter return setter

View File

@ -20,7 +20,11 @@ def BackgroundTask():
class State(rx.State): class State(rx.State):
counter: int = 0 counter: int = 0
_task_id: 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) @rx.event(background=True)
async def handle_event(self): async def handle_event(self):
@ -125,8 +129,8 @@ def BackgroundTask():
rx.input( rx.input(
id="iterations", id="iterations",
placeholder="Iterations", placeholder="Iterations",
value=State.iterations.to_string(), # pyright: ignore [reportAttributeAccessIssue] value=State.iterations.to_string(),
on_change=State.set_iterations, # pyright: ignore [reportAttributeAccessIssue] on_change=State.set_iterations,
), ),
rx.button( rx.button(
"Delayed Increment", "Delayed Increment",