Compare commits

...

16 Commits

Author SHA1 Message Date
Masen Furer
9308caa215
bump to v0.6.3.post1 2024-10-23 16:11:58 -07:00
Masen Furer
68de3f41c4
[ENG-3989] Ensure non-serialized states are present in StateManagerDisk (#4230)
If a non-root state was serialized, but its substates were not, then these
would not be populated when reloading the pickled state, because only substates
from the root were being populated with fresh versions.

Now, capture the substates from all fresh states and apply them to the
deserialized state for each substate to ensure that the entire state tree has
all substates instantiated after deserializing, even substates that were never
serialized originally.
2024-10-23 16:11:06 -07:00
Masen Furer
addf633692
pyproject.toml: bump to 0.6.3 2024-10-18 09:22:07 -07:00
Masen Furer
004518dd51
pyproject: bump to 0.6.3a4 2024-10-17 19:22:20 -07:00
Masen Furer
b112d35f62
Fix runtime error on python 3.11.0 (#4197)
All generic types present in a Union must be parametrized on 3.11.0 if any
other generic types in the union are parametrized.

This appears to be a bug in 3.11.0, as the behavior is not observed in 3.11.1
or 3.10; fixing here as this is technically more correct anyway and avoids a
crash.
2024-10-17 19:22:12 -07:00
Masen Furer
2aea1f29b6
pyproject: bump to 0.6.3a3 2024-10-17 16:58:16 -07:00
Masen Furer
0457cbd747
When REDIS_URL is set, use redis, regardless of config preference. (#4196)
We might change this down the road, but we don't want to introduce a breaking
change at this time.
2024-10-17 16:58:08 -07:00
Masen Furer
a6b2e640ac
[ENG-3954] Treat ArrayVar.foreach index as int (#4193)
* [ENG-3954] Treat ArrayVar.foreach index as int

* foreach: convert return value to a Var

When the value returned from the foreach is not hashable (mutable type), then
it will raise an exception if it is not first converted to a LiteralVar.
2024-10-17 16:58:07 -07:00
Masen Furer
542382c3e7
bump to 0.6.3a2 2024-10-16 15:15:08 -07:00
Khaleel Al-Adhami
20398c10f1
fix pyi for untyped event handlers (#4186)
* fix pyi for untyped event handlers

* no more empty lambdas
2024-10-16 15:14:58 -07:00
Masen Furer
f8881c391d
Arbitrary arg access two levels deep for untyped handler (#4180)
* Arbitrary arg access two levels deep for untyped handler

Provide drop-in compatibility with existing component wrapping code
that was accessing attributes on the default handler arg type.

* py3.9 compat
2024-10-16 15:14:58 -07:00
Masen Furer
c460040040
LiteralEventChainVar becomes an ArgsFunctionOperation (#4174)
* LiteralEventChainVar becomes an ArgsFunctionOperation

Instead of using the ArgsFunctionOperation to create the string representation
of the _js_expr, make the identity of the var an ArgsFunctionOperation so the
_args_names and _return_expr remain accessible.

Rely on the default behavior of ArgsFunctionOperation to create the
_cached_var_name / _js_expr value.

This allows the compat shim in `format_event_chain` to remain functional, as it
does special handling for ArgsFunctionOperation to retain the previous behavior
of that function (this was a regression introduced in 0.6.2).

* _var_type is EventChain; fix parent class order

* Re-fix LiteralEventChainVar inheritence list w/ comment

* [ENG-3942] LiteralEventVar becomes VarCallOperation

instead of using `.call` when constructing the `_js_expr`, have the identity of
a LiteralEventVar as a VarCallOperation to take advantage of the _var_data
carrying.

* add event overlords

* EventCallback descriptor always returns EventSpec from class

Relax actual `__get__` definition to support the multitude of overloads

* test case for event related vars carrying _var_data

---------

Co-authored-by: Khaleel Al-Adhami <khaleel.aladhami@gmail.com>
2024-10-16 15:14:58 -07:00
Khaleel Al-Adhami
19bfdc4035
disk is memory is disk (#4185) 2024-10-16 15:14:58 -07:00
Khaleel Al-Adhami
f133bf53cc
only treat dict object vars as key value mapping (#4177) 2024-10-16 15:14:58 -07:00
Masen Furer
11dcce3975
pin AppHarness tests to ubuntu-22.04 runner (#4173) 2024-10-16 15:14:57 -07:00
Masen Furer
3494a2d3f3
bump to 0.6.3a1 2024-10-14 08:57:37 -07:00
20 changed files with 282 additions and 168 deletions

View File

@ -14,7 +14,7 @@ env:
jobs:
check_latest_node:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: ['3.12']

View File

@ -24,7 +24,7 @@ jobs:
matrix:
state_manager: ['redis', 'memory']
python-version: ['3.11.5', '3.12.0']
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
services:
# Label used to access the service container
redis:

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "reflex"
version = "0.6.3dev1"
version = "0.6.3.post1"
description = "Web apps in pure Python."
license = "Apache-2.0"
authors = [
@ -103,4 +103,4 @@ lint.pydocstyle.convention = "google"
[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function"
asyncio_mode = "auto"
asyncio_mode = "auto"

View File

@ -31,7 +31,7 @@ class ErrorBoundary(Component):
on_click: Optional[EventType[[]]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,
on_error: Optional[EventType[[]]] = None,
on_error: Optional[EventType] = None,
on_focus: Optional[EventType[[]]] = None,
on_mount: Optional[EventType[[]]] = None,
on_mouse_down: Optional[EventType[[]]] = None,

View File

@ -45,6 +45,7 @@ from reflex.event import (
EventVar,
call_event_fn,
call_event_handler,
empty_event,
get_handler_args,
)
from reflex.style import Style, format_as_emotion
@ -623,21 +624,21 @@ class Component(BaseComponent, ABC):
"""
default_triggers = {
EventTriggers.ON_FOCUS: lambda: [],
EventTriggers.ON_BLUR: lambda: [],
EventTriggers.ON_CLICK: lambda: [],
EventTriggers.ON_CONTEXT_MENU: lambda: [],
EventTriggers.ON_DOUBLE_CLICK: lambda: [],
EventTriggers.ON_MOUSE_DOWN: lambda: [],
EventTriggers.ON_MOUSE_ENTER: lambda: [],
EventTriggers.ON_MOUSE_LEAVE: lambda: [],
EventTriggers.ON_MOUSE_MOVE: lambda: [],
EventTriggers.ON_MOUSE_OUT: lambda: [],
EventTriggers.ON_MOUSE_OVER: lambda: [],
EventTriggers.ON_MOUSE_UP: lambda: [],
EventTriggers.ON_SCROLL: lambda: [],
EventTriggers.ON_MOUNT: lambda: [],
EventTriggers.ON_UNMOUNT: lambda: [],
EventTriggers.ON_FOCUS: empty_event,
EventTriggers.ON_BLUR: empty_event,
EventTriggers.ON_CLICK: empty_event,
EventTriggers.ON_CONTEXT_MENU: empty_event,
EventTriggers.ON_DOUBLE_CLICK: empty_event,
EventTriggers.ON_MOUSE_DOWN: empty_event,
EventTriggers.ON_MOUSE_ENTER: empty_event,
EventTriggers.ON_MOUSE_LEAVE: empty_event,
EventTriggers.ON_MOUSE_MOVE: empty_event,
EventTriggers.ON_MOUSE_OUT: empty_event,
EventTriggers.ON_MOUSE_OVER: empty_event,
EventTriggers.ON_MOUSE_UP: empty_event,
EventTriggers.ON_SCROLL: empty_event,
EventTriggers.ON_MOUNT: empty_event,
EventTriggers.ON_UNMOUNT: empty_event,
}
# Look for component specific triggers,
@ -648,7 +649,7 @@ class Component(BaseComponent, ABC):
annotation = field.annotation
if (metadata := getattr(annotation, "__metadata__", None)) is not None:
args_spec = metadata[0]
default_triggers[field.name] = args_spec or (lambda: [])
default_triggers[field.name] = args_spec or (empty_event) # type: ignore
return default_triggers
def __repr__(self) -> str:
@ -1705,7 +1706,7 @@ class CustomComponent(Component):
value = self._create_event_chain(
value=value,
args_spec=event_triggers_in_component_declaration.get(
key, lambda: []
key, empty_event
),
)
self.props[format.to_camel_case(key)] = value

View File

@ -139,20 +139,20 @@ class DataEditor(NoSSRComponent):
on_cell_activated: Optional[EventType] = None,
on_cell_clicked: Optional[EventType] = None,
on_cell_context_menu: Optional[EventType] = None,
on_cell_edited: Optional[EventType[[]]] = None,
on_cell_edited: Optional[EventType] = None,
on_click: Optional[EventType[[]]] = None,
on_column_resize: Optional[EventType[[]]] = None,
on_column_resize: Optional[EventType] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_delete: Optional[EventType[[]]] = None,
on_delete: Optional[EventType] = None,
on_double_click: Optional[EventType[[]]] = None,
on_finished_editing: Optional[EventType[[]]] = None,
on_finished_editing: Optional[EventType] = None,
on_focus: Optional[EventType[[]]] = None,
on_group_header_clicked: Optional[EventType[[]]] = None,
on_group_header_context_menu: Optional[EventType[[]]] = None,
on_group_header_renamed: Optional[EventType[[]]] = None,
on_group_header_clicked: Optional[EventType] = None,
on_group_header_context_menu: Optional[EventType] = None,
on_group_header_renamed: Optional[EventType] = None,
on_header_clicked: Optional[EventType] = None,
on_header_context_menu: Optional[EventType] = None,
on_header_menu_click: Optional[EventType[[]]] = None,
on_header_menu_click: Optional[EventType] = None,
on_item_hovered: Optional[EventType] = None,
on_mount: Optional[EventType[[]]] = None,
on_mouse_down: Optional[EventType[[]]] = None,

View File

@ -58,7 +58,7 @@ class Audio(ReactPlayer):
on_play: Optional[EventType[[]]] = None,
on_playback_quality_change: Optional[EventType[[]]] = None,
on_playback_rate_change: Optional[EventType[[]]] = None,
on_progress: Optional[EventType[[]]] = None,
on_progress: Optional[EventType] = None,
on_ready: Optional[EventType[[]]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_seek: Optional[EventType] = None,

View File

@ -56,7 +56,7 @@ class ReactPlayer(NoSSRComponent):
on_play: Optional[EventType[[]]] = None,
on_playback_quality_change: Optional[EventType[[]]] = None,
on_playback_rate_change: Optional[EventType[[]]] = None,
on_progress: Optional[EventType[[]]] = None,
on_progress: Optional[EventType] = None,
on_ready: Optional[EventType[[]]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_seek: Optional[EventType] = None,

View File

@ -58,7 +58,7 @@ class Video(ReactPlayer):
on_play: Optional[EventType[[]]] = None,
on_playback_quality_change: Optional[EventType[[]]] = None,
on_playback_rate_change: Optional[EventType[[]]] = None,
on_progress: Optional[EventType[[]]] = None,
on_progress: Optional[EventType] = None,
on_ready: Optional[EventType[[]]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_seek: Optional[EventType] = None,

View File

@ -252,7 +252,7 @@ class Brush(Recharts):
A dict mapping the event trigger to the var that is passed to the handler.
"""
return {
EventTriggers.ON_CHANGE: lambda: [],
EventTriggers.ON_CHANGE: empty_event,
}
@ -293,10 +293,10 @@ class Cartesian(Recharts):
name: Var[Union[str, int]]
# The customized event handler of animation start
on_animation_start: EventHandler[lambda: []]
on_animation_start: EventHandler[empty_event]
# The customized event handler of animation end
on_animation_end: EventHandler[lambda: []]
on_animation_end: EventHandler[empty_event]
# The customized event handler of click on the component in this group
on_click: EventHandler[empty_event]

View File

@ -330,9 +330,9 @@ class RadarChart(ChartBase):
A dict mapping the event trigger to the var that is passed to the handler.
"""
return {
EventTriggers.ON_CLICK: lambda: [],
EventTriggers.ON_MOUSE_ENTER: lambda: [],
EventTriggers.ON_MOUSE_LEAVE: lambda: [],
EventTriggers.ON_CLICK: empty_event,
EventTriggers.ON_MOUSE_ENTER: empty_event,
EventTriggers.ON_MOUSE_LEAVE: empty_event,
}
@ -419,14 +419,14 @@ class ScatterChart(ChartBase):
A dict mapping the event trigger to the var that is passed to the handler.
"""
return {
EventTriggers.ON_CLICK: lambda: [],
EventTriggers.ON_MOUSE_DOWN: lambda: [],
EventTriggers.ON_MOUSE_UP: lambda: [],
EventTriggers.ON_MOUSE_MOVE: lambda: [],
EventTriggers.ON_MOUSE_OVER: lambda: [],
EventTriggers.ON_MOUSE_OUT: lambda: [],
EventTriggers.ON_MOUSE_ENTER: lambda: [],
EventTriggers.ON_MOUSE_LEAVE: lambda: [],
EventTriggers.ON_CLICK: empty_event,
EventTriggers.ON_MOUSE_DOWN: empty_event,
EventTriggers.ON_MOUSE_UP: empty_event,
EventTriggers.ON_MOUSE_MOVE: empty_event,
EventTriggers.ON_MOUSE_OVER: empty_event,
EventTriggers.ON_MOUSE_OUT: empty_event,
EventTriggers.ON_MOUSE_ENTER: empty_event,
EventTriggers.ON_MOUSE_LEAVE: empty_event,
}

View File

@ -103,14 +103,14 @@ class Pie(Recharts):
A dict mapping the event trigger to the var that is passed to the handler.
"""
return {
EventTriggers.ON_ANIMATION_START: lambda: [],
EventTriggers.ON_ANIMATION_END: lambda: [],
EventTriggers.ON_CLICK: lambda: [],
EventTriggers.ON_MOUSE_MOVE: lambda: [],
EventTriggers.ON_MOUSE_OVER: lambda: [],
EventTriggers.ON_MOUSE_OUT: lambda: [],
EventTriggers.ON_MOUSE_ENTER: lambda: [],
EventTriggers.ON_MOUSE_LEAVE: lambda: [],
EventTriggers.ON_ANIMATION_START: empty_event,
EventTriggers.ON_ANIMATION_END: empty_event,
EventTriggers.ON_CLICK: empty_event,
EventTriggers.ON_MOUSE_MOVE: empty_event,
EventTriggers.ON_MOUSE_OVER: empty_event,
EventTriggers.ON_MOUSE_OUT: empty_event,
EventTriggers.ON_MOUSE_ENTER: empty_event,
EventTriggers.ON_MOUSE_LEAVE: empty_event,
}
@ -167,8 +167,8 @@ class Radar(Recharts):
A dict mapping the event trigger to the var that is passed to the handler.
"""
return {
EventTriggers.ON_ANIMATION_START: lambda: [],
EventTriggers.ON_ANIMATION_END: lambda: [],
EventTriggers.ON_ANIMATION_START: empty_event,
EventTriggers.ON_ANIMATION_END: empty_event,
}
@ -219,14 +219,14 @@ class RadialBar(Recharts):
A dict mapping the event trigger to the var that is passed to the handler.
"""
return {
EventTriggers.ON_CLICK: lambda: [],
EventTriggers.ON_MOUSE_MOVE: lambda: [],
EventTriggers.ON_MOUSE_OVER: lambda: [],
EventTriggers.ON_MOUSE_OUT: lambda: [],
EventTriggers.ON_MOUSE_ENTER: lambda: [],
EventTriggers.ON_MOUSE_LEAVE: lambda: [],
EventTriggers.ON_ANIMATION_START: lambda: [],
EventTriggers.ON_ANIMATION_END: lambda: [],
EventTriggers.ON_CLICK: empty_event,
EventTriggers.ON_MOUSE_MOVE: empty_event,
EventTriggers.ON_MOUSE_OVER: empty_event,
EventTriggers.ON_MOUSE_OUT: empty_event,
EventTriggers.ON_MOUSE_ENTER: empty_event,
EventTriggers.ON_MOUSE_LEAVE: empty_event,
EventTriggers.ON_ANIMATION_START: empty_event,
EventTriggers.ON_ANIMATION_END: empty_event,
}
@ -392,12 +392,12 @@ class PolarRadiusAxis(Recharts):
A dict mapping the event trigger to the var that is passed to the handler.
"""
return {
EventTriggers.ON_CLICK: lambda: [],
EventTriggers.ON_MOUSE_MOVE: lambda: [],
EventTriggers.ON_MOUSE_OVER: lambda: [],
EventTriggers.ON_MOUSE_OUT: lambda: [],
EventTriggers.ON_MOUSE_ENTER: lambda: [],
EventTriggers.ON_MOUSE_LEAVE: lambda: [],
EventTriggers.ON_CLICK: empty_event,
EventTriggers.ON_MOUSE_MOVE: empty_event,
EventTriggers.ON_MOUSE_OVER: empty_event,
EventTriggers.ON_MOUSE_OUT: empty_event,
EventTriggers.ON_MOUSE_ENTER: empty_event,
EventTriggers.ON_MOUSE_LEAVE: empty_event,
}

View File

@ -122,16 +122,16 @@ class Editor(NoSSRComponent):
class_name: Optional[Any] = None,
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[EventType[[]]] = None,
on_change: Optional[EventType[[]]] = None,
on_blur: Optional[EventType] = None,
on_change: Optional[EventType] = None,
on_click: Optional[EventType[[]]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_copy: Optional[EventType[[]]] = None,
on_cut: Optional[EventType[[]]] = None,
on_copy: Optional[EventType] = None,
on_cut: Optional[EventType] = None,
on_double_click: Optional[EventType[[]]] = None,
on_focus: Optional[EventType[[]]] = None,
on_input: Optional[EventType[[]]] = None,
on_load: Optional[EventType[[]]] = None,
on_input: Optional[EventType] = None,
on_load: Optional[EventType] = None,
on_mount: Optional[EventType[[]]] = None,
on_mouse_down: Optional[EventType[[]]] = None,
on_mouse_enter: Optional[EventType[[]]] = None,
@ -140,12 +140,12 @@ class Editor(NoSSRComponent):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_paste: Optional[EventType[[]]] = None,
on_resize_editor: Optional[EventType[[]]] = None,
on_paste: Optional[EventType] = None,
on_resize_editor: Optional[EventType] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
toggle_code_view: Optional[EventType[[]]] = None,
toggle_full_screen: Optional[EventType[[]]] = None,
toggle_code_view: Optional[EventType] = None,
toggle_full_screen: Optional[EventType] = None,
**props,
) -> "Editor":
"""Create an instance of Editor. No children allowed.

View File

@ -22,6 +22,7 @@ from typing import (
TypeVar,
Union,
get_type_hints,
overload,
)
from typing_extensions import ParamSpec, get_args, get_origin
@ -32,14 +33,17 @@ from reflex.utils.exceptions import EventFnArgMismatch, EventHandlerArgMismatch
from reflex.utils.types import ArgsSpec, GenericType
from reflex.vars import VarData
from reflex.vars.base import (
CachedVarOperation,
LiteralNoneVar,
LiteralVar,
ToOperation,
Var,
cached_property_no_lock,
)
from reflex.vars.function import ArgsFunctionOperation, FunctionStringVar, FunctionVar
from reflex.vars.function import (
ArgsFunctionOperation,
FunctionStringVar,
FunctionVar,
VarOperationCall,
)
from reflex.vars.object import ObjectVar
try:
@ -395,11 +399,6 @@ class EventChain(EventActionsMixin):
invocation: Optional[Var] = dataclasses.field(default=None)
# These chains can be used for their side effects when no other events are desired.
stop_propagation = EventChain(events=[], args_spec=lambda: []).stop_propagation
prevent_default = EventChain(events=[], args_spec=lambda: []).prevent_default
@dataclasses.dataclass(
init=True,
frozen=True,
@ -463,6 +462,11 @@ def empty_event() -> Tuple[()]:
return tuple() # type: ignore
# These chains can be used for their side effects when no other events are desired.
stop_propagation = EventChain(events=[], args_spec=empty_event).stop_propagation
prevent_default = EventChain(events=[], args_spec=empty_event).prevent_default
T = TypeVar("T")
@ -1041,7 +1045,8 @@ def resolve_annotation(annotations: dict[str, Any], arg_name: str):
deprecation_version="0.6.3",
removal_version="0.7.0",
)
return JavascriptInputEvent
# Allow arbitrary attribute access two levels deep until removed.
return Dict[str, dict]
return annotation
@ -1258,7 +1263,7 @@ class EventVar(ObjectVar):
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class LiteralEventVar(CachedVarOperation, LiteralVar, EventVar):
class LiteralEventVar(VarOperationCall, LiteralVar, EventVar):
"""A literal event var."""
_var_value: EventSpec = dataclasses.field(default=None) # type: ignore
@ -1271,35 +1276,6 @@ class LiteralEventVar(CachedVarOperation, LiteralVar, EventVar):
"""
return hash((self.__class__.__name__, self._js_expr))
@cached_property_no_lock
def _cached_var_name(self) -> str:
"""The name of the var.
Returns:
The name of the var.
"""
return str(
FunctionStringVar("Event").call(
# event handler name
".".join(
filter(
None,
format.get_event_handler_parts(self._var_value.handler),
)
),
# event handler args
{str(name): value for name, value in self._var_value.args},
# event actions
self._var_value.event_actions,
# client handler name
*(
[self._var_value.client_handler_name]
if self._var_value.client_handler_name
else []
),
)
)
@classmethod
def create(
cls,
@ -1320,6 +1296,22 @@ class LiteralEventVar(CachedVarOperation, LiteralVar, EventVar):
_var_type=EventSpec,
_var_data=_var_data,
_var_value=value,
_func=FunctionStringVar("Event"),
_args=(
# event handler name
".".join(
filter(
None,
format.get_event_handler_parts(value.handler),
)
),
# event handler args
{str(name): value for name, value in value.args},
# event actions
value.event_actions,
# client handler name
*([value.client_handler_name] if value.client_handler_name else []),
),
)
@ -1332,7 +1324,10 @@ class EventChainVar(FunctionVar):
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class LiteralEventChainVar(CachedVarOperation, LiteralVar, EventChainVar):
# Note: LiteralVar is second in the inheritance list allowing it act like a
# CachedVarOperation (ArgsFunctionOperation) and get the _js_expr from the
# _cached_var_name property.
class LiteralEventChainVar(ArgsFunctionOperation, LiteralVar, EventChainVar):
"""A literal event chain var."""
_var_value: EventChain = dataclasses.field(default=None) # type: ignore
@ -1345,41 +1340,6 @@ class LiteralEventChainVar(CachedVarOperation, LiteralVar, EventChainVar):
"""
return hash((self.__class__.__name__, self._js_expr))
@cached_property_no_lock
def _cached_var_name(self) -> str:
"""The name of the var.
Returns:
The name of the var.
"""
sig = inspect.signature(self._var_value.args_spec) # type: ignore
if sig.parameters:
arg_def = tuple((f"_{p}" for p in sig.parameters))
arg_def_expr = LiteralVar.create([Var(_js_expr=arg) for arg in arg_def])
else:
# add a default argument for addEvents if none were specified in value.args_spec
# used to trigger the preventDefault() on the event.
arg_def = ("...args",)
arg_def_expr = Var(_js_expr="args")
if self._var_value.invocation is None:
invocation = FunctionStringVar.create("addEvents")
else:
invocation = self._var_value.invocation
return str(
ArgsFunctionOperation.create(
arg_def,
invocation.call(
LiteralVar.create(
[LiteralVar.create(event) for event in self._var_value.events]
),
arg_def_expr,
self._var_value.event_actions,
),
)
)
@classmethod
def create(
cls,
@ -1395,10 +1355,31 @@ class LiteralEventChainVar(CachedVarOperation, LiteralVar, EventChainVar):
Returns:
The created LiteralEventChainVar instance.
"""
sig = inspect.signature(value.args_spec) # type: ignore
if sig.parameters:
arg_def = tuple((f"_{p}" for p in sig.parameters))
arg_def_expr = LiteralVar.create([Var(_js_expr=arg) for arg in arg_def])
else:
# add a default argument for addEvents if none were specified in value.args_spec
# used to trigger the preventDefault() on the event.
arg_def = ("...args",)
arg_def_expr = Var(_js_expr="args")
if value.invocation is None:
invocation = FunctionStringVar.create("addEvents")
else:
invocation = value.invocation
return cls(
_js_expr="",
_var_type=EventChain,
_var_data=_var_data,
_args_names=arg_def,
_return_expr=invocation.call(
LiteralVar.create([LiteralVar.create(event) for event in value.events]),
arg_def_expr,
value.event_actions,
),
_var_value=value,
)
@ -1431,12 +1412,17 @@ class ToEventChainVarOperation(ToOperation, EventChainVar):
G = ParamSpec("G")
IndividualEventType = Union[EventSpec, EventHandler, Callable[G, Any], Var]
IndividualEventType = Union[EventSpec, EventHandler, Callable[G, Any], Var[Any]]
EventType = Union[IndividualEventType[G], List[IndividualEventType[G]]]
P = ParamSpec("P")
T = TypeVar("T")
V = TypeVar("V")
V2 = TypeVar("V2")
V3 = TypeVar("V3")
V4 = TypeVar("V4")
V5 = TypeVar("V5")
if sys.version_info >= (3, 10):
from typing import Concatenate
@ -1452,7 +1438,55 @@ if sys.version_info >= (3, 10):
"""
self.func = func
def __get__(self, instance, owner) -> Callable[P, T]:
@overload
def __get__(
self: EventCallback[[V], T], instance: None, owner
) -> Callable[[Union[Var[V], V]], EventSpec]: ...
@overload
def __get__(
self: EventCallback[[V, V2], T], instance: None, owner
) -> Callable[[Union[Var[V], V], Union[Var[V2], V2]], EventSpec]: ...
@overload
def __get__(
self: EventCallback[[V, V2, V3], T], instance: None, owner
) -> Callable[
[Union[Var[V], V], Union[Var[V2], V2], Union[Var[V3], V3]],
EventSpec,
]: ...
@overload
def __get__(
self: EventCallback[[V, V2, V3, V4], T], instance: None, owner
) -> Callable[
[
Union[Var[V], V],
Union[Var[V2], V2],
Union[Var[V3], V3],
Union[Var[V4], V4],
],
EventSpec,
]: ...
@overload
def __get__(
self: EventCallback[[V, V2, V3, V4, V5], T], instance: None, owner
) -> Callable[
[
Union[Var[V], V],
Union[Var[V2], V2],
Union[Var[V3], V3],
Union[Var[V4], V4],
Union[Var[V5], V5],
],
EventSpec,
]: ...
@overload
def __get__(self, instance, owner) -> Callable[P, T]: ...
def __get__(self, instance, owner) -> Callable:
"""Get the function with the instance bound to it.
Args:

View File

@ -2566,9 +2566,11 @@ class StateManager(Base, ABC):
The state manager (either disk, memory or redis).
"""
config = get_config()
if config.state_manager_mode == constants.StateManagerMode.DISK:
return StateManagerMemory(state=state)
if prerequisites.parse_redis_url() is not None:
config.state_manager_mode = constants.StateManagerMode.REDIS
if config.state_manager_mode == constants.StateManagerMode.MEMORY:
return StateManagerMemory(state=state)
if config.state_manager_mode == constants.StateManagerMode.DISK:
return StateManagerDisk(state=state)
if config.state_manager_mode == constants.StateManagerMode.REDIS:
redis = prerequisites.get_redis()
@ -2840,9 +2842,13 @@ class StateManagerDisk(StateManager):
for substate in state.get_substates():
substate_token = _substate_key(client_token, substate)
fresh_instance = await root_state.get_state(substate)
instance = await self.load_state(substate_token)
if instance is None:
instance = await root_state.get_state(substate)
if instance is not None:
# Ensure all substates exist, even if they weren't serialized previously.
instance.substates = fresh_instance.substates
else:
instance = fresh_instance
state.substates[substate.get_name()] = instance
instance.parent_state = state

View File

@ -10,6 +10,7 @@ from reflex.event import EventChain, EventHandler
from reflex.utils import format
from reflex.utils.exceptions import ReflexError
from reflex.utils.imports import ImportVar
from reflex.utils.types import get_origin
from reflex.vars import VarData
from reflex.vars.base import CallableVar, LiteralVar, Var
from reflex.vars.function import FunctionVar
@ -196,6 +197,10 @@ def convert(
isinstance(value, Breakpoints)
and all(not isinstance(v, dict) for v in value.values())
)
or (
isinstance(value, ObjectVar)
and not issubclass(get_origin(value._var_type) or value._var_type, dict)
)
else (key,)
)

View File

@ -429,7 +429,7 @@ def _generate_component_create_functiondef(
def figure_out_return_type(annotation: Any):
if inspect.isclass(annotation) and issubclass(annotation, inspect._empty):
return ast.Name(id="Optional[EventType[[]]]")
return ast.Name(id="Optional[EventType]")
if isinstance(annotation, str) and annotation.startswith("Tuple["):
inside_of_tuple = annotation.removeprefix("Tuple[").removesuffix("]")

View File

@ -1155,7 +1155,7 @@ class ArrayVar(Var[ARRAY_VAR_TYPE]):
function_var = ArgsFunctionOperation.create(tuple(), return_value)
else:
# generic number var
number_var = Var("").to(NumberVar)
number_var = Var("").to(NumberVar, int)
first_arg_type = self[number_var]._var_type
@ -1167,7 +1167,10 @@ class ArrayVar(Var[ARRAY_VAR_TYPE]):
_var_type=first_arg_type,
).guess_type()
function_var = ArgsFunctionOperation.create((arg_name,), fn(first_arg))
function_var = ArgsFunctionOperation.create(
(arg_name,),
Var.create(fn(first_arg)),
)
return map_array_operation(self, function_var)

View File

@ -2,11 +2,18 @@ from typing import List
import pytest
from reflex import event
from reflex.event import Event, EventHandler, EventSpec, call_event_handler, fix_events
from reflex.event import (
Event,
EventChain,
EventHandler,
EventSpec,
call_event_handler,
event,
fix_events,
)
from reflex.state import BaseState
from reflex.utils import format
from reflex.vars.base import LiteralVar, Var
from reflex.vars.base import Field, LiteralVar, Var, field
def make_var(value) -> Var:
@ -388,3 +395,28 @@ def test_event_actions_on_state():
assert sp_handler.event_actions == {"stopPropagation": True}
# should NOT affect other references to the handler
assert not handler.event_actions
def test_event_var_data():
class S(BaseState):
x: Field[int] = field(0)
@event
def s(self, value: int):
pass
# Handler doesn't have any _var_data because it's just a str
handler_var = Var.create(S.s)
assert handler_var._get_all_var_data() is None
# Ensure spec carries _var_data
spec_var = Var.create(S.s(S.x))
assert spec_var._get_all_var_data() == S.x._get_all_var_data()
# Needed to instantiate the EventChain
def _args_spec(value: Var[int]) -> tuple[Var[int]]:
return (value,)
# Ensure chain carries _var_data
chain_var = Var.create(EventChain(events=[S.s(S.x)], args_spec=_args_spec))
assert chain_var._get_all_var_data() == S.x._get_all_var_data()

View File

@ -3313,3 +3313,36 @@ def test_assignment_to_undeclared_vars():
state.handle_supported_regular_vars()
state.handle_non_var()
@pytest.mark.asyncio
async def test_deserialize_gc_state_disk(token):
"""Test that a state can be deserialized from disk with a grandchild state.
Args:
token: A token.
"""
class Root(BaseState):
pass
class State(Root):
num: int = 42
class Child(State):
foo: str = "bar"
dsm = StateManagerDisk(state=Root)
async with dsm.modify_state(token) as root:
s = await root.get_state(State)
s.num += 1
c = await root.get_state(Child)
assert s._get_was_touched()
assert not c._get_was_touched()
dsm2 = StateManagerDisk(state=Root)
root = await dsm2.get_state(token)
s = await root.get_state(State)
assert s.num == 43
c = await root.get_state(Child)
assert c.foo == "bar"