From 4260a0cfc344ecc82cfe684789151bb370adb627 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 30 Oct 2024 11:10:51 -0700 Subject: [PATCH 01/27] rx.event(background=True) (#4263) * event background True * fix typo * fix overloads * forgor * remove extra parens * more forgor --- reflex/app.py | 2 +- reflex/event.py | 118 ++++++++++++++++++---- reflex/experimental/misc.py | 2 +- reflex/state.py | 4 +- tests/integration/test_background_task.py | 25 ++--- tests/units/states/upload.py | 6 +- tests/units/test_app.py | 2 +- tests/units/test_state.py | 6 +- 8 files changed, 116 insertions(+), 49 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 5923e3389..e350be515 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -1389,7 +1389,7 @@ def upload(app: App): if isinstance(func, EventHandler): if func.is_background: raise UploadTypeError( - f"@rx.background is not supported for upload handler `{handler}`.", + f"@rx.event(background=True) is not supported for upload handler `{handler}`.", ) func = func.fn if isinstance(func, functools.partial): diff --git a/reflex/event.py b/reflex/event.py index 86620e65d..aa366e3bb 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -83,7 +83,7 @@ class Event: BACKGROUND_TASK_MARKER = "_reflex_background_task" -def background(fn): +def background(fn, *, __internal_reflex_call: bool = False): """Decorator to mark event handler as running in the background. Args: @@ -96,6 +96,13 @@ def background(fn): Raises: TypeError: If the function is not a coroutine function or async generator. """ + if not __internal_reflex_call: + console.deprecate( + "background-decorator", + "Use `rx.event(background=True)` instead.", + "0.6.5", + "0.7.0", + ) if not inspect.iscoroutinefunction(fn) and not inspect.isasyncgenfunction(fn): raise TypeError("Background task must be async function or generator.") setattr(fn, BACKGROUND_TASK_MARKER, True) @@ -1457,6 +1464,8 @@ V3 = TypeVar("V3") V4 = TypeVar("V4") V5 = TypeVar("V5") +background_event_decorator = background + if sys.version_info >= (3, 10): from typing import Concatenate @@ -1557,32 +1566,12 @@ if sys.version_info >= (3, 10): return partial(self.func, instance) # type: ignore - def event_handler(func: Callable[Concatenate[Any, P], T]) -> EventCallback[P, T]: - """Wrap a function to be used as an event. - Args: - func: The function to wrap. - - Returns: - The wrapped function. - """ - return func # type: ignore else: class EventCallback(Generic[P, T]): """A descriptor that wraps a function to be used as an event.""" - def event_handler(func: Callable[P, T]) -> Callable[P, T]: - """Wrap a function to be used as an event. - - Args: - func: The function to wrap. - - Returns: - The wrapped function. - """ - return func - G = ParamSpec("G") @@ -1608,8 +1597,93 @@ class EventNamespace(types.SimpleNamespace): EventChainVar = EventChainVar LiteralEventChainVar = LiteralEventChainVar EventType = EventType + EventCallback = EventCallback + + if sys.version_info >= (3, 10): + + @overload + @staticmethod + def __call__( + func: None = None, *, background: bool | None = None + ) -> Callable[[Callable[Concatenate[Any, P], T]], EventCallback[P, T]]: ... + + @overload + @staticmethod + def __call__( + func: Callable[Concatenate[Any, P], T], + *, + background: bool | None = None, + ) -> EventCallback[P, T]: ... + + @staticmethod + def __call__( + func: Callable[Concatenate[Any, P], T] | None = None, + *, + background: bool | None = None, + ) -> Union[ + EventCallback[P, T], + Callable[[Callable[Concatenate[Any, P], T]], EventCallback[P, T]], + ]: + """Wrap a function to be used as an event. + + Args: + func: The function to wrap. + background: Whether the event should be run in the background. Defaults to False. + + Returns: + The wrapped function. + """ + + def wrapper(func: Callable[Concatenate[Any, P], T]) -> EventCallback[P, T]: + if background is True: + return background_event_decorator(func, __internal_reflex_call=True) # type: ignore + return func # type: ignore + + if func is not None: + return wrapper(func) + return wrapper + else: + + @overload + @staticmethod + def __call__( + func: None = None, *, background: bool | None = None + ) -> Callable[[Callable[P, T]], Callable[P, T]]: ... + + @overload + @staticmethod + def __call__( + func: Callable[P, T], *, background: bool | None = None + ) -> Callable[P, T]: ... + + @staticmethod + def __call__( + func: Callable[P, T] | None = None, + *, + background: bool | None = None, + ) -> Union[ + Callable[P, T], + Callable[[Callable[P, T]], Callable[P, T]], + ]: + """Wrap a function to be used as an event. + + Args: + func: The function to wrap. + background: Whether the event should be run in the background. Defaults to False. + + Returns: + The wrapped function. + """ + + def wrapper(func: Callable[P, T]) -> Callable[P, T]: + if background is True: + return background_event_decorator(func, __internal_reflex_call=True) # type: ignore + return func # type: ignore + + if func is not None: + return wrapper(func) + return wrapper - __call__ = staticmethod(event_handler) get_event = staticmethod(get_event) get_hydrate_event = staticmethod(get_hydrate_event) fix_events = staticmethod(fix_events) diff --git a/reflex/experimental/misc.py b/reflex/experimental/misc.py index e3d237153..a2a5a0615 100644 --- a/reflex/experimental/misc.py +++ b/reflex/experimental/misc.py @@ -7,7 +7,7 @@ from typing import Any async def run_in_thread(func) -> Any: """Run a function in a separate thread. - To not block the UI event queue, run_in_thread must be inside inside a rx.background() decorated method. + To not block the UI event queue, run_in_thread must be inside inside a rx.event(background=True) decorated method. Args: func (callable): The non-async function to run. diff --git a/reflex/state.py b/reflex/state.py index 6e229b97d..2704d58f2 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -2346,7 +2346,7 @@ class StateProxy(wrapt.ObjectProxy): class State(rx.State): counter: int = 0 - @rx.background + @rx.event(background=True) async def bg_increment(self): await asyncio.sleep(1) async with self: @@ -3248,7 +3248,7 @@ class StateManagerRedis(StateManager): raise LockExpiredError( f"Lock expired for token {token} while processing. Consider increasing " f"`app.state_manager.lock_expiration` (currently {self.lock_expiration}) " - "or use `@rx.background` decorator for long-running tasks." + "or use `@rx.event(background=True)` decorator for long-running tasks." ) client_token, substate_name = _split_substate_key(token) # If the substate name on the token doesn't match the instance name, it cannot have a parent. diff --git a/tests/integration/test_background_task.py b/tests/integration/test_background_task.py index a445112f3..87aa1459b 100644 --- a/tests/integration/test_background_task.py +++ b/tests/integration/test_background_task.py @@ -1,4 +1,4 @@ -"""Test @rx.background task functionality.""" +"""Test @rx.event(background=True) task functionality.""" from typing import Generator @@ -22,8 +22,7 @@ def BackgroundTask(): _task_id: int = 0 iterations: int = 10 - @rx.background - @rx.event + @rx.event(background=True) async def handle_event(self): async with self: self._task_id += 1 @@ -32,8 +31,7 @@ def BackgroundTask(): self.counter += 1 await asyncio.sleep(0.005) - @rx.background - @rx.event + @rx.event(background=True) async def handle_event_yield_only(self): async with self: self._task_id += 1 @@ -48,7 +46,7 @@ def BackgroundTask(): def increment(self): self.counter += 1 - @rx.background + @rx.event(background=True) async def increment_arbitrary(self, amount: int): async with self: self.counter += int(amount) @@ -61,8 +59,7 @@ def BackgroundTask(): async def blocking_pause(self): await asyncio.sleep(0.02) - @rx.background - @rx.event + @rx.event(background=True) async def non_blocking_pause(self): await asyncio.sleep(0.02) @@ -74,15 +71,13 @@ def BackgroundTask(): self.counter += 1 await asyncio.sleep(0.005) - @rx.background - @rx.event + @rx.event(background=True) async def handle_racy_event(self): await asyncio.gather( self.racy_task(), self.racy_task(), self.racy_task(), self.racy_task() ) - @rx.background - @rx.event + @rx.event(background=True) async def nested_async_with_self(self): async with self: self.counter += 1 @@ -94,8 +89,7 @@ def BackgroundTask(): third_state = await self.get_state(ThirdState) await third_state._triple_count() - @rx.background - @rx.event + @rx.event(background=True) async def yield_in_async_with_self(self): async with self: self.counter += 1 @@ -103,8 +97,7 @@ def BackgroundTask(): self.counter += 1 class OtherState(rx.State): - @rx.background - @rx.event + @rx.event(background=True) async def get_other_state(self): async with self: state = await self.get_state(State) diff --git a/tests/units/states/upload.py b/tests/units/states/upload.py index f81e9f235..338025bcd 100644 --- a/tests/units/states/upload.py +++ b/tests/units/states/upload.py @@ -71,7 +71,7 @@ class FileUploadState(State): assert file.filename is not None self.img_list.append(file.filename) - @rx.background + @rx.event(background=True) async def bg_upload(self, files: List[rx.UploadFile]): """Background task cannot be upload handler. @@ -119,7 +119,7 @@ class ChildFileUploadState(FileStateBase1): assert file.filename is not None self.img_list.append(file.filename) - @rx.background + @rx.event(background=True) async def bg_upload(self, files: List[rx.UploadFile]): """Background task cannot be upload handler. @@ -167,7 +167,7 @@ class GrandChildFileUploadState(FileStateBase2): assert file.filename is not None self.img_list.append(file.filename) - @rx.background + @rx.event(background=True) async def bg_upload(self, files: List[rx.UploadFile]): """Background task cannot be upload handler. diff --git a/tests/units/test_app.py b/tests/units/test_app.py index 6bb81522f..7fba7ba1d 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -874,7 +874,7 @@ async def test_upload_file_background(state, tmp_path, token): await fn(request_mock, [file_mock]) assert ( err.value.args[0] - == f"@rx.background is not supported for upload handler `{state.get_full_name()}.bg_upload`." + == f"@rx.event(background=True) is not supported for upload handler `{state.get_full_name()}.bg_upload`." ) if isinstance(app.state_manager, StateManagerRedis): diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 89dd1fd3d..8397954cf 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -1965,7 +1965,7 @@ class BackgroundTaskState(BaseState): """ return self.order - @rx.background + @rx.event(background=True) async def background_task(self): """A background task that updates the state.""" async with self: @@ -2002,7 +2002,7 @@ class BackgroundTaskState(BaseState): self.other() # direct calling event handlers works in context self._private_method() - @rx.background + @rx.event(background=True) async def background_task_reset(self): """A background task that resets the state.""" with pytest.raises(ImmutableStateError): @@ -2016,7 +2016,7 @@ class BackgroundTaskState(BaseState): async with self: self.order.append("reset") - @rx.background + @rx.event(background=True) async def background_task_generator(self): """A background task generator that does nothing. From c8a7ee52bf8989942a2fb92e71d00e3ac6b8d864 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 30 Oct 2024 11:11:03 -0700 Subject: [PATCH 02/27] add type validation for state setattr (#4265) * add type validation for state setattr * add type to check to state setattr * add type validation to computed vars --- reflex/state.py | 17 ++++++++++++-- reflex/utils/types.py | 52 ++++++++++++++++++++++++++++++++++++++++++- reflex/vars/base.py | 48 ++++++++++++++++++++++++++++++--------- 3 files changed, 103 insertions(+), 14 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 2704d58f2..7bdbcdc2b 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -91,7 +91,7 @@ from reflex.utils.exceptions import ( ) from reflex.utils.exec import is_testing_env from reflex.utils.serializers import serializer -from reflex.utils.types import get_origin, override +from reflex.utils.types import _isinstance, get_origin, override from reflex.vars import VarData if TYPE_CHECKING: @@ -636,7 +636,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): def computed_var_func(state: Self): result = f(state) - if not isinstance(result, of_type): + if not _isinstance(result, of_type): console.warn( f"Inline ComputedVar {f} expected type {of_type}, got {type(result)}. " "You can specify expected type with `of_type` argument." @@ -1274,6 +1274,19 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): f"All state variables must be declared before they can be set." ) + fields = self.get_fields() + + if name in fields and not _isinstance( + value, (field_type := fields[name].outer_type_) + ): + console.deprecate( + "mismatched-type-assignment", + f"Tried to assign value {value} of type {type(value)} to field {type(self).__name__}.{name} of type {field_type}." + " This might lead to unexpected behavior.", + "0.6.5", + "0.7.0", + ) + # Set the attribute. super().__setattr__(name, value) diff --git a/reflex/utils/types.py b/reflex/utils/types.py index 3d7992011..baedcc5a0 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -510,16 +510,66 @@ def _issubclass(cls: GenericType, cls_check: GenericType, instance: Any = None) raise TypeError(f"Invalid type for issubclass: {cls_base}") from te -def _isinstance(obj: Any, cls: GenericType) -> bool: +def _isinstance(obj: Any, cls: GenericType, nested: bool = False) -> bool: """Check if an object is an instance of a class. Args: obj: The object to check. cls: The class to check against. + nested: Whether the check is nested. Returns: Whether the object is an instance of the class. """ + if cls is Any: + return True + + if cls is None or cls is type(None): + return obj is None + + if is_literal(cls): + return obj in get_args(cls) + + if is_union(cls): + return any(_isinstance(obj, arg) for arg in get_args(cls)) + + origin = get_origin(cls) + + if origin is None: + # cls is a simple class + return isinstance(obj, cls) + + args = get_args(cls) + + if not args: + # cls is a simple generic class + return isinstance(obj, origin) + + if nested and args: + if origin is list: + return isinstance(obj, list) and all( + _isinstance(item, args[0]) for item in obj + ) + if origin is tuple: + if args[-1] is Ellipsis: + return isinstance(obj, tuple) and all( + _isinstance(item, args[0]) for item in obj + ) + return ( + isinstance(obj, tuple) + and len(obj) == len(args) + and all(_isinstance(item, arg) for item, arg in zip(obj, args)) + ) + if origin is dict: + return isinstance(obj, dict) and all( + _isinstance(key, args[0]) and _isinstance(value, args[1]) + for key, value in obj.items() + ) + if origin is set: + return isinstance(obj, set) and all( + _isinstance(item, args[0]) for item in obj + ) + return isinstance(obj, get_base_class(cls)) diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 2f26e9170..78862aa17 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -63,7 +63,14 @@ from reflex.utils.imports import ( ParsedImportDict, parse_imports, ) -from reflex.utils.types import GenericType, Self, get_origin, has_args, unionize +from reflex.utils.types import ( + GenericType, + Self, + _isinstance, + get_origin, + has_args, + unionize, +) if TYPE_CHECKING: from reflex.state import BaseState @@ -1833,6 +1840,14 @@ class ComputedVar(Var[RETURN_TYPE]): "return", Any ) + if hint is Any: + console.deprecate( + "untyped-computed-var", + "ComputedVar should have a return type annotation.", + "0.6.5", + "0.7.0", + ) + kwargs.setdefault("_js_expr", fget.__name__) kwargs.setdefault("_var_type", hint) @@ -2026,17 +2041,28 @@ class ComputedVar(Var[RETURN_TYPE]): ) if not self._cache: - return self.fget(instance) + value = self.fget(instance) + else: + # handle caching + if not hasattr(instance, self._cache_attr) or self.needs_update(instance): + # Set cache attr on state instance. + setattr(instance, self._cache_attr, self.fget(instance)) + # Ensure the computed var gets serialized to redis. + instance._was_touched = True + # Set the last updated timestamp on the state instance. + setattr(instance, self._last_updated_attr, datetime.datetime.now()) + value = getattr(instance, self._cache_attr) - # handle caching - if not hasattr(instance, self._cache_attr) or self.needs_update(instance): - # Set cache attr on state instance. - setattr(instance, self._cache_attr, self.fget(instance)) - # Ensure the computed var gets serialized to redis. - instance._was_touched = True - # Set the last updated timestamp on the state instance. - setattr(instance, self._last_updated_attr, datetime.datetime.now()) - return getattr(instance, self._cache_attr) + if not _isinstance(value, self._var_type): + console.deprecate( + "mismatched-computed-var-return", + f"Computed var {type(instance).__name__}.{self._js_expr} returned value of type {type(value)}, " + f"expected {self._var_type}. This might cause unexpected behavior.", + "0.6.5", + "0.7.0", + ) + + return value def _deps( self, From 24363170d33bb2b4d4fc5041a38bf0b42085e234 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 30 Oct 2024 11:31:28 -0700 Subject: [PATCH 03/27] components as literal vars (#4223) * component as literal vars * fix pyi * use render * fix pyi * only render once * add type ignore * fix upload default value * remove testcases if you don't pass them * improve behavior * fix render * that's not how icon buttons work * upgrade to next js 15 and remove babel and enable turbo * upload is a silly guy * woops * how did this work before * set env variable * lower it even more * lower it even more * lower it even more * only do literals as component vars --- .../jinja/web/pages/utils.js.jinja2 | 12 +- reflex/.templates/web/utils/state.js | 4 +- reflex/components/base/bare.py | 75 ++++++- reflex/components/component.py | 211 +++++++++++++++++- reflex/components/core/upload.py | 102 +++++++-- reflex/components/core/upload.pyi | 6 +- reflex/components/dynamic.py | 8 +- reflex/components/tags/tag.py | 5 +- tests/integration/test_form_submit.py | 2 +- tests/integration/test_var_operations.py | 4 +- tests/units/components/forms/test_uploads.py | 205 ----------------- tests/units/components/test_component.py | 19 +- 12 files changed, 383 insertions(+), 270 deletions(-) delete mode 100644 tests/units/components/forms/test_uploads.py diff --git a/reflex/.templates/jinja/web/pages/utils.js.jinja2 b/reflex/.templates/jinja/web/pages/utils.js.jinja2 index 908482d24..624e3bee8 100644 --- a/reflex/.templates/jinja/web/pages/utils.js.jinja2 +++ b/reflex/.templates/jinja/web/pages/utils.js.jinja2 @@ -36,14 +36,10 @@ {# component: component dictionary #} {% macro render_tag(component) %} <{{component.name}} {{- render_props(component.props) }}> -{%- if component.args is not none -%} - {{- render_arg_content(component) }} -{%- else -%} - {{ component.contents }} - {% for child in component.children %} - {{ render(child) }} - {% endfor %} -{%- endif -%} +{{ component.contents }} +{% for child in component.children %} +{{ render(child) }} +{% endfor %} {%- endmacro %} diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index e577df67d..7d76b080a 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -15,7 +15,6 @@ import { } from "$/utils/context.js"; import debounce from "$/utils/helpers/debounce"; import throttle from "$/utils/helpers/throttle"; -import * as Babel from "@babel/standalone"; // Endpoint URLs. const EVENTURL = env.EVENT; @@ -139,8 +138,7 @@ export const evalReactComponent = async (component) => { if (!window.React && window.__reflex) { window.React = window.__reflex.react; } - const output = Babel.transform(component, { presets: ["react"] }).code; - const encodedJs = encodeURIComponent(output); + const encodedJs = encodeURIComponent(component); const dataUri = "data:text/javascript;charset=utf-8," + encodedJs; const module = await eval(`import(dataUri)`); return module.default; diff --git a/reflex/components/base/bare.py b/reflex/components/base/bare.py index ada511ef2..c70b4c844 100644 --- a/reflex/components/base/bare.py +++ b/reflex/components/base/bare.py @@ -4,10 +4,11 @@ from __future__ import annotations from typing import Any, Iterator -from reflex.components.component import Component +from reflex.components.component import Component, LiteralComponentVar from reflex.components.tags import Tag from reflex.components.tags.tagless import Tagless -from reflex.vars import ArrayVar, BooleanVar, ObjectVar, Var +from reflex.utils.imports import ParsedImportDict +from reflex.vars import BooleanVar, ObjectVar, Var class Bare(Component): @@ -31,9 +32,77 @@ class Bare(Component): contents = str(contents) if contents is not None else "" return cls(contents=contents) # type: ignore + def _get_all_hooks_internal(self) -> dict[str, None]: + """Include the hooks for the component. + + Returns: + The hooks for the component. + """ + hooks = super()._get_all_hooks_internal() + if isinstance(self.contents, LiteralComponentVar): + hooks |= self.contents._var_value._get_all_hooks_internal() + return hooks + + def _get_all_hooks(self) -> dict[str, None]: + """Include the hooks for the component. + + Returns: + The hooks for the component. + """ + hooks = super()._get_all_hooks() + if isinstance(self.contents, LiteralComponentVar): + hooks |= self.contents._var_value._get_all_hooks() + return hooks + + def _get_all_imports(self) -> ParsedImportDict: + """Include the imports for the component. + + Returns: + The imports for the component. + """ + imports = super()._get_all_imports() + if isinstance(self.contents, LiteralComponentVar): + var_data = self.contents._get_all_var_data() + if var_data: + imports |= {k: list(v) for k, v in var_data.imports} + return imports + + def _get_all_dynamic_imports(self) -> set[str]: + """Get dynamic imports for the component. + + Returns: + The dynamic imports. + """ + dynamic_imports = super()._get_all_dynamic_imports() + if isinstance(self.contents, LiteralComponentVar): + dynamic_imports |= self.contents._var_value._get_all_dynamic_imports() + return dynamic_imports + + def _get_all_custom_code(self) -> set[str]: + """Get custom code for the component. + + Returns: + The custom code. + """ + custom_code = super()._get_all_custom_code() + if isinstance(self.contents, LiteralComponentVar): + custom_code |= self.contents._var_value._get_all_custom_code() + return custom_code + + def _get_all_refs(self) -> set[str]: + """Get the refs for the children of the component. + + Returns: + The refs for the children. + """ + refs = super()._get_all_refs() + if isinstance(self.contents, LiteralComponentVar): + refs |= self.contents._var_value._get_all_refs() + return refs + def _render(self) -> Tag: if isinstance(self.contents, Var): - if isinstance(self.contents, (BooleanVar, ObjectVar, ArrayVar)): + if isinstance(self.contents, (BooleanVar, ObjectVar)): return Tagless(contents=f"{{{str(self.contents.to_string())}}}") return Tagless(contents=f"{{{str(self.contents)}}}") return Tagless(contents=str(self.contents)) diff --git a/reflex/components/component.py b/reflex/components/component.py index 5c6234749..399becee9 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -3,6 +3,7 @@ from __future__ import annotations import copy +import dataclasses import typing from abc import ABC, abstractmethod from functools import lru_cache, wraps @@ -59,7 +60,15 @@ from reflex.utils.imports import ( parse_imports, ) from reflex.vars import VarData -from reflex.vars.base import LiteralVar, Var +from reflex.vars.base import ( + CachedVarOperation, + LiteralVar, + Var, + cached_property_no_lock, +) +from reflex.vars.function import ArgsFunctionOperation, FunctionStringVar +from reflex.vars.number import ternary_operation +from reflex.vars.object import ObjectVar from reflex.vars.sequence import LiteralArrayVar @@ -2345,3 +2354,203 @@ class MemoizationLeaf(Component): load_dynamic_serializer() + + +class ComponentVar(Var[Component], python_types=BaseComponent): + """A Var that represents a Component.""" + + +def empty_component() -> Component: + """Create an empty component. + + Returns: + An empty component. + """ + from reflex.components.base.bare import Bare + + return Bare.create("") + + +def render_dict_to_var(tag: dict | Component | str, imported_names: set[str]) -> Var: + """Convert a render dict to a Var. + + Args: + tag: The render dict. + imported_names: The names of the imported components. + + Returns: + The Var. + """ + if not isinstance(tag, dict): + if isinstance(tag, Component): + return render_dict_to_var(tag.render(), imported_names) + return Var.create(tag) + + if "iterable" in tag: + function_return = Var.create( + [ + render_dict_to_var(child.render(), imported_names) + for child in tag["children"] + ] + ) + + func = ArgsFunctionOperation.create( + (tag["arg_var_name"], tag["index_var_name"]), + function_return, + ) + + return FunctionStringVar.create("Array.prototype.map.call").call( + tag["iterable"] + if not isinstance(tag["iterable"], ObjectVar) + else tag["iterable"].items(), + func, + ) + + if tag["name"] == "match": + element = tag["cond"] + + conditionals = tag["default"] + + for case in tag["match_cases"][::-1]: + condition = case[0].to_string() == element.to_string() + for pattern in case[1:-1]: + condition = condition | (pattern.to_string() == element.to_string()) + + conditionals = ternary_operation( + condition, + case[-1], + conditionals, + ) + + return conditionals + + if "cond" in tag: + return ternary_operation( + tag["cond"], + render_dict_to_var(tag["true_value"], imported_names), + render_dict_to_var(tag["false_value"], imported_names) + if tag["false_value"] is not None + else Var.create(None), + ) + + props = {} + + special_props = [] + + for prop_str in tag["props"]: + if "=" not in prop_str: + special_props.append(Var(prop_str).to(ObjectVar)) + continue + prop = prop_str.index("=") + key = prop_str[:prop] + value = prop_str[prop + 2 : -1] + props[key] = value + + props = Var.create({Var.create(k): Var(v) for k, v in props.items()}) + + for prop in special_props: + props = props.merge(prop) + + contents = tag["contents"][1:-1] if tag["contents"] else None + + raw_tag_name = tag.get("name") + tag_name = Var(raw_tag_name or "Fragment") + + tag_name = ( + Var.create(raw_tag_name) + if raw_tag_name + and raw_tag_name.split(".")[0] not in imported_names + and raw_tag_name.lower() == raw_tag_name + else tag_name + ) + + return FunctionStringVar.create( + "jsx", + ).call( + tag_name, + props, + *([Var(contents)] if contents is not None else []), + *[render_dict_to_var(child, imported_names) for child in tag["children"]], + ) + + +@dataclasses.dataclass( + eq=False, + frozen=True, +) +class LiteralComponentVar(CachedVarOperation, LiteralVar, ComponentVar): + """A Var that represents a Component.""" + + _var_value: BaseComponent = dataclasses.field(default_factory=empty_component) + + @cached_property_no_lock + def _cached_var_name(self) -> str: + """Get the name of the var. + + Returns: + The name of the var. + """ + var_data = self._get_all_var_data() + if var_data is not None: + # flatten imports + imported_names = {j.alias or j.name for i in var_data.imports for j in i[1]} + else: + imported_names = set() + return str(render_dict_to_var(self._var_value.render(), imported_names)) + + @cached_property_no_lock + def _cached_get_all_var_data(self) -> VarData | None: + """Get the VarData for the var. + + Returns: + The VarData for the var. + """ + return VarData.merge( + VarData( + imports={ + "@emotion/react": [ + ImportVar(tag="jsx"), + ], + } + ), + VarData( + imports=self._var_value._get_all_imports(), + ), + VarData( + imports={ + "react": [ + ImportVar(tag="Fragment"), + ], + } + ), + ) + + def __hash__(self) -> int: + """Get the hash of the var. + + Returns: + The hash of the var. + """ + return hash((self.__class__.__name__, self._js_expr)) + + @classmethod + def create( + cls, + value: Component, + _var_data: VarData | None = None, + ): + """Create a var from a value. + + Args: + value: The value of the var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The var. + """ + return LiteralComponentVar( + _js_expr="", + _var_type=type(value), + _var_data=_var_data, + _var_value=value, + ) diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index c454cba7d..787cca9d0 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -5,11 +5,17 @@ from __future__ import annotations from pathlib import Path from typing import Any, Callable, ClassVar, Dict, List, Optional, Tuple -from reflex.components.component import Component, ComponentNamespace, MemoizationLeaf +from reflex.components.component import ( + Component, + ComponentNamespace, + MemoizationLeaf, + StatefulComponent, +) from reflex.components.el.elements.forms import Input from reflex.components.radix.themes.layout.box import Box from reflex.config import environment from reflex.constants import Dirs +from reflex.constants.compiler import Imports from reflex.event import ( CallableEventSpec, EventChain, @@ -19,9 +25,10 @@ from reflex.event import ( call_script, parse_args_spec, ) +from reflex.utils import format from reflex.utils.imports import ImportVar from reflex.vars import VarData -from reflex.vars.base import CallableVar, LiteralVar, Var +from reflex.vars.base import CallableVar, LiteralVar, Var, get_unique_variable_name from reflex.vars.sequence import LiteralStringVar DEFAULT_UPLOAD_ID: str = "default" @@ -179,9 +186,7 @@ class Upload(MemoizationLeaf): library = "react-dropzone@14.2.10" - tag = "ReactDropzone" - - is_default = True + tag = "" # The list of accepted file types. This should be a dictionary of MIME types as keys and array of file formats as # values. @@ -201,7 +206,7 @@ class Upload(MemoizationLeaf): min_size: Var[int] # Whether to allow multiple files to be uploaded. - multiple: Var[bool] = True # type: ignore + multiple: Var[bool] # Whether to disable click to upload. no_click: Var[bool] @@ -232,6 +237,8 @@ class Upload(MemoizationLeaf): # Mark the Upload component as used in the app. cls.is_used = True + props.setdefault("multiple", True) + # Apply the default classname given_class_name = props.pop("class_name", []) if isinstance(given_class_name, str): @@ -243,17 +250,6 @@ class Upload(MemoizationLeaf): upload_props = { key: value for key, value in props.items() if key in supported_props } - # The file input to use. - upload = Input.create(type="file") - upload.special_props = [Var(_js_expr="{...getInputProps()}", _var_type=None)] - - # The dropzone to use. - zone = Box.create( - upload, - *children, - **{k: v for k, v in props.items() if k not in supported_props}, - ) - zone.special_props = [Var(_js_expr="{...getRootProps()}", _var_type=None)] # Create the component. upload_props["id"] = props.get("id", DEFAULT_UPLOAD_ID) @@ -275,9 +271,74 @@ class Upload(MemoizationLeaf): ), ) upload_props["on_drop"] = on_drop + + input_props_unique_name = get_unique_variable_name() + root_props_unique_name = get_unique_variable_name() + + event_var, callback_str = StatefulComponent._get_memoized_event_triggers( + Box.create(on_click=upload_props["on_drop"]) # type: ignore + )["on_click"] + + upload_props["on_drop"] = event_var + + upload_props = { + format.to_camel_case(key): value for key, value in upload_props.items() + } + + use_dropzone_arguements = { + "onDrop": event_var, + **upload_props, + } + + left_side = f"const {{getRootProps: {root_props_unique_name}, getInputProps: {input_props_unique_name}}} " + right_side = f"useDropzone({str(Var.create(use_dropzone_arguements))})" + + var_data = VarData.merge( + VarData( + imports=Imports.EVENTS, + hooks={ + "const [addEvents, connectError] = useContext(EventLoopContext);": None + }, + ), + event_var._get_all_var_data(), + VarData( + hooks={ + callback_str: None, + f"{left_side} = {right_side};": None, + }, + imports={ + "react-dropzone": "useDropzone", + **Imports.EVENTS, + }, + ), + ) + + # The file input to use. + upload = Input.create(type="file") + upload.special_props = [ + Var( + _js_expr=f"{{...{input_props_unique_name}()}}", + _var_type=None, + _var_data=var_data, + ) + ] + + # The dropzone to use. + zone = Box.create( + upload, + *children, + **{k: v for k, v in props.items() if k not in supported_props}, + ) + zone.special_props = [ + Var( + _js_expr=f"{{...{root_props_unique_name}()}}", + _var_type=None, + _var_data=var_data, + ) + ] + return super().create( zone, - **upload_props, ) @classmethod @@ -295,11 +356,6 @@ class Upload(MemoizationLeaf): return (arg_value[0], placeholder) return arg_value - def _render(self): - out = super()._render() - out.args = ("getRootProps", "getInputProps") - return out - @staticmethod def _get_app_wrap_components() -> dict[tuple[int, str], Component]: return { diff --git a/reflex/components/core/upload.pyi b/reflex/components/core/upload.pyi index 43ce3b63d..e5c73bd14 100644 --- a/reflex/components/core/upload.pyi +++ b/reflex/components/core/upload.pyi @@ -6,7 +6,11 @@ from pathlib import Path from typing import Any, ClassVar, Dict, List, Optional, Union, overload -from reflex.components.component import Component, ComponentNamespace, MemoizationLeaf +from reflex.components.component import ( + Component, + ComponentNamespace, + MemoizationLeaf, +) from reflex.constants import Dirs from reflex.event import ( CallableEventSpec, diff --git a/reflex/components/dynamic.py b/reflex/components/dynamic.py index e391c9fd0..c0e172224 100644 --- a/reflex/components/dynamic.py +++ b/reflex/components/dynamic.py @@ -63,6 +63,9 @@ def load_dynamic_serializer(): """ # Causes a circular import, so we import here. from reflex.compiler import templates, utils + from reflex.components.base.bare import Bare + + component = Bare.create(Var.create(component)) rendered_components = {} # Include dynamic imports in the shared component. @@ -127,14 +130,15 @@ def load_dynamic_serializer(): module_code_lines[ix] = line.replace( "export function", "export default function", 1 ) + line_stripped = line.strip() + if line_stripped.startswith("{") and line_stripped.endswith("}"): + module_code_lines[ix] = line_stripped[1:-1] module_code_lines.insert(0, "const React = window.__reflex.react;") return "\n".join( [ "//__reflex_evaluate", - "/** @jsx jsx */", - "const { jsx } = window.__reflex['@emotion/react']", *module_code_lines, ] ) diff --git a/reflex/components/tags/tag.py b/reflex/components/tags/tag.py index d577abc6e..0587c61ed 100644 --- a/reflex/components/tags/tag.py +++ b/reflex/components/tags/tag.py @@ -3,7 +3,7 @@ from __future__ import annotations import dataclasses -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Union from reflex.event import EventChain from reflex.utils import format, types @@ -23,9 +23,6 @@ class Tag: # The inner contents of the tag. contents: str = "" - # Args to pass to the tag. - args: Optional[Tuple[str, ...]] = None - # Special props that aren't key value pairs. special_props: List[Var] = dataclasses.field(default_factory=list) diff --git a/tests/integration/test_form_submit.py b/tests/integration/test_form_submit.py index a020a7e15..3bfcf6e8c 100644 --- a/tests/integration/test_form_submit.py +++ b/tests/integration/test_form_submit.py @@ -121,7 +121,7 @@ def FormSubmitName(form_component): on_change=rx.console_log, ), rx.button("Submit", type_="submit"), - rx.icon_button(FormState.val, icon=rx.icon(tag="plus")), + rx.icon_button(rx.icon(tag="plus")), ), on_submit=FormState.form_submit, custom_attrs={"action": "/invalid"}, diff --git a/tests/integration/test_var_operations.py b/tests/integration/test_var_operations.py index 919a39f3b..cae56e1a8 100644 --- a/tests/integration/test_var_operations.py +++ b/tests/integration/test_var_operations.py @@ -793,8 +793,8 @@ def test_var_operations(driver, var_operations: AppHarness): ("foreach_list_ix", "1\n2"), ("foreach_list_nested", "1\n1\n2"), # rx.memo component with state - ("memo_comp", "[1,2]10"), - ("memo_comp_nested", "[3,4]5"), + ("memo_comp", "1210"), + ("memo_comp_nested", "345"), # foreach in a match ("foreach_in_match", "first\nsecond\nthird"), ] diff --git a/tests/units/components/forms/test_uploads.py b/tests/units/components/forms/test_uploads.py deleted file mode 100644 index 3b2ee014f..000000000 --- a/tests/units/components/forms/test_uploads.py +++ /dev/null @@ -1,205 +0,0 @@ -import pytest - -import reflex as rx - - -@pytest.fixture -def upload_root_component(): - """A test upload component function. - - Returns: - A test upload component function. - """ - - def upload_root_component(): - return rx.upload.root( - rx.button("select file"), - rx.text("Drag and drop files here or click to select files"), - border="1px dotted black", - ) - - return upload_root_component() - - -@pytest.fixture -def upload_component(): - """A test upload component function. - - Returns: - A test upload component function. - """ - - def upload_component(): - return rx.upload( - rx.button("select file"), - rx.text("Drag and drop files here or click to select files"), - border="1px dotted black", - ) - - return upload_component() - - -@pytest.fixture -def upload_component_id_special(): - def upload_component(): - return rx.upload( - rx.button("select file"), - rx.text("Drag and drop files here or click to select files"), - border="1px dotted black", - id="#spec!`al-_98ID", - ) - - return upload_component() - - -@pytest.fixture -def upload_component_with_props(): - """A test upload component with props function. - - Returns: - A test upload component with props function. - """ - - def upload_component_with_props(): - return rx.upload( - rx.button("select file"), - rx.text("Drag and drop files here or click to select files"), - border="1px dotted black", - no_drag=True, - max_files=2, - ) - - return upload_component_with_props() - - -def test_upload_root_component_render(upload_root_component): - """Test that the render function is set correctly. - - Args: - upload_root_component: component fixture - """ - upload = upload_root_component.render() - - # upload - assert upload["name"] == "ReactDropzone" - assert upload["props"] == [ - 'id={"default"}', - "multiple={true}", - "onDrop={e => setFilesById(filesById => {\n" - " const updatedFilesById = Object.assign({}, filesById);\n" - ' updatedFilesById["default"] = e;\n' - " return updatedFilesById;\n" - " })\n" - " }", - "ref={ref_default}", - ] - assert upload["args"] == ("getRootProps", "getInputProps") - - # box inside of upload - [box] = upload["children"] - assert box["name"] == "RadixThemesBox" - assert box["props"] == [ - 'className={"rx-Upload"}', - 'css={({ ["border"] : "1px dotted black" })}', - "{...getRootProps()}", - ] - - # input, button and text inside of box - [input, button, text] = box["children"] - assert input["name"] == "input" - assert input["props"] == ['type={"file"}', "{...getInputProps()}"] - - assert button["name"] == "RadixThemesButton" - assert button["children"][0]["contents"] == '{"select file"}' - - assert text["name"] == "RadixThemesText" - assert ( - text["children"][0]["contents"] - == '{"Drag and drop files here or click to select files"}' - ) - - -def test_upload_component_render(upload_component): - """Test that the render function is set correctly. - - Args: - upload_component: component fixture - """ - upload = upload_component.render() - - # upload - assert upload["name"] == "ReactDropzone" - assert upload["props"] == [ - 'id={"default"}', - "multiple={true}", - "onDrop={e => setFilesById(filesById => {\n" - " const updatedFilesById = Object.assign({}, filesById);\n" - ' updatedFilesById["default"] = e;\n' - " return updatedFilesById;\n" - " })\n" - " }", - "ref={ref_default}", - ] - assert upload["args"] == ("getRootProps", "getInputProps") - - # box inside of upload - [box] = upload["children"] - assert box["name"] == "RadixThemesBox" - assert box["props"] == [ - 'className={"rx-Upload"}', - 'css={({ ["border"] : "1px dotted black", ["padding"] : "5em", ["textAlign"] : "center" })}', - "{...getRootProps()}", - ] - - # input, button and text inside of box - [input, button, text] = box["children"] - assert input["name"] == "input" - assert input["props"] == ['type={"file"}', "{...getInputProps()}"] - - assert button["name"] == "RadixThemesButton" - assert button["children"][0]["contents"] == '{"select file"}' - - assert text["name"] == "RadixThemesText" - assert ( - text["children"][0]["contents"] - == '{"Drag and drop files here or click to select files"}' - ) - - -def test_upload_component_with_props_render(upload_component_with_props): - """Test that the render function is set correctly. - - Args: - upload_component_with_props: component fixture - """ - upload = upload_component_with_props.render() - - assert upload["props"] == [ - 'id={"default"}', - "maxFiles={2}", - "multiple={true}", - "noDrag={true}", - "onDrop={e => setFilesById(filesById => {\n" - " const updatedFilesById = Object.assign({}, filesById);\n" - ' updatedFilesById["default"] = e;\n' - " return updatedFilesById;\n" - " })\n" - " }", - "ref={ref_default}", - ] - - -def test_upload_component_id_with_special_chars(upload_component_id_special): - upload = upload_component_id_special.render() - - assert upload["props"] == [ - r'id={"#spec!`al-_98ID"}', - "multiple={true}", - "onDrop={e => setFilesById(filesById => {\n" - " const updatedFilesById = Object.assign({}, filesById);\n" - ' updatedFilesById["#spec!`al-_98ID"] = e;\n' - " return updatedFilesById;\n" - " })\n" - " }", - "ref={ref__spec_al__98ID}", - ] diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index b7b721a92..c2d73aca5 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -642,21 +642,18 @@ def test_component_create_unallowed_types(children, test_component): "name": "Fragment", "props": [], "contents": "", - "args": None, "special_props": [], "children": [ { "name": "RadixThemesText", "props": ['as={"p"}'], "contents": "", - "args": None, "special_props": [], "children": [ { "name": "", "props": [], "contents": '{"first_text"}', - "args": None, "special_props": [], "children": [], "autofocus": False, @@ -671,15 +668,12 @@ def test_component_create_unallowed_types(children, test_component): ( (rx.text("first_text"), rx.text("second_text")), { - "args": None, "autofocus": False, "children": [ { - "args": None, "autofocus": False, "children": [ { - "args": None, "autofocus": False, "children": [], "contents": '{"first_text"}', @@ -694,11 +688,9 @@ def test_component_create_unallowed_types(children, test_component): "special_props": [], }, { - "args": None, "autofocus": False, "children": [ { - "args": None, "autofocus": False, "children": [], "contents": '{"second_text"}', @@ -722,15 +714,12 @@ def test_component_create_unallowed_types(children, test_component): ( (rx.text("first_text"), rx.box((rx.text("second_text"),))), { - "args": None, "autofocus": False, "children": [ { - "args": None, "autofocus": False, "children": [ { - "args": None, "autofocus": False, "children": [], "contents": '{"first_text"}', @@ -745,19 +734,15 @@ def test_component_create_unallowed_types(children, test_component): "special_props": [], }, { - "args": None, "autofocus": False, "children": [ { - "args": None, "autofocus": False, "children": [ { - "args": None, "autofocus": False, "children": [ { - "args": None, "autofocus": False, "children": [], "contents": '{"second_text"}', @@ -1117,10 +1102,10 @@ def test_component_with_only_valid_children(fixture, request): @pytest.mark.parametrize( "component,rendered", [ - (rx.text("hi"), '\n {"hi"}\n'), + (rx.text("hi"), '\n\n{"hi"}\n'), ( rx.box(rx.heading("test", size="3")), - '\n \n {"test"}\n\n', + '\n\n\n\n{"test"}\n\n', ), ], ) From 0bdc82888939531d85aa7118f3f9dfa4c93ecf8a Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Wed, 30 Oct 2024 21:58:22 +0100 Subject: [PATCH 04/27] cleanup dead code (#4271) --- reflex/vars/base.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 78862aa17..b06e7b7c9 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -619,10 +619,6 @@ class Var(Generic[VAR_TYPE]): """ from .object import ObjectVar - base_type = var_type - if types.is_optional(base_type): - base_type = types.get_args(base_type)[0] - fixed_output_type = get_origin(output) or output # If the first argument is a python type, we map it to the corresponding Var type. From 141cb8d21b39f01ee5509ba384597f6595315fae Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 30 Oct 2024 15:16:16 -0700 Subject: [PATCH 05/27] unbreak ci lighthouse (#4273) * unbreak ci lighthouse * forgot to precommit --- benchmarks/benchmark_lighthouse.py | 3 +-- reflex/components/radix/themes/components/slider.py | 4 ++-- reflex/components/radix/themes/components/slider.pyi | 8 ++++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/benchmarks/benchmark_lighthouse.py b/benchmarks/benchmark_lighthouse.py index 25d5eaac4..d428c16e6 100644 --- a/benchmarks/benchmark_lighthouse.py +++ b/benchmarks/benchmark_lighthouse.py @@ -42,8 +42,7 @@ def get_lighthouse_scores(directory_path: str | Path) -> dict: try: for filename in directory_path.iterdir(): if filename.suffix == ".json" and filename.stem != "manifest": - file_path = directory_path / filename - data = json.loads(file_path.read_text()) + data = json.loads(filename.read_text()) # Extract scores and add them to the dictionary with the filename as key scores[data["finalUrl"].replace("http://localhost:3000/", "/")] = { "performance_score": data["categories"]["performance"]["score"], diff --git a/reflex/components/radix/themes/components/slider.py b/reflex/components/radix/themes/components/slider.py index 0d99eda27..bf0e5c454 100644 --- a/reflex/components/radix/themes/components/slider.py +++ b/reflex/components/radix/themes/components/slider.py @@ -16,8 +16,8 @@ from ..base import ( def on_value_event_spec( - value: Var[List[int | float]], -) -> Tuple[Var[List[int | float]]]: + value: Var[List[Union[int, float]]], +) -> Tuple[Var[List[Union[int, float]]]]: """Event handler spec for the value event. Args: diff --git a/reflex/components/radix/themes/components/slider.pyi b/reflex/components/radix/themes/components/slider.pyi index dec836835..8939ea23e 100644 --- a/reflex/components/radix/themes/components/slider.pyi +++ b/reflex/components/radix/themes/components/slider.pyi @@ -13,8 +13,8 @@ from reflex.vars.base import Var from ..base import RadixThemesComponent def on_value_event_spec( - value: Var[List[int | float]], -) -> Tuple[Var[List[int | float]]]: ... + value: Var[List[Union[int, float]]], +) -> Tuple[Var[List[Union[int, float]]]]: ... class Slider(RadixThemesComponent): @overload @@ -138,7 +138,7 @@ class Slider(RadixThemesComponent): autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, on_blur: Optional[EventType[[]]] = None, - on_change: Optional[EventType[List[int | float]]] = None, + on_change: Optional[EventType[List[Union[int, float]]]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, @@ -153,7 +153,7 @@ class Slider(RadixThemesComponent): on_mouse_up: Optional[EventType[[]]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, - on_value_commit: Optional[EventType[List[int | float]]] = None, + on_value_commit: Optional[EventType[List[Union[int, float]]]] = None, **props, ) -> "Slider": """Create a Slider component. From c288741cabc171f4fe884ca8918be21289a1f852 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 30 Oct 2024 15:16:34 -0700 Subject: [PATCH 06/27] resync steps with task advance (#4275) --- reflex/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reflex/app.py b/reflex/app.py index e350be515..88fc9d473 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -929,6 +929,8 @@ class App(MiddlewareMixin, LifespanMixin, Base): ) compile_results.append((stateful_components_path, stateful_components_code)) + progress.advance(task) + # Compile the root document before fork. compile_results.append( compiler.compile_document_root( From 2ab662b757b6af00599e1d18ce334e6889f98ac6 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Wed, 30 Oct 2024 16:50:19 -0700 Subject: [PATCH 07/27] [ENG-4013] Catch more exceptions for dill pickle fallback (#4270) Additionally catch TypeError, IndexError, and ValueError which may be thrown when attempting to pickle unpicklable objects. --- reflex/state.py | 13 +++++++++++-- tests/units/test_state.py | 17 ++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 7bdbcdc2b..cc9dda05b 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -105,6 +105,15 @@ var = computed_var # If the state is this large, it's considered a performance issue. TOO_LARGE_SERIALIZED_STATE = 100 * 1024 # 100kb +# Errors caught during pickling of state +HANDLED_PICKLE_ERRORS = ( + pickle.PicklingError, + AttributeError, + IndexError, + TypeError, + ValueError, +) + def _no_chain_background_task( state_cls: Type["BaseState"], name: str, fn: Callable @@ -2076,7 +2085,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): """ try: return pickle.dumps((self._to_schema(), self)) - except (pickle.PicklingError, AttributeError) as og_pickle_error: + except HANDLED_PICKLE_ERRORS as og_pickle_error: error = ( f"Failed to serialize state {self.get_full_name()} due to unpicklable object. " "This state will not be persisted. " @@ -2090,7 +2099,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): f"Pickle error: {og_pickle_error}. " "Consider `pip install 'dill>=0.3.8'` for more exotic serialization support." ) - except (pickle.PicklingError, TypeError, ValueError) as ex: + except HANDLED_PICKLE_ERRORS as ex: error += f"Dill was also unable to pickle the state: {ex}" console.warn(error) return b"" diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 8397954cf..fe2f652ac 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -8,6 +8,7 @@ import functools import json import os import sys +import threading from textwrap import dedent from typing import Any, AsyncGenerator, Callable, Dict, List, Optional, Union from unittest.mock import AsyncMock, Mock @@ -3390,9 +3391,15 @@ def test_fallback_pickle(): assert unpickled_state._f() == 420 assert unpickled_state._o._f() == 42 + # Threading locks are unpicklable normally, and raise TypeError instead of PicklingError. + state2 = DillState(_reflex_internal_init=True) # type: ignore + state2._g = threading.Lock() + pk2 = state2._serialize() + unpickled_state2 = BaseState._deserialize(pk2) + assert isinstance(unpickled_state2._g, type(threading.Lock())) + # Some object, like generator, are still unpicklable with dill. - state._g = (i for i in range(10)) - pk = state._serialize() - assert len(pk) == 0 - with pytest.raises(EOFError): - BaseState._deserialize(pk) + state3 = DillState(_reflex_internal_init=True) # type: ignore + state3._g = (i for i in range(10)) + pk3 = state3._serialize() + assert len(pk3) == 0 From 4254eadce3019f6651d17084e4c35152bfba5d9b Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 30 Oct 2024 16:52:16 -0700 Subject: [PATCH 08/27] use better typing for on_load (#4274) * use better typing for on_load * make app dataclass * get it right pyright * make lifespan into a dataclass --- reflex/app.py | 60 ++++++++++++++++----------------- reflex/app_mixins/lifespan.py | 6 +++- reflex/app_mixins/middleware.py | 4 ++- reflex/app_mixins/mixin.py | 5 +-- reflex/page.py | 3 +- tests/units/test_app.py | 9 +---- tests/units/test_state.py | 1 + 7 files changed, 45 insertions(+), 43 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 88fc9d473..5367ef20b 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -46,7 +46,6 @@ from starlette_admin.contrib.sqla.view import ModelView from reflex import constants from reflex.admin import AdminDash from reflex.app_mixins import AppMixin, LifespanMixin, MiddlewareMixin -from reflex.base import Base from reflex.compiler import compiler from reflex.compiler import utils as compiler_utils from reflex.compiler.compiler import ( @@ -70,7 +69,14 @@ from reflex.components.core.client_side_routing import ( from reflex.components.core.upload import Upload, get_upload_dir from reflex.components.radix import themes from reflex.config import environment, get_config -from reflex.event import Event, EventHandler, EventSpec, window_alert +from reflex.event import ( + Event, + EventHandler, + EventSpec, + EventType, + IndividualEventType, + window_alert, +) from reflex.model import Model, get_db_status from reflex.page import ( DECORATED_PAGES, @@ -189,11 +195,12 @@ class UnevaluatedPage: title: Union[Var, str, None] description: Union[Var, str, None] image: str - on_load: Union[EventHandler, EventSpec, List[Union[EventHandler, EventSpec]], None] + on_load: Union[EventType[[]], None] meta: List[Dict[str, str]] -class App(MiddlewareMixin, LifespanMixin, Base): +@dataclasses.dataclass() +class App(MiddlewareMixin, LifespanMixin): """The main Reflex app that encapsulates the backend and frontend. Every Reflex app needs an app defined in its main module. @@ -215,24 +222,26 @@ class App(MiddlewareMixin, LifespanMixin, Base): """ # The global [theme](https://reflex.dev/docs/styling/theming/#theme) for the entire app. - theme: Optional[Component] = themes.theme(accent_color="blue") + theme: Optional[Component] = dataclasses.field( + default_factory=lambda: themes.theme(accent_color="blue") + ) # The [global style](https://reflex.dev/docs/styling/overview/#global-styles}) for the app. - style: ComponentStyle = {} + style: ComponentStyle = dataclasses.field(default_factory=dict) # A list of URLs to [stylesheets](https://reflex.dev/docs/styling/custom-stylesheets/) to include in the app. - stylesheets: List[str] = [] + stylesheets: List[str] = dataclasses.field(default_factory=list) # A component that is present on every page (defaults to the Connection Error banner). overlay_component: Optional[Union[Component, ComponentCallable]] = ( - default_overlay_component() + dataclasses.field(default_factory=default_overlay_component) ) # Error boundary component to wrap the app with. error_boundary: Optional[ComponentCallable] = default_error_boundary # Components to add to the head of every page. - head_components: List[Component] = [] + head_components: List[Component] = dataclasses.field(default_factory=list) # The Socket.IO AsyncServer instance. sio: Optional[AsyncServer] = None @@ -244,10 +253,12 @@ class App(MiddlewareMixin, LifespanMixin, Base): html_custom_attrs: Optional[Dict[str, str]] = None # A map from a route to an unevaluated page. PRIVATE. - unevaluated_pages: Dict[str, UnevaluatedPage] = {} + unevaluated_pages: Dict[str, UnevaluatedPage] = dataclasses.field( + default_factory=dict + ) # A map from a page route to the component to render. Users should use `add_page`. PRIVATE. - pages: Dict[str, Component] = {} + pages: Dict[str, Component] = dataclasses.field(default_factory=dict) # The backend API object. PRIVATE. api: FastAPI = None # type: ignore @@ -259,7 +270,9 @@ class App(MiddlewareMixin, LifespanMixin, Base): _state_manager: Optional[StateManager] = None # Mapping from a route to event handlers to trigger when the page loads. PRIVATE. - load_events: Dict[str, List[Union[EventHandler, EventSpec]]] = {} + load_events: Dict[str, List[IndividualEventType[[]]]] = dataclasses.field( + default_factory=dict + ) # Admin dashboard to view and manage the database. PRIVATE. admin_dash: Optional[AdminDash] = None @@ -268,7 +281,7 @@ class App(MiddlewareMixin, LifespanMixin, Base): event_namespace: Optional[EventNamespace] = None # Background tasks that are currently running. PRIVATE. - background_tasks: Set[asyncio.Task] = set() + background_tasks: Set[asyncio.Task] = dataclasses.field(default_factory=set) # Frontend Error Handler Function frontend_exception_handler: Callable[[Exception], None] = ( @@ -280,23 +293,14 @@ class App(MiddlewareMixin, LifespanMixin, Base): [Exception], Union[EventSpec, List[EventSpec], None] ] = default_backend_exception_handler - def __init__(self, **kwargs): + def __post_init__(self): """Initialize the app. - Args: - **kwargs: Kwargs to initialize the app with. - Raises: ValueError: If the event namespace is not provided in the config. Also, if there are multiple client subclasses of rx.BaseState(Subclasses of rx.BaseState should consist of the DefaultState and the client app state). """ - if "connect_error_component" in kwargs: - raise ValueError( - "`connect_error_component` is deprecated, use `overlay_component` instead" - ) - super().__init__(**kwargs) - # Special case to allow test cases have multiple subclasses of rx.BaseState. if not is_testing_env() and BaseState.__subclasses__() != [State]: # Only rx.State is allowed as Base State subclass. @@ -471,9 +475,7 @@ class App(MiddlewareMixin, LifespanMixin, Base): title: str | Var | None = None, description: str | Var | None = None, image: str = constants.DefaultPage.IMAGE, - on_load: ( - EventHandler | EventSpec | list[EventHandler | EventSpec] | None - ) = None, + on_load: EventType[[]] | None = None, meta: list[dict[str, str]] = constants.DefaultPage.META_LIST, ): """Add a page to the app. @@ -559,7 +561,7 @@ class App(MiddlewareMixin, LifespanMixin, Base): self._check_routes_conflict(route) self.pages[route] = component - def get_load_events(self, route: str) -> list[EventHandler | EventSpec]: + def get_load_events(self, route: str) -> list[IndividualEventType[[]]]: """Get the load events for a route. Args: @@ -618,9 +620,7 @@ class App(MiddlewareMixin, LifespanMixin, Base): title: str = constants.Page404.TITLE, image: str = constants.Page404.IMAGE, description: str = constants.Page404.DESCRIPTION, - on_load: ( - EventHandler | EventSpec | list[EventHandler | EventSpec] | None - ) = None, + on_load: EventType[[]] | None = None, meta: list[dict[str, str]] = constants.DefaultPage.META_LIST, ): """Define a custom 404 page for any url having no match. diff --git a/reflex/app_mixins/lifespan.py b/reflex/app_mixins/lifespan.py index ef882a2ea..52bf0be1d 100644 --- a/reflex/app_mixins/lifespan.py +++ b/reflex/app_mixins/lifespan.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import contextlib +import dataclasses import functools import inspect from typing import Callable, Coroutine, Set, Union @@ -16,11 +17,14 @@ from reflex.utils.exceptions import InvalidLifespanTaskType from .mixin import AppMixin +@dataclasses.dataclass class LifespanMixin(AppMixin): """A Mixin that allow tasks to run during the whole app lifespan.""" # Lifespan tasks that are planned to run. - lifespan_tasks: Set[Union[asyncio.Task, Callable]] = set() + lifespan_tasks: Set[Union[asyncio.Task, Callable]] = dataclasses.field( + default_factory=set + ) @contextlib.asynccontextmanager async def _run_lifespan_tasks(self, app: FastAPI): diff --git a/reflex/app_mixins/middleware.py b/reflex/app_mixins/middleware.py index 1e42faf18..30593d9ae 100644 --- a/reflex/app_mixins/middleware.py +++ b/reflex/app_mixins/middleware.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +import dataclasses from typing import List from reflex.event import Event @@ -12,11 +13,12 @@ from reflex.state import BaseState, StateUpdate from .mixin import AppMixin +@dataclasses.dataclass class MiddlewareMixin(AppMixin): """Middleware Mixin that allow to add middleware to the app.""" # Middleware to add to the app. Users should use `add_middleware`. PRIVATE. - middleware: List[Middleware] = [] + middleware: List[Middleware] = dataclasses.field(default_factory=list) def _init_mixin(self): self.middleware.append(HydrateMiddleware()) diff --git a/reflex/app_mixins/mixin.py b/reflex/app_mixins/mixin.py index ed301c495..23207a462 100644 --- a/reflex/app_mixins/mixin.py +++ b/reflex/app_mixins/mixin.py @@ -1,9 +1,10 @@ """Default mixin for all app mixins.""" -from reflex.base import Base +import dataclasses -class AppMixin(Base): +@dataclasses.dataclass +class AppMixin: """Define the base class for all app mixins.""" def _init_mixin(self): diff --git a/reflex/page.py b/reflex/page.py index 52f0c8efc..87a8c49c2 100644 --- a/reflex/page.py +++ b/reflex/page.py @@ -6,6 +6,7 @@ from collections import defaultdict from typing import Any, Dict, List from reflex.config import get_config +from reflex.event import EventType DECORATED_PAGES: Dict[str, List] = defaultdict(list) @@ -17,7 +18,7 @@ def page( description: str | None = None, meta: list[Any] | None = None, script_tags: list[Any] | None = None, - on_load: Any | list[Any] | None = None, + on_load: EventType[[]] | None = None, ): """Decorate a function as a page. diff --git a/tests/units/test_app.py b/tests/units/test_app.py index 7fba7ba1d..1e34a67c3 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -1211,7 +1211,7 @@ async def test_process_events(mocker, token: str): ], ) def test_overlay_component( - state: State | None, + state: Type[State] | None, overlay_component: Component | ComponentCallable | None, exp_page_child: Type[Component] | None, ): @@ -1403,13 +1403,6 @@ def test_app_state_determination(): assert a4.state is not None -# for coverage -def test_raise_on_connect_error(): - """Test that the connect_error function is called.""" - with pytest.raises(ValueError): - App(connect_error_component="Foo") - - def test_raise_on_state(): """Test that the state is set.""" # state kwargs is deprecated, we just make sure the app is created anyway. diff --git a/tests/units/test_state.py b/tests/units/test_state.py index fe2f652ac..271f2e794 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -2725,6 +2725,7 @@ class OnLoadState(State): num: int = 0 + @rx.event def test_handler(self): """Test handler.""" self.num += 1 From a2126beca1c6396177195d6d66d2c01500f1e8f0 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 30 Oct 2024 17:31:38 -0700 Subject: [PATCH 09/27] generate docs for event handlers (#4277) --- reflex/components/base/error_boundary.pyi | 1 + reflex/components/base/script.pyi | 3 + reflex/components/core/clipboard.pyi | 1 + reflex/components/core/upload.pyi | 3 + reflex/components/datadisplay/dataeditor.pyi | 16 ++++ reflex/components/el/elements/forms.pyi | 12 +++ reflex/components/moment/moment.pyi | 1 + reflex/components/next/image.pyi | 2 + reflex/components/plotly/plotly.pyi | 20 +++++ .../components/radix/primitives/accordion.pyi | 1 + reflex/components/radix/primitives/drawer.py | 6 +- reflex/components/radix/primitives/drawer.pyi | 24 ++++- reflex/components/radix/primitives/form.pyi | 6 ++ reflex/components/radix/primitives/slider.pyi | 2 + reflex/components/radix/themes/color_mode.pyi | 1 + .../radix/themes/components/alert_dialog.pyi | 4 + .../radix/themes/components/checkbox.pyi | 3 + .../radix/themes/components/context_menu.pyi | 10 +++ .../radix/themes/components/dialog.pyi | 7 ++ .../radix/themes/components/dropdown_menu.pyi | 12 +++ .../radix/themes/components/hover_card.pyi | 2 + .../radix/themes/components/popover.pyi | 7 ++ .../radix/themes/components/radio_cards.pyi | 1 + .../radix/themes/components/radio_group.pyi | 1 + .../themes/components/segmented_control.pyi | 1 + .../radix/themes/components/select.pyi | 9 ++ .../radix/themes/components/slider.pyi | 2 + .../radix/themes/components/switch.pyi | 1 + .../radix/themes/components/tabs.pyi | 2 + .../radix/themes/components/text_area.pyi | 5 ++ .../radix/themes/components/text_field.pyi | 10 +++ .../radix/themes/components/tooltip.pyi | 3 + reflex/components/react_player/audio.pyi | 16 ++++ .../components/react_player/react_player.pyi | 16 ++++ reflex/components/react_player/video.pyi | 16 ++++ reflex/components/recharts/cartesian.pyi | 87 +++++++++++++++++++ reflex/components/recharts/charts.pyi | 49 +++++++++++ reflex/components/recharts/general.pyi | 9 ++ reflex/components/recharts/polar.pyi | 8 ++ reflex/components/suneditor/editor.pyi | 9 ++ reflex/utils/pyi_generator.py | 8 +- 41 files changed, 389 insertions(+), 8 deletions(-) diff --git a/reflex/components/base/error_boundary.pyi b/reflex/components/base/error_boundary.pyi index c84742851..154ec0d13 100644 --- a/reflex/components/base/error_boundary.pyi +++ b/reflex/components/base/error_boundary.pyi @@ -50,6 +50,7 @@ class ErrorBoundary(Component): Args: *children: The children of the component. + on_error: Fired when the boundary catches an error. Fallback_component: Rendered instead of the children when an error is caught. style: The style of the component. key: A unique key for the component. diff --git a/reflex/components/base/script.pyi b/reflex/components/base/script.pyi index 2d13180bc..be4517f44 100644 --- a/reflex/components/base/script.pyi +++ b/reflex/components/base/script.pyi @@ -65,6 +65,9 @@ class Script(Component): *children: The children of the component. src: Required unless inline script is used strategy: When the script will execute: afterInteractive (defer-like behavior) | beforeInteractive | lazyOnload (async-like behavior) + on_load: Triggered when the script is loading + on_ready: Triggered when the script has loaded + on_error: Triggered when the script has errored style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/core/clipboard.pyi b/reflex/components/core/clipboard.pyi index 489a9bcc5..1284b8050 100644 --- a/reflex/components/core/clipboard.pyi +++ b/reflex/components/core/clipboard.pyi @@ -50,6 +50,7 @@ class Clipboard(Fragment): Args: *children: The children of the component. targets: The element ids to attach the event listener to. Defaults to all child components or the document. + on_paste: Called when the user pastes data into the document. Data is a list of tuples of (mime_type, data). Binary types will be base64 encoded as a data uri. on_paste_event_actions: Save the original event actions for the on_paste event. style: The style of the component. key: A unique key for the component. diff --git a/reflex/components/core/upload.pyi b/reflex/components/core/upload.pyi index e5c73bd14..a63854ef2 100644 --- a/reflex/components/core/upload.pyi +++ b/reflex/components/core/upload.pyi @@ -146,6 +146,7 @@ class Upload(MemoizationLeaf): no_click: Whether to disable click to upload. no_drag: Whether to disable drag and drop. no_keyboard: Whether to disable using the space/enter keys to upload. + on_drop: Fired when files are dropped. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -211,6 +212,7 @@ class StyledUpload(Upload): no_click: Whether to disable click to upload. no_drag: Whether to disable drag and drop. no_keyboard: Whether to disable using the space/enter keys to upload. + on_drop: Fired when files are dropped. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -276,6 +278,7 @@ class UploadNamespace(ComponentNamespace): no_click: Whether to disable click to upload. no_drag: Whether to disable drag and drop. no_keyboard: Whether to disable using the space/enter keys to upload. + on_drop: Fired when files are dropped. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/datadisplay/dataeditor.pyi b/reflex/components/datadisplay/dataeditor.pyi index 6402aaf42..558333cec 100644 --- a/reflex/components/datadisplay/dataeditor.pyi +++ b/reflex/components/datadisplay/dataeditor.pyi @@ -253,6 +253,22 @@ class DataEditor(NoSSRComponent): scroll_offset_x: Initial scroll offset on the horizontal axis. scroll_offset_y: Initial scroll offset on the vertical axis. theme: global theme + on_cell_activated: Fired when a cell is activated. + on_cell_clicked: Fired when a cell is clicked. + on_cell_context_menu: Fired when a cell is right-clicked. + on_cell_edited: Fired when a cell is edited. + on_group_header_clicked: Fired when a group header is clicked. + on_group_header_context_menu: Fired when a group header is right-clicked. + on_group_header_renamed: Fired when a group header is renamed. + on_header_clicked: Fired when a header is clicked. + on_header_context_menu: Fired when a header is right-clicked. + on_header_menu_click: Fired when a header menu item is clicked. + on_item_hovered: Fired when an item is hovered. + on_delete: Fired when a selection is deleted. + on_finished_editing: Fired when editing is finished. + on_row_appended: Fired when a row is appended. + on_selection_cleared: Fired when the selection is cleared. + on_column_resize: Fired when a column is resized. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/el/elements/forms.pyi b/reflex/components/el/elements/forms.pyi index 2f889bc38..bc9bc9689 100644 --- a/reflex/components/el/elements/forms.pyi +++ b/reflex/components/el/elements/forms.pyi @@ -356,6 +356,7 @@ class Form(BaseHTML): target: Where to display the response after submitting the form reset_on_submit: If true, the form will be cleared after submit. handle_submit_unique_name: The name used to make this form's submit handler function unique. + on_submit: Fired when the form is submitted access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. @@ -520,6 +521,11 @@ class Input(BaseHTML): type: Specifies the type of input use_map: Name of the image map used with the input value: Value of the input + on_change: Fired when the input value changes + on_focus: Fired when the input gains focus + on_blur: Fired when the input loses focus + on_key_down: Fired when a key is pressed down + on_key_up: Fired when a key is released access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. @@ -1269,6 +1275,7 @@ class Select(BaseHTML): name: Name of the select, used when submitting the form required: Indicates that the select control must have a selected option size: Number of visible options in a drop-down list + on_change: Fired when the select value changes access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. @@ -1397,6 +1404,11 @@ class Textarea(BaseHTML): rows: Visible number of lines in the text control value: The controlled value of the textarea, read only unless used with on_change wrap: How the text in the textarea is to be wrapped when submitting the form + on_change: Fired when the input value changes + on_focus: Fired when the input gains focus + on_blur: Fired when the input loses focus + on_key_down: Fired when a key is pressed down + on_key_up: Fired when a key is released access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. diff --git a/reflex/components/moment/moment.pyi b/reflex/components/moment/moment.pyi index 415d0f049..16221ff63 100644 --- a/reflex/components/moment/moment.pyi +++ b/reflex/components/moment/moment.pyi @@ -101,6 +101,7 @@ class Moment(NoSSRComponent): local: Outputs the result in local time. tz: Display the date in the given timezone. locale: The locale to use when rendering. + on_change: Fires when the date changes. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/next/image.pyi b/reflex/components/next/image.pyi index 5d72584a9..405f8ac52 100644 --- a/reflex/components/next/image.pyi +++ b/reflex/components/next/image.pyi @@ -72,6 +72,8 @@ class Image(NextComponent): placeholder: A placeholder to use while the image is loading. Possible values are blur, empty, or data:image/.... Defaults to empty. loading: Allows passing CSS styles to the underlying image element. style: Var[Any] The loading behavior of the image. Defaults to lazy. Can hurt performance, recommended to use `priority` instead. blurDataURL: A Data URL to be used as a placeholder image before the src image successfully loads. Only takes effect when combined with placeholder="blur". + on_load: Fires when the image has loaded. + on_error: Fires when the image has an error. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/plotly/plotly.pyi b/reflex/components/plotly/plotly.pyi index 29464415d..186b78a68 100644 --- a/reflex/components/plotly/plotly.pyi +++ b/reflex/components/plotly/plotly.pyi @@ -89,6 +89,26 @@ class Plotly(NoSSRComponent): template: The template for visual appearance of the graph. config: The config of the graph. use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is layed out (zoom, pan, etc). + on_relayouting: Fired while the plot is being layed out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occuring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/primitives/accordion.pyi b/reflex/components/radix/primitives/accordion.pyi index b975cfebe..abf7283c6 100644 --- a/reflex/components/radix/primitives/accordion.pyi +++ b/reflex/components/radix/primitives/accordion.pyi @@ -280,6 +280,7 @@ class AccordionRoot(AccordionComponent): duration: The time in milliseconds to animate open and close easing: The easing function to use for the animation. show_dividers: Whether to show divider lines between items. + on_value_change: Fired when the opened the accordions changes. color_scheme: The color scheme of the component. variant: The variant of the component. as_child: Change the default rendered element for the one passed as a child. diff --git a/reflex/components/radix/primitives/drawer.py b/reflex/components/radix/primitives/drawer.py index 6fe4d10dd..d478dc171 100644 --- a/reflex/components/radix/primitives/drawer.py +++ b/reflex/components/radix/primitives/drawer.py @@ -60,7 +60,7 @@ class DrawerRoot(DrawerComponent): # When `True`, it prevents scroll restoration. Defaults to `True`. preventScrollRestoration: Var[bool] - # Fires when the drawer is opened. + # Fires when the drawer is opened or closed. on_open_change: EventHandler[identity_event(bool)] @@ -79,8 +79,8 @@ class DrawerTrigger(DrawerComponent): """Create a new DrawerTrigger instance. Args: - children: The children of the element. - props: The properties of the element. + *children: The children of the element. + **props: The properties of the element. Returns: The new DrawerTrigger instance. diff --git a/reflex/components/radix/primitives/drawer.pyi b/reflex/components/radix/primitives/drawer.pyi index c4493ee9b..ea2dd8dcf 100644 --- a/reflex/components/radix/primitives/drawer.pyi +++ b/reflex/components/radix/primitives/drawer.pyi @@ -119,6 +119,7 @@ class DrawerRoot(DrawerComponent): modal: When `False`, it allows to interact with elements outside of the drawer without closing it. Defaults to `True`. direction: Direction of the drawer. Defaults to `"bottom"` preventScrollRestoration: When `True`, it prevents scroll restoration. Defaults to `True`. + on_open_change: Fires when the drawer is opened or closed. as_child: Change the default rendered element for the one passed as a child. style: The style of the component. key: A unique key for the component. @@ -166,8 +167,15 @@ class DrawerTrigger(DrawerComponent): """Create a new DrawerTrigger instance. Args: - children: The children of the element. - props: The properties of the element. + *children: The children of the element. + as_child: Change the default rendered element for the one passed as a child. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the element. Returns: The new DrawerTrigger instance. @@ -360,8 +368,15 @@ class DrawerClose(DrawerTrigger): """Create a new DrawerTrigger instance. Args: - children: The children of the element. - props: The properties of the element. + *children: The children of the element. + as_child: Change the default rendered element for the one passed as a child. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the element. Returns: The new DrawerTrigger instance. @@ -529,6 +544,7 @@ class Drawer(ComponentNamespace): modal: When `False`, it allows to interact with elements outside of the drawer without closing it. Defaults to `True`. direction: Direction of the drawer. Defaults to `"bottom"` preventScrollRestoration: When `True`, it prevents scroll restoration. Defaults to `True`. + on_open_change: Fires when the drawer is opened or closed. as_child: Change the default rendered element for the one passed as a child. style: The style of the component. key: A unique key for the component. diff --git a/reflex/components/radix/primitives/form.pyi b/reflex/components/radix/primitives/form.pyi index 2139f0c23..c4dce0a36 100644 --- a/reflex/components/radix/primitives/form.pyi +++ b/reflex/components/radix/primitives/form.pyi @@ -137,6 +137,7 @@ class FormRoot(FormComponent, HTMLForm): Args: *children: The children of the form. + on_clear_server_errors: Fired when the errors are cleared. as_child: Change the default rendered element for the one passed as a child. accept: MIME types the server accepts for file upload accept_charset: Character encodings to be used for form submission @@ -149,6 +150,7 @@ class FormRoot(FormComponent, HTMLForm): target: Where to display the response after submitting the form reset_on_submit: If true, the form will be cleared after submit. handle_submit_unique_name: The name used to make this form's submit handler function unique. + on_submit: Fired when the form is submitted access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. @@ -602,6 +604,7 @@ class Form(FormRoot): Args: *children: The children of the form. + on_clear_server_errors: Fired when the errors are cleared. as_child: Change the default rendered element for the one passed as a child. accept: MIME types the server accepts for file upload accept_charset: Character encodings to be used for form submission @@ -614,6 +617,7 @@ class Form(FormRoot): target: Where to display the response after submitting the form reset_on_submit: If true, the form will be cleared after submit. handle_submit_unique_name: The name used to make this form's submit handler function unique. + on_submit: Fired when the form is submitted access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. @@ -724,6 +728,7 @@ class FormNamespace(ComponentNamespace): Args: *children: The children of the form. + on_clear_server_errors: Fired when the errors are cleared. as_child: Change the default rendered element for the one passed as a child. accept: MIME types the server accepts for file upload accept_charset: Character encodings to be used for form submission @@ -736,6 +741,7 @@ class FormNamespace(ComponentNamespace): target: Where to display the response after submitting the form reset_on_submit: If true, the form will be cleared after submit. handle_submit_unique_name: The name used to make this form's submit handler function unique. + on_submit: Fired when the form is submitted access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. diff --git a/reflex/components/radix/primitives/slider.pyi b/reflex/components/radix/primitives/slider.pyi index 3d57fbe5c..03a30f164 100644 --- a/reflex/components/radix/primitives/slider.pyi +++ b/reflex/components/radix/primitives/slider.pyi @@ -117,6 +117,8 @@ class SliderRoot(SliderComponent): Args: *children: The children of the component. + on_value_change: Fired when the value of a thumb changes. + on_value_commit: Fired when a thumb is released. as_child: Change the default rendered element for the one passed as a child. style: The style of the component. key: A unique key for the component. diff --git a/reflex/components/radix/themes/color_mode.pyi b/reflex/components/radix/themes/color_mode.pyi index d856b0bbd..76bffcdf5 100644 --- a/reflex/components/radix/themes/color_mode.pyi +++ b/reflex/components/radix/themes/color_mode.pyi @@ -416,6 +416,7 @@ class ColorModeSwitch(Switch): color_scheme: Override theme color for switch high_contrast: Whether to render the switch with higher contrast color against background radius: Override theme radius for switch: "none" | "small" | "full" + on_change: Props to rename Fired when the value of the switch changes style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/alert_dialog.pyi b/reflex/components/radix/themes/components/alert_dialog.pyi index 771def7d9..f4b674ce2 100644 --- a/reflex/components/radix/themes/components/alert_dialog.pyi +++ b/reflex/components/radix/themes/components/alert_dialog.pyi @@ -55,6 +55,7 @@ class AlertDialogRoot(RadixThemesComponent): Args: *children: Child components. open: The controlled open state of the dialog. + on_open_change: Fired when the open state changes. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -186,6 +187,9 @@ class AlertDialogContent(elements.Div, RadixThemesComponent): *children: Child components. size: The size of the content. force_mount: Whether to force mount the content on open. + on_open_auto_focus: Fired when the dialog is opened. + on_close_auto_focus: Fired when the dialog is closed. + on_escape_key_down: Fired when the escape key is pressed. access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. diff --git a/reflex/components/radix/themes/components/checkbox.pyi b/reflex/components/radix/themes/components/checkbox.pyi index 90a9220ef..8f69a0f73 100644 --- a/reflex/components/radix/themes/components/checkbox.pyi +++ b/reflex/components/radix/themes/components/checkbox.pyi @@ -151,6 +151,7 @@ class Checkbox(RadixThemesComponent): required: Whether the checkbox is required name: The name of the checkbox control when submitting the form. value: The value of the checkbox control when submitting the form. + on_change: Props to rename Fired when the checkbox is checked or unchecked. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -297,6 +298,7 @@ class HighLevelCheckbox(RadixThemesComponent): required: Whether the checkbox is required name: The name of the checkbox control when submitting the form. value: The value of the checkbox control when submitting the form. + on_change: Props to rename Fired when the checkbox is checked or unchecked. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -441,6 +443,7 @@ class CheckboxNamespace(ComponentNamespace): required: Whether the checkbox is required name: The name of the checkbox control when submitting the form. value: The value of the checkbox control when submitting the form. + on_change: Props to rename Fired when the checkbox is checked or unchecked. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/context_menu.pyi b/reflex/components/radix/themes/components/context_menu.pyi index 56d8200e0..bfb88b303 100644 --- a/reflex/components/radix/themes/components/context_menu.pyi +++ b/reflex/components/radix/themes/components/context_menu.pyi @@ -52,6 +52,7 @@ class ContextMenuRoot(RadixThemesComponent): Args: *children: Child components. modal: The modality of the context menu. When set to true, interaction with outside elements will be disabled and only menu content will be visible to screen readers. + on_open_change: Fired when the open state changes. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -238,6 +239,11 @@ class ContextMenuContent(RadixThemesComponent): high_contrast: Whether to render the button with higher contrast color against background align_offset: The vertical distance in pixels from the anchor. avoid_collisions: When true, overrides the side and aligns preferences to prevent collisions with boundary edges. + on_close_auto_focus: Fired when the context menu is closed. + on_escape_key_down: Fired when the escape key is pressed. + on_pointer_down_outside: Fired when a pointer down event happens outside the context menu. + on_focus_outside: Fired when focus moves outside the context menu. + on_interact_outside: Fired when interacting outside the context menu. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -393,6 +399,10 @@ class ContextMenuSubContent(RadixThemesComponent): Args: *children: Child components. loop: When true, keyboard navigation will loop from last item to first, and vice versa. + on_escape_key_down: Fired when the escape key is pressed. + on_pointer_down_outside: Fired when a pointer down event happens outside the context menu. + on_focus_outside: Fired when focus moves outside the context menu. + on_interact_outside: Fired when interacting outside the context menu. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/dialog.pyi b/reflex/components/radix/themes/components/dialog.pyi index 0713461e9..a277fc775 100644 --- a/reflex/components/radix/themes/components/dialog.pyi +++ b/reflex/components/radix/themes/components/dialog.pyi @@ -53,6 +53,7 @@ class DialogRoot(RadixThemesComponent): Args: *children: Child components. open: The controlled open state of the dialog. + on_open_change: Fired when the open state changes. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -233,6 +234,11 @@ class DialogContent(elements.Div, RadixThemesComponent): Args: *children: Child components. size: DialogContent size "1" - "4" + on_open_auto_focus: Fired when the dialog is opened. + on_close_auto_focus: Fired when the dialog is closed. + on_escape_key_down: Fired when the escape key is pressed. + on_pointer_down_outside: Fired when the pointer is down outside the dialog. + on_interact_outside: Fired when the pointer interacts outside the dialog. access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. @@ -395,6 +401,7 @@ class Dialog(ComponentNamespace): Args: *children: Child components. open: The controlled open state of the dialog. + on_open_change: Fired when the open state changes. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/dropdown_menu.pyi b/reflex/components/radix/themes/components/dropdown_menu.pyi index 8de273be9..ccb2ef347 100644 --- a/reflex/components/radix/themes/components/dropdown_menu.pyi +++ b/reflex/components/radix/themes/components/dropdown_menu.pyi @@ -65,6 +65,7 @@ class DropdownMenuRoot(RadixThemesComponent): open: The controlled open state of the dropdown menu. Must be used in conjunction with onOpenChange. modal: The modality of the dropdown menu. When set to true, interaction with outside elements will be disabled and only menu content will be visible to screen readers. Defaults to True. dir: The reading direction of submenus when applicable. If omitted, inherits globally from DirectionProvider or assumes LTR (left-to-right) reading mode. + on_open_change: Fired when the open state changes. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -280,6 +281,11 @@ class DropdownMenuContent(RadixThemesComponent): arrow_padding: The padding between the arrow and the edges of the content. If your content has border-radius, this will prevent it from overflowing the corners. Defaults to 0. sticky: The sticky behavior on the align axis. "partial" will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless. Defaults to "partial". hide_when_detached: Whether to hide the content when the trigger becomes fully occluded. Defaults to False. + on_close_auto_focus: Fired when the dialog is closed. + on_escape_key_down: Fired when the escape key is pressed. + on_pointer_down_outside: Fired when the pointer is down outside the dialog. + on_focus_outside: Fired when focus moves outside the dialog. + on_interact_outside: Fired when the pointer interacts outside the dialog. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -377,6 +383,7 @@ class DropdownMenuSub(RadixThemesComponent): *children: Child components. open: The controlled open state of the submenu. Must be used in conjunction with `on_open_change`. default_open: The open state of the submenu when it is initially rendered. Use when you do not need to control its open state. + on_open_change: Fired when the open state changes. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -459,6 +466,10 @@ class DropdownMenuSubContent(RadixThemesComponent): arrow_padding: The padding between the arrow and the edges of the content. If your content has border-radius, this will prevent it from overflowing the corners. Defaults to 0. sticky: The sticky behavior on the align axis. "partial" will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless. Defaults to "partial". hide_when_detached: Whether to hide the content when the trigger becomes fully occluded. Defaults to False. + on_escape_key_down: Fired when the escape key is pressed. + on_pointer_down_outside: Fired when the pointer is down outside the dialog. + on_focus_outside: Fired when focus moves outside the dialog. + on_interact_outside: Fired when the pointer interacts outside the dialog. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -580,6 +591,7 @@ class DropdownMenuItem(RadixThemesComponent): as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. Defaults to False. disabled: When true, prevents the user from interacting with the item. text_value: Optional text used for typeahead purposes. By default the typeahead behavior will use the .textContent of the item. Use this when the content is complex, or you have non-textual content inside. + on_select: Fired when the item is selected. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/hover_card.pyi b/reflex/components/radix/themes/components/hover_card.pyi index fa169c852..50c646971 100644 --- a/reflex/components/radix/themes/components/hover_card.pyi +++ b/reflex/components/radix/themes/components/hover_card.pyi @@ -59,6 +59,7 @@ class HoverCardRoot(RadixThemesComponent): open: The controlled open state of the hover card. Must be used in conjunction with onOpenChange. open_delay: The duration from when the mouse enters the trigger until the hover card opens. close_delay: The duration from when the mouse leaves the trigger until the hover card closes. + on_open_change: Fired when the open state changes. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -272,6 +273,7 @@ class HoverCard(ComponentNamespace): open: The controlled open state of the hover card. Must be used in conjunction with onOpenChange. open_delay: The duration from when the mouse enters the trigger until the hover card opens. close_delay: The duration from when the mouse leaves the trigger until the hover card closes. + on_open_change: Fired when the open state changes. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/popover.pyi b/reflex/components/radix/themes/components/popover.pyi index 218392517..35f6854f6 100644 --- a/reflex/components/radix/themes/components/popover.pyi +++ b/reflex/components/radix/themes/components/popover.pyi @@ -55,6 +55,7 @@ class PopoverRoot(RadixThemesComponent): *children: Child components. open: The controlled open state of the popover. modal: The modality of the popover. When set to true, interaction with outside elements will be disabled and only popover content will be visible to screen readers. + on_open_change: Fired when the open state changes. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -207,6 +208,12 @@ class PopoverContent(elements.Div, RadixThemesComponent): align: The preferred alignment against the anchor. May change when collisions occur. align_offset: The vertical distance in pixels from the anchor. avoid_collisions: When true, overrides the side andalign preferences to prevent collisions with boundary edges. + on_open_auto_focus: Fired when the dialog is opened. + on_close_auto_focus: Fired when the dialog is closed. + on_escape_key_down: Fired when the escape key is pressed. + on_pointer_down_outside: Fired when the pointer is down outside the dialog. + on_focus_outside: Fired when focus moves outside the dialog. + on_interact_outside: Fired when the pointer interacts outside the dialog. access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. diff --git a/reflex/components/radix/themes/components/radio_cards.pyi b/reflex/components/radix/themes/components/radio_cards.pyi index d2f6a1425..ac6a645a9 100644 --- a/reflex/components/radix/themes/components/radio_cards.pyi +++ b/reflex/components/radix/themes/components/radio_cards.pyi @@ -201,6 +201,7 @@ class RadioCardsRoot(RadixThemesComponent): orientation: The orientation of the component. dir: The reading direction of the radio group. If omitted, inherits globally from DirectionProvider or assumes LTR (left-to-right) reading mode. loop: When true, keyboard navigation will loop from last item to first, and vice versa. + on_value_change: Event handler called when the value changes. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/radio_group.pyi b/reflex/components/radix/themes/components/radio_group.pyi index f928421c5..5ff5d6e7c 100644 --- a/reflex/components/radix/themes/components/radio_group.pyi +++ b/reflex/components/radix/themes/components/radio_group.pyi @@ -146,6 +146,7 @@ class RadioGroupRoot(RadixThemesComponent): disabled: Whether the radio group is disabled name: The name of the group. Submitted with its owning form as part of a name/value pair. required: Whether the radio group is required + on_change: Props to rename Fired when the value of the radio group changes. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/segmented_control.pyi b/reflex/components/radix/themes/components/segmented_control.pyi index cb1990a7c..d74c26be9 100644 --- a/reflex/components/radix/themes/components/segmented_control.pyi +++ b/reflex/components/radix/themes/components/segmented_control.pyi @@ -148,6 +148,7 @@ class SegmentedControlRoot(RadixThemesComponent): radius: The radius of the segmented control: "none" | "small" | "medium" | "large" | "full" default_value: The default value of the segmented control. value: The current value of the segmented control. + on_change: Handles the `onChange` event for the SegmentedControl component. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/select.pyi b/reflex/components/radix/themes/components/select.pyi index e0e184482..d43ac7327 100644 --- a/reflex/components/radix/themes/components/select.pyi +++ b/reflex/components/radix/themes/components/select.pyi @@ -77,6 +77,8 @@ class SelectRoot(RadixThemesComponent): name: The name of the select control when submitting the form. disabled: When True, prevents the user from interacting with select. required: When True, indicates that the user must select a value before the owning form can be submitted. + on_change: Props to rename Fired when the value of the select changes. + on_open_change: Fired when the select is opened or closed. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -351,6 +353,9 @@ class SelectContent(RadixThemesComponent): side_offset: The distance in pixels from the anchor. Only available when position is set to popper. align: The preferred alignment against the anchor. May change when collisions occur. Only available when position is set to popper. align_offset: The vertical distance in pixels from the anchor. Only available when position is set to popper. + on_close_auto_focus: Fired when the select content is closed. + on_escape_key_down: Fired when the escape key is pressed. + on_pointer_down_outside: Fired when a pointer down event happens outside the select content. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -719,6 +724,8 @@ class HighLevelSelect(SelectRoot): name: The name of the select control when submitting the form. disabled: When True, prevents the user from interacting with select. required: When True, indicates that the user must select a value before the owning form can be submitted. + on_change: Props to rename Fired when the value of the select changes. + on_open_change: Fired when the select is opened or closed. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -893,6 +900,8 @@ class Select(ComponentNamespace): name: The name of the select control when submitting the form. disabled: When True, prevents the user from interacting with select. required: When True, indicates that the user must select a value before the owning form can be submitted. + on_change: Props to rename Fired when the value of the select changes. + on_open_change: Fired when the select is opened or closed. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/slider.pyi b/reflex/components/radix/themes/components/slider.pyi index 8939ea23e..5ac3c275f 100644 --- a/reflex/components/radix/themes/components/slider.pyi +++ b/reflex/components/radix/themes/components/slider.pyi @@ -175,6 +175,8 @@ class Slider(RadixThemesComponent): step: The step value of the slider. disabled: Whether the slider is disabled orientation: The orientation of the slider. + on_change: Props to rename Fired when the value of the slider changes. + on_value_commit: Fired when a thumb is released after being dragged. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/switch.pyi b/reflex/components/radix/themes/components/switch.pyi index f8871872a..aebeb761e 100644 --- a/reflex/components/radix/themes/components/switch.pyi +++ b/reflex/components/radix/themes/components/switch.pyi @@ -155,6 +155,7 @@ class Switch(RadixThemesComponent): color_scheme: Override theme color for switch high_contrast: Whether to render the switch with higher contrast color against background radius: Override theme radius for switch: "none" | "small" | "full" + on_change: Props to rename Fired when the value of the switch changes style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/tabs.pyi b/reflex/components/radix/themes/components/tabs.pyi index 4272bf2a3..ac6109209 100644 --- a/reflex/components/radix/themes/components/tabs.pyi +++ b/reflex/components/radix/themes/components/tabs.pyi @@ -70,6 +70,7 @@ class TabsRoot(RadixThemesComponent): orientation: The orientation of the tabs. dir: Reading direction of the tabs. activation_mode: The mode of activation for the tabs. "automatic" will activate the tab when focused. "manual" will activate the tab when clicked. + on_change: Props to rename Fired when the value of the tabs changes. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -369,6 +370,7 @@ class Tabs(ComponentNamespace): orientation: The orientation of the tabs. dir: Reading direction of the tabs. activation_mode: The mode of activation for the tabs. "automatic" will activate the tab when focused. "manual" will activate the tab when clicked. + on_change: Props to rename Fired when the value of the tabs changes. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/text_area.pyi b/reflex/components/radix/themes/components/text_area.pyi index f234d9150..099a1700a 100644 --- a/reflex/components/radix/themes/components/text_area.pyi +++ b/reflex/components/radix/themes/components/text_area.pyi @@ -214,6 +214,11 @@ class TextArea(RadixThemesComponent, elements.Textarea): auto_height: Automatically fit the content height to the text (use min-height with this prop) cols: Visible width of the text control, in average character widths enter_key_submit: Enter key submits form (shift-enter adds new line) + on_change: Fired when the input value changes + on_focus: Fired when the input gains focus + on_blur: Fired when the input loses focus + on_key_down: Fired when a key is pressed down + on_key_up: Fired when a key is released access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. diff --git a/reflex/components/radix/themes/components/text_field.pyi b/reflex/components/radix/themes/components/text_field.pyi index 7ea0860b5..ffe827aff 100644 --- a/reflex/components/radix/themes/components/text_field.pyi +++ b/reflex/components/radix/themes/components/text_field.pyi @@ -188,6 +188,11 @@ class TextFieldRoot(elements.Div, RadixThemesComponent): required: Indicates that the input is required type: Specifies the type of input value: Value of the input + on_change: Fired when the value of the textarea changes. + on_focus: Fired when the textarea is focused. + on_blur: Fired when the textarea is blurred. + on_key_down: Fired when a key is pressed down. + on_key_up: Fired when a key is released. access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. @@ -500,6 +505,11 @@ class TextField(ComponentNamespace): required: Indicates that the input is required type: Specifies the type of input value: Value of the input + on_change: Fired when the value of the textarea changes. + on_focus: Fired when the textarea is focused. + on_blur: Fired when the textarea is blurred. + on_key_down: Fired when a key is pressed down. + on_key_up: Fired when a key is released. access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. diff --git a/reflex/components/radix/themes/components/tooltip.pyi b/reflex/components/radix/themes/components/tooltip.pyi index e78dd926d..42875e1e5 100644 --- a/reflex/components/radix/themes/components/tooltip.pyi +++ b/reflex/components/radix/themes/components/tooltip.pyi @@ -106,6 +106,9 @@ class Tooltip(RadixThemesComponent): disable_hoverable_content: Prevents Tooltip content from remaining open when hovering. force_mount: Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. aria_label: By default, screenreaders will announce the content inside the component. If this is not descriptive enough, or you have content that cannot be announced, use aria-label as a more descriptive label. + on_open_change: Fired when the open state changes. + on_escape_key_down: Fired when the escape key is pressed. + on_pointer_down_outside: Fired when the pointer is down outside the tooltip. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/react_player/audio.pyi b/reflex/components/react_player/audio.pyi index 1841829af..5c8cc85df 100644 --- a/reflex/components/react_player/audio.pyi +++ b/reflex/components/react_player/audio.pyi @@ -82,6 +82,22 @@ class Audio(ReactPlayer): muted: Mutes the player width: Set the width of the player: ex:640px height: Set the height of the player: ex:640px + on_ready: Called when media is loaded and ready to play. If playing is set to true, media will play immediately. + on_start: Called when media starts playing. + on_play: Called when media starts or resumes playing after pausing or buffering. + on_progress: Callback containing played and loaded progress as a fraction, and playedSeconds and loadedSeconds in seconds. eg { played: 0.12, playedSeconds: 11.3, loaded: 0.34, loadedSeconds: 16.7 } + on_duration: Callback containing duration of the media, in seconds. + on_pause: Called when media is paused. + on_buffer: Called when media starts buffering. + on_buffer_end: Called when media has finished buffering. Works for files, YouTube and Facebook. + on_seek: Called when media seeks with seconds parameter. + on_playback_rate_change: Called when playback rate of the player changed. Only supported by YouTube, Vimeo (if enabled), Wistia, and file paths. + on_playback_quality_change: Called when playback quality of the player changed. Only supported by YouTube (if enabled). + on_ended: Called when media finishes playing. Does not fire when loop is set to true. + on_error: Called when an error occurs whilst attempting to play media. + on_click_preview: Called when user clicks the light mode preview. + on_enable_pip: Called when picture-in-picture mode is enabled. + on_disable_pip: Called when picture-in-picture mode is disabled. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/react_player/react_player.pyi b/reflex/components/react_player/react_player.pyi index e4027cf40..ebae93f71 100644 --- a/reflex/components/react_player/react_player.pyi +++ b/reflex/components/react_player/react_player.pyi @@ -85,6 +85,22 @@ class ReactPlayer(NoSSRComponent): muted: Mutes the player width: Set the width of the player: ex:640px height: Set the height of the player: ex:640px + on_ready: Called when media is loaded and ready to play. If playing is set to true, media will play immediately. + on_start: Called when media starts playing. + on_play: Called when media starts or resumes playing after pausing or buffering. + on_progress: Callback containing played and loaded progress as a fraction, and playedSeconds and loadedSeconds in seconds. eg { played: 0.12, playedSeconds: 11.3, loaded: 0.34, loadedSeconds: 16.7 } + on_duration: Callback containing duration of the media, in seconds. + on_pause: Called when media is paused. + on_buffer: Called when media starts buffering. + on_buffer_end: Called when media has finished buffering. Works for files, YouTube and Facebook. + on_seek: Called when media seeks with seconds parameter. + on_playback_rate_change: Called when playback rate of the player changed. Only supported by YouTube, Vimeo (if enabled), Wistia, and file paths. + on_playback_quality_change: Called when playback quality of the player changed. Only supported by YouTube (if enabled). + on_ended: Called when media finishes playing. Does not fire when loop is set to true. + on_error: Called when an error occurs whilst attempting to play media. + on_click_preview: Called when user clicks the light mode preview. + on_enable_pip: Called when picture-in-picture mode is enabled. + on_disable_pip: Called when picture-in-picture mode is disabled. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/react_player/video.pyi b/reflex/components/react_player/video.pyi index a05e3747b..4a6bb81b3 100644 --- a/reflex/components/react_player/video.pyi +++ b/reflex/components/react_player/video.pyi @@ -82,6 +82,22 @@ class Video(ReactPlayer): muted: Mutes the player width: Set the width of the player: ex:640px height: Set the height of the player: ex:640px + on_ready: Called when media is loaded and ready to play. If playing is set to true, media will play immediately. + on_start: Called when media starts playing. + on_play: Called when media starts or resumes playing after pausing or buffering. + on_progress: Callback containing played and loaded progress as a fraction, and playedSeconds and loadedSeconds in seconds. eg { played: 0.12, playedSeconds: 11.3, loaded: 0.34, loadedSeconds: 16.7 } + on_duration: Callback containing duration of the media, in seconds. + on_pause: Called when media is paused. + on_buffer: Called when media starts buffering. + on_buffer_end: Called when media has finished buffering. Works for files, YouTube and Facebook. + on_seek: Called when media seeks with seconds parameter. + on_playback_rate_change: Called when playback rate of the player changed. Only supported by YouTube, Vimeo (if enabled), Wistia, and file paths. + on_playback_quality_change: Called when playback quality of the player changed. Only supported by YouTube (if enabled). + on_ended: Called when media finishes playing. Does not fire when loop is set to true. + on_error: Called when an error occurs whilst attempting to play media. + on_click_preview: Called when user clicks the light mode preview. + on_enable_pip: Called when picture-in-picture mode is enabled. + on_disable_pip: Called when picture-in-picture mode is disabled. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/recharts/cartesian.pyi b/reflex/components/recharts/cartesian.pyi index 9772be792..983de30e3 100644 --- a/reflex/components/recharts/cartesian.pyi +++ b/reflex/components/recharts/cartesian.pyi @@ -168,6 +168,13 @@ class Axis(Recharts): min_tick_gap: The minimum gap between two adjacent labels. Default: 5 stroke: The stroke color of axis. Default: rx.color("gray", 9) text_anchor: The text anchor of axis. Default: "middle" + on_click: The customized event handler of click on the ticks of this axis + on_mouse_down: The customized event handler of mousedown on the ticks of this axis + on_mouse_up: The customized event handler of mouseup on the ticks of this axis + on_mouse_move: The customized event handler of mousemove on the ticks of this axis + on_mouse_out: The customized event handler of mouseout on the ticks of this axis + on_mouse_enter: The customized event handler of mouseenter on the ticks of this axis + on_mouse_leave: The customized event handler of mouseleave on the ticks of this axis style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -347,6 +354,13 @@ class XAxis(Axis): min_tick_gap: The minimum gap between two adjacent labels. Default: 5 stroke: The stroke color of axis. Default: rx.color("gray", 9) text_anchor: The text anchor of axis. Default: "middle" + on_click: The customized event handler of click on the ticks of this axis + on_mouse_down: The customized event handler of mousedown on the ticks of this axis + on_mouse_up: The customized event handler of mouseup on the ticks of this axis + on_mouse_move: The customized event handler of mousemove on the ticks of this axis + on_mouse_out: The customized event handler of mouseout on the ticks of this axis + on_mouse_enter: The customized event handler of mouseenter on the ticks of this axis + on_mouse_leave: The customized event handler of mouseleave on the ticks of this axis style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -522,6 +536,13 @@ class YAxis(Axis): min_tick_gap: The minimum gap between two adjacent labels. Default: 5 stroke: The stroke color of axis. Default: rx.color("gray", 9) text_anchor: The text anchor of axis. Default: "middle" + on_click: The customized event handler of click on the ticks of this axis + on_mouse_down: The customized event handler of mousedown on the ticks of this axis + on_mouse_up: The customized event handler of mouseup on the ticks of this axis + on_mouse_move: The customized event handler of mousemove on the ticks of this axis + on_mouse_out: The customized event handler of mouseout on the ticks of this axis + on_mouse_enter: The customized event handler of mouseenter on the ticks of this axis + on_mouse_leave: The customized event handler of mouseleave on the ticks of this axis style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -787,6 +808,16 @@ class Cartesian(Recharts): animation_easing: The type of easing function. Default: "ease" unit: The unit of data. This option will be used in tooltip. name: The name of data. This option will be used in tooltip and legend to represent the component. If no value was set to this option, the value of dataKey will be used alternatively. + on_animation_start: The customized event handler of animation start + on_animation_end: The customized event handler of animation end + on_click: The customized event handler of click on the component in this group + on_mouse_down: The customized event handler of mousedown on the component in this group + on_mouse_up: The customized event handler of mouseup on the component in this group + on_mouse_move: The customized event handler of mousemove on the component in this group + on_mouse_over: The customized event handler of mouseover on the component in this group + on_mouse_out: The customized event handler of mouseout on the component in this group + on_mouse_enter: The customized event handler of mouseenter on the component in this group + on_mouse_leave: The customized event handler of mouseleave on the component in this group style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -965,6 +996,16 @@ class Area(Cartesian): animation_easing: The type of easing function. Default: "ease" unit: The unit of data. This option will be used in tooltip. name: The name of data. This option will be used in tooltip and legend to represent the component. If no value was set to this option, the value of dataKey will be used alternatively. + on_animation_start: The customized event handler of animation start + on_animation_end: The customized event handler of animation end + on_click: The customized event handler of click on the component in this group + on_mouse_down: The customized event handler of mousedown on the component in this group + on_mouse_up: The customized event handler of mouseup on the component in this group + on_mouse_move: The customized event handler of mousemove on the component in this group + on_mouse_over: The customized event handler of mouseover on the component in this group + on_mouse_out: The customized event handler of mouseout on the component in this group + on_mouse_enter: The customized event handler of mouseenter on the component in this group + on_mouse_leave: The customized event handler of mouseleave on the component in this group style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -1096,6 +1137,16 @@ class Bar(Cartesian): animation_begin: Specifies when the animation should begin, the unit of this option is ms. Default: 0 animation_duration: Specifies the duration of animation, the unit of this option is ms. Default: 1500 animation_easing: The type of easing function. Default: "ease" + on_animation_start: The customized event handler of animation start + on_animation_end: The customized event handler of animation end + on_click: The customized event handler of click on the component in this group + on_mouse_down: The customized event handler of mousedown on the component in this group + on_mouse_up: The customized event handler of mouseup on the component in this group + on_mouse_move: The customized event handler of mousemove on the component in this group + on_mouse_over: The customized event handler of mouseover on the component in this group + on_mouse_out: The customized event handler of mouseout on the component in this group + on_mouse_enter: The customized event handler of mouseenter on the component in this group + on_mouse_leave: The customized event handler of mouseleave on the component in this group style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -1270,6 +1321,16 @@ class Line(Cartesian): animation_duration: Specifies the duration of animation, the unit of this option is ms. Default: 1500 animation_easing: The type of easing function. Default: "ease" name: The name of data. This option will be used in tooltip and legend to represent the component. If no value was set to this option, the value of dataKey will be used alternatively. + on_animation_start: The customized event handler of animation start + on_animation_end: The customized event handler of animation end + on_click: The customized event handler of click on the component in this group + on_mouse_down: The customized event handler of mousedown on the component in this group + on_mouse_up: The customized event handler of mouseup on the component in this group + on_mouse_move: The customized event handler of mousemove on the component in this group + on_mouse_over: The customized event handler of mouseover on the component in this group + on_mouse_out: The customized event handler of mouseout on the component in this group + on_mouse_enter: The customized event handler of mouseenter on the component in this group + on_mouse_leave: The customized event handler of mouseleave on the component in this group style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -1397,6 +1458,14 @@ class Scatter(Recharts): animation_begin: Specifies when the animation should begin, the unit of this option is ms. Default: 0 animation_duration: Specifies the duration of animation, the unit of this option is ms. Default: 1500 animation_easing: The type of easing function. Default: "ease" + on_click: The customized event handler of click on the component in this group + on_mouse_down: The customized event handler of mousedown on the component in this group + on_mouse_up: The customized event handler of mouseup on the component in this group + on_mouse_move: The customized event handler of mousemove on the component in this group + on_mouse_over: The customized event handler of mouseover on the component in this group + on_mouse_out: The customized event handler of mouseout on the component in this group + on_mouse_enter: The customized event handler of mouseenter on the component in this group + on_mouse_leave: The customized event handler of mouseleave on the component in this group style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -1503,6 +1572,16 @@ class Funnel(Recharts): animation_easing: The type of easing function. 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'linear'. Default "ease" stroke: Stroke color. Default: rx.color("gray", 3) trapezoids: The coordinates of all the trapezoids in the funnel, usually calculated internally. + on_animation_start: The customized event handler of animation start + on_animation_end: The customized event handler of animation end + on_click: The customized event handler of click on the component in this group + on_mouse_down: The customized event handler of mousedown on the component in this group + on_mouse_up: The customized event handler of mouseup on the component in this group + on_mouse_move: The customized event handler of mousemove on the component in this group + on_mouse_over: The customized event handler of mouseover on the component in this group + on_mouse_out: The customized event handler of mouseout on the component in this group + on_mouse_enter: The customized event handler of mouseenter on the component in this group + on_mouse_leave: The customized event handler of mouseleave on the component in this group style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -1757,6 +1836,14 @@ class ReferenceDot(Reference): r: The radius of dot. fill: The color of the area fill. stroke: The color of the line stroke. + on_click: The customized event handler of click on the component in this chart + on_mouse_down: The customized event handler of mousedown on the component in this chart + on_mouse_up: The customized event handler of mouseup on the component in this chart + on_mouse_over: The customized event handler of mouseover on the component in this chart + on_mouse_out: The customized event handler of mouseout on the component in this chart + on_mouse_enter: The customized event handler of mouseenter on the component in this chart + on_mouse_move: The customized event handler of mousemove on the component in this chart + on_mouse_leave: The customized event handler of mouseleave on the component in this chart x_axis_id: The id of x-axis which is corresponding to the data. Default: 0 y_axis_id: The id of y-axis which is corresponding to the data. Default: 0 if_overflow: Defines how to draw the reference line if it falls partly outside the canvas. If set to 'discard', the reference line will not be drawn at all. If set to 'hidden', the reference line will be clipped to the canvas. If set to 'visible', the reference line will be drawn completely. If set to 'extendDomain', the domain of the overflown axis will be extended such that the reference line fits into the canvas. Default: "discard" diff --git a/reflex/components/recharts/charts.pyi b/reflex/components/recharts/charts.pyi index f0b494ff8..2c6d7fffa 100644 --- a/reflex/components/recharts/charts.pyi +++ b/reflex/components/recharts/charts.pyi @@ -51,6 +51,10 @@ class ChartBase(RechartsCharts): *children: The children of the chart component. width: The width of chart container. String or Integer height: The height of chart container. + on_click: The customized event handler of click on the component in this chart + on_mouse_enter: The customized event handler of mouseenter on the component in this chart + on_mouse_move: The customized event handler of mousemove on the component in this chart + on_mouse_leave: The customized event handler of mouseleave on the component in this chart style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -125,6 +129,10 @@ class CategoricalChartBase(ChartBase): stack_offset: The type of offset function used to generate the lower and upper values in the series array. The four types are built-in offsets in d3-shape. 'expand' | 'none' | 'wiggle' | 'silhouette' width: The width of chart container. String or Integer height: The height of chart container. + on_click: The customized event handler of click on the component in this chart + on_mouse_enter: The customized event handler of mouseenter on the component in this chart + on_mouse_move: The customized event handler of mousemove on the component in this chart + on_mouse_leave: The customized event handler of mouseleave on the component in this chart style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -207,6 +215,10 @@ class AreaChart(CategoricalChartBase): stack_offset: The type of offset function used to generate the lower and upper values in the series array. The four types are built-in offsets in d3-shape. 'expand' | 'none' | 'wiggle' | 'silhouette' width: The width of chart container. String or Integer height: The height of chart container. + on_click: The customized event handler of click on the component in this chart + on_mouse_enter: The customized event handler of mouseenter on the component in this chart + on_mouse_move: The customized event handler of mousemove on the component in this chart + on_mouse_leave: The customized event handler of mouseleave on the component in this chart style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -291,6 +303,10 @@ class BarChart(CategoricalChartBase): layout: The layout of area in the chart. 'horizontal' | 'vertical'. Default: "horizontal" width: The width of chart container. String or Integer height: The height of chart container. + on_click: The customized event handler of click on the component in this chart + on_mouse_enter: The customized event handler of mouseenter on the component in this chart + on_mouse_move: The customized event handler of mousemove on the component in this chart + on_mouse_leave: The customized event handler of mouseleave on the component in this chart style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -365,6 +381,10 @@ class LineChart(CategoricalChartBase): stack_offset: The type of offset function used to generate the lower and upper values in the series array. The four types are built-in offsets in d3-shape. 'expand' | 'none' | 'wiggle' | 'silhouette' width: The width of chart container. String or Integer height: The height of chart container. + on_click: The customized event handler of click on the component in this chart + on_mouse_enter: The customized event handler of mouseenter on the component in this chart + on_mouse_move: The customized event handler of mousemove on the component in this chart + on_mouse_leave: The customized event handler of mouseleave on the component in this chart style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -455,6 +475,10 @@ class ComposedChart(CategoricalChartBase): stack_offset: The type of offset function used to generate the lower and upper values in the series array. The four types are built-in offsets in d3-shape. 'expand' | 'none' | 'wiggle' | 'silhouette' width: The width of chart container. String or Integer height: The height of chart container. + on_click: The customized event handler of click on the component in this chart + on_mouse_enter: The customized event handler of mouseenter on the component in this chart + on_mouse_move: The customized event handler of mousemove on the component in this chart + on_mouse_leave: The customized event handler of mouseleave on the component in this chart style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -505,8 +529,16 @@ class PieChart(ChartBase): Args: *children: The children of the chart component. margin: The sizes of whitespace around the chart, i.e. {"top": 50, "right": 30, "left": 20, "bottom": 5}. + on_mouse_down: The customized event handler of mousedown on the sectors in this group + on_mouse_up: The customized event handler of mouseup on the sectors in this group + on_mouse_over: The customized event handler of mouseover on the sectors in this group + on_mouse_out: The customized event handler of mouseout on the sectors in this group width: The width of chart container. String or Integer height: The height of chart container. + on_click: The customized event handler of click on the component in this chart + on_mouse_enter: The customized event handler of mouseenter on the component in this chart + on_mouse_move: The customized event handler of mousemove on the component in this chart + on_mouse_leave: The customized event handler of mouseleave on the component in this chart style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -562,6 +594,9 @@ class RadarChart(ChartBase): outer_radius: The outer radius of last circle grid. If set a percentage, the final value is obtained by multiplying the percentage of maxRadius which is calculated by the width, height, cx, cy. Number | Percentage. Default: "80%" width: The width of chart container. String or Integer height: The height of chart container. + on_click: The customized event handler of click on the component in this chart + on_mouse_enter: The customized event handler of mouseenter on the component in this chart + on_mouse_leave: The customized event handler of mouseleave on the component in this chart style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -634,6 +669,10 @@ class RadialBarChart(ChartBase): bar_size: The size of each bar. If the barSize is not specified, the size of bar will be calculated by the barCategoryGap, barGap and the quantity of bar groups. width: The width of chart container. String or Integer height: The height of chart container. + on_click: The customized event handler of click on the component in this chart + on_mouse_enter: The customized event handler of mouseenter on the component in this chart + on_mouse_move: The customized event handler of mousemove on the component in this chart + on_mouse_leave: The customized event handler of mouseleave on the component in this chart style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -680,6 +719,10 @@ class ScatterChart(ChartBase): margin: The sizes of whitespace around the chart. Default: {"top": 5, "right": 5, "bottom": 5, "left": 5} width: The width of chart container. String or Integer height: The height of chart container. + on_click: The customized event handler of click on the component in this chart + on_mouse_enter: The customized event handler of mouseenter on the component in this chart + on_mouse_move: The customized event handler of mousemove on the component in this chart + on_mouse_leave: The customized event handler of mouseleave on the component in this chart style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -736,6 +779,10 @@ class FunnelChart(ChartBase): stroke: The stroke color of each bar. String | Object width: The width of chart container. String or Integer height: The height of chart container. + on_click: The customized event handler of click on the component in this chart + on_mouse_enter: The customized event handler of mouseenter on the component in this chart + on_mouse_move: The customized event handler of mousemove on the component in this chart + on_mouse_leave: The customized event handler of mouseleave on the component in this chart style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -809,6 +856,8 @@ class Treemap(RechartsCharts): animation_begin: Specifies when the animation should begin, the unit of this option is ms. Default: 0 animation_duration: Specifies the duration of animation, the unit of this option is ms. Default: 1500 animation_easing: The type of easing function. 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'linear'. Default: "ease" + on_animation_start: The customized event handler of animation start + on_animation_end: The customized event handler of animation end style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/recharts/general.pyi b/reflex/components/recharts/general.pyi index 4a8f61c5e..affe362bb 100644 --- a/reflex/components/recharts/general.pyi +++ b/reflex/components/recharts/general.pyi @@ -61,6 +61,7 @@ class ResponsiveContainer(Recharts, MemoizationLeaf): min_width: The minimum width of chart container. Number min_height: The minimum height of chart container. Number debounce: If specified a positive number, debounced function will be used to handle the resize event. Default: 0 + on_resize: If specified provides a callback providing the updated chart width and height values. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -175,6 +176,14 @@ class Legend(Recharts): chart_width: The width of chart container, usually calculated internally. chart_height: The height of chart container, usually calculated internally. margin: The margin of chart container, usually calculated internally. + on_click: The customized event handler of click on the items in this group + on_mouse_down: The customized event handler of mousedown on the items in this group + on_mouse_up: The customized event handler of mouseup on the items in this group + on_mouse_move: The customized event handler of mousemove on the items in this group + on_mouse_over: The customized event handler of mouseover on the items in this group + on_mouse_out: The customized event handler of mouseout on the items in this group + on_mouse_enter: The customized event handler of mouseenter on the items in this group + on_mouse_leave: The customized event handler of mouseleave on the items in this group style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/recharts/polar.pyi b/reflex/components/recharts/polar.pyi index c02596006..025f8443d 100644 --- a/reflex/components/recharts/polar.pyi +++ b/reflex/components/recharts/polar.pyi @@ -391,6 +391,14 @@ class PolarAngleAxis(Recharts): orientation: The orientation of axis text. Default: "outer" stroke: The stroke color of axis. Default: rx.color("gray", 10) allow_duplicated_category: Allow the axis has duplicated categorys or not when the type of axis is "category". Default: True + on_click: The customized event handler of click on the ticks of this axis. + on_mouse_down: The customized event handler of mousedown on the the ticks of this axis. + on_mouse_up: The customized event handler of mouseup on the ticks of this axis. + on_mouse_move: The customized event handler of mousemove on the ticks of this axis. + on_mouse_over: The customized event handler of mouseover on the ticks of this axis. + on_mouse_out: The customized event handler of mouseout on the ticks of this axis. + on_mouse_enter: The customized event handler of moustenter on the ticks of this axis. + on_mouse_leave: The customized event handler of mouseleave on the ticks of this axis. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/suneditor/editor.pyi b/reflex/components/suneditor/editor.pyi index 73dd38fdc..3734d2f16 100644 --- a/reflex/components/suneditor/editor.pyi +++ b/reflex/components/suneditor/editor.pyi @@ -172,6 +172,15 @@ class Editor(NoSSRComponent): hide: Hide the editor default: False hide_toolbar: Hide the editor toolbar default: False disable_toolbar: Disable the editor toolbar default: False + on_change: Fired when the editor content changes. + on_input: Fired when the something is inputted in the editor. + on_blur: Fired when the editor loses focus. + on_load: Fired when the editor is loaded. + on_copy: Fired when the editor content is copied. + on_cut: Fired when the editor content is cut. + on_paste: Fired when the editor content is pasted. + toggle_code_view: Fired when the code view is toggled. + toggle_full_screen: Fired when the full screen mode is toggled. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/utils/pyi_generator.py b/reflex/utils/pyi_generator.py index 1fc17341b..667015768 100644 --- a/reflex/utils/pyi_generator.py +++ b/reflex/utils/pyi_generator.py @@ -568,6 +568,7 @@ def _generate_component_create_functiondef( ) for trigger in sorted(event_triggers) ) + logger.debug(f"Generated {clz.__name__}.create method with {len(kwargs)} kwargs") create_args = ast.arguments( args=[ast.arg(arg="cls")], @@ -578,12 +579,17 @@ def _generate_component_create_functiondef( kwarg=ast.arg(arg="props"), defaults=[], ) + definition = ast.FunctionDef( name="create", args=create_args, body=[ ast.Expr( - value=ast.Constant(value=_generate_docstrings(all_classes, all_props)) + value=ast.Constant( + value=_generate_docstrings( + all_classes, [*all_props, *event_triggers] + ) + ), ), ast.Expr( value=ast.Ellipsis(), From e5e494108e44ec45abdb594e98ae24199a251db3 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 30 Oct 2024 17:48:23 -0700 Subject: [PATCH 10/27] improve page title default (#4278) --- reflex/utils/format.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 65c0f049b..a914a585c 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -197,8 +197,16 @@ def make_default_page_title(app_name: str, route: str) -> str: Returns: The default page title. """ - title = constants.DefaultPage.TITLE.format(app_name, route) - return to_title_case(title, " ") + route_parts = [ + part + for part in route.split("/") + if part and not (part.startswith("[") and part.endswith("]")) + ] + + title = constants.DefaultPage.TITLE.format( + app_name, route_parts[-1] if route_parts else constants.PageNames.INDEX_ROUTE + ) + return to_title_case(title) def _escape_js_string(string: str) -> str: From 84b0864e7ea141cdbada1267aaac863ffd03e256 Mon Sep 17 00:00:00 2001 From: Nikhil Rao Date: Wed, 30 Oct 2024 17:57:38 -0700 Subject: [PATCH 11/27] Add option to scroll to bottom (#4276) * Add option to scroll to bottom * use var shenangins --------- Co-authored-by: Khaleel Al-Adhami --- reflex/event.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/reflex/event.py b/reflex/event.py index aa366e3bb..14249091e 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -767,18 +767,25 @@ def set_focus(ref: str) -> EventSpec: ) -def scroll_to(elem_id: str) -> EventSpec: +def scroll_to(elem_id: str, align_to_top: bool | Var[bool] = True) -> EventSpec: """Select the id of a html element for scrolling into view. Args: - elem_id: the id of the element + elem_id: The id of the element to scroll to. + align_to_top: Whether to scroll to the top (True) or bottom (False) of the element. Returns: An EventSpec to scroll the page to the selected element. """ - js_code = f"document.getElementById('{elem_id}').scrollIntoView();" + get_element_by_id = FunctionStringVar.create("document.getElementById") - return call_script(js_code) + return call_script( + get_element_by_id(elem_id) + .call(elem_id) + .to(ObjectVar) + .scrollIntoView.to(FunctionVar) + .call(align_to_top) + ) def set_value(ref: str, value: Any) -> EventSpec: From a968231750970cc8c1ebd71d95a2c64b95c8fa57 Mon Sep 17 00:00:00 2001 From: Elijah Ahianyo Date: Thu, 31 Oct 2024 09:33:49 +0000 Subject: [PATCH 12/27] [ENG-3892]Shiki codeblock support decorations (#4234) * Shiki codeblock support decorations * add decorations to useEffect * fix pyright * validate decorations dict * Fix exception message plus unit tests * possible test fix * fix pyright * possible tests fix * cast decorations before creating codeblock * `plain` is not a valid theme * pyi fix * address PR comment --- .../.templates/web/components/shiki/code.js | 27 ++++---- .../datadisplay/shiki_code_block.py | 39 +++++++++++- .../datadisplay/shiki_code_block.pyi | 50 ++++++++++----- reflex/components/props.py | 34 ++++++++++ reflex/components/sonner/toast.py | 33 +--------- reflex/components/sonner/toast.pyi | 9 +-- reflex/utils/exceptions.py | 4 ++ tests/units/components/test_props.py | 63 +++++++++++++++++++ 8 files changed, 193 insertions(+), 66 deletions(-) create mode 100644 tests/units/components/test_props.py diff --git a/reflex/.templates/web/components/shiki/code.js b/reflex/.templates/web/components/shiki/code.js index c655c3a60..22b3693b4 100644 --- a/reflex/.templates/web/components/shiki/code.js +++ b/reflex/.templates/web/components/shiki/code.js @@ -1,26 +1,31 @@ import { useEffect, useState } from "react" import { codeToHtml} from "shiki" -export function Code ({code, theme, language, transformers, ...divProps}) { +/** + * Code component that uses Shiki to convert code to HTML and render it. + * + * @param code - The code to be highlighted. + * @param theme - The theme to be used for highlighting. + * @param language - The language of the code. + * @param transformers - The transformers to be applied to the code. + * @param decorations - The decorations to be applied to the code. + * @param divProps - Additional properties to be passed to the div element. + * @returns The rendered code block. + */ +export function Code ({code, theme, language, transformers, decorations, ...divProps}) { const [codeResult, setCodeResult] = useState("") useEffect(() => { async function fetchCode() { - let final_code; - - if (Array.isArray(code)) { - final_code = code[0]; - } else { - final_code = code; - } - const result = await codeToHtml(final_code, { + const result = await codeToHtml(code, { lang: language, theme, - transformers + transformers, + decorations }); setCodeResult(result); } fetchCode(); - }, [code, language, theme, transformers] + }, [code, language, theme, transformers, decorations] ) return ( diff --git a/reflex/components/datadisplay/shiki_code_block.py b/reflex/components/datadisplay/shiki_code_block.py index 6ce6916c6..07f09c6f6 100644 --- a/reflex/components/datadisplay/shiki_code_block.py +++ b/reflex/components/datadisplay/shiki_code_block.py @@ -12,6 +12,7 @@ from reflex.components.core.colors import color from reflex.components.core.cond import color_mode_cond from reflex.components.el.elements.forms import Button from reflex.components.lucide.icon import Icon +from reflex.components.props import NoExtrasAllowedProps from reflex.components.radix.themes.layout.box import Box from reflex.event import call_script, set_clipboard from reflex.style import Style @@ -253,6 +254,7 @@ LiteralCodeLanguage = Literal[ "pascal", "perl", "php", + "plain", "plsql", "po", "postcss", @@ -369,10 +371,11 @@ LiteralCodeTheme = Literal[ "nord", "one-dark-pro", "one-light", - "plain", "plastic", "poimandres", "red", + # rose-pine themes dont work with the current version of shikijs transformers + # https://github.com/shikijs/shiki/issues/730 "rose-pine", "rose-pine-dawn", "rose-pine-moon", @@ -390,6 +393,23 @@ LiteralCodeTheme = Literal[ ] +class Position(NoExtrasAllowedProps): + """Position of the decoration.""" + + line: int + character: int + + +class ShikiDecorations(NoExtrasAllowedProps): + """Decorations for the code block.""" + + start: Union[int, Position] + end: Union[int, Position] + tag_name: str = "span" + properties: dict[str, Any] = {} + always_wrap: bool = False + + class ShikiBaseTransformers(Base): """Base for creating transformers.""" @@ -537,6 +557,9 @@ class ShikiCodeBlock(Component): [] ) + # The decorations to use for the syntax highlighter. + decorations: Var[list[ShikiDecorations]] = Var.create([]) + @classmethod def create( cls, @@ -555,6 +578,7 @@ class ShikiCodeBlock(Component): # Separate props for the code block and the wrapper code_block_props = {} code_wrapper_props = {} + decorations = props.pop("decorations", []) class_props = cls.get_props() @@ -564,6 +588,15 @@ class ShikiCodeBlock(Component): value ) + # cast decorations into ShikiDecorations. + decorations = [ + ShikiDecorations(**decoration) + if not isinstance(decoration, ShikiDecorations) + else decoration + for decoration in decorations + ] + code_block_props["decorations"] = decorations + code_block_props["code"] = children[0] code_block = super().create(**code_block_props) @@ -676,10 +709,10 @@ class ShikiHighLevelCodeBlock(ShikiCodeBlock): show_line_numbers: Var[bool] # Whether a copy button should appear. - can_copy: Var[bool] = Var.create(False) + can_copy: bool = False # copy_button: A custom copy button to override the default one. - copy_button: Var[Optional[Union[Component, bool]]] = Var.create(None) + copy_button: Optional[Union[Component, bool]] = None @classmethod def create( diff --git a/reflex/components/datadisplay/shiki_code_block.pyi b/reflex/components/datadisplay/shiki_code_block.pyi index bcf2719e9..e2475aed5 100644 --- a/reflex/components/datadisplay/shiki_code_block.pyi +++ b/reflex/components/datadisplay/shiki_code_block.pyi @@ -7,6 +7,7 @@ from typing import Any, Dict, Literal, Optional, Union, overload from reflex.base import Base from reflex.components.component import Component, ComponentNamespace +from reflex.components.props import NoExtrasAllowedProps from reflex.event import EventType from reflex.style import Style from reflex.vars.base import Var @@ -192,6 +193,7 @@ LiteralCodeLanguage = Literal[ "pascal", "perl", "php", + "plain", "plsql", "po", "postcss", @@ -308,7 +310,6 @@ LiteralCodeTheme = Literal[ "nord", "one-dark-pro", "one-light", - "plain", "plastic", "poimandres", "red", @@ -328,6 +329,17 @@ LiteralCodeTheme = Literal[ "vitesse-light", ] +class Position(NoExtrasAllowedProps): + line: int + character: int + +class ShikiDecorations(NoExtrasAllowedProps): + start: Union[int, Position] + end: Union[int, Position] + tag_name: str + properties: dict[str, Any] + always_wrap: bool + class ShikiBaseTransformers(Base): library: str fns: list[FunctionStringVar] @@ -479,6 +491,7 @@ class ShikiCodeBlock(Component): "pascal", "perl", "php", + "plain", "plsql", "po", "postcss", @@ -694,6 +707,7 @@ class ShikiCodeBlock(Component): "pascal", "perl", "php", + "plain", "plsql", "po", "postcss", @@ -815,7 +829,6 @@ class ShikiCodeBlock(Component): "nord", "one-dark-pro", "one-light", - "plain", "plastic", "poimandres", "red", @@ -870,7 +883,6 @@ class ShikiCodeBlock(Component): "nord", "one-dark-pro", "one-light", - "plain", "plastic", "poimandres", "red", @@ -906,6 +918,9 @@ class ShikiCodeBlock(Component): list[Union[ShikiBaseTransformers, dict[str, Any]]], ] ] = None, + decorations: Optional[ + Union[Var[list[ShikiDecorations]], list[ShikiDecorations]] + ] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -938,6 +953,7 @@ class ShikiCodeBlock(Component): themes: The set of themes to use for different modes. code: The code to display. transformers: The transformers to use for the syntax highlighter. + decorations: The decorations to use for the syntax highlighter. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -965,10 +981,8 @@ class ShikiHighLevelCodeBlock(ShikiCodeBlock): *children, use_transformers: Optional[Union[Var[bool], bool]] = None, show_line_numbers: Optional[Union[Var[bool], bool]] = None, - can_copy: Optional[Union[Var[bool], bool]] = None, - copy_button: Optional[ - Union[Component, Var[Optional[Union[Component, bool]]], bool] - ] = None, + can_copy: Optional[bool] = None, + copy_button: Optional[Union[Component, bool]] = None, language: Optional[ Union[ Literal[ @@ -1104,6 +1118,7 @@ class ShikiHighLevelCodeBlock(ShikiCodeBlock): "pascal", "perl", "php", + "plain", "plsql", "po", "postcss", @@ -1319,6 +1334,7 @@ class ShikiHighLevelCodeBlock(ShikiCodeBlock): "pascal", "perl", "php", + "plain", "plsql", "po", "postcss", @@ -1440,7 +1456,6 @@ class ShikiHighLevelCodeBlock(ShikiCodeBlock): "nord", "one-dark-pro", "one-light", - "plain", "plastic", "poimandres", "red", @@ -1495,7 +1510,6 @@ class ShikiHighLevelCodeBlock(ShikiCodeBlock): "nord", "one-dark-pro", "one-light", - "plain", "plastic", "poimandres", "red", @@ -1531,6 +1545,9 @@ class ShikiHighLevelCodeBlock(ShikiCodeBlock): list[Union[ShikiBaseTransformers, dict[str, Any]]], ] ] = None, + decorations: Optional[ + Union[Var[list[ShikiDecorations]], list[ShikiDecorations]] + ] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -1567,6 +1584,7 @@ class ShikiHighLevelCodeBlock(ShikiCodeBlock): themes: The set of themes to use for different modes. code: The code to display. transformers: The transformers to use for the syntax highlighter. + decorations: The decorations to use for the syntax highlighter. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -1593,10 +1611,8 @@ class CodeblockNamespace(ComponentNamespace): *children, use_transformers: Optional[Union[Var[bool], bool]] = None, show_line_numbers: Optional[Union[Var[bool], bool]] = None, - can_copy: Optional[Union[Var[bool], bool]] = None, - copy_button: Optional[ - Union[Component, Var[Optional[Union[Component, bool]]], bool] - ] = None, + can_copy: Optional[bool] = None, + copy_button: Optional[Union[Component, bool]] = None, language: Optional[ Union[ Literal[ @@ -1732,6 +1748,7 @@ class CodeblockNamespace(ComponentNamespace): "pascal", "perl", "php", + "plain", "plsql", "po", "postcss", @@ -1947,6 +1964,7 @@ class CodeblockNamespace(ComponentNamespace): "pascal", "perl", "php", + "plain", "plsql", "po", "postcss", @@ -2068,7 +2086,6 @@ class CodeblockNamespace(ComponentNamespace): "nord", "one-dark-pro", "one-light", - "plain", "plastic", "poimandres", "red", @@ -2123,7 +2140,6 @@ class CodeblockNamespace(ComponentNamespace): "nord", "one-dark-pro", "one-light", - "plain", "plastic", "poimandres", "red", @@ -2159,6 +2175,9 @@ class CodeblockNamespace(ComponentNamespace): list[Union[ShikiBaseTransformers, dict[str, Any]]], ] ] = None, + decorations: Optional[ + Union[Var[list[ShikiDecorations]], list[ShikiDecorations]] + ] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -2195,6 +2214,7 @@ class CodeblockNamespace(ComponentNamespace): themes: The set of themes to use for different modes. code: The code to display. transformers: The transformers to use for the syntax highlighter. + decorations: The decorations to use for the syntax highlighter. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/props.py b/reflex/components/props.py index 92dbe8144..adce134fc 100644 --- a/reflex/components/props.py +++ b/reflex/components/props.py @@ -2,8 +2,11 @@ from __future__ import annotations +from pydantic import ValidationError + from reflex.base import Base from reflex.utils import format +from reflex.utils.exceptions import InvalidPropValueError from reflex.vars.object import LiteralObjectVar @@ -40,3 +43,34 @@ class PropsBase(Base): format.to_camel_case(key): value for key, value in super().dict(*args, **kwargs).items() } + + +class NoExtrasAllowedProps(Base): + """A class that holds props to be passed or applied to a component with no extra props allowed.""" + + def __init__(self, component_name=None, **kwargs): + """Initialize the props. + + Args: + component_name: The custom name of the component. + kwargs: Kwargs to initialize the props. + + Raises: + InvalidPropValueError: If invalid props are passed on instantiation. + """ + component_name = component_name or type(self).__name__ + try: + super().__init__(**kwargs) + except ValidationError as e: + invalid_fields = ", ".join([error["loc"][0] for error in e.errors()]) # type: ignore + supported_props_str = ", ".join(f'"{field}"' for field in self.get_fields()) + raise InvalidPropValueError( + f"Invalid prop(s) {invalid_fields} for {component_name!r}. Supported props are {supported_props_str}" + ) from None + + class Config: + """Pydantic config.""" + + arbitrary_types_allowed = True + use_enum_values = True + extra = "forbid" diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py index 65f1157d3..175c68f63 100644 --- a/reflex/components/sonner/toast.py +++ b/reflex/components/sonner/toast.py @@ -4,12 +4,10 @@ from __future__ import annotations from typing import Any, ClassVar, Literal, Optional, Union -from pydantic import ValidationError - from reflex.base import Base from reflex.components.component import Component, ComponentNamespace from reflex.components.lucide.icon import Icon -from reflex.components.props import PropsBase +from reflex.components.props import NoExtrasAllowedProps, PropsBase from reflex.event import ( EventSpec, call_script, @@ -72,7 +70,7 @@ def _toast_callback_signature(toast: Var) -> list[Var]: ] -class ToastProps(PropsBase): +class ToastProps(PropsBase, NoExtrasAllowedProps): """Props for the toast component.""" # Toast's title, renders above the description. @@ -132,24 +130,6 @@ class ToastProps(PropsBase): # Function that gets called when the toast disappears automatically after it's timeout (duration` prop). on_auto_close: Optional[Any] - def __init__(self, **kwargs): - """Initialize the props. - - Args: - kwargs: Kwargs to initialize the props. - - Raises: - ValueError: If invalid props are passed on instantiation. - """ - try: - super().__init__(**kwargs) - except ValidationError as e: - invalid_fields = ", ".join([error["loc"][0] for error in e.errors()]) # type: ignore - supported_props_str = ", ".join(f'"{field}"' for field in self.get_fields()) - raise ValueError( - f"Invalid prop(s) {invalid_fields} for rx.toast. Supported props are {supported_props_str}" - ) from None - def dict(self, *args, **kwargs) -> dict[str, Any]: """Convert the object to a dictionary. @@ -181,13 +161,6 @@ class ToastProps(PropsBase): ) return d - class Config: - """Pydantic config.""" - - arbitrary_types_allowed = True - use_enum_values = True - extra = "forbid" - class Toaster(Component): """A Toaster Component for displaying toast notifications.""" @@ -281,7 +254,7 @@ class Toaster(Component): if message == "" and ("title" not in props or "description" not in props): raise ValueError("Toast message or title or description must be provided.") if props: - args = LiteralVar.create(ToastProps(**props)) + args = LiteralVar.create(ToastProps(component_name="rx.toast", **props)) # type: ignore toast = f"{toast_command}(`{message}`, {str(args)})" else: toast = f"{toast_command}(`{message}`)" diff --git a/reflex/components/sonner/toast.pyi b/reflex/components/sonner/toast.pyi index 976f9f310..10168257e 100644 --- a/reflex/components/sonner/toast.pyi +++ b/reflex/components/sonner/toast.pyi @@ -8,7 +8,7 @@ from typing import Any, ClassVar, Dict, Literal, Optional, Union, overload from reflex.base import Base from reflex.components.component import Component, ComponentNamespace from reflex.components.lucide.icon import Icon -from reflex.components.props import PropsBase +from reflex.components.props import NoExtrasAllowedProps, PropsBase from reflex.event import EventSpec, EventType from reflex.style import Style from reflex.utils.serializers import serializer @@ -31,7 +31,7 @@ class ToastAction(Base): @serializer def serialize_action(action: ToastAction) -> dict: ... -class ToastProps(PropsBase): +class ToastProps(PropsBase, NoExtrasAllowedProps): title: Optional[Union[str, Var]] description: Optional[Union[str, Var]] close_button: Optional[bool] @@ -52,11 +52,6 @@ class ToastProps(PropsBase): def dict(self, *args, **kwargs) -> dict[str, Any]: ... - class Config: - arbitrary_types_allowed = True - use_enum_values = True - extra = "forbid" - class Toaster(Component): is_used: ClassVar[bool] = False diff --git a/reflex/utils/exceptions.py b/reflex/utils/exceptions.py index f16338513..6d4a0bbc7 100644 --- a/reflex/utils/exceptions.py +++ b/reflex/utils/exceptions.py @@ -143,3 +143,7 @@ class EnvironmentVarValueError(ReflexError, ValueError): class DynamicComponentInvalidSignature(ReflexError, TypeError): """Raised when a dynamic component has an invalid signature.""" + + +class InvalidPropValueError(ReflexError): + """Raised when a prop value is invalid.""" diff --git a/tests/units/components/test_props.py b/tests/units/components/test_props.py new file mode 100644 index 000000000..8ab07f135 --- /dev/null +++ b/tests/units/components/test_props.py @@ -0,0 +1,63 @@ +import pytest + +from reflex.components.props import NoExtrasAllowedProps +from reflex.utils.exceptions import InvalidPropValueError + +try: + from pydantic.v1 import ValidationError +except ModuleNotFoundError: + from pydantic import ValidationError + + +class PropA(NoExtrasAllowedProps): + """Base prop class.""" + + foo: str + bar: str + + +class PropB(NoExtrasAllowedProps): + """Prop class with nested props.""" + + foobar: str + foobaz: PropA + + +@pytest.mark.parametrize( + "props_class, kwargs, should_raise", + [ + (PropA, {"foo": "value", "bar": "another_value"}, False), + (PropA, {"fooz": "value", "bar": "another_value"}, True), + ( + PropB, + { + "foobaz": {"foo": "value", "bar": "another_value"}, + "foobar": "foo_bar_value", + }, + False, + ), + ( + PropB, + { + "fooba": {"foo": "value", "bar": "another_value"}, + "foobar": "foo_bar_value", + }, + True, + ), + ( + PropB, + { + "foobaz": {"foobar": "value", "bar": "another_value"}, + "foobar": "foo_bar_value", + }, + True, + ), + ], +) +def test_no_extras_allowed_props(props_class, kwargs, should_raise): + if should_raise: + with pytest.raises((ValidationError, InvalidPropValueError)): + props_class(**kwargs) + else: + props_instance = props_class(**kwargs) + assert isinstance(props_instance, props_class) From c8cecbf3cc63f3ef512d60776df7fa9390130954 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 31 Oct 2024 11:20:15 -0700 Subject: [PATCH 13/27] fix typo in dataeditor prop (#4281) * fix typo in dataeditor prop * move other ones as well --- reflex/components/datadisplay/dataeditor.py | 4 ++-- reflex/components/datadisplay/dataeditor.pyi | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/reflex/components/datadisplay/dataeditor.py b/reflex/components/datadisplay/dataeditor.py index 1caf053b5..27ca62d93 100644 --- a/reflex/components/datadisplay/dataeditor.py +++ b/reflex/components/datadisplay/dataeditor.py @@ -206,7 +206,7 @@ class DataEditor(NoSSRComponent): get_cell_content: Var[str] # Allow selection for copying. - get_cell_for_selection: Var[bool] + get_cells_for_selection: Var[bool] # Allow paste. on_paste: Var[bool] @@ -424,7 +424,7 @@ class DataEditor(NoSSRComponent): props["theme"] = DataEditorTheme(**theme) # Allow by default to select a region of cells in the grid. - props.setdefault("get_cell_for_selection", True) + props.setdefault("get_cells_for_selection", True) # Disable on_paste by default if not provided. props.setdefault("on_paste", False) diff --git a/reflex/components/datadisplay/dataeditor.pyi b/reflex/components/datadisplay/dataeditor.pyi index 558333cec..b1ff93c38 100644 --- a/reflex/components/datadisplay/dataeditor.pyi +++ b/reflex/components/datadisplay/dataeditor.pyi @@ -140,7 +140,7 @@ class DataEditor(NoSSRComponent): ] = None, data: Optional[Union[List[List[Any]], Var[List[List[Any]]]]] = None, get_cell_content: Optional[Union[Var[str], str]] = None, - get_cell_for_selection: Optional[Union[Var[bool], bool]] = None, + get_cells_for_selection: Optional[Union[Var[bool], bool]] = None, on_paste: Optional[Union[Var[bool], bool]] = None, draw_focus_ring: Optional[Union[Var[bool], bool]] = None, fixed_shadow_x: Optional[Union[Var[bool], bool]] = None, @@ -228,7 +228,7 @@ class DataEditor(NoSSRComponent): columns: Headers of the columns for the data grid. data: The data. get_cell_content: The name of the callback used to find the data to display. - get_cell_for_selection: Allow selection for copying. + get_cells_for_selection: Allow selection for copying. on_paste: Allow paste. draw_focus_ring: Controls the drawing of the focus ring. fixed_shadow_x: Enables or disables the overlay shadow when scrolling horizontally. From c07eb2a6a04e0694d27e6b5e256cc219ac68af0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Thu, 31 Oct 2024 12:45:28 -0700 Subject: [PATCH 14/27] [ENG-3943]type check for event handler if spec arg are typed (#4046) * type check for event handler if spec arg are typed * fix the typecheck logic * rearrange logic pieces * add try except * add try except around compare * change form and improve type checking * print key instead * dang it darglint * change wording * add basic test to cover it * add a slightly more complicated test * challenge it a bit by doing small capital list * add multiple argspec * fix slider event order * i hate 3.9 * add note for UnionType * move function to types * add a test for type hint is subclass * make on submit dict str any * add testing for dict cases * add check against any * accept dict str str * bruh i used i twice * escape strings and print actual error message * disable the error and print deprecation warning instead * disable tests * fix doc message --------- Co-authored-by: Khaleel Al-Adhami --- reflex/components/component.py | 12 +- reflex/components/el/elements/forms.py | 11 +- reflex/components/el/elements/forms.pyi | 5 +- reflex/components/radix/primitives/form.pyi | 12 +- .../radix/themes/components/slider.py | 22 +-- .../radix/themes/components/slider.pyi | 28 +++- reflex/event.py | 154 ++++++++++++++++-- reflex/utils/exceptions.py | 6 +- reflex/utils/pyi_generator.py | 48 ++++-- reflex/utils/types.py | 66 ++++++++ tests/units/components/test_component.py | 49 +++++- tests/units/utils/test_utils.py | 43 ++++- 12 files changed, 387 insertions(+), 69 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 399becee9..85db3906d 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -480,6 +480,7 @@ class Component(BaseComponent, ABC): kwargs["event_triggers"][key] = self._create_event_chain( value=value, # type: ignore args_spec=component_specific_triggers[key], + key=key, ) # Remove any keys that were added as events. @@ -540,12 +541,14 @@ class Component(BaseComponent, ABC): List[Union[EventHandler, EventSpec, EventVar]], Callable, ], + key: Optional[str] = None, ) -> Union[EventChain, Var]: """Create an event chain from a variety of input types. Args: args_spec: The args_spec of the event trigger being bound. value: The value to create the event chain from. + key: The key of the event trigger being bound. Returns: The event chain. @@ -560,7 +563,7 @@ class Component(BaseComponent, ABC): elif isinstance(value, EventVar): value = [value] elif issubclass(value._var_type, (EventChain, EventSpec)): - return self._create_event_chain(args_spec, value.guess_type()) + return self._create_event_chain(args_spec, value.guess_type(), key=key) else: raise ValueError( f"Invalid event chain: {str(value)} of type {value._var_type}" @@ -579,10 +582,10 @@ class Component(BaseComponent, ABC): for v in value: if isinstance(v, (EventHandler, EventSpec)): # Call the event handler to get the event. - events.append(call_event_handler(v, args_spec)) + events.append(call_event_handler(v, args_spec, key=key)) elif isinstance(v, Callable): # Call the lambda to get the event chain. - result = call_event_fn(v, args_spec) + result = call_event_fn(v, args_spec, key=key) if isinstance(result, Var): raise ValueError( f"Invalid event chain: {v}. Cannot use a Var-returning " @@ -599,7 +602,7 @@ class Component(BaseComponent, ABC): result = call_event_fn(value, args_spec) if isinstance(result, Var): # Recursively call this function if the lambda returned an EventChain Var. - return self._create_event_chain(args_spec, result) + return self._create_event_chain(args_spec, result, key=key) events = [*result] # Otherwise, raise an error. @@ -1722,6 +1725,7 @@ class CustomComponent(Component): args_spec=event_triggers_in_component_declaration.get( key, empty_event ), + key=key, ) self.props[format.to_camel_case(key)] = value continue diff --git a/reflex/components/el/elements/forms.py b/reflex/components/el/elements/forms.py index 7cb776ee9..4caf14b41 100644 --- a/reflex/components/el/elements/forms.py +++ b/reflex/components/el/elements/forms.py @@ -111,6 +111,15 @@ def on_submit_event_spec() -> Tuple[Var[Dict[str, Any]]]: return (FORM_DATA,) +def on_submit_string_event_spec() -> Tuple[Var[Dict[str, str]]]: + """Event handler spec for the on_submit event. + + Returns: + The event handler spec. + """ + return (FORM_DATA,) + + class Form(BaseHTML): """Display the form element.""" @@ -150,7 +159,7 @@ class Form(BaseHTML): handle_submit_unique_name: Var[str] # Fired when the form is submitted - on_submit: EventHandler[on_submit_event_spec] + on_submit: EventHandler[on_submit_event_spec, on_submit_string_event_spec] @classmethod def create(cls, *children, **props): diff --git a/reflex/components/el/elements/forms.pyi b/reflex/components/el/elements/forms.pyi index bc9bc9689..a8e9b6174 100644 --- a/reflex/components/el/elements/forms.pyi +++ b/reflex/components/el/elements/forms.pyi @@ -271,6 +271,7 @@ class Fieldset(Element): ... def on_submit_event_spec() -> Tuple[Var[Dict[str, Any]]]: ... +def on_submit_string_event_spec() -> Tuple[Var[Dict[str, str]]]: ... class Form(BaseHTML): @overload @@ -337,7 +338,9 @@ class Form(BaseHTML): on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, on_scroll: Optional[EventType[[]]] = None, - on_submit: Optional[EventType[Dict[str, Any]]] = None, + on_submit: Optional[ + Union[EventType[Dict[str, Any]], EventType[Dict[str, str]]] + ] = None, on_unmount: Optional[EventType[[]]] = None, **props, ) -> "Form": diff --git a/reflex/components/radix/primitives/form.pyi b/reflex/components/radix/primitives/form.pyi index c4dce0a36..72595a933 100644 --- a/reflex/components/radix/primitives/form.pyi +++ b/reflex/components/radix/primitives/form.pyi @@ -129,7 +129,9 @@ class FormRoot(FormComponent, HTMLForm): on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, on_scroll: Optional[EventType[[]]] = None, - on_submit: Optional[EventType[Dict[str, Any]]] = None, + on_submit: Optional[ + Union[EventType[Dict[str, Any]], EventType[Dict[str, str]]] + ] = None, on_unmount: Optional[EventType[[]]] = None, **props, ) -> "FormRoot": @@ -596,7 +598,9 @@ class Form(FormRoot): on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, on_scroll: Optional[EventType[[]]] = None, - on_submit: Optional[EventType[Dict[str, Any]]] = None, + on_submit: Optional[ + Union[EventType[Dict[str, Any]], EventType[Dict[str, str]]] + ] = None, on_unmount: Optional[EventType[[]]] = None, **props, ) -> "Form": @@ -720,7 +724,9 @@ class FormNamespace(ComponentNamespace): on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, on_scroll: Optional[EventType[[]]] = None, - on_submit: Optional[EventType[Dict[str, Any]]] = None, + on_submit: Optional[ + Union[EventType[Dict[str, Any]], EventType[Dict[str, str]]] + ] = None, on_unmount: Optional[EventType[[]]] = None, **props, ) -> "Form": diff --git a/reflex/components/radix/themes/components/slider.py b/reflex/components/radix/themes/components/slider.py index bf0e5c454..bb017ea73 100644 --- a/reflex/components/radix/themes/components/slider.py +++ b/reflex/components/radix/themes/components/slider.py @@ -2,11 +2,11 @@ from __future__ import annotations -from typing import List, Literal, Optional, Tuple, Union +from typing import List, Literal, Optional, Union from reflex.components.component import Component from reflex.components.core.breakpoints import Responsive -from reflex.event import EventHandler +from reflex.event import EventHandler, identity_event from reflex.vars.base import Var from ..base import ( @@ -14,19 +14,11 @@ from ..base import ( RadixThemesComponent, ) - -def on_value_event_spec( - value: Var[List[Union[int, float]]], -) -> Tuple[Var[List[Union[int, float]]]]: - """Event handler spec for the value event. - - Args: - value: The value of the event. - - Returns: - The event handler spec. - """ - return (value,) # type: ignore +on_value_event_spec = ( + identity_event(list[Union[int, float]]), + identity_event(list[int]), + identity_event(list[float]), +) class Slider(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/slider.pyi b/reflex/components/radix/themes/components/slider.pyi index 5ac3c275f..b2f155fe6 100644 --- a/reflex/components/radix/themes/components/slider.pyi +++ b/reflex/components/radix/themes/components/slider.pyi @@ -3,18 +3,20 @@ # ------------------- DO NOT EDIT ---------------------- # This file was generated by `reflex/utils/pyi_generator.py`! # ------------------------------------------------------ -from typing import Any, Dict, List, Literal, Optional, Tuple, Union, overload +from typing import Any, Dict, List, Literal, Optional, Union, overload from reflex.components.core.breakpoints import Breakpoints -from reflex.event import EventType +from reflex.event import EventType, identity_event from reflex.style import Style from reflex.vars.base import Var from ..base import RadixThemesComponent -def on_value_event_spec( - value: Var[List[Union[int, float]]], -) -> Tuple[Var[List[Union[int, float]]]]: ... +on_value_event_spec = ( + identity_event(list[Union[int, float]]), + identity_event(list[int]), + identity_event(list[float]), +) class Slider(RadixThemesComponent): @overload @@ -138,7 +140,13 @@ class Slider(RadixThemesComponent): autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, on_blur: Optional[EventType[[]]] = None, - on_change: Optional[EventType[List[Union[int, float]]]] = None, + on_change: Optional[ + Union[ + EventType[list[Union[int, float]]], + EventType[list[int]], + EventType[list[float]], + ] + ] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, @@ -153,7 +161,13 @@ class Slider(RadixThemesComponent): on_mouse_up: Optional[EventType[[]]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, - on_value_commit: Optional[EventType[List[Union[int, float]]]] = None, + on_value_commit: Optional[ + Union[ + EventType[list[Union[int, float]]], + EventType[list[int]], + EventType[list[float]], + ] + ] = None, **props, ) -> "Slider": """Create a Slider component. diff --git a/reflex/event.py b/reflex/event.py index 14249091e..c2e6955f6 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -29,8 +29,12 @@ from typing_extensions import ParamSpec, Protocol, get_args, get_origin from reflex import constants from reflex.utils import console, format -from reflex.utils.exceptions import EventFnArgMismatch, EventHandlerArgMismatch -from reflex.utils.types import ArgsSpec, GenericType +from reflex.utils.exceptions import ( + EventFnArgMismatch, + EventHandlerArgMismatch, + EventHandlerArgTypeMismatch, +) +from reflex.utils.types import ArgsSpec, GenericType, typehint_issubclass from reflex.vars import VarData from reflex.vars.base import ( LiteralVar, @@ -401,7 +405,9 @@ class EventChain(EventActionsMixin): default_factory=list ) - args_spec: Optional[Callable] = dataclasses.field(default=None) + args_spec: Optional[Union[Callable, Sequence[Callable]]] = dataclasses.field( + default=None + ) invocation: Optional[Var] = dataclasses.field(default=None) @@ -1053,7 +1059,8 @@ def get_hydrate_event(state) -> str: def call_event_handler( event_handler: EventHandler | EventSpec, - arg_spec: ArgsSpec, + arg_spec: ArgsSpec | Sequence[ArgsSpec], + key: Optional[str] = None, ) -> EventSpec: """Call an event handler to get the event spec. @@ -1064,12 +1071,16 @@ def call_event_handler( Args: event_handler: The event handler. arg_spec: The lambda that define the argument(s) to pass to the event handler. + key: The key to pass to the event handler. Raises: EventHandlerArgMismatch: if number of arguments expected by event_handler doesn't match the spec. Returns: The event spec from calling the event handler. + + # noqa: DAR401 failure + """ parsed_args = parse_args_spec(arg_spec) # type: ignore @@ -1077,19 +1088,113 @@ def call_event_handler( # Handle partial application of EventSpec args return event_handler.add_args(*parsed_args) - args = inspect.getfullargspec(event_handler.fn).args - n_args = len(args) - 1 # subtract 1 for bound self arg - if n_args == len(parsed_args): - return event_handler(*parsed_args) # type: ignore - else: + provided_callback_fullspec = inspect.getfullargspec(event_handler.fn) + + provided_callback_n_args = ( + len(provided_callback_fullspec.args) - 1 + ) # subtract 1 for bound self arg + + if provided_callback_n_args != len(parsed_args): raise EventHandlerArgMismatch( "The number of arguments accepted by " - f"{event_handler.fn.__qualname__} ({n_args}) " + f"{event_handler.fn.__qualname__} ({provided_callback_n_args}) " "does not match the arguments passed by the event trigger: " f"{[str(v) for v in parsed_args]}\n" "See https://reflex.dev/docs/events/event-arguments/" ) + all_arg_spec = [arg_spec] if not isinstance(arg_spec, Sequence) else arg_spec + + event_spec_return_types = list( + filter( + lambda event_spec_return_type: event_spec_return_type is not None + and get_origin(event_spec_return_type) is tuple, + (get_type_hints(arg_spec).get("return", None) for arg_spec in all_arg_spec), + ) + ) + + if event_spec_return_types: + failures = [] + + for event_spec_index, event_spec_return_type in enumerate( + event_spec_return_types + ): + args = get_args(event_spec_return_type) + + args_types_without_vars = [ + arg if get_origin(arg) is not Var else get_args(arg)[0] for arg in args + ] + + try: + type_hints_of_provided_callback = get_type_hints(event_handler.fn) + except NameError: + type_hints_of_provided_callback = {} + + failed_type_check = False + + # check that args of event handler are matching the spec if type hints are provided + for i, arg in enumerate(provided_callback_fullspec.args[1:]): + if arg not in type_hints_of_provided_callback: + continue + + try: + compare_result = typehint_issubclass( + args_types_without_vars[i], type_hints_of_provided_callback[arg] + ) + except TypeError: + # TODO: In 0.7.0, remove this block and raise the exception + # raise TypeError( + # f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_handler.fn.__qualname__} provided for {key}." + # ) from e + console.warn( + f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_handler.fn.__qualname__} provided for {key}." + ) + compare_result = False + + if compare_result: + continue + else: + failure = EventHandlerArgTypeMismatch( + f"Event handler {key} expects {args_types_without_vars[i]} for argument {arg} but got {type_hints_of_provided_callback[arg]} as annotated in {event_handler.fn.__qualname__} instead." + ) + failures.append(failure) + failed_type_check = True + break + + if not failed_type_check: + if event_spec_index: + args = get_args(event_spec_return_types[0]) + + args_types_without_vars = [ + arg if get_origin(arg) is not Var else get_args(arg)[0] + for arg in args + ] + + expect_string = ", ".join( + repr(arg) for arg in args_types_without_vars + ).replace("[", "\\[") + + given_string = ", ".join( + repr(type_hints_of_provided_callback.get(arg, Any)) + for arg in provided_callback_fullspec.args[1:] + ).replace("[", "\\[") + + console.warn( + f"Event handler {key} expects ({expect_string}) -> () but got ({given_string}) -> () as annotated in {event_handler.fn.__qualname__} instead. " + f"This may lead to unexpected behavior but is intentionally ignored for {key}." + ) + return event_handler(*parsed_args) + + if failures: + console.deprecate( + "Mismatched event handler argument types", + "\n".join([str(f) for f in failures]), + "0.6.5", + "0.7.0", + ) + + return event_handler(*parsed_args) # type: ignore + def unwrap_var_annotation(annotation: GenericType): """Unwrap a Var annotation or return it as is if it's not Var[X]. @@ -1128,7 +1233,7 @@ def resolve_annotation(annotations: dict[str, Any], arg_name: str): return annotation -def parse_args_spec(arg_spec: ArgsSpec): +def parse_args_spec(arg_spec: ArgsSpec | Sequence[ArgsSpec]): """Parse the args provided in the ArgsSpec of an event trigger. Args: @@ -1137,6 +1242,8 @@ def parse_args_spec(arg_spec: ArgsSpec): Returns: The parsed args. """ + # if there's multiple, the first is the default + arg_spec = arg_spec[0] if isinstance(arg_spec, Sequence) else arg_spec spec = inspect.getfullargspec(arg_spec) annotations = get_type_hints(arg_spec) @@ -1152,13 +1259,18 @@ def parse_args_spec(arg_spec: ArgsSpec): ) -def check_fn_match_arg_spec(fn: Callable, arg_spec: ArgsSpec) -> List[Var]: +def check_fn_match_arg_spec( + fn: Callable, + arg_spec: ArgsSpec, + key: Optional[str] = None, +) -> List[Var]: """Ensures that the function signature matches the passed argument specification or raises an EventFnArgMismatch if they do not. Args: fn: The function to be validated. arg_spec: The argument specification for the event trigger. + key: The key to pass to the event handler. Returns: The parsed arguments from the argument specification. @@ -1184,7 +1296,11 @@ def check_fn_match_arg_spec(fn: Callable, arg_spec: ArgsSpec) -> List[Var]: return parsed_args -def call_event_fn(fn: Callable, arg_spec: ArgsSpec) -> list[EventSpec] | Var: +def call_event_fn( + fn: Callable, + arg_spec: ArgsSpec, + key: Optional[str] = None, +) -> list[EventSpec] | Var: """Call a function to a list of event specs. The function should return a single EventSpec, a list of EventSpecs, or a @@ -1193,6 +1309,7 @@ def call_event_fn(fn: Callable, arg_spec: ArgsSpec) -> list[EventSpec] | Var: Args: fn: The function to call. arg_spec: The argument spec for the event trigger. + key: The key to pass to the event handler. Returns: The event specs from calling the function or a Var. @@ -1205,7 +1322,7 @@ def call_event_fn(fn: Callable, arg_spec: ArgsSpec) -> list[EventSpec] | Var: from reflex.utils.exceptions import EventHandlerValueError # Check that fn signature matches arg_spec - parsed_args = check_fn_match_arg_spec(fn, arg_spec) + parsed_args = check_fn_match_arg_spec(fn, arg_spec, key=key) # Call the function with the parsed args. out = fn(*parsed_args) @@ -1223,7 +1340,7 @@ def call_event_fn(fn: Callable, arg_spec: ArgsSpec) -> list[EventSpec] | Var: for e in out: if isinstance(e, EventHandler): # An un-called EventHandler gets all of the args of the event trigger. - e = call_event_handler(e, arg_spec) + e = call_event_handler(e, arg_spec, key=key) # Make sure the event spec is valid. if not isinstance(e, EventSpec): @@ -1433,7 +1550,12 @@ class LiteralEventChainVar(ArgsFunctionOperation, LiteralVar, EventChainVar): Returns: The created LiteralEventChainVar instance. """ - sig = inspect.signature(value.args_spec) # type: ignore + arg_spec = ( + value.args_spec[0] + if isinstance(value.args_spec, Sequence) + else value.args_spec + ) + sig = inspect.signature(arg_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]) diff --git a/reflex/utils/exceptions.py b/reflex/utils/exceptions.py index 6d4a0bbc7..661f29095 100644 --- a/reflex/utils/exceptions.py +++ b/reflex/utils/exceptions.py @@ -90,7 +90,11 @@ class MatchTypeError(ReflexError, TypeError): class EventHandlerArgMismatch(ReflexError, TypeError): - """Raised when the number of args accepted by an EventHandler is differs from that provided by the event trigger.""" + """Raised when the number of args accepted by an EventHandler differs from that provided by the event trigger.""" + + +class EventHandlerArgTypeMismatch(ReflexError, TypeError): + """Raised when the annotations of args accepted by an EventHandler differs from the spec of the event trigger.""" class EventFnArgMismatch(ReflexError, TypeError): diff --git a/reflex/utils/pyi_generator.py b/reflex/utils/pyi_generator.py index 667015768..342277cad 100644 --- a/reflex/utils/pyi_generator.py +++ b/reflex/utils/pyi_generator.py @@ -490,7 +490,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="EventType") if not isinstance(annotation, str) and get_origin(annotation) is tuple: arguments = get_args(annotation) @@ -509,20 +509,13 @@ def _generate_component_create_functiondef( # Create EventType using the joined string event_type = ast.Name(id=f"EventType[{args_str}]") - # Wrap in Optional - optional_type = ast.Subscript( - value=ast.Name(id="Optional"), - slice=ast.Index(value=event_type), - ctx=ast.Load(), - ) - - return ast.Name(id=ast.unparse(optional_type)) + return event_type if isinstance(annotation, str) and annotation.startswith("Tuple["): inside_of_tuple = annotation.removeprefix("Tuple[").removesuffix("]") if inside_of_tuple == "()": - return ast.Name(id="Optional[EventType[[]]]") + return ast.Name(id="EventType[[]]") arguments = [""] @@ -548,10 +541,8 @@ def _generate_component_create_functiondef( for argument in arguments ] - return ast.Name( - id=f"Optional[EventType[{', '.join(arguments_without_var)}]]" - ) - return ast.Name(id="Optional[EventType]") + return ast.Name(id=f"EventType[{', '.join(arguments_without_var)}]") + return ast.Name(id="EventType") event_triggers = clz().get_event_triggers() @@ -560,8 +551,33 @@ def _generate_component_create_functiondef( ( ast.arg( arg=trigger, - annotation=figure_out_return_type( - inspect.signature(event_triggers[trigger]).return_annotation + annotation=ast.Subscript( + ast.Name("Optional"), + ast.Index( # type: ignore + value=ast.Name( + id=ast.unparse( + figure_out_return_type( + inspect.signature(event_specs).return_annotation + ) + if not isinstance( + event_specs := event_triggers[trigger], tuple + ) + else ast.Subscript( + ast.Name("Union"), + ast.Tuple( + [ + figure_out_return_type( + inspect.signature( + event_spec + ).return_annotation + ) + for event_spec in event_specs + ] + ), + ) + ) + ) + ), ), ), ast.Constant(value=None), diff --git a/reflex/utils/types.py b/reflex/utils/types.py index baedcc5a0..d58825ed5 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -774,3 +774,69 @@ def validate_parameter_literals(func): # Store this here for performance. StateBases = get_base_class(StateVar) StateIterBases = get_base_class(StateIterVar) + + +def typehint_issubclass(possible_subclass: Any, possible_superclass: Any) -> bool: + """Check if a type hint is a subclass of another type hint. + + Args: + possible_subclass: The type hint to check. + possible_superclass: The type hint to check against. + + Returns: + Whether the type hint is a subclass of the other type hint. + """ + if possible_superclass is Any: + return True + if possible_subclass is Any: + return False + + provided_type_origin = get_origin(possible_subclass) + accepted_type_origin = get_origin(possible_superclass) + + if provided_type_origin is None and accepted_type_origin is None: + # In this case, we are dealing with a non-generic type, so we can use issubclass + return issubclass(possible_subclass, possible_superclass) + + # Remove this check when Python 3.10 is the minimum supported version + if hasattr(types, "UnionType"): + provided_type_origin = ( + Union if provided_type_origin is types.UnionType else provided_type_origin + ) + accepted_type_origin = ( + Union if accepted_type_origin is types.UnionType else accepted_type_origin + ) + + # Get type arguments (e.g., [float, int] for Dict[float, int]) + provided_args = get_args(possible_subclass) + accepted_args = get_args(possible_superclass) + + if accepted_type_origin is Union: + if provided_type_origin is not Union: + return any( + typehint_issubclass(possible_subclass, accepted_arg) + for accepted_arg in accepted_args + ) + return all( + any( + typehint_issubclass(provided_arg, accepted_arg) + for accepted_arg in accepted_args + ) + for provided_arg in provided_args + ) + + # Check if the origin of both types is the same (e.g., list for List[int]) + # This probably should be issubclass instead of == + if (provided_type_origin or possible_subclass) != ( + accepted_type_origin or possible_superclass + ): + return False + + # Ensure all specific types are compatible with accepted types + # Note this is not necessarily correct, as it doesn't check against contravariance and covariance + # It also ignores when the length of the arguments is different + return all( + typehint_issubclass(provided_arg, accepted_arg) + for provided_arg, accepted_arg in zip(provided_args, accepted_args) + if accepted_arg is not Any + ) diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index c2d73aca5..a614fd715 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -20,13 +20,17 @@ from reflex.event import ( EventChain, EventHandler, empty_event, + identity_event, input_event, parse_args_spec, ) from reflex.state import BaseState from reflex.style import Style from reflex.utils import imports -from reflex.utils.exceptions import EventFnArgMismatch, EventHandlerArgMismatch +from reflex.utils.exceptions import ( + EventFnArgMismatch, + EventHandlerArgMismatch, +) from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict, parse_imports from reflex.vars import VarData from reflex.vars.base import LiteralVar, Var @@ -43,6 +47,18 @@ def test_state(): def do_something_arg(self, arg): pass + def do_something_with_bool(self, arg: bool): + pass + + def do_something_with_int(self, arg: int): + pass + + def do_something_with_list_int(self, arg: list[int]): + pass + + def do_something_with_list_str(self, arg: list[str]): + pass + return TestState @@ -95,8 +111,10 @@ def component2() -> Type[Component]: """ return { **super().get_event_triggers(), - "on_open": lambda e0: [e0], - "on_close": lambda e0: [e0], + "on_open": identity_event(bool), + "on_close": identity_event(bool), + "on_user_visited_count_changed": identity_event(int), + "on_user_list_changed": identity_event(List[str]), } def _get_imports(self) -> ParsedImportDict: @@ -582,7 +600,14 @@ def test_get_event_triggers(component1, component2): assert component1().get_event_triggers().keys() == default_triggers assert ( component2().get_event_triggers().keys() - == {"on_open", "on_close", "on_prop_event"} | default_triggers + == { + "on_open", + "on_close", + "on_prop_event", + "on_user_visited_count_changed", + "on_user_list_changed", + } + | default_triggers ) @@ -903,6 +928,22 @@ def test_invalid_event_handler_args(component2, test_state): on_prop_event=[test_state.do_something_arg, test_state.do_something] ) + # Enable when 0.7.0 happens + # # Event Handler types must match + # with pytest.raises(EventHandlerArgTypeMismatch): + # component2.create( + # on_user_visited_count_changed=test_state.do_something_with_bool + # ) + # with pytest.raises(EventHandlerArgTypeMismatch): + # component2.create(on_user_list_changed=test_state.do_something_with_int) + # with pytest.raises(EventHandlerArgTypeMismatch): + # component2.create(on_user_list_changed=test_state.do_something_with_list_int) + + # component2.create(on_open=test_state.do_something_with_int) + # component2.create(on_open=test_state.do_something_with_bool) + # component2.create(on_user_visited_count_changed=test_state.do_something_with_int) + # component2.create(on_user_list_changed=test_state.do_something_with_list_str) + # lambda cannot return weird values. with pytest.raises(ValueError): component2.create(on_click=lambda: 1) diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index 81579acc7..dd88138bf 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -2,7 +2,7 @@ import os import typing from functools import cached_property from pathlib import Path -from typing import Any, ClassVar, List, Literal, Type, Union +from typing import Any, ClassVar, Dict, List, Literal, Type, Union import pytest import typer @@ -77,6 +77,47 @@ def test_is_generic_alias(cls: type, expected: bool): assert types.is_generic_alias(cls) == expected +@pytest.mark.parametrize( + ("subclass", "superclass", "expected"), + [ + *[ + (base_type, base_type, True) + for base_type in [int, float, str, bool, list, dict] + ], + *[ + (one_type, another_type, False) + for one_type in [int, float, str, list, dict] + for another_type in [int, float, str, list, dict] + if one_type != another_type + ], + (bool, int, True), + (int, bool, False), + (list, List, True), + (list, List[str], True), # this is wrong, but it's a limitation of the function + (List, list, True), + (List[int], list, True), + (List[int], List, True), + (List[int], List[str], False), + (List[int], List[int], True), + (List[int], List[float], False), + (List[int], List[Union[int, float]], True), + (List[int], List[Union[float, str]], False), + (Union[int, float], List[Union[int, float]], False), + (Union[int, float], Union[int, float, str], True), + (Union[int, float], Union[str, float], False), + (Dict[str, int], Dict[str, int], True), + (Dict[str, bool], Dict[str, int], True), + (Dict[str, int], Dict[str, bool], False), + (Dict[str, Any], dict[str, str], False), + (Dict[str, str], dict[str, str], True), + (Dict[str, str], dict[str, Any], True), + (Dict[str, Any], dict[str, Any], True), + ], +) +def test_typehint_issubclass(subclass, superclass, expected): + assert types.typehint_issubclass(subclass, superclass) == expected + + def test_validate_invalid_bun_path(mocker): """Test that an error is thrown when a custom specified bun path is not valid or does not exist. From dbc9ab2d6382fbbbd7acb8ed0acce529937f2514 Mon Sep 17 00:00:00 2001 From: Elijah Ahianyo Date: Fri, 1 Nov 2024 00:14:53 +0000 Subject: [PATCH 15/27] change gallery link to Templates (#4283) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c965b00f..527cca980 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,7 @@ You can create a multi-page app by adding more pages.
-📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Component Library](https://reflex.dev/docs/library)   |   🖼️ [Gallery](https://reflex.dev/docs/gallery)   |   🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)   +📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Component Library](https://reflex.dev/docs/library)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)  
From bcd51779e629898d2bb1d899e2ae49c4e2631570 Mon Sep 17 00:00:00 2001 From: Elijah Ahianyo Date: Fri, 1 Nov 2024 10:55:02 +0000 Subject: [PATCH 16/27] [GTM-345]Define component props in class for doc discoverability (#4183) * Define component props in class for doc discoverability * add other files * fix accordion typing * add more * pyright fix * pyi fix * pyi fix fr * resinstate connection banner api * precommit fix * fix reflex-web test for select * exclude props we don't need from compiling. use regular model fields where necessary * fix counter tests * list partial fix * list fix * list fix * precommit fix * Accept suggestions * Update reflex/components/radix/primitives/accordion.py Co-authored-by: Masen Furer * address missed comment * pyright fix --------- Co-authored-by: Masen Furer --- reflex/components/datadisplay/code.py | 17 ++- reflex/components/datadisplay/code.pyi | 16 +-- .../components/radix/primitives/accordion.py | 17 ++- .../components/radix/primitives/accordion.pyi | 8 +- reflex/components/radix/themes/color_mode.py | 24 +++-- reflex/components/radix/themes/color_mode.pyi | 14 ++- .../radix/themes/components/slider.py | 6 +- .../radix/themes/components/slider.pyi | 4 +- reflex/components/radix/themes/layout/list.py | 30 +++--- .../components/radix/themes/layout/list.pyi | 100 ++++++++++++++++-- .../components/radix/themes/layout/stack.py | 12 +-- .../components/radix/themes/layout/stack.pyi | 55 +++++++--- 12 files changed, 227 insertions(+), 76 deletions(-) diff --git a/reflex/components/datadisplay/code.py b/reflex/components/datadisplay/code.py index 3b4fa39d1..53761284a 100644 --- a/reflex/components/datadisplay/code.py +++ b/reflex/components/datadisplay/code.py @@ -391,7 +391,7 @@ class CodeBlock(Component): theme: Var[Union[Theme, str]] = Theme.one_light # The language to use. - language: Var[LiteralCodeLanguage] = "python" # type: ignore + language: Var[LiteralCodeLanguage] = Var.create("python") # The code to display. code: Var[str] @@ -411,6 +411,12 @@ class CodeBlock(Component): # Props passed down to the code tag. code_tag_props: Var[Dict[str, str]] + # Whether a copy button should appear. + can_copy: Optional[bool] = False + + # A custom copy button to override the default one. + copy_button: Optional[Union[bool, Component]] = None + def add_imports(self) -> ImportDict: """Add imports for the CodeBlock component. @@ -448,16 +454,12 @@ class CodeBlock(Component): def create( cls, *children, - can_copy: Optional[bool] = False, - copy_button: Optional[Union[bool, Component]] = None, **props, ): """Create a text component. Args: *children: The children of the component. - can_copy: Whether a copy button should appears. - copy_button: A custom copy button to override the default one. **props: The props to pass to the component. Returns: @@ -465,6 +467,8 @@ class CodeBlock(Component): """ # This component handles style in a special prop. custom_style = props.pop("custom_style", {}) + can_copy = props.pop("can_copy", False) + copy_button = props.pop("copy_button", None) if "theme" not in props: # Default color scheme responds to global color mode. @@ -536,6 +540,9 @@ class CodeBlock(Component): return out + def _exclude_props(self) -> list[str]: + return ["can_copy", "copy_button"] + class CodeblockNamespace(ComponentNamespace): """Namespace for the CodeBlock component.""" diff --git a/reflex/components/datadisplay/code.pyi b/reflex/components/datadisplay/code.pyi index 0bf3dd956..1e4f7110c 100644 --- a/reflex/components/datadisplay/code.pyi +++ b/reflex/components/datadisplay/code.pyi @@ -356,8 +356,6 @@ class CodeBlock(Component): def create( # type: ignore cls, *children, - can_copy: Optional[bool] = False, - copy_button: Optional[Union[Component, bool]] = None, theme: Optional[Union[Theme, Var[Union[Theme, str]], str]] = None, language: Optional[ Union[ @@ -933,6 +931,8 @@ class CodeBlock(Component): wrap_long_lines: Optional[Union[Var[bool], bool]] = None, custom_style: Optional[Dict[str, Union[str, Var, Color]]] = None, code_tag_props: Optional[Union[Dict[str, str], Var[Dict[str, str]]]] = None, + can_copy: Optional[bool] = None, + copy_button: Optional[Union[Component, bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -960,8 +960,6 @@ class CodeBlock(Component): Args: *children: The children of the component. - can_copy: Whether a copy button should appears. - copy_button: A custom copy button to override the default one. theme: The theme to use ("light" or "dark"). language: The language to use. code: The code to display. @@ -970,6 +968,8 @@ class CodeBlock(Component): wrap_long_lines: Whether to wrap long lines. custom_style: A custom style for the code block. code_tag_props: Props passed down to the code tag. + can_copy: Whether a copy button should appear. + copy_button: A custom copy button to override the default one. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -991,8 +991,6 @@ class CodeblockNamespace(ComponentNamespace): @staticmethod def __call__( *children, - can_copy: Optional[bool] = False, - copy_button: Optional[Union[Component, bool]] = None, theme: Optional[Union[Theme, Var[Union[Theme, str]], str]] = None, language: Optional[ Union[ @@ -1568,6 +1566,8 @@ class CodeblockNamespace(ComponentNamespace): wrap_long_lines: Optional[Union[Var[bool], bool]] = None, custom_style: Optional[Dict[str, Union[str, Var, Color]]] = None, code_tag_props: Optional[Union[Dict[str, str], Var[Dict[str, str]]]] = None, + can_copy: Optional[bool] = None, + copy_button: Optional[Union[Component, bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -1595,8 +1595,6 @@ class CodeblockNamespace(ComponentNamespace): Args: *children: The children of the component. - can_copy: Whether a copy button should appears. - copy_button: A custom copy button to override the default one. theme: The theme to use ("light" or "dark"). language: The language to use. code: The code to display. @@ -1605,6 +1603,8 @@ class CodeblockNamespace(ComponentNamespace): wrap_long_lines: Whether to wrap long lines. custom_style: A custom style for the code block. code_tag_props: Props passed down to the code tag. + can_copy: Whether a copy button should appear. + copy_button: A custom copy button to override the default one. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/primitives/accordion.py b/reflex/components/radix/primitives/accordion.py index bbcecb1d8..272274723 100644 --- a/reflex/components/radix/primitives/accordion.py +++ b/reflex/components/radix/primitives/accordion.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, List, Literal, Optional, Tuple, Union +from typing import Any, List, Literal, Tuple, Union from reflex.components.component import Component, ComponentNamespace from reflex.components.core.colors import color @@ -193,6 +193,11 @@ class AccordionItem(AccordionComponent): # When true, prevents the user from interacting with the item. disabled: Var[bool] + # The header of the accordion item. + header: Var[Union[Component, str]] + # The content of the accordion item. + content: Var[Union[Component, str]] = Var.create(None) + _valid_children: List[str] = [ "AccordionHeader", "AccordionTrigger", @@ -205,21 +210,20 @@ class AccordionItem(AccordionComponent): def create( cls, *children, - header: Optional[Component | Var] = None, - content: Optional[Component | Var] = None, **props, ) -> Component: """Create an accordion item. Args: *children: The list of children to use if header and content are not provided. - header: The header of the accordion item. - content: The content of the accordion item. **props: Additional properties to apply to the accordion item. Returns: The accordion item. """ + header = props.pop("header", None) + content = props.pop("content", None) + # The item requires a value to toggle (use a random unique name if not provided). value = props.pop("value", get_uuid_string_var()) @@ -291,6 +295,9 @@ class AccordionItem(AccordionComponent): }, } + def _exclude_props(self) -> list[str]: + return ["header", "content"] + class AccordionHeader(AccordionComponent): """An accordion component.""" diff --git a/reflex/components/radix/primitives/accordion.pyi b/reflex/components/radix/primitives/accordion.pyi index abf7283c6..c0047442b 100644 --- a/reflex/components/radix/primitives/accordion.pyi +++ b/reflex/components/radix/primitives/accordion.pyi @@ -303,10 +303,10 @@ class AccordionItem(AccordionComponent): def create( # type: ignore cls, *children, - header: Optional[Union[Component, Var]] = None, - content: Optional[Union[Component, Var]] = None, value: Optional[Union[Var[str], str]] = None, disabled: Optional[Union[Var[bool], bool]] = None, + header: Optional[Union[Component, Var[Union[Component, str]], str]] = None, + content: Optional[Union[Component, Var[Union[Component, str]], str]] = None, color_scheme: Optional[ Union[ Literal[ @@ -403,10 +403,10 @@ class AccordionItem(AccordionComponent): Args: *children: The list of children to use if header and content are not provided. - header: The header of the accordion item. - content: The content of the accordion item. value: A unique identifier for the item. disabled: When true, prevents the user from interacting with the item. + header: The header of the accordion item. + content: The content of the accordion item. color_scheme: The color scheme of the component. variant: The variant of the component. as_child: Change the default rendered element for the one passed as a child. diff --git a/reflex/components/radix/themes/color_mode.py b/reflex/components/radix/themes/color_mode.py index b1083ba94..a01d40e07 100644 --- a/reflex/components/radix/themes/color_mode.py +++ b/reflex/components/radix/themes/color_mode.py @@ -17,7 +17,7 @@ rx.text( from __future__ import annotations -from typing import Dict, List, Literal, get_args +from typing import Dict, List, Literal, Optional, Union, get_args from reflex.components.component import BaseComponent from reflex.components.core.cond import Cond, color_mode_cond, cond @@ -96,26 +96,31 @@ def _set_static_default(props, position, prop, default): class ColorModeIconButton(IconButton): """Icon Button for toggling light / dark mode via toggle_color_mode.""" + # The position of the icon button. Follow document flow if None. + position: Optional[Union[LiteralPosition, Var[LiteralPosition]]] = None + + # Allow picking the "system" value for the color mode. + allow_system: bool = False + @classmethod def create( cls, - position: LiteralPosition | None = None, - allow_system: bool = False, **props, ): - """Create a icon button component that calls toggle_color_mode on click. + """Create an icon button component that calls toggle_color_mode on click. Args: - position: The position of the icon button. Follow document flow if None. - allow_system: Allow picking the "system" value for the color mode. **props: The props to pass to the component. Returns: The button component. """ + position = props.pop("position", None) + allow_system = props.pop("allow_system", False) + # position is used to set nice defaults for positioning the icon button if isinstance(position, Var): - _set_var_default(props, position, "position", "fixed", position) + _set_var_default(props, position, "position", "fixed", position) # type: ignore _set_var_default(props, position, "bottom", "2rem") _set_var_default(props, position, "top", "2rem") _set_var_default(props, position, "left", "2rem") @@ -155,12 +160,15 @@ class ColorModeIconButton(IconButton): color_mode_item("system"), ), ) - return super().create( + return IconButton.create( ColorModeIcon.create(), on_click=toggle_color_mode, **props, ) + def _exclude_props(self) -> list[str]: + return ["position", "allow_system"] + class ColorModeSwitch(Switch): """Switch for toggling light / dark mode via toggle_color_mode.""" diff --git a/reflex/components/radix/themes/color_mode.pyi b/reflex/components/radix/themes/color_mode.pyi index 76bffcdf5..a5b7b31ec 100644 --- a/reflex/components/radix/themes/color_mode.pyi +++ b/reflex/components/radix/themes/color_mode.pyi @@ -75,6 +75,18 @@ class ColorModeIconButton(IconButton): def create( # type: ignore cls, *children, + position: Optional[ + Union[ + Literal["bottom-left", "bottom-right", "top-left", "top-right"], + Union[ + Literal["bottom-left", "bottom-right", "top-left", "top-right"], + Var[ + Literal["bottom-left", "bottom-right", "top-left", "top-right"] + ], + ], + ] + ] = None, + allow_system: Optional[bool] = None, as_child: Optional[Union[Var[bool], bool]] = None, size: Optional[ Union[ @@ -226,7 +238,7 @@ class ColorModeIconButton(IconButton): on_unmount: Optional[EventType[[]]] = None, **props, ) -> "ColorModeIconButton": - """Create a icon button component that calls toggle_color_mode on click. + """Create an icon button component that calls toggle_color_mode on click. Args: position: The position of the icon button. Follow document flow if None. diff --git a/reflex/components/radix/themes/components/slider.py b/reflex/components/radix/themes/components/slider.py index bb017ea73..4f456cdca 100644 --- a/reflex/components/radix/themes/components/slider.py +++ b/reflex/components/radix/themes/components/slider.py @@ -53,6 +53,9 @@ class Slider(RadixThemesComponent): # The name of the slider. Submitted with its owning form as part of a name/value pair. name: Var[str] + # The width of the slider. + width: Var[Optional[str]] = Var.create("100%") + # The minimum value of the slider. min: Var[Union[float, int]] @@ -81,20 +84,19 @@ class Slider(RadixThemesComponent): def create( cls, *children, - width: Optional[str] = "100%", **props, ) -> Component: """Create a Slider component. Args: *children: The children of the component. - width: The width of the slider. **props: The properties of the component. Returns: The component. """ default_value = props.pop("default_value", [50]) + width = props.pop("width", "100%") if isinstance(default_value, Var): if issubclass(default_value._var_type, (int, float)): diff --git a/reflex/components/radix/themes/components/slider.pyi b/reflex/components/radix/themes/components/slider.pyi index b2f155fe6..f77573d44 100644 --- a/reflex/components/radix/themes/components/slider.pyi +++ b/reflex/components/radix/themes/components/slider.pyi @@ -24,7 +24,6 @@ class Slider(RadixThemesComponent): def create( # type: ignore cls, *children, - width: Optional[str] = "100%", as_child: Optional[Union[Var[bool], bool]] = None, size: Optional[ Union[ @@ -123,6 +122,7 @@ class Slider(RadixThemesComponent): Union[List[Union[float, int]], Var[List[Union[float, int]]]] ] = None, name: Optional[Union[Var[str], str]] = None, + width: Optional[Union[Var[Optional[str]], str]] = None, min: Optional[Union[Var[Union[float, int]], float, int]] = None, max: Optional[Union[Var[Union[float, int]], float, int]] = None, step: Optional[Union[Var[Union[float, int]], float, int]] = None, @@ -174,7 +174,6 @@ class Slider(RadixThemesComponent): Args: *children: The children of the component. - width: The width of the slider. as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. size: Button size "1" - "3" variant: Variant of button @@ -184,6 +183,7 @@ class Slider(RadixThemesComponent): default_value: The value of the slider when initially rendered. Use when you do not need to control the state of the slider. value: The controlled value of the slider. Must be used in conjunction with onValueChange. name: The name of the slider. Submitted with its owning form as part of a name/value pair. + width: The width of the slider. min: The minimum value of the slider. max: The maximum value of the slider. step: The step value of the slider. diff --git a/reflex/components/radix/themes/layout/list.py b/reflex/components/radix/themes/layout/list.py index 699028380..d83fd168b 100644 --- a/reflex/components/radix/themes/layout/list.py +++ b/reflex/components/radix/themes/layout/list.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Iterable, Literal, Optional, Union +from typing import Any, Iterable, Literal, Union from reflex.components.component import Component, ComponentNamespace from reflex.components.core.foreach import Foreach @@ -44,27 +44,30 @@ class BaseList(Component): # The style of the list. Default to "none". list_style_type: Var[ Union[LiteralListStyleTypeUnordered, LiteralListStyleTypeOrdered] - ] + ] = Var.create("none") + + # A list of items to add to the list. + items: Var[Iterable] = Var.create([]) @classmethod def create( cls, *children, - items: Optional[Var[Iterable]] = None, **props, ): """Create a list component. Args: *children: The children of the component. - items: A list of items to add to the list. **props: The properties of the component. Returns: The list component. """ + items = props.pop("items", None) list_style_type = props.pop("list_style_type", "none") + if not children and items is not None: if isinstance(items, Var): children = [Foreach.create(items, ListItem.create)] @@ -87,6 +90,9 @@ class BaseList(Component): "direction": "column", } + def _exclude_props(self) -> list[str]: + return ["items", "list_style_type"] + class UnorderedList(BaseList, Ul): """Display an unordered list.""" @@ -97,22 +103,21 @@ class UnorderedList(BaseList, Ul): def create( cls, *children, - items: Optional[Var[Iterable]] = None, - list_style_type: LiteralListStyleTypeUnordered = "disc", **props, ): - """Create a unordered list component. + """Create an unordered list component. Args: *children: The children of the component. - items: A list of items to add to the list. - list_style_type: The style of the list. **props: The properties of the component. Returns: The list component. """ + items = props.pop("items", None) + list_style_type = props.pop("list_style_type", "disc") + props["margin_left"] = props.get("margin_left", "1.5rem") return super().create( *children, items=items, list_style_type=list_style_type, **props @@ -128,22 +133,21 @@ class OrderedList(BaseList, Ol): def create( cls, *children, - items: Optional[Var[Iterable]] = None, - list_style_type: LiteralListStyleTypeOrdered = "decimal", **props, ): """Create an ordered list component. Args: *children: The children of the component. - items: A list of items to add to the list. - list_style_type: The style of the list. **props: The properties of the component. Returns: The list component. """ + items = props.pop("items", None) + list_style_type = props.pop("list_style_type", "decimal") + props["margin_left"] = props.get("margin_left", "1.5rem") return super().create( *children, items=items, list_style_type=list_style_type, **props diff --git a/reflex/components/radix/themes/layout/list.pyi b/reflex/components/radix/themes/layout/list.pyi index b72afbaa7..1ce6359a4 100644 --- a/reflex/components/radix/themes/layout/list.pyi +++ b/reflex/components/radix/themes/layout/list.pyi @@ -35,7 +35,6 @@ class BaseList(Component): def create( # type: ignore cls, *children, - items: Optional[Union[Iterable, Var[Iterable]]] = None, list_style_type: Optional[ Union[ Literal[ @@ -78,6 +77,7 @@ class BaseList(Component): ], ] ] = None, + items: Optional[Union[Iterable, Var[Iterable]]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -105,8 +105,8 @@ class BaseList(Component): Args: *children: The children of the component. - items: A list of items to add to the list. list_style_type: The style of the list. Default to "none". + items: A list of items to add to the list. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -129,8 +129,49 @@ class UnorderedList(BaseList, Ul): def create( # type: ignore cls, *children, + list_style_type: Optional[ + Union[ + Literal[ + "armenian", + "decimal", + "decimal-leading-zero", + "georgian", + "hiragana", + "katakana", + "lower-alpha", + "lower-greek", + "lower-latin", + "lower-roman", + "none", + "upper-alpha", + "upper-latin", + "upper-roman", + ], + Literal["circle", "disc", "none", "square"], + Var[ + Union[ + Literal[ + "armenian", + "decimal", + "decimal-leading-zero", + "georgian", + "hiragana", + "katakana", + "lower-alpha", + "lower-greek", + "lower-latin", + "lower-roman", + "none", + "upper-alpha", + "upper-latin", + "upper-roman", + ], + Literal["circle", "disc", "none", "square"], + ] + ], + ] + ] = None, items: Optional[Union[Iterable, Var[Iterable]]] = None, - list_style_type: Optional[LiteralListStyleTypeUnordered] = "disc", access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, auto_capitalize: Optional[ Union[Var[Union[bool, int, str]], bool, int, str] @@ -178,12 +219,12 @@ class UnorderedList(BaseList, Ul): on_unmount: Optional[EventType[[]]] = None, **props, ) -> "UnorderedList": - """Create a unordered list component. + """Create an unordered list component. Args: *children: The children of the component. + list_style_type: The style of the list. Default to "none". items: A list of items to add to the list. - list_style_type: The style of the list. access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. @@ -220,8 +261,49 @@ class OrderedList(BaseList, Ol): def create( # type: ignore cls, *children, + list_style_type: Optional[ + Union[ + Literal[ + "armenian", + "decimal", + "decimal-leading-zero", + "georgian", + "hiragana", + "katakana", + "lower-alpha", + "lower-greek", + "lower-latin", + "lower-roman", + "none", + "upper-alpha", + "upper-latin", + "upper-roman", + ], + Literal["circle", "disc", "none", "square"], + Var[ + Union[ + Literal[ + "armenian", + "decimal", + "decimal-leading-zero", + "georgian", + "hiragana", + "katakana", + "lower-alpha", + "lower-greek", + "lower-latin", + "lower-roman", + "none", + "upper-alpha", + "upper-latin", + "upper-roman", + ], + Literal["circle", "disc", "none", "square"], + ] + ], + ] + ] = None, items: Optional[Union[Iterable, Var[Iterable]]] = None, - list_style_type: Optional[LiteralListStyleTypeOrdered] = "decimal", reversed: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, start: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, type: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, @@ -276,8 +358,8 @@ class OrderedList(BaseList, Ol): Args: *children: The children of the component. + list_style_type: The style of the list. Default to "none". items: A list of items to add to the list. - list_style_type: The style of the list. reversed: Reverses the order of the list. start: Specifies the start value of the first list item in an ordered list. type: Specifies the kind of marker to use in the list (letters or numbers). @@ -406,7 +488,6 @@ class List(ComponentNamespace): @staticmethod def __call__( *children, - items: Optional[Union[Iterable, Var[Iterable]]] = None, list_style_type: Optional[ Union[ Literal[ @@ -449,6 +530,7 @@ class List(ComponentNamespace): ], ] ] = None, + items: Optional[Union[Iterable, Var[Iterable]]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -476,8 +558,8 @@ class List(ComponentNamespace): Args: *children: The children of the component. - items: A list of items to add to the list. list_style_type: The style of the list. Default to "none". + items: A list of items to add to the list. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/layout/stack.py b/reflex/components/radix/themes/layout/stack.py index 94bba4fb6..d11c3488b 100644 --- a/reflex/components/radix/themes/layout/stack.py +++ b/reflex/components/radix/themes/layout/stack.py @@ -12,20 +12,22 @@ from .flex import Flex, LiteralFlexDirection class Stack(Flex): """A stack component.""" + # The spacing between each stack item. + spacing: Var[LiteralSpacing] = Var.create("3") + + # The alignment of the stack items. + align: Var[LiteralAlign] = Var.create("start") + @classmethod def create( cls, *children, - spacing: LiteralSpacing = "3", - align: LiteralAlign = "start", **props, ) -> Component: """Create a new instance of the component. Args: *children: The children of the stack. - spacing: The spacing between each stack item. - align: The alignment of the stack items. **props: The properties of the stack. Returns: @@ -39,8 +41,6 @@ class Stack(Flex): return super().create( *children, - spacing=spacing, - align=align, **props, ) diff --git a/reflex/components/radix/themes/layout/stack.pyi b/reflex/components/radix/themes/layout/stack.pyi index 5eed3db46..c4b475218 100644 --- a/reflex/components/radix/themes/layout/stack.pyi +++ b/reflex/components/radix/themes/layout/stack.pyi @@ -10,7 +10,6 @@ from reflex.event import EventType from reflex.style import Style from reflex.vars.base import Var -from ..base import LiteralAlign, LiteralSpacing from .flex import Flex class Stack(Flex): @@ -19,8 +18,18 @@ class Stack(Flex): def create( # type: ignore cls, *children, - spacing: Optional[LiteralSpacing] = "3", - align: Optional[LiteralAlign] = "start", + spacing: Optional[ + Union[ + Literal["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], + Var[Literal["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]], + ] + ] = None, + align: Optional[ + Union[ + Literal["baseline", "center", "end", "start", "stretch"], + Var[Literal["baseline", "center", "end", "start", "stretch"]], + ] + ] = None, as_child: Optional[Union[Var[bool], bool]] = None, direction: Optional[ Union[ @@ -114,8 +123,8 @@ class Stack(Flex): Args: *children: The children of the stack. - spacing: The spacing between each stack item. - align: The alignment of the stack items. + spacing: Gap between children: "0" - "9" + align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch" as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. direction: How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse" justify: Alignment of children along the cross axis: "start" | "center" | "end" | "between" @@ -155,14 +164,24 @@ class VStack(Stack): def create( # type: ignore cls, *children, - spacing: Optional[LiteralSpacing] = "3", - align: Optional[LiteralAlign] = "start", direction: Optional[ Union[ Literal["column", "column-reverse", "row", "row-reverse"], Var[Literal["column", "column-reverse", "row", "row-reverse"]], ] ] = None, + spacing: Optional[ + Union[ + Literal["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], + Var[Literal["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]], + ] + ] = None, + align: Optional[ + Union[ + Literal["baseline", "center", "end", "start", "stretch"], + Var[Literal["baseline", "center", "end", "start", "stretch"]], + ] + ] = None, as_child: Optional[Union[Var[bool], bool]] = None, justify: Optional[ Union[ @@ -239,9 +258,9 @@ class VStack(Stack): Args: *children: The children of the stack. - spacing: The spacing between each stack item. - align: The alignment of the stack items. direction: How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse" + spacing: Gap between children: "0" - "9" + align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch" as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. justify: Alignment of children along the cross axis: "start" | "center" | "end" | "between" wrap: Whether children should wrap when they reach the end of their container: "nowrap" | "wrap" | "wrap-reverse" @@ -280,14 +299,24 @@ class HStack(Stack): def create( # type: ignore cls, *children, - spacing: Optional[LiteralSpacing] = "3", - align: Optional[LiteralAlign] = "start", direction: Optional[ Union[ Literal["column", "column-reverse", "row", "row-reverse"], Var[Literal["column", "column-reverse", "row", "row-reverse"]], ] ] = None, + spacing: Optional[ + Union[ + Literal["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], + Var[Literal["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]], + ] + ] = None, + align: Optional[ + Union[ + Literal["baseline", "center", "end", "start", "stretch"], + Var[Literal["baseline", "center", "end", "start", "stretch"]], + ] + ] = None, as_child: Optional[Union[Var[bool], bool]] = None, justify: Optional[ Union[ @@ -364,9 +393,9 @@ class HStack(Stack): Args: *children: The children of the stack. - spacing: The spacing between each stack item. - align: The alignment of the stack items. direction: How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse" + spacing: Gap between children: "0" - "9" + align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch" as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. justify: Alignment of children along the cross axis: "start" | "center" | "end" | "between" wrap: Whether children should wrap when they reach the end of their container: "nowrap" | "wrap" | "wrap-reverse" From b70f33d9721caf295473fb20a64ca29d67dbc57b Mon Sep 17 00:00:00 2001 From: Tom Gotsman <64492814+tgberkeley@users.noreply.github.com> Date: Fri, 1 Nov 2024 23:14:46 +0000 Subject: [PATCH 17/27] Update overlay props (#4261) * hover card and one prop for dialog * add missing props of drawer * fix context and dropdown menu * add popover props * fix hover card and alert dialog final * fix pyi * update drawer pyi * pyi fix again * fix from masen changes * fix pyi * fix pyi again * ruff fix --------- Co-authored-by: Tom Gotsman --- reflex/components/radix/primitives/drawer.py | 45 +++++-- reflex/components/radix/primitives/drawer.pyi | 105 ++++++++++++--- .../radix/themes/components/alert_dialog.py | 3 + .../radix/themes/components/alert_dialog.pyi | 2 + .../radix/themes/components/context_menu.py | 122 ++++++++++++++++-- .../radix/themes/components/context_menu.pyi | 109 ++++++++++++++-- .../radix/themes/components/dialog.py | 3 + .../radix/themes/components/dialog.pyi | 4 + .../radix/themes/components/dropdown_menu.py | 7 - .../radix/themes/components/dropdown_menu.pyi | 4 - .../radix/themes/components/hover_card.py | 17 ++- .../radix/themes/components/hover_card.pyi | 29 +++++ .../radix/themes/components/popover.py | 14 +- .../radix/themes/components/popover.pyi | 17 +++ reflex/experimental/layout.pyi | 14 +- 15 files changed, 422 insertions(+), 73 deletions(-) diff --git a/reflex/components/radix/primitives/drawer.py b/reflex/components/radix/primitives/drawer.py index d478dc171..dca6bb7e1 100644 --- a/reflex/components/radix/primitives/drawer.py +++ b/reflex/components/radix/primitives/drawer.py @@ -33,14 +33,29 @@ class DrawerRoot(DrawerComponent): alias = "Vaul" + tag + # The open state of the drawer when it is initially rendered. Use when you do not need to control its open state. + default_open: Var[bool] + # Whether the drawer is open or not. open: Var[bool] - # Enable background scaling, it requires an element with [vaul-drawer-wrapper] data attribute to scale its background. - should_scale_background: Var[bool] + # Fires when the drawer is opened or closed. + on_open_change: EventHandler[identity_event(bool)] - # Number between 0 and 1 that determines when the drawer should be closed. - close_threshold: Var[float] + # When `False`, it allows interaction with elements outside of the drawer without closing it. Defaults to `True`. + modal: Var[bool] + + # Direction of the drawer. This adjusts the animations and the drag direction. Defaults to `"bottom"` + direction: Var[LiteralDirectionType] + + # Gets triggered after the open or close animation ends, it receives an open argument with the open state of the drawer by the time the function was triggered. + on_animation_end: EventHandler[identity_event(bool)] + + # When `False`, dragging, clicking outside, pressing esc, etc. will not close the drawer. Use this in combination with the open prop, otherwise you won't be able to open/close the drawer. + dismissible: Var[bool] + + # When `True`, dragging will only be possible by the handle. + handle_only: Var[bool] # Array of numbers from 0 to 100 that corresponds to % of the screen a given snap point should take up. Should go from least visible. Also Accept px values, which doesn't take screen height into account. snap_points: Optional[List[Union[str, float]]] @@ -51,17 +66,14 @@ class DrawerRoot(DrawerComponent): # Duration for which the drawer is not draggable after scrolling content inside of the drawer. Defaults to 500ms scroll_lock_timeout: Var[int] - # When `False`, it allows to interact with elements outside of the drawer without closing it. Defaults to `True`. - modal: Var[bool] - - # Direction of the drawer. Defaults to `"bottom"` - direction: Var[LiteralDirectionType] - # When `True`, it prevents scroll restoration. Defaults to `True`. preventScrollRestoration: Var[bool] - # Fires when the drawer is opened or closed. - on_open_change: EventHandler[identity_event(bool)] + # Enable background scaling, it requires container element with `vaul-drawer-wrapper` attribute to scale its background. + should_scale_background: Var[bool] + + # Number between 0 and 1 that determines when the drawer should be closed. + close_threshold: Var[float] class DrawerTrigger(DrawerComponent): @@ -263,6 +275,14 @@ class DrawerDescription(DrawerComponent): return {"css": base_style} +class DrawerHandle(DrawerComponent): + """A description for the drawer.""" + + tag = "Drawer.Handle" + + alias = "Vaul" + tag + + class Drawer(ComponentNamespace): """A namespace for Drawer components.""" @@ -274,6 +294,7 @@ class Drawer(ComponentNamespace): close = staticmethod(DrawerClose.create) title = staticmethod(DrawerTitle.create) description = staticmethod(DrawerDescription.create) + handle = staticmethod(DrawerHandle.create) drawer = Drawer() diff --git a/reflex/components/radix/primitives/drawer.pyi b/reflex/components/radix/primitives/drawer.pyi index ea2dd8dcf..3f43401a4 100644 --- a/reflex/components/radix/primitives/drawer.pyi +++ b/reflex/components/radix/primitives/drawer.pyi @@ -67,12 +67,8 @@ class DrawerRoot(DrawerComponent): def create( # type: ignore cls, *children, + default_open: Optional[Union[Var[bool], bool]] = None, open: Optional[Union[Var[bool], bool]] = None, - should_scale_background: Optional[Union[Var[bool], bool]] = None, - close_threshold: Optional[Union[Var[float], float]] = None, - snap_points: Optional[List[Union[float, str]]] = None, - fade_from_index: Optional[Union[Var[int], int]] = None, - scroll_lock_timeout: Optional[Union[Var[int], int]] = None, modal: Optional[Union[Var[bool], bool]] = None, direction: Optional[ Union[ @@ -80,7 +76,14 @@ class DrawerRoot(DrawerComponent): Var[Literal["bottom", "left", "right", "top"]], ] ] = None, + dismissible: Optional[Union[Var[bool], bool]] = None, + handle_only: Optional[Union[Var[bool], bool]] = None, + snap_points: Optional[List[Union[float, str]]] = None, + fade_from_index: Optional[Union[Var[int], int]] = None, + scroll_lock_timeout: Optional[Union[Var[int], int]] = None, preventScrollRestoration: Optional[Union[Var[bool], bool]] = None, + should_scale_background: Optional[Union[Var[bool], bool]] = None, + close_threshold: Optional[Union[Var[float], float]] = None, as_child: Optional[Union[Var[bool], bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, @@ -88,6 +91,7 @@ class DrawerRoot(DrawerComponent): class_name: Optional[Any] = None, autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, + on_animation_end: Optional[EventType[bool]] = None, on_blur: Optional[EventType[[]]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, @@ -110,16 +114,20 @@ class DrawerRoot(DrawerComponent): Args: *children: The children of the component. + default_open: The open state of the drawer when it is initially rendered. Use when you do not need to control its open state. open: Whether the drawer is open or not. - should_scale_background: Enable background scaling, it requires an element with [vaul-drawer-wrapper] data attribute to scale its background. - close_threshold: Number between 0 and 1 that determines when the drawer should be closed. + on_open_change: Fires when the drawer is opened or closed. + modal: When `False`, it allows interaction with elements outside of the drawer without closing it. Defaults to `True`. + direction: Direction of the drawer. This adjusts the animations and the drag direction. Defaults to `"bottom"` + on_animation_end: Gets triggered after the open or close animation ends, it receives an open argument with the open state of the drawer by the time the function was triggered. + dismissible: When `False`, dragging, clicking outside, pressing esc, etc. will not close the drawer. Use this in combination with the open prop, otherwise you won't be able to open/close the drawer. + handle_only: When `True`, dragging will only be possible by the handle. snap_points: Array of numbers from 0 to 100 that corresponds to % of the screen a given snap point should take up. Should go from least visible. Also Accept px values, which doesn't take screen height into account. fade_from_index: Index of a snapPoint from which the overlay fade should be applied. Defaults to the last snap point. scroll_lock_timeout: Duration for which the drawer is not draggable after scrolling content inside of the drawer. Defaults to 500ms - modal: When `False`, it allows to interact with elements outside of the drawer without closing it. Defaults to `True`. - direction: Direction of the drawer. Defaults to `"bottom"` preventScrollRestoration: When `True`, it prevents scroll restoration. Defaults to `True`. - on_open_change: Fires when the drawer is opened or closed. + should_scale_background: Enable background scaling, it requires container element with `vaul-drawer-wrapper` attribute to scale its background. + close_threshold: Number between 0 and 1 that determines when the drawer should be closed. as_child: Change the default rendered element for the one passed as a child. style: The style of the component. key: A unique key for the component. @@ -479,6 +487,54 @@ class DrawerDescription(DrawerComponent): """ ... +class DrawerHandle(DrawerComponent): + @overload + @classmethod + def create( # type: ignore + cls, + *children, + as_child: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, + on_blur: Optional[EventType[[]]] = None, + on_click: Optional[EventType[[]]] = None, + on_context_menu: Optional[EventType[[]]] = None, + on_double_click: Optional[EventType[[]]] = None, + on_focus: Optional[EventType[[]]] = None, + on_mount: Optional[EventType[[]]] = None, + on_mouse_down: Optional[EventType[[]]] = None, + on_mouse_enter: Optional[EventType[[]]] = None, + on_mouse_leave: Optional[EventType[[]]] = None, + on_mouse_move: Optional[EventType[[]]] = None, + on_mouse_out: Optional[EventType[[]]] = None, + on_mouse_over: Optional[EventType[[]]] = None, + on_mouse_up: Optional[EventType[[]]] = None, + on_scroll: Optional[EventType[[]]] = None, + on_unmount: Optional[EventType[[]]] = None, + **props, + ) -> "DrawerHandle": + """Create the component. + + Args: + *children: The children of the component. + as_child: Change the default rendered element for the one passed as a child. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The props of the component. + + Returns: + The component. + """ + ... + class Drawer(ComponentNamespace): root = staticmethod(DrawerRoot.create) trigger = staticmethod(DrawerTrigger.create) @@ -488,16 +544,13 @@ class Drawer(ComponentNamespace): close = staticmethod(DrawerClose.create) title = staticmethod(DrawerTitle.create) description = staticmethod(DrawerDescription.create) + handle = staticmethod(DrawerHandle.create) @staticmethod def __call__( *children, + default_open: Optional[Union[Var[bool], bool]] = None, open: Optional[Union[Var[bool], bool]] = None, - should_scale_background: Optional[Union[Var[bool], bool]] = None, - close_threshold: Optional[Union[Var[float], float]] = None, - snap_points: Optional[List[Union[float, str]]] = None, - fade_from_index: Optional[Union[Var[int], int]] = None, - scroll_lock_timeout: Optional[Union[Var[int], int]] = None, modal: Optional[Union[Var[bool], bool]] = None, direction: Optional[ Union[ @@ -505,7 +558,14 @@ class Drawer(ComponentNamespace): Var[Literal["bottom", "left", "right", "top"]], ] ] = None, + dismissible: Optional[Union[Var[bool], bool]] = None, + handle_only: Optional[Union[Var[bool], bool]] = None, + snap_points: Optional[List[Union[float, str]]] = None, + fade_from_index: Optional[Union[Var[int], int]] = None, + scroll_lock_timeout: Optional[Union[Var[int], int]] = None, preventScrollRestoration: Optional[Union[Var[bool], bool]] = None, + should_scale_background: Optional[Union[Var[bool], bool]] = None, + close_threshold: Optional[Union[Var[float], float]] = None, as_child: Optional[Union[Var[bool], bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, @@ -513,6 +573,7 @@ class Drawer(ComponentNamespace): class_name: Optional[Any] = None, autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, + on_animation_end: Optional[EventType[bool]] = None, on_blur: Optional[EventType[[]]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, @@ -535,16 +596,20 @@ class Drawer(ComponentNamespace): Args: *children: The children of the component. + default_open: The open state of the drawer when it is initially rendered. Use when you do not need to control its open state. open: Whether the drawer is open or not. - should_scale_background: Enable background scaling, it requires an element with [vaul-drawer-wrapper] data attribute to scale its background. - close_threshold: Number between 0 and 1 that determines when the drawer should be closed. + on_open_change: Fires when the drawer is opened or closed. + modal: When `False`, it allows interaction with elements outside of the drawer without closing it. Defaults to `True`. + direction: Direction of the drawer. This adjusts the animations and the drag direction. Defaults to `"bottom"` + on_animation_end: Gets triggered after the open or close animation ends, it receives an open argument with the open state of the drawer by the time the function was triggered. + dismissible: When `False`, dragging, clicking outside, pressing esc, etc. will not close the drawer. Use this in combination with the open prop, otherwise you won't be able to open/close the drawer. + handle_only: When `True`, dragging will only be possible by the handle. snap_points: Array of numbers from 0 to 100 that corresponds to % of the screen a given snap point should take up. Should go from least visible. Also Accept px values, which doesn't take screen height into account. fade_from_index: Index of a snapPoint from which the overlay fade should be applied. Defaults to the last snap point. scroll_lock_timeout: Duration for which the drawer is not draggable after scrolling content inside of the drawer. Defaults to 500ms - modal: When `False`, it allows to interact with elements outside of the drawer without closing it. Defaults to `True`. - direction: Direction of the drawer. Defaults to `"bottom"` preventScrollRestoration: When `True`, it prevents scroll restoration. Defaults to `True`. - on_open_change: Fires when the drawer is opened or closed. + should_scale_background: Enable background scaling, it requires container element with `vaul-drawer-wrapper` attribute to scale its background. + close_threshold: Number between 0 and 1 that determines when the drawer should be closed. as_child: Change the default rendered element for the one passed as a child. style: The style of the component. key: A unique key for the component. diff --git a/reflex/components/radix/themes/components/alert_dialog.py b/reflex/components/radix/themes/components/alert_dialog.py index f3c8ec319..12ac64b90 100644 --- a/reflex/components/radix/themes/components/alert_dialog.py +++ b/reflex/components/radix/themes/components/alert_dialog.py @@ -24,6 +24,9 @@ class AlertDialogRoot(RadixThemesComponent): # Fired when the open state changes. on_open_change: EventHandler[identity_event(bool)] + # The open state of the dialog when it is initially rendered. Use when you do not need to control its open state. + default_open: Var[bool] + class AlertDialogTrigger(RadixThemesTriggerComponent): """Wraps the control that will open the dialog.""" diff --git a/reflex/components/radix/themes/components/alert_dialog.pyi b/reflex/components/radix/themes/components/alert_dialog.pyi index f4b674ce2..543497f4b 100644 --- a/reflex/components/radix/themes/components/alert_dialog.pyi +++ b/reflex/components/radix/themes/components/alert_dialog.pyi @@ -23,6 +23,7 @@ class AlertDialogRoot(RadixThemesComponent): cls, *children, open: Optional[Union[Var[bool], bool]] = None, + default_open: Optional[Union[Var[bool], bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -56,6 +57,7 @@ class AlertDialogRoot(RadixThemesComponent): *children: Child components. open: The controlled open state of the dialog. on_open_change: Fired when the open state changes. + default_open: The open state of the dialog when it is initially rendered. Use when you do not need to control its open state. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/context_menu.py b/reflex/components/radix/themes/components/context_menu.py index 3eb54a457..b3f55f8ba 100644 --- a/reflex/components/radix/themes/components/context_menu.py +++ b/reflex/components/radix/themes/components/context_menu.py @@ -1,6 +1,6 @@ """Interactive components provided by @radix-ui/themes.""" -from typing import List, Literal +from typing import Dict, List, Literal, Union from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive @@ -12,6 +12,21 @@ from ..base import ( RadixThemesComponent, ) +LiteralDirType = Literal["ltr", "rtl"] + +LiteralSizeType = Literal["1", "2"] + +LiteralVariantType = Literal["solid", "soft"] + +LiteralSideType = Literal["top", "right", "bottom", "left"] + +LiteralAlignType = Literal["start", "center", "end"] + +LiteralStickyType = Literal[ + "partial", + "always", +] + class ContextMenuRoot(RadixThemesComponent): """Menu representing a set of actions, displayed at the origin of a pointer right-click or long-press.""" @@ -26,6 +41,9 @@ class ContextMenuRoot(RadixThemesComponent): # Fired when the open state changes. on_open_change: EventHandler[identity_event(bool)] + # The reading direction of submenus when applicable. If omitted, inherits globally from DirectionProvider or assumes LTR (left-to-right) reading mode. + dir: Var[LiteralDirType] + class ContextMenuTrigger(RadixThemesComponent): """Wraps the element that will open the context menu.""" @@ -45,25 +63,52 @@ class ContextMenuContent(RadixThemesComponent): tag = "ContextMenu.Content" - # Button size "1" - "4" - size: Var[Responsive[Literal["1", "2"]]] + # Dropdown Menu Content size "1" - "2" + size: Var[Responsive[LiteralSizeType]] - # Variant of button: "solid" | "soft" | "outline" | "ghost" - variant: Var[Literal["solid", "soft"]] + # Variant of Dropdown Menu Content: "solid" | "soft" + variant: Var[LiteralVariantType] - # Override theme color for button + # Override theme color for Dropdown Menu Content color_scheme: Var[LiteralAccentColor] - # Whether to render the button with higher contrast color against background + # Renders the Dropdown Menu Content in higher contrast high_contrast: Var[bool] - # The vertical distance in pixels from the anchor. - align_offset: Var[int] + # Change the default rendered element for the one passed as a child, merging their props and behavior. Defaults to False. + as_child: Var[bool] - # When true, overrides the side and aligns preferences to prevent collisions with boundary edges. + # When True, keyboard navigation will loop from last item to first, and vice versa. Defaults to False. + loop: Var[bool] + + # Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. + force_mount: Var[bool] + + # The preferred side of the trigger to render against when open. Will be reversed when collisions occur and `avoid_collisions` is enabled.The position of the tooltip. Defaults to "top". + side: Var[LiteralSideType] + + # The distance in pixels from the trigger. Defaults to 0. + side_offset: Var[Union[float, int]] + + # The preferred alignment against the trigger. May change when collisions occur. Defaults to "center". + align: Var[LiteralAlignType] + + # An offset in pixels from the "start" or "end" alignment options. + align_offset: Var[Union[float, int]] + + # When true, overrides the side and align preferences to prevent collisions with boundary edges. Defaults to True. avoid_collisions: Var[bool] - # Fired when the context menu is closed. + # The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for all sides), or a partial padding object, for example: { "top": 20, "left": 20 }. Defaults to 0. + collision_padding: Var[Union[float, int, Dict[str, Union[float, int]]]] + + # The sticky behavior on the align axis. "partial" will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless. Defaults to "partial". + sticky: Var[LiteralStickyType] + + # Whether to hide the content when the trigger becomes fully occluded. Defaults to False. + hide_when_detached: Var[bool] + + # Fired when focus moves back after closing. on_close_auto_focus: EventHandler[empty_event] # Fired when the escape key is pressed. @@ -75,7 +120,7 @@ class ContextMenuContent(RadixThemesComponent): # Fired when focus moves outside the context menu. on_focus_outside: EventHandler[empty_event] - # Fired when interacting outside the context menu. + # Fired when the pointer interacts outside the context menu. on_interact_outside: EventHandler[empty_event] @@ -84,15 +129,30 @@ class ContextMenuSub(RadixThemesComponent): tag = "ContextMenu.Sub" + # The controlled open state of the submenu. Must be used in conjunction with `on_open_change`. + open: Var[bool] + + # The open state of the submenu when it is initially rendered. Use when you do not need to control its open state. + default_open: Var[bool] + + # Fired when the open state changes. + on_open_change: EventHandler[identity_event(bool)] + class ContextMenuSubTrigger(RadixThemesComponent): """An item that opens a submenu.""" tag = "ContextMenu.SubTrigger" + # Change the default rendered element for the one passed as a child, merging their props and behavior. Defaults to False. + as_child: Var[bool] + # Whether the trigger is disabled disabled: Var[bool] + # Optional text used for typeahead purposes. By default the typeahead behavior will use the .textContent of the item. Use this when the content is complex, or you have non-textual content inside. + text_value: Var[str] + _valid_parents: List[str] = ["ContextMenuContent", "ContextMenuSub"] @@ -101,9 +161,33 @@ class ContextMenuSubContent(RadixThemesComponent): tag = "ContextMenu.SubContent" - # When true, keyboard navigation will loop from last item to first, and vice versa. + # Change the default rendered element for the one passed as a child, merging their props and behavior. Defaults to False. + as_child: Var[bool] + + # When True, keyboard navigation will loop from last item to first, and vice versa. Defaults to False. loop: Var[bool] + # Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. + force_mount: Var[bool] + + # The distance in pixels from the trigger. Defaults to 0. + side_offset: Var[Union[float, int]] + + # An offset in pixels from the "start" or "end" alignment options. + align_offset: Var[Union[float, int]] + + # When true, overrides the side and align preferences to prevent collisions with boundary edges. Defaults to True. + avoid_collisions: Var[bool] + + # The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for all sides), or a partial padding object, for example: { "top": 20, "left": 20 }. Defaults to 0. + collision_padding: Var[Union[float, int, Dict[str, Union[float, int]]]] + + # The sticky behavior on the align axis. "partial" will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless. Defaults to "partial". + sticky: Var[LiteralStickyType] + + # Whether to hide the content when the trigger becomes fully occluded. Defaults to False. + hide_when_detached: Var[bool] + _valid_parents: List[str] = ["ContextMenuSub"] # Fired when the escape key is pressed. @@ -130,8 +214,20 @@ class ContextMenuItem(RadixThemesComponent): # Shortcut to render a menu item as a link shortcut: Var[str] + # Change the default rendered element for the one passed as a child, merging their props and behavior. Defaults to False. + as_child: Var[bool] + + # When true, prevents the user from interacting with the item. + disabled: Var[bool] + + # Optional text used for typeahead purposes. By default the typeahead behavior will use the content of the item. Use this when the content is complex, or you have non-textual content inside. + text_value: Var[str] + _valid_parents: List[str] = ["ContextMenuContent", "ContextMenuSubContent"] + # Fired when the item is selected. + on_select: EventHandler[empty_event] + class ContextMenuSeparator(RadixThemesComponent): """Separates items in a context menu.""" diff --git a/reflex/components/radix/themes/components/context_menu.pyi b/reflex/components/radix/themes/components/context_menu.pyi index bfb88b303..f7eb4682a 100644 --- a/reflex/components/radix/themes/components/context_menu.pyi +++ b/reflex/components/radix/themes/components/context_menu.pyi @@ -13,6 +13,13 @@ from reflex.vars.base import Var from ..base import RadixThemesComponent +LiteralDirType = Literal["ltr", "rtl"] +LiteralSizeType = Literal["1", "2"] +LiteralVariantType = Literal["solid", "soft"] +LiteralSideType = Literal["top", "right", "bottom", "left"] +LiteralAlignType = Literal["start", "center", "end"] +LiteralStickyType = Literal["partial", "always"] + class ContextMenuRoot(RadixThemesComponent): @overload @classmethod @@ -20,6 +27,7 @@ class ContextMenuRoot(RadixThemesComponent): cls, *children, modal: Optional[Union[Var[bool], bool]] = None, + dir: Optional[Union[Literal["ltr", "rtl"], Var[Literal["ltr", "rtl"]]]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -53,6 +61,7 @@ class ContextMenuRoot(RadixThemesComponent): *children: Child components. modal: The modality of the context menu. When set to true, interaction with outside elements will be disabled and only menu content will be visible to screen readers. on_open_change: Fired when the open state changes. + dir: The reading direction of submenus when applicable. If omitted, inherits globally from DirectionProvider or assumes LTR (left-to-right) reading mode. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -196,8 +205,36 @@ class ContextMenuContent(RadixThemesComponent): ] ] = None, high_contrast: Optional[Union[Var[bool], bool]] = None, - align_offset: Optional[Union[Var[int], int]] = None, + as_child: Optional[Union[Var[bool], bool]] = None, + loop: Optional[Union[Var[bool], bool]] = None, + force_mount: Optional[Union[Var[bool], bool]] = None, + side: Optional[ + Union[ + Literal["bottom", "left", "right", "top"], + Var[Literal["bottom", "left", "right", "top"]], + ] + ] = None, + side_offset: Optional[Union[Var[Union[float, int]], float, int]] = None, + align: Optional[ + Union[ + Literal["center", "end", "start"], + Var[Literal["center", "end", "start"]], + ] + ] = None, + align_offset: Optional[Union[Var[Union[float, int]], float, int]] = None, avoid_collisions: Optional[Union[Var[bool], bool]] = None, + collision_padding: Optional[ + Union[ + Dict[str, Union[float, int]], + Var[Union[Dict[str, Union[float, int]], float, int]], + float, + int, + ] + ] = None, + sticky: Optional[ + Union[Literal["always", "partial"], Var[Literal["always", "partial"]]] + ] = None, + hide_when_detached: Optional[Union[Var[bool], bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -233,17 +270,26 @@ class ContextMenuContent(RadixThemesComponent): Args: *children: Child components. - size: Button size "1" - "4" - variant: Variant of button: "solid" | "soft" | "outline" | "ghost" - color_scheme: Override theme color for button - high_contrast: Whether to render the button with higher contrast color against background - align_offset: The vertical distance in pixels from the anchor. - avoid_collisions: When true, overrides the side and aligns preferences to prevent collisions with boundary edges. - on_close_auto_focus: Fired when the context menu is closed. + size: Dropdown Menu Content size "1" - "2" + variant: Variant of Dropdown Menu Content: "solid" | "soft" + color_scheme: Override theme color for Dropdown Menu Content + high_contrast: Renders the Dropdown Menu Content in higher contrast + as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. Defaults to False. + loop: When True, keyboard navigation will loop from last item to first, and vice versa. Defaults to False. + force_mount: Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. + side: The preferred side of the trigger to render against when open. Will be reversed when collisions occur and `avoid_collisions` is enabled.The position of the tooltip. Defaults to "top". + side_offset: The distance in pixels from the trigger. Defaults to 0. + align: The preferred alignment against the trigger. May change when collisions occur. Defaults to "center". + align_offset: An offset in pixels from the "start" or "end" alignment options. + avoid_collisions: When true, overrides the side and align preferences to prevent collisions with boundary edges. Defaults to True. + collision_padding: The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for all sides), or a partial padding object, for example: { "top": 20, "left": 20 }. Defaults to 0. + sticky: The sticky behavior on the align axis. "partial" will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless. Defaults to "partial". + hide_when_detached: Whether to hide the content when the trigger becomes fully occluded. Defaults to False. + on_close_auto_focus: Fired when focus moves back after closing. on_escape_key_down: Fired when the escape key is pressed. on_pointer_down_outside: Fired when a pointer down event happens outside the context menu. on_focus_outside: Fired when focus moves outside the context menu. - on_interact_outside: Fired when interacting outside the context menu. + on_interact_outside: Fired when the pointer interacts outside the context menu. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -263,6 +309,8 @@ class ContextMenuSub(RadixThemesComponent): def create( # type: ignore cls, *children, + open: Optional[Union[Var[bool], bool]] = None, + default_open: Optional[Union[Var[bool], bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -282,6 +330,7 @@ class ContextMenuSub(RadixThemesComponent): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, + on_open_change: Optional[EventType[bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, @@ -293,6 +342,9 @@ class ContextMenuSub(RadixThemesComponent): Args: *children: Child components. + open: The controlled open state of the submenu. Must be used in conjunction with `on_open_change`. + default_open: The open state of the submenu when it is initially rendered. Use when you do not need to control its open state. + on_open_change: Fired when the open state changes. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -312,7 +364,9 @@ class ContextMenuSubTrigger(RadixThemesComponent): def create( # type: ignore cls, *children, + as_child: Optional[Union[Var[bool], bool]] = None, disabled: Optional[Union[Var[bool], bool]] = None, + text_value: Optional[Union[Var[str], str]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -343,7 +397,9 @@ class ContextMenuSubTrigger(RadixThemesComponent): Args: *children: Child components. + as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. Defaults to False. disabled: Whether the trigger is disabled + text_value: Optional text used for typeahead purposes. By default the typeahead behavior will use the .textContent of the item. Use this when the content is complex, or you have non-textual content inside. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -363,7 +419,24 @@ class ContextMenuSubContent(RadixThemesComponent): def create( # type: ignore cls, *children, + as_child: Optional[Union[Var[bool], bool]] = None, loop: Optional[Union[Var[bool], bool]] = None, + force_mount: Optional[Union[Var[bool], bool]] = None, + side_offset: Optional[Union[Var[Union[float, int]], float, int]] = None, + align_offset: Optional[Union[Var[Union[float, int]], float, int]] = None, + avoid_collisions: Optional[Union[Var[bool], bool]] = None, + collision_padding: Optional[ + Union[ + Dict[str, Union[float, int]], + Var[Union[Dict[str, Union[float, int]], float, int]], + float, + int, + ] + ] = None, + sticky: Optional[ + Union[Literal["always", "partial"], Var[Literal["always", "partial"]]] + ] = None, + hide_when_detached: Optional[Union[Var[bool], bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -398,7 +471,15 @@ class ContextMenuSubContent(RadixThemesComponent): Args: *children: Child components. - loop: When true, keyboard navigation will loop from last item to first, and vice versa. + as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. Defaults to False. + loop: When True, keyboard navigation will loop from last item to first, and vice versa. Defaults to False. + force_mount: Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. + side_offset: The distance in pixels from the trigger. Defaults to 0. + align_offset: An offset in pixels from the "start" or "end" alignment options. + avoid_collisions: When true, overrides the side and align preferences to prevent collisions with boundary edges. Defaults to True. + collision_padding: The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for all sides), or a partial padding object, for example: { "top": 20, "left": 20 }. Defaults to 0. + sticky: The sticky behavior on the align axis. "partial" will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless. Defaults to "partial". + hide_when_detached: Whether to hide the content when the trigger becomes fully occluded. Defaults to False. on_escape_key_down: Fired when the escape key is pressed. on_pointer_down_outside: Fired when a pointer down event happens outside the context menu. on_focus_outside: Fired when focus moves outside the context menu. @@ -485,6 +566,9 @@ class ContextMenuItem(RadixThemesComponent): ] ] = None, shortcut: Optional[Union[Var[str], str]] = None, + as_child: Optional[Union[Var[bool], bool]] = None, + disabled: Optional[Union[Var[bool], bool]] = None, + text_value: Optional[Union[Var[str], str]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -505,6 +589,7 @@ class ContextMenuItem(RadixThemesComponent): on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, on_scroll: Optional[EventType[[]]] = None, + on_select: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, ) -> "ContextMenuItem": @@ -517,6 +602,10 @@ class ContextMenuItem(RadixThemesComponent): *children: Child components. color_scheme: Override theme color for button shortcut: Shortcut to render a menu item as a link + as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. Defaults to False. + disabled: When true, prevents the user from interacting with the item. + text_value: Optional text used for typeahead purposes. By default the typeahead behavior will use the content of the item. Use this when the content is complex, or you have non-textual content inside. + on_select: Fired when the item is selected. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/dialog.py b/reflex/components/radix/themes/components/dialog.py index e8da506ed..840a50ecb 100644 --- a/reflex/components/radix/themes/components/dialog.py +++ b/reflex/components/radix/themes/components/dialog.py @@ -25,6 +25,9 @@ class DialogRoot(RadixThemesComponent): # Fired when the open state changes. on_open_change: EventHandler[identity_event(bool)] + # The open state of the dialog when it is initially rendered. Use when you do not need to control its open state. + default_open: Var[bool] + class DialogTrigger(RadixThemesTriggerComponent): """Trigger an action or event, to open a Dialog modal.""" diff --git a/reflex/components/radix/themes/components/dialog.pyi b/reflex/components/radix/themes/components/dialog.pyi index a277fc775..a6d8e24f7 100644 --- a/reflex/components/radix/themes/components/dialog.pyi +++ b/reflex/components/radix/themes/components/dialog.pyi @@ -21,6 +21,7 @@ class DialogRoot(RadixThemesComponent): cls, *children, open: Optional[Union[Var[bool], bool]] = None, + default_open: Optional[Union[Var[bool], bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -54,6 +55,7 @@ class DialogRoot(RadixThemesComponent): *children: Child components. open: The controlled open state of the dialog. on_open_change: Fired when the open state changes. + default_open: The open state of the dialog when it is initially rendered. Use when you do not need to control its open state. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -369,6 +371,7 @@ class Dialog(ComponentNamespace): def __call__( *children, open: Optional[Union[Var[bool], bool]] = None, + default_open: Optional[Union[Var[bool], bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -402,6 +405,7 @@ class Dialog(ComponentNamespace): *children: Child components. open: The controlled open state of the dialog. on_open_change: Fired when the open state changes. + default_open: The open state of the dialog when it is initially rendered. Use when you do not need to control its open state. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/dropdown_menu.py b/reflex/components/radix/themes/components/dropdown_menu.py index 885e8df35..ee9040501 100644 --- a/reflex/components/radix/themes/components/dropdown_menu.py +++ b/reflex/components/radix/themes/components/dropdown_menu.py @@ -23,7 +23,6 @@ LiteralSideType = Literal["top", "right", "bottom", "left"] LiteralAlignType = Literal["start", "center", "end"] - LiteralStickyType = Literal[ "partial", "always", @@ -110,9 +109,6 @@ class DropdownMenuContent(RadixThemesComponent): # The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for all sides), or a partial padding object, for example: { "top": 20, "left": 20 }. Defaults to 0. collision_padding: Var[Union[float, int, Dict[str, Union[float, int]]]] - # The padding between the arrow and the edges of the content. If your content has border-radius, this will prevent it from overflowing the corners. Defaults to 0. - arrow_padding: Var[Union[float, int]] - # The sticky behavior on the align axis. "partial" will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless. Defaults to "partial". sticky: Var[LiteralStickyType] @@ -193,9 +189,6 @@ class DropdownMenuSubContent(RadixThemesComponent): # The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for all sides), or a partial padding object, for example: { "top": 20, "left": 20 }. Defaults to 0. collision_padding: Var[Union[float, int, Dict[str, Union[float, int]]]] - # The padding between the arrow and the edges of the content. If your content has border-radius, this will prevent it from overflowing the corners. Defaults to 0. - arrow_padding: Var[Union[float, int]] - # The sticky behavior on the align axis. "partial" will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless. Defaults to "partial". sticky: Var[LiteralStickyType] diff --git a/reflex/components/radix/themes/components/dropdown_menu.pyi b/reflex/components/radix/themes/components/dropdown_menu.pyi index ccb2ef347..980a33bb5 100644 --- a/reflex/components/radix/themes/components/dropdown_menu.pyi +++ b/reflex/components/radix/themes/components/dropdown_menu.pyi @@ -225,7 +225,6 @@ class DropdownMenuContent(RadixThemesComponent): int, ] ] = None, - arrow_padding: Optional[Union[Var[Union[float, int]], float, int]] = None, sticky: Optional[ Union[Literal["always", "partial"], Var[Literal["always", "partial"]]] ] = None, @@ -278,7 +277,6 @@ class DropdownMenuContent(RadixThemesComponent): align_offset: An offset in pixels from the "start" or "end" alignment options. avoid_collisions: When true, overrides the side and align preferences to prevent collisions with boundary edges. Defaults to True. collision_padding: The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for all sides), or a partial padding object, for example: { "top": 20, "left": 20 }. Defaults to 0. - arrow_padding: The padding between the arrow and the edges of the content. If your content has border-radius, this will prevent it from overflowing the corners. Defaults to 0. sticky: The sticky behavior on the align axis. "partial" will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless. Defaults to "partial". hide_when_detached: Whether to hide the content when the trigger becomes fully occluded. Defaults to False. on_close_auto_focus: Fired when the dialog is closed. @@ -417,7 +415,6 @@ class DropdownMenuSubContent(RadixThemesComponent): int, ] ] = None, - arrow_padding: Optional[Union[Var[Union[float, int]], float, int]] = None, sticky: Optional[ Union[Literal["always", "partial"], Var[Literal["always", "partial"]]] ] = None, @@ -463,7 +460,6 @@ class DropdownMenuSubContent(RadixThemesComponent): align_offset: An offset in pixels from the "start" or "end" alignment options. avoid_collisions: When true, overrides the side and align preferences to prevent collisions with boundary edges. Defaults to True. collision_padding: The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for all sides), or a partial padding object, for example: { "top": 20, "left": 20 }. Defaults to 0. - arrow_padding: The padding between the arrow and the edges of the content. If your content has border-radius, this will prevent it from overflowing the corners. Defaults to 0. sticky: The sticky behavior on the align axis. "partial" will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless. Defaults to "partial". hide_when_detached: Whether to hide the content when the trigger becomes fully occluded. Defaults to False. on_escape_key_down: Fired when the escape key is pressed. diff --git a/reflex/components/radix/themes/components/hover_card.py b/reflex/components/radix/themes/components/hover_card.py index e76184795..10002c7e6 100644 --- a/reflex/components/radix/themes/components/hover_card.py +++ b/reflex/components/radix/themes/components/hover_card.py @@ -1,6 +1,6 @@ """Interactive components provided by @radix-ui/themes.""" -from typing import Literal +from typing import Dict, Literal, Union from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive @@ -55,9 +55,24 @@ class HoverCardContent(elements.Div, RadixThemesComponent): # The preferred alignment against the trigger. May change when collisions occur. align: Var[Literal["start", "center", "end"]] + # An offset in pixels from the "start" or "end" alignment options. + align_offset: Var[int] + # Whether or not the hover card should avoid collisions with its trigger. avoid_collisions: Var[bool] + # The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for all sides), or a partial padding object, for example: { top: 20, left: 20 }. + collision_padding: Var[Union[float, int, Dict[str, Union[float, int]]]] + + # The sticky behavior on the align axis. "partial" will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless + sticky: Var[Literal["partial", "always"]] + + # Whether to hide the content when the trigger becomes fully occluded. + hide_when_detached: Var[bool] + + # Hovercard size "1" - "3" + size: Var[Responsive[Literal["1", "2", "3"]]] + class HoverCard(ComponentNamespace): """For sighted users to preview content available behind a link.""" diff --git a/reflex/components/radix/themes/components/hover_card.pyi b/reflex/components/radix/themes/components/hover_card.pyi index 50c646971..e8b5b43ee 100644 --- a/reflex/components/radix/themes/components/hover_card.pyi +++ b/reflex/components/radix/themes/components/hover_card.pyi @@ -138,7 +138,31 @@ class HoverCardContent(elements.Div, RadixThemesComponent): Var[Literal["center", "end", "start"]], ] ] = None, + align_offset: Optional[Union[Var[int], int]] = None, avoid_collisions: Optional[Union[Var[bool], bool]] = None, + collision_padding: Optional[ + Union[ + Dict[str, Union[float, int]], + Var[Union[Dict[str, Union[float, int]], float, int]], + float, + int, + ] + ] = None, + sticky: Optional[ + Union[Literal["always", "partial"], Var[Literal["always", "partial"]]] + ] = None, + hide_when_detached: Optional[Union[Var[bool], bool]] = None, + size: Optional[ + Union[ + Breakpoints[str, Literal["1", "2", "3"]], + Literal["1", "2", "3"], + Var[ + Union[ + Breakpoints[str, Literal["1", "2", "3"]], Literal["1", "2", "3"] + ] + ], + ] + ] = None, access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, auto_capitalize: Optional[ Union[Var[Union[bool, int, str]], bool, int, str] @@ -196,7 +220,12 @@ class HoverCardContent(elements.Div, RadixThemesComponent): side: The preferred side of the trigger to render against when open. Will be reversed when collisions occur and avoidCollisions is enabled. side_offset: The distance in pixels from the trigger. align: The preferred alignment against the trigger. May change when collisions occur. + align_offset: An offset in pixels from the "start" or "end" alignment options. avoid_collisions: Whether or not the hover card should avoid collisions with its trigger. + collision_padding: The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for all sides), or a partial padding object, for example: { top: 20, left: 20 }. + sticky: The sticky behavior on the align axis. "partial" will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless + hide_when_detached: Whether to hide the content when the trigger becomes fully occluded. + size: Hovercard size "1" - "3" access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. diff --git a/reflex/components/radix/themes/components/popover.py b/reflex/components/radix/themes/components/popover.py index 2535a8a22..6997ec5c5 100644 --- a/reflex/components/radix/themes/components/popover.py +++ b/reflex/components/radix/themes/components/popover.py @@ -1,6 +1,6 @@ """Interactive components provided by @radix-ui/themes.""" -from typing import Literal +from typing import Dict, Literal, Union from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive @@ -28,6 +28,9 @@ class PopoverRoot(RadixThemesComponent): # Fired when the open state changes. on_open_change: EventHandler[identity_event(bool)] + # The open state of the popover when it is initially rendered. Use when you do not need to control its open state. + default_open: Var[bool] + class PopoverTrigger(RadixThemesTriggerComponent): """Wraps the control that will open the popover.""" @@ -58,6 +61,15 @@ class PopoverContent(elements.Div, RadixThemesComponent): # When true, overrides the side andalign preferences to prevent collisions with boundary edges. avoid_collisions: Var[bool] + # The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for all sides), or a partial padding object, for example: { "top": 20, "left": 20 }. Defaults to 0. + collision_padding: Var[Union[float, int, Dict[str, Union[float, int]]]] + + # The sticky behavior on the align axis. "partial" will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless. Defaults to "partial". + sticky: Var[Literal["partial", "always"]] + + # Whether to hide the content when the trigger becomes fully occluded. Defaults to False. + hide_when_detached: Var[bool] + # Fired when the dialog is opened. on_open_auto_focus: EventHandler[empty_event] diff --git a/reflex/components/radix/themes/components/popover.pyi b/reflex/components/radix/themes/components/popover.pyi index 35f6854f6..dfe7a3a68 100644 --- a/reflex/components/radix/themes/components/popover.pyi +++ b/reflex/components/radix/themes/components/popover.pyi @@ -22,6 +22,7 @@ class PopoverRoot(RadixThemesComponent): *children, open: Optional[Union[Var[bool], bool]] = None, modal: Optional[Union[Var[bool], bool]] = None, + default_open: Optional[Union[Var[bool], bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -56,6 +57,7 @@ class PopoverRoot(RadixThemesComponent): open: The controlled open state of the popover. modal: The modality of the popover. When set to true, interaction with outside elements will be disabled and only popover content will be visible to screen readers. on_open_change: Fired when the open state changes. + default_open: The open state of the popover when it is initially rendered. Use when you do not need to control its open state. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -142,6 +144,18 @@ class PopoverContent(elements.Div, RadixThemesComponent): ] = None, align_offset: Optional[Union[Var[int], int]] = None, avoid_collisions: Optional[Union[Var[bool], bool]] = None, + collision_padding: Optional[ + Union[ + Dict[str, Union[float, int]], + Var[Union[Dict[str, Union[float, int]], float, int]], + float, + int, + ] + ] = None, + sticky: Optional[ + Union[Literal["always", "partial"], Var[Literal["always", "partial"]]] + ] = None, + hide_when_detached: Optional[Union[Var[bool], bool]] = None, access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, auto_capitalize: Optional[ Union[Var[Union[bool, int, str]], bool, int, str] @@ -208,6 +222,9 @@ class PopoverContent(elements.Div, RadixThemesComponent): align: The preferred alignment against the anchor. May change when collisions occur. align_offset: The vertical distance in pixels from the anchor. avoid_collisions: When true, overrides the side andalign preferences to prevent collisions with boundary edges. + collision_padding: The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for all sides), or a partial padding object, for example: { "top": 20, "left": 20 }. Defaults to 0. + sticky: The sticky behavior on the align axis. "partial" will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless. Defaults to "partial". + hide_when_detached: Whether to hide the content when the trigger becomes fully occluded. Defaults to False. on_open_auto_focus: Fired when the dialog is opened. on_close_auto_focus: Fired when the dialog is closed. on_escape_key_down: Fired when the escape key is pressed. diff --git a/reflex/experimental/layout.pyi b/reflex/experimental/layout.pyi index dcdac5b5d..f0e94a62d 100644 --- a/reflex/experimental/layout.pyi +++ b/reflex/experimental/layout.pyi @@ -95,12 +95,8 @@ class DrawerSidebar(DrawerRoot): def create( # type: ignore cls, *children, + default_open: Optional[Union[Var[bool], bool]] = None, open: Optional[Union[Var[bool], bool]] = None, - should_scale_background: Optional[Union[Var[bool], bool]] = None, - close_threshold: Optional[Union[Var[float], float]] = None, - snap_points: Optional[List[Union[float, str]]] = None, - fade_from_index: Optional[Union[Var[int], int]] = None, - scroll_lock_timeout: Optional[Union[Var[int], int]] = None, modal: Optional[Union[Var[bool], bool]] = None, direction: Optional[ Union[ @@ -108,7 +104,14 @@ class DrawerSidebar(DrawerRoot): Var[Literal["bottom", "left", "right", "top"]], ] ] = None, + dismissible: Optional[Union[Var[bool], bool]] = None, + handle_only: Optional[Union[Var[bool], bool]] = None, + snap_points: Optional[List[Union[float, str]]] = None, + fade_from_index: Optional[Union[Var[int], int]] = None, + scroll_lock_timeout: Optional[Union[Var[int], int]] = None, preventScrollRestoration: Optional[Union[Var[bool], bool]] = None, + should_scale_background: Optional[Union[Var[bool], bool]] = None, + close_threshold: Optional[Union[Var[float], float]] = None, as_child: Optional[Union[Var[bool], bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, @@ -116,6 +119,7 @@ class DrawerSidebar(DrawerRoot): class_name: Optional[Any] = None, autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, + on_animation_end: Optional[EventType[bool]] = None, on_blur: Optional[EventType[[]]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, From 0d9fc53a7df8720790cc5db83476b40eaa0d7e04 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 4 Nov 2024 10:11:04 -0800 Subject: [PATCH 18/27] [REF-3961] move "warn_if_too_large" logic into BaseState (#4284) Check for too large serialized state whenever `BaseState._serialize` is used, so it can apply to all state managers, not just `StateManagerRedis`. --- reflex/state.py | 54 ++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index cc9dda05b..ee0aa3e93 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -104,6 +104,8 @@ var = computed_var # If the state is this large, it's considered a performance issue. TOO_LARGE_SERIALIZED_STATE = 100 * 1024 # 100kb +# Only warn about each state class size once. +_WARNED_ABOUT_STATE_SIZE: Set[str] = set() # Errors caught during pickling of state HANDLED_PICKLE_ERRORS = ( @@ -2046,6 +2048,27 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): state["__dict__"].pop(inherited_var_name, None) return state + def _warn_if_too_large( + self, + pickle_state_size: int, + ): + """Print a warning when the state is too large. + + Args: + pickle_state_size: The size of the pickled state. + """ + state_full_name = self.get_full_name() + if ( + state_full_name not in _WARNED_ABOUT_STATE_SIZE + and pickle_state_size > TOO_LARGE_SERIALIZED_STATE + and self.substates + ): + console.warn( + f"State {state_full_name} serializes to {pickle_state_size} bytes " + "which may present performance issues. Consider reducing the size of this state." + ) + _WARNED_ABOUT_STATE_SIZE.add(state_full_name) + @classmethod @functools.lru_cache() def _to_schema(cls) -> str: @@ -2084,7 +2107,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): The serialized state. """ try: - return pickle.dumps((self._to_schema(), self)) + pickle_state = pickle.dumps((self._to_schema(), self)) + self._warn_if_too_large(len(pickle_state)) + return pickle_state except HANDLED_PICKLE_ERRORS as og_pickle_error: error = ( f"Failed to serialize state {self.get_full_name()} due to unpicklable object. " @@ -3075,9 +3100,6 @@ class StateManagerRedis(StateManager): b"evicted", } - # Only warn about each state class size once. - _warned_about_state_size: ClassVar[Set[str]] = set() - async def _get_parent_state( self, token: str, state: BaseState | None = None ) -> BaseState | None: @@ -3221,29 +3243,6 @@ class StateManagerRedis(StateManager): return state._get_root_state() return state - def _warn_if_too_large( - self, - state: BaseState, - pickle_state_size: int, - ): - """Print a warning when the state is too large. - - Args: - state: The state to check. - pickle_state_size: The size of the pickled state. - """ - state_full_name = state.get_full_name() - if ( - state_full_name not in self._warned_about_state_size - and pickle_state_size > TOO_LARGE_SERIALIZED_STATE - and state.substates - ): - console.warn( - f"State {state_full_name} serializes to {pickle_state_size} bytes " - "which may present performance issues. Consider reducing the size of this state." - ) - self._warned_about_state_size.add(state_full_name) - @override async def set_state( self, @@ -3294,7 +3293,6 @@ class StateManagerRedis(StateManager): # Persist only the given state (parents or substates are excluded by BaseState.__getstate__). if state._get_was_touched(): pickle_state = state._serialize() - self._warn_if_too_large(state, len(pickle_state)) if pickle_state: await self.redis.set( _substate_key(client_token, state), From 163acf70a280e63c59237856b6ccf1811bd0e34b Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 4 Nov 2024 10:11:51 -0800 Subject: [PATCH 19/27] [ENG-759] [ENG-1104] patch `json.dumps` to handle `__wrapped__` objects (#4166) * [ENG-1104] patch `json.dumps` to handle `__wrapped__` objects Unwrap proxied objects if the default serializer doesn't work. * pre-commit fixup * Skip default fallback logic when `cls` is specified `cls` will provide its own default serialization mechanism, passing a `cls` Encoder class is now also a way to opt-out of our patching shenanigans and just use your own code. This will work, provided the library doing the JSON encoding isn't also using their own custom class. * Override JSONEncoder.default instead of json.dumps Many libraries (like httpx, used by openai), will use `from json import dumps`, and if they do that before `reflex.state` gets imported, then they will get the original dumps function instead of our patched one. To workaround this, monkeypatch the `JSONEncoder.default` function instead. This is also nicer behavior for custom subclasses of JSONEncoder; if someone wants to opt-out of our monkeypatching, they can simply not call `super().default(o)` in their subclass, which by default only raises a TypeError. --------- Co-authored-by: Nikhil Rao --- reflex/state.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/reflex/state.py b/reflex/state.py index ee0aa3e93..44ccebc30 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -8,6 +8,7 @@ import copy import dataclasses import functools import inspect +import json import pickle import sys import uuid @@ -3713,6 +3714,29 @@ def serialize_mutable_proxy(mp: MutableProxy): return mp.__wrapped__ +_orig_json_JSONEncoder_default = json.JSONEncoder.default + + +def _json_JSONEncoder_default_wrapper(self: json.JSONEncoder, o: Any) -> Any: + """Wrap JSONEncoder.default to handle MutableProxy objects. + + Args: + self: the JSONEncoder instance. + o: the object to serialize. + + Returns: + A JSON-able object. + """ + try: + return o.__wrapped__ + except AttributeError: + pass + return _orig_json_JSONEncoder_default(self, o) + + +json.JSONEncoder.default = _json_JSONEncoder_default_wrapper + + class ImmutableMutableProxy(MutableProxy): """A proxy for a mutable object that tracks changes. From 6394a6dfc5eda88fd8a8d47b63ca8fc9aa0150c4 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 4 Nov 2024 10:31:24 -0800 Subject: [PATCH 20/27] raise error when get package manager is not found (#4289) * raise error when get package manager is not found * add escape hatch * handle installing frontend packages more gracefully * fix no return * dang it darglint --- reflex/utils/exec.py | 6 ++-- reflex/utils/prerequisites.py | 56 +++++++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index bdc9be4ae..467c5fa2c 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -467,9 +467,11 @@ def output_system_info(): console.debug(f"{dep}") console.debug( - f"Using package installer at: {prerequisites.get_install_package_manager()}" # type: ignore + f"Using package installer at: {prerequisites.get_install_package_manager(on_failure_return_none=True)}" # type: ignore ) - console.debug(f"Using package executer at: {prerequisites.get_package_manager()}") # type: ignore + console.debug( + f"Using package executer at: {prerequisites.get_package_manager(on_failure_return_none=True)}" + ) # type: ignore if system != "Windows": console.debug(f"Unzip path: {path_ops.which('unzip')}") diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index d70e7706f..19d4966bb 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -204,10 +204,13 @@ def get_bun_version() -> version.Version | None: return None -def get_install_package_manager() -> str | None: +def get_install_package_manager(on_failure_return_none: bool = False) -> str | None: """Get the package manager executable for installation. Currently, bun is used for installation only. + Args: + on_failure_return_none: Whether to return None on failure. + Returns: The path to the package manager. """ @@ -217,21 +220,29 @@ def get_install_package_manager() -> str | None: or windows_check_onedrive_in_path() or windows_npm_escape_hatch() ): - return get_package_manager() + return get_package_manager(on_failure_return_none) return str(get_config().bun_path) -def get_package_manager() -> str | None: +def get_package_manager(on_failure_return_none: bool = False) -> str | None: """Get the package manager executable for running app. Currently on unix systems, npm is used for running the app only. + Args: + on_failure_return_none: Whether to return None on failure. + Returns: The path to the package manager. + + Raises: + FileNotFoundError: If the package manager is not found. """ npm_path = path_ops.get_npm_path() if npm_path is not None: - npm_path = str(Path(npm_path).resolve()) - return npm_path + return str(Path(npm_path).resolve()) + if on_failure_return_none: + return None + raise FileNotFoundError("NPM not found. You may need to run `reflex init`.") def windows_check_onedrive_in_path() -> bool: @@ -920,20 +931,39 @@ def install_frontend_packages(packages: set[str], config: Config): packages: A list of package names to be installed. config: The config object. + Raises: + FileNotFoundError: If the package manager is not found. + Example: >>> install_frontend_packages(["react", "react-dom"], get_config()) """ # unsupported archs(arm and 32bit machines) will use npm anyway. so we dont have to run npm twice fallback_command = ( - get_package_manager() - if not constants.IS_WINDOWS - or constants.IS_WINDOWS - and is_windows_bun_supported() - and not windows_check_onedrive_in_path() + get_package_manager(on_failure_return_none=True) + if ( + not constants.IS_WINDOWS + or constants.IS_WINDOWS + and is_windows_bun_supported() + and not windows_check_onedrive_in_path() + ) else None ) + + install_package_manager = ( + get_install_package_manager(on_failure_return_none=True) or fallback_command + ) + + if install_package_manager is None: + raise FileNotFoundError( + "Could not find a package manager to install frontend packages. You may need to run `reflex init`." + ) + + fallback_command = ( + fallback_command if fallback_command is not install_package_manager else None + ) + processes.run_process_with_fallback( - [get_install_package_manager(), "install"], # type: ignore + [install_package_manager, "install"], # type: ignore fallback=fallback_command, analytics_enabled=True, show_status_message="Installing base frontend packages", @@ -944,7 +974,7 @@ def install_frontend_packages(packages: set[str], config: Config): if config.tailwind is not None: processes.run_process_with_fallback( [ - get_install_package_manager(), + install_package_manager, "add", "-d", constants.Tailwind.VERSION, @@ -960,7 +990,7 @@ def install_frontend_packages(packages: set[str], config: Config): # Install custom packages defined in frontend_packages if len(packages) > 0: processes.run_process_with_fallback( - [get_install_package_manager(), "add", *packages], + [install_package_manager, "add", *packages], fallback=fallback_command, analytics_enabled=True, show_status_message="Installing frontend packages from config and components", From ca81e623db25fb94c70ee5a2d9bbe47ede7d78e8 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 4 Nov 2024 10:33:07 -0800 Subject: [PATCH 21/27] add noop event (#4288) * add noop event * fix pyi * get it right pyright * why * remove silly events * fix tests * remove semi colon * errors in merging --- reflex/.templates/web/utils/state.js | 35 ++-- reflex/__init__.py | 3 + reflex/__init__.pyi | 3 + reflex/components/base/script.py | 8 +- reflex/components/component.py | 36 ++-- reflex/components/core/clipboard.py | 4 +- reflex/components/core/clipboard.pyi | 4 +- reflex/components/core/debounce.py | 4 +- reflex/components/core/upload.py | 8 +- reflex/components/datadisplay/dataeditor.py | 36 ++-- .../datadisplay/shiki_code_block.py | 6 +- reflex/components/moment/moment.py | 4 +- reflex/components/next/image.py | 6 +- reflex/components/radix/primitives/drawer.py | 16 +- reflex/components/radix/primitives/form.py | 4 +- .../radix/themes/components/alert_dialog.py | 10 +- .../radix/themes/components/checkbox.py | 6 +- .../radix/themes/components/context_menu.py | 26 +-- .../radix/themes/components/dialog.py | 14 +- .../radix/themes/components/dropdown_menu.py | 26 +-- .../radix/themes/components/hover_card.py | 4 +- .../radix/themes/components/popover.py | 16 +- .../radix/themes/components/radio_cards.py | 4 +- .../radix/themes/components/radio_group.py | 4 +- .../radix/themes/components/select.py | 12 +- .../radix/themes/components/slider.py | 8 +- .../radix/themes/components/slider.pyi | 8 +- .../radix/themes/components/switch.py | 4 +- .../radix/themes/components/tabs.py | 4 +- .../radix/themes/components/tooltip.py | 8 +- .../components/react_player/react_player.py | 34 ++-- reflex/components/recharts/cartesian.py | 90 +++++----- reflex/components/recharts/charts.py | 44 ++--- reflex/components/recharts/general.py | 20 +-- reflex/components/recharts/polar.py | 66 ++++---- reflex/components/sonner/toast.py | 9 +- reflex/components/suneditor/editor.py | 16 +- reflex/event.py | 157 +++++++++++------- reflex/experimental/client_state.py | 6 +- reflex/experimental/layout.py | 4 +- reflex/utils/format.py | 12 +- tests/units/components/test_component.py | 16 +- .../test_component_future_annotations.py | 6 +- tests/units/test_event.py | 38 +++-- 44 files changed, 453 insertions(+), 396 deletions(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 7d76b080a..66df09ee1 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -178,11 +178,6 @@ export const applyEvent = async (event, socket) => { return false; } - if (event.name == "_console") { - console.log(event.payload.message); - return false; - } - if (event.name == "_remove_cookie") { cookies.remove(event.payload.key, { ...event.payload.options }); queueEventIfSocketExists(initialEvents(), socket); @@ -213,12 +208,6 @@ export const applyEvent = async (event, socket) => { return false; } - if (event.name == "_set_clipboard") { - const content = event.payload.content; - navigator.clipboard.writeText(content); - return false; - } - if (event.name == "_download") { const a = document.createElement("a"); a.hidden = true; @@ -233,11 +222,6 @@ export const applyEvent = async (event, socket) => { return false; } - if (event.name == "_alert") { - alert(event.payload.message); - return false; - } - if (event.name == "_set_focus") { const ref = event.payload.ref in refs ? refs[event.payload.ref] : event.payload.ref; @@ -254,6 +238,25 @@ export const applyEvent = async (event, socket) => { return false; } + if (event.name == "_call_function") { + try { + const eval_result = event.payload.function(); + if (event.payload.callback) { + if (!!eval_result && typeof eval_result.then === "function") { + event.payload.callback(await eval_result); + } else { + event.payload.callback(eval_result); + } + } + } catch (e) { + console.log("_call_function", e); + if (window && window?.onerror) { + window.onerror(e.message, null, null, null, e); + } + } + return false; + } + if (event.name == "_call_script") { try { const eval_result = eval(event.payload.javascript_code); diff --git a/reflex/__init__.py b/reflex/__init__.py index ffc4426f9..acba5936a 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -303,10 +303,13 @@ _MAPPING: dict = { "EventHandler", "background", "call_script", + "call_function", + "run_script", "clear_local_storage", "clear_session_storage", "console_log", "download", + "noop", "prevent_default", "redirect", "remove_cookie", diff --git a/reflex/__init__.pyi b/reflex/__init__.pyi index aa1c92b72..a0f60ea57 100644 --- a/reflex/__init__.pyi +++ b/reflex/__init__.pyi @@ -155,17 +155,20 @@ from .constants import Env as Env from .event import EventChain as EventChain from .event import EventHandler as EventHandler from .event import background as background +from .event import call_function as call_function from .event import call_script as call_script from .event import clear_local_storage as clear_local_storage from .event import clear_session_storage as clear_session_storage from .event import console_log as console_log from .event import download as download from .event import event as event +from .event import noop as noop from .event import prevent_default as prevent_default from .event import redirect as redirect from .event import remove_cookie as remove_cookie from .event import remove_local_storage as remove_local_storage from .event import remove_session_storage as remove_session_storage +from .event import run_script as run_script from .event import scroll_to as scroll_to from .event import set_clipboard as set_clipboard from .event import set_focus as set_focus diff --git a/reflex/components/base/script.py b/reflex/components/base/script.py index eb37d53e7..15145ecbf 100644 --- a/reflex/components/base/script.py +++ b/reflex/components/base/script.py @@ -8,7 +8,7 @@ from __future__ import annotations from typing import Literal from reflex.components.component import Component -from reflex.event import EventHandler, empty_event +from reflex.event import EventHandler, no_args_event_spec from reflex.vars.base import LiteralVar, Var @@ -35,13 +35,13 @@ class Script(Component): ) # Triggered when the script is loading - on_load: EventHandler[empty_event] + on_load: EventHandler[no_args_event_spec] # Triggered when the script has loaded - on_ready: EventHandler[empty_event] + on_ready: EventHandler[no_args_event_spec] # Triggered when the script has errored - on_error: EventHandler[empty_event] + on_error: EventHandler[no_args_event_spec] @classmethod def create(cls, *children, **props) -> Component: diff --git a/reflex/components/component.py b/reflex/components/component.py index 85db3906d..470ba1145 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -47,8 +47,8 @@ from reflex.event import ( EventVar, call_event_fn, call_event_handler, - empty_event, get_handler_args, + no_args_event_spec, ) from reflex.style import Style, format_as_emotion from reflex.utils import format, imports, types @@ -637,21 +637,21 @@ class Component(BaseComponent, ABC): """ default_triggers = { - 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, + EventTriggers.ON_FOCUS: no_args_event_spec, + EventTriggers.ON_BLUR: no_args_event_spec, + EventTriggers.ON_CLICK: no_args_event_spec, + EventTriggers.ON_CONTEXT_MENU: no_args_event_spec, + EventTriggers.ON_DOUBLE_CLICK: no_args_event_spec, + EventTriggers.ON_MOUSE_DOWN: no_args_event_spec, + EventTriggers.ON_MOUSE_ENTER: no_args_event_spec, + EventTriggers.ON_MOUSE_LEAVE: no_args_event_spec, + EventTriggers.ON_MOUSE_MOVE: no_args_event_spec, + EventTriggers.ON_MOUSE_OUT: no_args_event_spec, + EventTriggers.ON_MOUSE_OVER: no_args_event_spec, + EventTriggers.ON_MOUSE_UP: no_args_event_spec, + EventTriggers.ON_SCROLL: no_args_event_spec, + EventTriggers.ON_MOUNT: no_args_event_spec, + EventTriggers.ON_UNMOUNT: no_args_event_spec, } # Look for component specific triggers, @@ -662,7 +662,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 (empty_event) # type: ignore + default_triggers[field.name] = args_spec or (no_args_event_spec) # type: ignore return default_triggers def __repr__(self) -> str: @@ -1723,7 +1723,7 @@ class CustomComponent(Component): value = self._create_event_chain( value=value, args_spec=event_triggers_in_component_declaration.get( - key, empty_event + key, no_args_event_spec ), key=key, ) diff --git a/reflex/components/core/clipboard.py b/reflex/components/core/clipboard.py index cce0af4a7..6d6a38acc 100644 --- a/reflex/components/core/clipboard.py +++ b/reflex/components/core/clipboard.py @@ -6,7 +6,7 @@ from typing import Dict, List, Tuple, Union from reflex.components.base.fragment import Fragment from reflex.components.tags.tag import Tag -from reflex.event import EventChain, EventHandler, identity_event +from reflex.event import EventChain, EventHandler, passthrough_event_spec from reflex.utils.format import format_prop, wrap from reflex.utils.imports import ImportVar from reflex.vars import get_unique_variable_name @@ -20,7 +20,7 @@ class Clipboard(Fragment): targets: Var[List[str]] # Called when the user pastes data into the document. Data is a list of tuples of (mime_type, data). Binary types will be base64 encoded as a data uri. - on_paste: EventHandler[identity_event(List[Tuple[str, str]])] + on_paste: EventHandler[passthrough_event_spec(List[Tuple[str, str]])] # Save the original event actions for the on_paste event. on_paste_event_actions: Var[Dict[str, Union[bool, int]]] diff --git a/reflex/components/core/clipboard.pyi b/reflex/components/core/clipboard.pyi index 1284b8050..e51bad112 100644 --- a/reflex/components/core/clipboard.pyi +++ b/reflex/components/core/clipboard.pyi @@ -6,7 +6,9 @@ from typing import Any, Dict, List, Optional, Union, overload from reflex.components.base.fragment import Fragment -from reflex.event import EventType +from reflex.event import ( + EventType, +) from reflex.style import Style from reflex.utils.imports import ImportVar from reflex.vars.base import Var diff --git a/reflex/components/core/debounce.py b/reflex/components/core/debounce.py index 86efb7dcd..12cc94426 100644 --- a/reflex/components/core/debounce.py +++ b/reflex/components/core/debounce.py @@ -6,7 +6,7 @@ from typing import Any, Type, Union from reflex.components.component import Component from reflex.constants import EventTriggers -from reflex.event import EventHandler, empty_event +from reflex.event import EventHandler, no_args_event_spec from reflex.vars import VarData from reflex.vars.base import Var @@ -46,7 +46,7 @@ class DebounceInput(Component): element: Var[Type[Component]] # Fired when the input value changes - on_change: EventHandler[empty_event] + on_change: EventHandler[no_args_event_spec] @classmethod def create(cls, *children: Component, **props: Any) -> Component: diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index 787cca9d0..a2d86b910 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -22,8 +22,8 @@ from reflex.event import ( EventHandler, EventSpec, call_event_fn, - call_script, parse_args_spec, + run_script, ) from reflex.utils import format from reflex.utils.imports import ImportVar @@ -106,8 +106,8 @@ def clear_selected_files(id_: str = DEFAULT_UPLOAD_ID) -> EventSpec: """ # UploadFilesProvider assigns a special function to clear selected files # into the shared global refs object to make it accessible outside a React - # component via `call_script` (otherwise backend could never clear files). - return call_script(f"refs['__clear_selected_files']({id_!r})") + # component via `run_script` (otherwise backend could never clear files). + return run_script(f"refs['__clear_selected_files']({id_!r})") def cancel_upload(upload_id: str) -> EventSpec: @@ -119,7 +119,7 @@ def cancel_upload(upload_id: str) -> EventSpec: Returns: An event spec that cancels the upload when triggered. """ - return call_script( + return run_script( f"upload_controllers[{str(LiteralVar.create(upload_id))}]?.abort()" ) diff --git a/reflex/components/datadisplay/dataeditor.py b/reflex/components/datadisplay/dataeditor.py index 27ca62d93..860f75007 100644 --- a/reflex/components/datadisplay/dataeditor.py +++ b/reflex/components/datadisplay/dataeditor.py @@ -10,7 +10,7 @@ from typing_extensions import TypedDict from reflex.base import Base from reflex.components.component import Component, NoSSRComponent from reflex.components.literals import LiteralRowMarker -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.utils import console, format, types from reflex.utils.imports import ImportDict, ImportVar from reflex.utils.serializers import serializer @@ -284,56 +284,58 @@ class DataEditor(NoSSRComponent): theme: Var[Union[DataEditorTheme, Dict]] # Fired when a cell is activated. - on_cell_activated: EventHandler[identity_event(Tuple[int, int])] + on_cell_activated: EventHandler[passthrough_event_spec(Tuple[int, int])] # Fired when a cell is clicked. - on_cell_clicked: EventHandler[identity_event(Tuple[int, int])] + on_cell_clicked: EventHandler[passthrough_event_spec(Tuple[int, int])] # Fired when a cell is right-clicked. - on_cell_context_menu: EventHandler[identity_event(Tuple[int, int])] + on_cell_context_menu: EventHandler[passthrough_event_spec(Tuple[int, int])] # Fired when a cell is edited. - on_cell_edited: EventHandler[identity_event(Tuple[int, int], GridCell)] + on_cell_edited: EventHandler[passthrough_event_spec(Tuple[int, int], GridCell)] # Fired when a group header is clicked. - on_group_header_clicked: EventHandler[identity_event(Tuple[int, int], GridCell)] + on_group_header_clicked: EventHandler[ + passthrough_event_spec(Tuple[int, int], GridCell) + ] # Fired when a group header is right-clicked. on_group_header_context_menu: EventHandler[ - identity_event(int, GroupHeaderClickedEventArgs) + passthrough_event_spec(int, GroupHeaderClickedEventArgs) ] # Fired when a group header is renamed. - on_group_header_renamed: EventHandler[identity_event(str, str)] + on_group_header_renamed: EventHandler[passthrough_event_spec(str, str)] # Fired when a header is clicked. - on_header_clicked: EventHandler[identity_event(Tuple[int, int])] + on_header_clicked: EventHandler[passthrough_event_spec(Tuple[int, int])] # Fired when a header is right-clicked. - on_header_context_menu: EventHandler[identity_event(Tuple[int, int])] + on_header_context_menu: EventHandler[passthrough_event_spec(Tuple[int, int])] # Fired when a header menu item is clicked. - on_header_menu_click: EventHandler[identity_event(int, Rectangle)] + on_header_menu_click: EventHandler[passthrough_event_spec(int, Rectangle)] # Fired when an item is hovered. - on_item_hovered: EventHandler[identity_event(Tuple[int, int])] + on_item_hovered: EventHandler[passthrough_event_spec(Tuple[int, int])] # Fired when a selection is deleted. - on_delete: EventHandler[identity_event(GridSelection)] + on_delete: EventHandler[passthrough_event_spec(GridSelection)] # Fired when editing is finished. on_finished_editing: EventHandler[ - identity_event(Union[GridCell, None], tuple[int, int]) + passthrough_event_spec(Union[GridCell, None], tuple[int, int]) ] # Fired when a row is appended. - on_row_appended: EventHandler[empty_event] + on_row_appended: EventHandler[no_args_event_spec] # Fired when the selection is cleared. - on_selection_cleared: EventHandler[empty_event] + on_selection_cleared: EventHandler[no_args_event_spec] # Fired when a column is resized. - on_column_resize: EventHandler[identity_event(GridColumn, int)] + on_column_resize: EventHandler[passthrough_event_spec(GridColumn, int)] def add_imports(self) -> ImportDict: """Add imports for the component. diff --git a/reflex/components/datadisplay/shiki_code_block.py b/reflex/components/datadisplay/shiki_code_block.py index 07f09c6f6..21f22424d 100644 --- a/reflex/components/datadisplay/shiki_code_block.py +++ b/reflex/components/datadisplay/shiki_code_block.py @@ -14,7 +14,7 @@ from reflex.components.el.elements.forms import Button from reflex.components.lucide.icon import Icon from reflex.components.props import NoExtrasAllowedProps from reflex.components.radix.themes.layout.box import Box -from reflex.event import call_script, set_clipboard +from reflex.event import run_script, set_clipboard from reflex.style import Style from reflex.utils.exceptions import VarTypeError from reflex.utils.imports import ImportVar @@ -30,7 +30,7 @@ def copy_script() -> Any: Returns: Any: The result of calling the script. """ - return call_script( + return run_script( f""" // Event listener for the parent click document.addEventListener('click', function(event) {{ @@ -68,7 +68,7 @@ document.addEventListener('click', function(event) {{ }} else {{ // console.error('Parent element not found.'); }} -}}); +}}) """ ) diff --git a/reflex/components/moment/moment.py b/reflex/components/moment/moment.py index 4ac835b35..d5d6d8f7c 100644 --- a/reflex/components/moment/moment.py +++ b/reflex/components/moment/moment.py @@ -4,7 +4,7 @@ import dataclasses from typing import List, Optional from reflex.components.component import NoSSRComponent -from reflex.event import EventHandler, identity_event +from reflex.event import EventHandler, passthrough_event_spec from reflex.utils.imports import ImportDict from reflex.vars.base import LiteralVar, Var @@ -96,7 +96,7 @@ class Moment(NoSSRComponent): locale: Var[str] # Fires when the date changes. - on_change: EventHandler[identity_event(str)] + on_change: EventHandler[passthrough_event_spec(str)] def add_imports(self) -> ImportDict: """Add the imports for the Moment component. diff --git a/reflex/components/next/image.py b/reflex/components/next/image.py index fe74b0935..237c308ce 100644 --- a/reflex/components/next/image.py +++ b/reflex/components/next/image.py @@ -2,7 +2,7 @@ from typing import Any, Literal, Optional, Union -from reflex.event import EventHandler, empty_event +from reflex.event import EventHandler, no_args_event_spec from reflex.utils import types from reflex.vars.base import Var @@ -56,10 +56,10 @@ class Image(NextComponent): blurDataURL: Var[str] # Fires when the image has loaded. - on_load: EventHandler[empty_event] + on_load: EventHandler[no_args_event_spec] # Fires when the image has an error. - on_error: EventHandler[empty_event] + on_error: EventHandler[no_args_event_spec] @classmethod def create( diff --git a/reflex/components/radix/primitives/drawer.py b/reflex/components/radix/primitives/drawer.py index dca6bb7e1..f99342a58 100644 --- a/reflex/components/radix/primitives/drawer.py +++ b/reflex/components/radix/primitives/drawer.py @@ -10,7 +10,7 @@ from reflex.components.component import Component, ComponentNamespace from reflex.components.radix.primitives.base import RadixPrimitiveComponent from reflex.components.radix.themes.base import Theme from reflex.components.radix.themes.layout.flex import Flex -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.utils import console from reflex.vars.base import Var @@ -40,7 +40,7 @@ class DrawerRoot(DrawerComponent): open: Var[bool] # Fires when the drawer is opened or closed. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] # When `False`, it allows interaction with elements outside of the drawer without closing it. Defaults to `True`. modal: Var[bool] @@ -49,7 +49,7 @@ class DrawerRoot(DrawerComponent): direction: Var[LiteralDirectionType] # Gets triggered after the open or close animation ends, it receives an open argument with the open state of the drawer by the time the function was triggered. - on_animation_end: EventHandler[identity_event(bool)] + on_animation_end: EventHandler[passthrough_event_spec(bool)] # When `False`, dragging, clicking outside, pressing esc, etc. will not close the drawer. Use this in combination with the open prop, otherwise you won't be able to open/close the drawer. dismissible: Var[bool] @@ -141,19 +141,19 @@ class DrawerContent(DrawerComponent): return {"css": base_style} # Fired when the drawer content is opened. Deprecated. - on_open_auto_focus: EventHandler[empty_event] + on_open_auto_focus: EventHandler[no_args_event_spec] # Fired when the drawer content is closed. Deprecated. - on_close_auto_focus: EventHandler[empty_event] + on_close_auto_focus: EventHandler[no_args_event_spec] # Fired when the escape key is pressed. Deprecated. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] # Fired when the pointer is down outside the drawer content. Deprecated. - on_pointer_down_outside: EventHandler[empty_event] + on_pointer_down_outside: EventHandler[no_args_event_spec] # Fired when interacting outside the drawer content. Deprecated. - on_interact_outside: EventHandler[empty_event] + on_interact_outside: EventHandler[no_args_event_spec] @classmethod def create(cls, *children, **props): diff --git a/reflex/components/radix/primitives/form.py b/reflex/components/radix/primitives/form.py index 4d4be7e40..f2b8201ad 100644 --- a/reflex/components/radix/primitives/form.py +++ b/reflex/components/radix/primitives/form.py @@ -8,7 +8,7 @@ from reflex.components.component import ComponentNamespace from reflex.components.core.debounce import DebounceInput from reflex.components.el.elements.forms import Form as HTMLForm from reflex.components.radix.themes.components.text_field import TextFieldRoot -from reflex.event import EventHandler, empty_event +from reflex.event import EventHandler, no_args_event_spec from reflex.vars.base import Var from .base import RadixPrimitiveComponentWithClassName @@ -28,7 +28,7 @@ class FormRoot(FormComponent, HTMLForm): alias = "RadixFormRoot" # Fired when the errors are cleared. - on_clear_server_errors: EventHandler[empty_event] + on_clear_server_errors: EventHandler[no_args_event_spec] def add_style(self) -> dict[str, Any] | None: """Add style to the component. diff --git a/reflex/components/radix/themes/components/alert_dialog.py b/reflex/components/radix/themes/components/alert_dialog.py index 12ac64b90..36d38532c 100644 --- a/reflex/components/radix/themes/components/alert_dialog.py +++ b/reflex/components/radix/themes/components/alert_dialog.py @@ -5,7 +5,7 @@ from typing import Literal from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.el import elements -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var from ..base import RadixThemesComponent, RadixThemesTriggerComponent @@ -22,7 +22,7 @@ class AlertDialogRoot(RadixThemesComponent): open: Var[bool] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] # The open state of the dialog when it is initially rendered. Use when you do not need to control its open state. default_open: Var[bool] @@ -46,13 +46,13 @@ class AlertDialogContent(elements.Div, RadixThemesComponent): force_mount: Var[bool] # Fired when the dialog is opened. - on_open_auto_focus: EventHandler[empty_event] + on_open_auto_focus: EventHandler[no_args_event_spec] # Fired when the dialog is closed. - on_close_auto_focus: EventHandler[empty_event] + on_close_auto_focus: EventHandler[no_args_event_spec] # Fired when the escape key is pressed. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] class AlertDialogTitle(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/checkbox.py b/reflex/components/radix/themes/components/checkbox.py index 2944b1f11..1460382f5 100644 --- a/reflex/components/radix/themes/components/checkbox.py +++ b/reflex/components/radix/themes/components/checkbox.py @@ -6,7 +6,7 @@ from reflex.components.component import Component, ComponentNamespace 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, identity_event +from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import LiteralVar, Var from ..base import ( @@ -61,7 +61,7 @@ class Checkbox(RadixThemesComponent): _rename_props = {"onChange": "onCheckedChange"} # Fired when the checkbox is checked or unchecked. - on_change: EventHandler[identity_event(bool)] + on_change: EventHandler[passthrough_event_spec(bool)] class HighLevelCheckbox(RadixThemesComponent): @@ -112,7 +112,7 @@ class HighLevelCheckbox(RadixThemesComponent): _rename_props = {"onChange": "onCheckedChange"} # Fired when the checkbox is checked or unchecked. - on_change: EventHandler[identity_event(bool)] + on_change: EventHandler[passthrough_event_spec(bool)] @classmethod def create(cls, text: Var[str] = LiteralVar.create(""), **props) -> Component: diff --git a/reflex/components/radix/themes/components/context_menu.py b/reflex/components/radix/themes/components/context_menu.py index b3f55f8ba..7b2032a42 100644 --- a/reflex/components/radix/themes/components/context_menu.py +++ b/reflex/components/radix/themes/components/context_menu.py @@ -4,7 +4,7 @@ from typing import Dict, List, Literal, Union from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -39,7 +39,7 @@ class ContextMenuRoot(RadixThemesComponent): _invalid_children: List[str] = ["ContextMenuItem"] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] # The reading direction of submenus when applicable. If omitted, inherits globally from DirectionProvider or assumes LTR (left-to-right) reading mode. dir: Var[LiteralDirType] @@ -109,19 +109,19 @@ class ContextMenuContent(RadixThemesComponent): hide_when_detached: Var[bool] # Fired when focus moves back after closing. - on_close_auto_focus: EventHandler[empty_event] + on_close_auto_focus: EventHandler[no_args_event_spec] # Fired when the escape key is pressed. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] # Fired when a pointer down event happens outside the context menu. - on_pointer_down_outside: EventHandler[empty_event] + on_pointer_down_outside: EventHandler[no_args_event_spec] # Fired when focus moves outside the context menu. - on_focus_outside: EventHandler[empty_event] + on_focus_outside: EventHandler[no_args_event_spec] # Fired when the pointer interacts outside the context menu. - on_interact_outside: EventHandler[empty_event] + on_interact_outside: EventHandler[no_args_event_spec] class ContextMenuSub(RadixThemesComponent): @@ -136,7 +136,7 @@ class ContextMenuSub(RadixThemesComponent): default_open: Var[bool] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] class ContextMenuSubTrigger(RadixThemesComponent): @@ -191,16 +191,16 @@ class ContextMenuSubContent(RadixThemesComponent): _valid_parents: List[str] = ["ContextMenuSub"] # Fired when the escape key is pressed. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] # Fired when a pointer down event happens outside the context menu. - on_pointer_down_outside: EventHandler[empty_event] + on_pointer_down_outside: EventHandler[no_args_event_spec] # Fired when focus moves outside the context menu. - on_focus_outside: EventHandler[empty_event] + on_focus_outside: EventHandler[no_args_event_spec] # Fired when interacting outside the context menu. - on_interact_outside: EventHandler[empty_event] + on_interact_outside: EventHandler[no_args_event_spec] class ContextMenuItem(RadixThemesComponent): @@ -226,7 +226,7 @@ class ContextMenuItem(RadixThemesComponent): _valid_parents: List[str] = ["ContextMenuContent", "ContextMenuSubContent"] # Fired when the item is selected. - on_select: EventHandler[empty_event] + on_select: EventHandler[no_args_event_spec] class ContextMenuSeparator(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/dialog.py b/reflex/components/radix/themes/components/dialog.py index 840a50ecb..5d33cbc5f 100644 --- a/reflex/components/radix/themes/components/dialog.py +++ b/reflex/components/radix/themes/components/dialog.py @@ -5,7 +5,7 @@ from typing import Literal from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.el import elements -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -23,7 +23,7 @@ class DialogRoot(RadixThemesComponent): open: Var[bool] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] # The open state of the dialog when it is initially rendered. Use when you do not need to control its open state. default_open: Var[bool] @@ -50,19 +50,19 @@ class DialogContent(elements.Div, RadixThemesComponent): size: Var[Responsive[Literal["1", "2", "3", "4"]]] # Fired when the dialog is opened. - on_open_auto_focus: EventHandler[empty_event] + on_open_auto_focus: EventHandler[no_args_event_spec] # Fired when the dialog is closed. - on_close_auto_focus: EventHandler[empty_event] + on_close_auto_focus: EventHandler[no_args_event_spec] # Fired when the escape key is pressed. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] # Fired when the pointer is down outside the dialog. - on_pointer_down_outside: EventHandler[empty_event] + on_pointer_down_outside: EventHandler[no_args_event_spec] # Fired when the pointer interacts outside the dialog. - on_interact_outside: EventHandler[empty_event] + on_interact_outside: EventHandler[no_args_event_spec] class DialogDescription(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/dropdown_menu.py b/reflex/components/radix/themes/components/dropdown_menu.py index ee9040501..0d2ac38e2 100644 --- a/reflex/components/radix/themes/components/dropdown_menu.py +++ b/reflex/components/radix/themes/components/dropdown_menu.py @@ -4,7 +4,7 @@ from typing import Dict, List, Literal, Union from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -49,7 +49,7 @@ class DropdownMenuRoot(RadixThemesComponent): _invalid_children: List[str] = ["DropdownMenuItem"] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] class DropdownMenuTrigger(RadixThemesTriggerComponent): @@ -116,19 +116,19 @@ class DropdownMenuContent(RadixThemesComponent): hide_when_detached: Var[bool] # Fired when the dialog is closed. - on_close_auto_focus: EventHandler[empty_event] + on_close_auto_focus: EventHandler[no_args_event_spec] # Fired when the escape key is pressed. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] # Fired when the pointer is down outside the dialog. - on_pointer_down_outside: EventHandler[empty_event] + on_pointer_down_outside: EventHandler[no_args_event_spec] # Fired when focus moves outside the dialog. - on_focus_outside: EventHandler[empty_event] + on_focus_outside: EventHandler[no_args_event_spec] # Fired when the pointer interacts outside the dialog. - on_interact_outside: EventHandler[empty_event] + on_interact_outside: EventHandler[no_args_event_spec] class DropdownMenuSubTrigger(RadixThemesTriggerComponent): @@ -160,7 +160,7 @@ class DropdownMenuSub(RadixThemesComponent): default_open: Var[bool] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] class DropdownMenuSubContent(RadixThemesComponent): @@ -198,16 +198,16 @@ class DropdownMenuSubContent(RadixThemesComponent): _valid_parents: List[str] = ["DropdownMenuSub"] # Fired when the escape key is pressed. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] # Fired when the pointer is down outside the dialog. - on_pointer_down_outside: EventHandler[empty_event] + on_pointer_down_outside: EventHandler[no_args_event_spec] # Fired when focus moves outside the dialog. - on_focus_outside: EventHandler[empty_event] + on_focus_outside: EventHandler[no_args_event_spec] # Fired when the pointer interacts outside the dialog. - on_interact_outside: EventHandler[empty_event] + on_interact_outside: EventHandler[no_args_event_spec] class DropdownMenuItem(RadixThemesComponent): @@ -233,7 +233,7 @@ class DropdownMenuItem(RadixThemesComponent): _valid_parents: List[str] = ["DropdownMenuContent", "DropdownMenuSubContent"] # Fired when the item is selected. - on_select: EventHandler[empty_event] + on_select: EventHandler[no_args_event_spec] class DropdownMenuSeparator(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/hover_card.py b/reflex/components/radix/themes/components/hover_card.py index 10002c7e6..6babb10d2 100644 --- a/reflex/components/radix/themes/components/hover_card.py +++ b/reflex/components/radix/themes/components/hover_card.py @@ -5,7 +5,7 @@ from typing import Dict, Literal, Union from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.el import elements -from reflex.event import EventHandler, identity_event +from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -32,7 +32,7 @@ class HoverCardRoot(RadixThemesComponent): close_delay: Var[int] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] class HoverCardTrigger(RadixThemesTriggerComponent): diff --git a/reflex/components/radix/themes/components/popover.py b/reflex/components/radix/themes/components/popover.py index 6997ec5c5..bcafe6d38 100644 --- a/reflex/components/radix/themes/components/popover.py +++ b/reflex/components/radix/themes/components/popover.py @@ -5,7 +5,7 @@ from typing import Dict, Literal, Union from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.el import elements -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -26,7 +26,7 @@ class PopoverRoot(RadixThemesComponent): modal: Var[bool] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] # The open state of the popover when it is initially rendered. Use when you do not need to control its open state. default_open: Var[bool] @@ -71,22 +71,22 @@ class PopoverContent(elements.Div, RadixThemesComponent): hide_when_detached: Var[bool] # Fired when the dialog is opened. - on_open_auto_focus: EventHandler[empty_event] + on_open_auto_focus: EventHandler[no_args_event_spec] # Fired when the dialog is closed. - on_close_auto_focus: EventHandler[empty_event] + on_close_auto_focus: EventHandler[no_args_event_spec] # Fired when the escape key is pressed. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] # Fired when the pointer is down outside the dialog. - on_pointer_down_outside: EventHandler[empty_event] + on_pointer_down_outside: EventHandler[no_args_event_spec] # Fired when focus moves outside the dialog. - on_focus_outside: EventHandler[empty_event] + on_focus_outside: EventHandler[no_args_event_spec] # Fired when the pointer interacts outside the dialog. - on_interact_outside: EventHandler[empty_event] + on_interact_outside: EventHandler[no_args_event_spec] class PopoverClose(RadixThemesTriggerComponent): diff --git a/reflex/components/radix/themes/components/radio_cards.py b/reflex/components/radix/themes/components/radio_cards.py index e0aa2a749..e075a1ba2 100644 --- a/reflex/components/radix/themes/components/radio_cards.py +++ b/reflex/components/radix/themes/components/radio_cards.py @@ -4,7 +4,7 @@ from types import SimpleNamespace from typing import Literal, Union from reflex.components.core.breakpoints import Responsive -from reflex.event import EventHandler, identity_event +from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var from ..base import LiteralAccentColor, RadixThemesComponent @@ -65,7 +65,7 @@ class RadioCardsRoot(RadixThemesComponent): loop: Var[bool] # Event handler called when the value changes. - on_value_change: EventHandler[identity_event(str)] + on_value_change: EventHandler[passthrough_event_spec(str)] class RadioCardsItem(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/radio_group.py b/reflex/components/radix/themes/components/radio_group.py index a55ca3a41..df3843cf9 100644 --- a/reflex/components/radix/themes/components/radio_group.py +++ b/reflex/components/radix/themes/components/radio_group.py @@ -9,7 +9,7 @@ from reflex.components.component import Component, ComponentNamespace 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, identity_event +from reflex.event import EventHandler, passthrough_event_spec from reflex.utils import types from reflex.vars.base import LiteralVar, Var from reflex.vars.sequence import StringVar @@ -59,7 +59,7 @@ class RadioGroupRoot(RadixThemesComponent): _rename_props = {"onChange": "onValueChange"} # Fired when the value of the radio group changes. - on_change: EventHandler[identity_event(str)] + on_change: EventHandler[passthrough_event_spec(str)] class RadioGroupItem(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/select.py b/reflex/components/radix/themes/components/select.py index 47a1eaf3f..f71276c8b 100644 --- a/reflex/components/radix/themes/components/select.py +++ b/reflex/components/radix/themes/components/select.py @@ -5,7 +5,7 @@ from typing import List, Literal, Union import reflex as rx from reflex.components.component import Component, ComponentNamespace from reflex.components.core.breakpoints import Responsive -from reflex.event import empty_event, identity_event +from reflex.event import no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -48,10 +48,10 @@ class SelectRoot(RadixThemesComponent): _rename_props = {"onChange": "onValueChange"} # Fired when the value of the select changes. - on_change: rx.EventHandler[identity_event(str)] + on_change: rx.EventHandler[passthrough_event_spec(str)] # Fired when the select is opened or closed. - on_open_change: rx.EventHandler[identity_event(bool)] + on_open_change: rx.EventHandler[passthrough_event_spec(bool)] class SelectTrigger(RadixThemesComponent): @@ -104,13 +104,13 @@ class SelectContent(RadixThemesComponent): align_offset: Var[int] # Fired when the select content is closed. - on_close_auto_focus: rx.EventHandler[empty_event] + on_close_auto_focus: rx.EventHandler[no_args_event_spec] # Fired when the escape key is pressed. - on_escape_key_down: rx.EventHandler[empty_event] + on_escape_key_down: rx.EventHandler[no_args_event_spec] # Fired when a pointer down event happens outside the select content. - on_pointer_down_outside: rx.EventHandler[empty_event] + on_pointer_down_outside: rx.EventHandler[no_args_event_spec] class SelectGroup(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/slider.py b/reflex/components/radix/themes/components/slider.py index 4f456cdca..d833ef782 100644 --- a/reflex/components/radix/themes/components/slider.py +++ b/reflex/components/radix/themes/components/slider.py @@ -6,7 +6,7 @@ from typing import List, Literal, Optional, Union from reflex.components.component import Component from reflex.components.core.breakpoints import Responsive -from reflex.event import EventHandler, identity_event +from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -15,9 +15,9 @@ from ..base import ( ) on_value_event_spec = ( - identity_event(list[Union[int, float]]), - identity_event(list[int]), - identity_event(list[float]), + passthrough_event_spec(list[Union[int, float]]), + passthrough_event_spec(list[int]), + passthrough_event_spec(list[float]), ) diff --git a/reflex/components/radix/themes/components/slider.pyi b/reflex/components/radix/themes/components/slider.pyi index f77573d44..0d1b17e5f 100644 --- a/reflex/components/radix/themes/components/slider.pyi +++ b/reflex/components/radix/themes/components/slider.pyi @@ -6,16 +6,16 @@ from typing import Any, Dict, List, Literal, Optional, Union, overload from reflex.components.core.breakpoints import Breakpoints -from reflex.event import EventType, identity_event +from reflex.event import EventType, passthrough_event_spec from reflex.style import Style from reflex.vars.base import Var from ..base import RadixThemesComponent on_value_event_spec = ( - identity_event(list[Union[int, float]]), - identity_event(list[int]), - identity_event(list[float]), + passthrough_event_spec(list[Union[int, float]]), + passthrough_event_spec(list[int]), + passthrough_event_spec(list[float]), ) class Slider(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/switch.py b/reflex/components/radix/themes/components/switch.py index 13be32d83..dea4913aa 100644 --- a/reflex/components/radix/themes/components/switch.py +++ b/reflex/components/radix/themes/components/switch.py @@ -3,7 +3,7 @@ from typing import Literal from reflex.components.core.breakpoints import Responsive -from reflex.event import EventHandler, identity_event +from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -59,7 +59,7 @@ class Switch(RadixThemesComponent): _rename_props = {"onChange": "onCheckedChange"} # Fired when the value of the switch changes - on_change: EventHandler[identity_event(bool)] + on_change: EventHandler[passthrough_event_spec(bool)] switch = Switch.create diff --git a/reflex/components/radix/themes/components/tabs.py b/reflex/components/radix/themes/components/tabs.py index 12359b528..ac04c1a3d 100644 --- a/reflex/components/radix/themes/components/tabs.py +++ b/reflex/components/radix/themes/components/tabs.py @@ -7,7 +7,7 @@ from typing import Any, Dict, List, Literal from reflex.components.component import Component, ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.core.colors import color -from reflex.event import EventHandler, identity_event +from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -42,7 +42,7 @@ class TabsRoot(RadixThemesComponent): _rename_props = {"onChange": "onValueChange"} # Fired when the value of the tabs changes. - on_change: EventHandler[identity_event(str)] + on_change: EventHandler[passthrough_event_spec(str)] def add_style(self) -> Dict[str, Any] | None: """Add style for the component. diff --git a/reflex/components/radix/themes/components/tooltip.py b/reflex/components/radix/themes/components/tooltip.py index ac35c86d1..3bc61f545 100644 --- a/reflex/components/radix/themes/components/tooltip.py +++ b/reflex/components/radix/themes/components/tooltip.py @@ -3,7 +3,7 @@ from typing import Dict, Literal, Union from reflex.components.component import Component -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.utils import format from reflex.vars.base import Var @@ -85,13 +85,13 @@ class Tooltip(RadixThemesComponent): aria_label: Var[str] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] # Fired when the escape key is pressed. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] # Fired when the pointer is down outside the tooltip. - on_pointer_down_outside: EventHandler[empty_event] + on_pointer_down_outside: EventHandler[no_args_event_spec] @classmethod def create(cls, *children, **props) -> Component: diff --git a/reflex/components/react_player/react_player.py b/reflex/components/react_player/react_player.py index b2c58b754..fb0319ceb 100644 --- a/reflex/components/react_player/react_player.py +++ b/reflex/components/react_player/react_player.py @@ -5,7 +5,7 @@ from __future__ import annotations from typing_extensions import TypedDict from reflex.components.component import NoSSRComponent -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var @@ -57,49 +57,49 @@ class ReactPlayer(NoSSRComponent): height: Var[str] # Called when media is loaded and ready to play. If playing is set to true, media will play immediately. - on_ready: EventHandler[empty_event] + on_ready: EventHandler[no_args_event_spec] # Called when media starts playing. - on_start: EventHandler[empty_event] + on_start: EventHandler[no_args_event_spec] # Called when media starts or resumes playing after pausing or buffering. - on_play: EventHandler[empty_event] + on_play: EventHandler[no_args_event_spec] # Callback containing played and loaded progress as a fraction, and playedSeconds and loadedSeconds in seconds. eg { played: 0.12, playedSeconds: 11.3, loaded: 0.34, loadedSeconds: 16.7 } - on_progress: EventHandler[identity_event(Progress)] + on_progress: EventHandler[passthrough_event_spec(Progress)] # Callback containing duration of the media, in seconds. - on_duration: EventHandler[identity_event(float)] + on_duration: EventHandler[passthrough_event_spec(float)] # Called when media is paused. - on_pause: EventHandler[empty_event] + on_pause: EventHandler[no_args_event_spec] # Called when media starts buffering. - on_buffer: EventHandler[empty_event] + on_buffer: EventHandler[no_args_event_spec] # Called when media has finished buffering. Works for files, YouTube and Facebook. - on_buffer_end: EventHandler[empty_event] + on_buffer_end: EventHandler[no_args_event_spec] # Called when media seeks with seconds parameter. - on_seek: EventHandler[identity_event(float)] + on_seek: EventHandler[passthrough_event_spec(float)] # Called when playback rate of the player changed. Only supported by YouTube, Vimeo (if enabled), Wistia, and file paths. - on_playback_rate_change: EventHandler[empty_event] + on_playback_rate_change: EventHandler[no_args_event_spec] # Called when playback quality of the player changed. Only supported by YouTube (if enabled). - on_playback_quality_change: EventHandler[empty_event] + on_playback_quality_change: EventHandler[no_args_event_spec] # Called when media finishes playing. Does not fire when loop is set to true. - on_ended: EventHandler[empty_event] + on_ended: EventHandler[no_args_event_spec] # Called when an error occurs whilst attempting to play media. - on_error: EventHandler[empty_event] + on_error: EventHandler[no_args_event_spec] # Called when user clicks the light mode preview. - on_click_preview: EventHandler[empty_event] + on_click_preview: EventHandler[no_args_event_spec] # Called when picture-in-picture mode is enabled. - on_enable_pip: EventHandler[empty_event] + on_enable_pip: EventHandler[no_args_event_spec] # Called when picture-in-picture mode is disabled. - on_disable_pip: EventHandler[empty_event] + on_disable_pip: EventHandler[no_args_event_spec] diff --git a/reflex/components/recharts/cartesian.py b/reflex/components/recharts/cartesian.py index 865b50a32..028bcb4e4 100644 --- a/reflex/components/recharts/cartesian.py +++ b/reflex/components/recharts/cartesian.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List, Union from reflex.constants import EventTriggers from reflex.constants.colors import Color -from reflex.event import EventHandler, empty_event +from reflex.event import EventHandler, no_args_event_spec from reflex.vars.base import LiteralVar, Var from .recharts import ( @@ -109,25 +109,25 @@ class Axis(Recharts): text_anchor: Var[LiteralTextAnchor] # The customized event handler of click on the ticks of this axis - on_click: EventHandler[empty_event] + on_click: EventHandler[no_args_event_spec] # The customized event handler of mousedown on the ticks of this axis - on_mouse_down: EventHandler[empty_event] + on_mouse_down: EventHandler[no_args_event_spec] # The customized event handler of mouseup on the ticks of this axis - on_mouse_up: EventHandler[empty_event] + on_mouse_up: EventHandler[no_args_event_spec] # The customized event handler of mousemove on the ticks of this axis - on_mouse_move: EventHandler[empty_event] + on_mouse_move: EventHandler[no_args_event_spec] # The customized event handler of mouseout on the ticks of this axis - on_mouse_out: EventHandler[empty_event] + on_mouse_out: EventHandler[no_args_event_spec] # The customized event handler of mouseenter on the ticks of this axis - on_mouse_enter: EventHandler[empty_event] + on_mouse_enter: EventHandler[no_args_event_spec] # The customized event handler of mouseleave on the ticks of this axis - on_mouse_leave: EventHandler[empty_event] + on_mouse_leave: EventHandler[no_args_event_spec] class XAxis(Axis): @@ -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: empty_event, + EventTriggers.ON_CHANGE: no_args_event_spec, } @@ -293,34 +293,34 @@ class Cartesian(Recharts): name: Var[Union[str, int]] # The customized event handler of animation start - on_animation_start: EventHandler[empty_event] + on_animation_start: EventHandler[no_args_event_spec] # The customized event handler of animation end - on_animation_end: EventHandler[empty_event] + on_animation_end: EventHandler[no_args_event_spec] # The customized event handler of click on the component in this group - on_click: EventHandler[empty_event] + on_click: EventHandler[no_args_event_spec] # The customized event handler of mousedown on the component in this group - on_mouse_down: EventHandler[empty_event] + on_mouse_down: EventHandler[no_args_event_spec] # The customized event handler of mouseup on the component in this group - on_mouse_up: EventHandler[empty_event] + on_mouse_up: EventHandler[no_args_event_spec] # The customized event handler of mousemove on the component in this group - on_mouse_move: EventHandler[empty_event] + on_mouse_move: EventHandler[no_args_event_spec] # The customized event handler of mouseover on the component in this group - on_mouse_over: EventHandler[empty_event] + on_mouse_over: EventHandler[no_args_event_spec] # The customized event handler of mouseout on the component in this group - on_mouse_out: EventHandler[empty_event] + on_mouse_out: EventHandler[no_args_event_spec] # The customized event handler of mouseenter on the component in this group - on_mouse_enter: EventHandler[empty_event] + on_mouse_enter: EventHandler[no_args_event_spec] # The customized event handler of mouseleave on the component in this group - on_mouse_leave: EventHandler[empty_event] + on_mouse_leave: EventHandler[no_args_event_spec] class Area(Cartesian): @@ -526,28 +526,28 @@ class Scatter(Recharts): animation_easing: Var[LiteralAnimationEasing] # The customized event handler of click on the component in this group - on_click: EventHandler[empty_event] + on_click: EventHandler[no_args_event_spec] # The customized event handler of mousedown on the component in this group - on_mouse_down: EventHandler[empty_event] + on_mouse_down: EventHandler[no_args_event_spec] # The customized event handler of mouseup on the component in this group - on_mouse_up: EventHandler[empty_event] + on_mouse_up: EventHandler[no_args_event_spec] # The customized event handler of mousemove on the component in this group - on_mouse_move: EventHandler[empty_event] + on_mouse_move: EventHandler[no_args_event_spec] # The customized event handler of mouseover on the component in this group - on_mouse_over: EventHandler[empty_event] + on_mouse_over: EventHandler[no_args_event_spec] # The customized event handler of mouseout on the component in this group - on_mouse_out: EventHandler[empty_event] + on_mouse_out: EventHandler[no_args_event_spec] # The customized event handler of mouseenter on the component in this group - on_mouse_enter: EventHandler[empty_event] + on_mouse_enter: EventHandler[no_args_event_spec] # The customized event handler of mouseleave on the component in this group - on_mouse_leave: EventHandler[empty_event] + on_mouse_leave: EventHandler[no_args_event_spec] class Funnel(Recharts): @@ -591,34 +591,34 @@ class Funnel(Recharts): _valid_children: List[str] = ["LabelList", "Cell"] # The customized event handler of animation start - on_animation_start: EventHandler[empty_event] + on_animation_start: EventHandler[no_args_event_spec] # The customized event handler of animation end - on_animation_end: EventHandler[empty_event] + on_animation_end: EventHandler[no_args_event_spec] # The customized event handler of click on the component in this group - on_click: EventHandler[empty_event] + on_click: EventHandler[no_args_event_spec] # The customized event handler of mousedown on the component in this group - on_mouse_down: EventHandler[empty_event] + on_mouse_down: EventHandler[no_args_event_spec] # The customized event handler of mouseup on the component in this group - on_mouse_up: EventHandler[empty_event] + on_mouse_up: EventHandler[no_args_event_spec] # The customized event handler of mousemove on the component in this group - on_mouse_move: EventHandler[empty_event] + on_mouse_move: EventHandler[no_args_event_spec] # The customized event handler of mouseover on the component in this group - on_mouse_over: EventHandler[empty_event] + on_mouse_over: EventHandler[no_args_event_spec] # The customized event handler of mouseout on the component in this group - on_mouse_out: EventHandler[empty_event] + on_mouse_out: EventHandler[no_args_event_spec] # The customized event handler of mouseenter on the component in this group - on_mouse_enter: EventHandler[empty_event] + on_mouse_enter: EventHandler[no_args_event_spec] # The customized event handler of mouseleave on the component in this group - on_mouse_leave: EventHandler[empty_event] + on_mouse_leave: EventHandler[no_args_event_spec] class ErrorBar(Recharts): @@ -715,28 +715,28 @@ class ReferenceDot(Reference): _valid_children: List[str] = ["Label"] # The customized event handler of click on the component in this chart - on_click: EventHandler[empty_event] + on_click: EventHandler[no_args_event_spec] # The customized event handler of mousedown on the component in this chart - on_mouse_down: EventHandler[empty_event] + on_mouse_down: EventHandler[no_args_event_spec] # The customized event handler of mouseup on the component in this chart - on_mouse_up: EventHandler[empty_event] + on_mouse_up: EventHandler[no_args_event_spec] # The customized event handler of mouseover on the component in this chart - on_mouse_over: EventHandler[empty_event] + on_mouse_over: EventHandler[no_args_event_spec] # The customized event handler of mouseout on the component in this chart - on_mouse_out: EventHandler[empty_event] + on_mouse_out: EventHandler[no_args_event_spec] # The customized event handler of mouseenter on the component in this chart - on_mouse_enter: EventHandler[empty_event] + on_mouse_enter: EventHandler[no_args_event_spec] # The customized event handler of mousemove on the component in this chart - on_mouse_move: EventHandler[empty_event] + on_mouse_move: EventHandler[no_args_event_spec] # The customized event handler of mouseleave on the component in this chart - on_mouse_leave: EventHandler[empty_event] + on_mouse_leave: EventHandler[no_args_event_spec] class ReferenceArea(Recharts): diff --git a/reflex/components/recharts/charts.py b/reflex/components/recharts/charts.py index d7e07b2d8..13f125213 100644 --- a/reflex/components/recharts/charts.py +++ b/reflex/components/recharts/charts.py @@ -8,7 +8,7 @@ from reflex.components.component import Component from reflex.components.recharts.general import ResponsiveContainer from reflex.constants import EventTriggers from reflex.constants.colors import Color -from reflex.event import EventHandler, empty_event +from reflex.event import EventHandler, no_args_event_spec from reflex.vars.base import Var from .recharts import ( @@ -31,16 +31,16 @@ class ChartBase(RechartsCharts): height: Var[Union[str, int]] = "100%" # type: ignore # The customized event handler of click on the component in this chart - on_click: EventHandler[empty_event] + on_click: EventHandler[no_args_event_spec] # The customized event handler of mouseenter on the component in this chart - on_mouse_enter: EventHandler[empty_event] + on_mouse_enter: EventHandler[no_args_event_spec] # The customized event handler of mousemove on the component in this chart - on_mouse_move: EventHandler[empty_event] + on_mouse_move: EventHandler[no_args_event_spec] # The customized event handler of mouseleave on the component in this chart - on_mouse_leave: EventHandler[empty_event] + on_mouse_leave: EventHandler[no_args_event_spec] @staticmethod def _ensure_valid_dimension(name: str, value: Any) -> None: @@ -270,16 +270,16 @@ class PieChart(ChartBase): ] # The customized event handler of mousedown on the sectors in this group - on_mouse_down: EventHandler[empty_event] + on_mouse_down: EventHandler[no_args_event_spec] # The customized event handler of mouseup on the sectors in this group - on_mouse_up: EventHandler[empty_event] + on_mouse_up: EventHandler[no_args_event_spec] # The customized event handler of mouseover on the sectors in this group - on_mouse_over: EventHandler[empty_event] + on_mouse_over: EventHandler[no_args_event_spec] # The customized event handler of mouseout on the sectors in this group - on_mouse_out: EventHandler[empty_event] + on_mouse_out: EventHandler[no_args_event_spec] class RadarChart(ChartBase): @@ -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: empty_event, - EventTriggers.ON_MOUSE_ENTER: empty_event, - EventTriggers.ON_MOUSE_LEAVE: empty_event, + EventTriggers.ON_CLICK: no_args_event_spec, + EventTriggers.ON_MOUSE_ENTER: no_args_event_spec, + EventTriggers.ON_MOUSE_LEAVE: no_args_event_spec, } @@ -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: 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, + EventTriggers.ON_CLICK: no_args_event_spec, + EventTriggers.ON_MOUSE_DOWN: no_args_event_spec, + EventTriggers.ON_MOUSE_UP: no_args_event_spec, + EventTriggers.ON_MOUSE_MOVE: no_args_event_spec, + EventTriggers.ON_MOUSE_OVER: no_args_event_spec, + EventTriggers.ON_MOUSE_OUT: no_args_event_spec, + EventTriggers.ON_MOUSE_ENTER: no_args_event_spec, + EventTriggers.ON_MOUSE_LEAVE: no_args_event_spec, } @@ -488,10 +488,10 @@ class Treemap(RechartsCharts): animation_easing: Var[LiteralAnimationEasing] # The customized event handler of animation start - on_animation_start: EventHandler[empty_event] + on_animation_start: EventHandler[no_args_event_spec] # The customized event handler of animation end - on_animation_end: EventHandler[empty_event] + on_animation_end: EventHandler[no_args_event_spec] @classmethod def create(cls, *children, **props) -> Component: diff --git a/reflex/components/recharts/general.py b/reflex/components/recharts/general.py index 641e1562a..124ef744d 100644 --- a/reflex/components/recharts/general.py +++ b/reflex/components/recharts/general.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List, Union from reflex.components.component import MemoizationLeaf from reflex.constants.colors import Color -from reflex.event import EventHandler, empty_event +from reflex.event import EventHandler, no_args_event_spec from reflex.vars.base import LiteralVar, Var from .recharts import ( @@ -46,7 +46,7 @@ class ResponsiveContainer(Recharts, MemoizationLeaf): debounce: Var[int] # If specified provides a callback providing the updated chart width and height values. - on_resize: EventHandler[empty_event] + on_resize: EventHandler[no_args_event_spec] # Valid children components _valid_children: List[str] = [ @@ -104,28 +104,28 @@ class Legend(Recharts): margin: Var[Dict[str, Any]] # The customized event handler of click on the items in this group - on_click: EventHandler[empty_event] + on_click: EventHandler[no_args_event_spec] # The customized event handler of mousedown on the items in this group - on_mouse_down: EventHandler[empty_event] + on_mouse_down: EventHandler[no_args_event_spec] # The customized event handler of mouseup on the items in this group - on_mouse_up: EventHandler[empty_event] + on_mouse_up: EventHandler[no_args_event_spec] # The customized event handler of mousemove on the items in this group - on_mouse_move: EventHandler[empty_event] + on_mouse_move: EventHandler[no_args_event_spec] # The customized event handler of mouseover on the items in this group - on_mouse_over: EventHandler[empty_event] + on_mouse_over: EventHandler[no_args_event_spec] # The customized event handler of mouseout on the items in this group - on_mouse_out: EventHandler[empty_event] + on_mouse_out: EventHandler[no_args_event_spec] # The customized event handler of mouseenter on the items in this group - on_mouse_enter: EventHandler[empty_event] + on_mouse_enter: EventHandler[no_args_event_spec] # The customized event handler of mouseleave on the items in this group - on_mouse_leave: EventHandler[empty_event] + on_mouse_leave: EventHandler[no_args_event_spec] class GraphingTooltip(Recharts): diff --git a/reflex/components/recharts/polar.py b/reflex/components/recharts/polar.py index ccb96f180..0aedf4893 100644 --- a/reflex/components/recharts/polar.py +++ b/reflex/components/recharts/polar.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List, Union from reflex.constants import EventTriggers from reflex.constants.colors import Color -from reflex.event import EventHandler, empty_event +from reflex.event import EventHandler, no_args_event_spec from reflex.vars.base import LiteralVar, Var from .recharts import ( @@ -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: 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, + EventTriggers.ON_ANIMATION_START: no_args_event_spec, + EventTriggers.ON_ANIMATION_END: no_args_event_spec, + EventTriggers.ON_CLICK: no_args_event_spec, + EventTriggers.ON_MOUSE_MOVE: no_args_event_spec, + EventTriggers.ON_MOUSE_OVER: no_args_event_spec, + EventTriggers.ON_MOUSE_OUT: no_args_event_spec, + EventTriggers.ON_MOUSE_ENTER: no_args_event_spec, + EventTriggers.ON_MOUSE_LEAVE: no_args_event_spec, } @@ -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: empty_event, - EventTriggers.ON_ANIMATION_END: empty_event, + EventTriggers.ON_ANIMATION_START: no_args_event_spec, + EventTriggers.ON_ANIMATION_END: no_args_event_spec, } @@ -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: 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, + EventTriggers.ON_CLICK: no_args_event_spec, + EventTriggers.ON_MOUSE_MOVE: no_args_event_spec, + EventTriggers.ON_MOUSE_OVER: no_args_event_spec, + EventTriggers.ON_MOUSE_OUT: no_args_event_spec, + EventTriggers.ON_MOUSE_ENTER: no_args_event_spec, + EventTriggers.ON_MOUSE_LEAVE: no_args_event_spec, + EventTriggers.ON_ANIMATION_START: no_args_event_spec, + EventTriggers.ON_ANIMATION_END: no_args_event_spec, } @@ -277,28 +277,28 @@ class PolarAngleAxis(Recharts): _valid_children: List[str] = ["Label"] # The customized event handler of click on the ticks of this axis. - on_click: EventHandler[empty_event] + on_click: EventHandler[no_args_event_spec] # The customized event handler of mousedown on the the ticks of this axis. - on_mouse_down: EventHandler[empty_event] + on_mouse_down: EventHandler[no_args_event_spec] # The customized event handler of mouseup on the ticks of this axis. - on_mouse_up: EventHandler[empty_event] + on_mouse_up: EventHandler[no_args_event_spec] # The customized event handler of mousemove on the ticks of this axis. - on_mouse_move: EventHandler[empty_event] + on_mouse_move: EventHandler[no_args_event_spec] # The customized event handler of mouseover on the ticks of this axis. - on_mouse_over: EventHandler[empty_event] + on_mouse_over: EventHandler[no_args_event_spec] # The customized event handler of mouseout on the ticks of this axis. - on_mouse_out: EventHandler[empty_event] + on_mouse_out: EventHandler[no_args_event_spec] # The customized event handler of moustenter on the ticks of this axis. - on_mouse_enter: EventHandler[empty_event] + on_mouse_enter: EventHandler[no_args_event_spec] # The customized event handler of mouseleave on the ticks of this axis. - on_mouse_leave: EventHandler[empty_event] + on_mouse_leave: EventHandler[no_args_event_spec] class PolarGrid(Recharts): @@ -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: 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_CLICK: no_args_event_spec, + EventTriggers.ON_MOUSE_MOVE: no_args_event_spec, + EventTriggers.ON_MOUSE_OVER: no_args_event_spec, + EventTriggers.ON_MOUSE_OUT: no_args_event_spec, + EventTriggers.ON_MOUSE_ENTER: no_args_event_spec, + EventTriggers.ON_MOUSE_LEAVE: no_args_event_spec, } diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py index 175c68f63..c226c7bd0 100644 --- a/reflex/components/sonner/toast.py +++ b/reflex/components/sonner/toast.py @@ -8,10 +8,7 @@ from reflex.base import Base from reflex.components.component import Component, ComponentNamespace from reflex.components.lucide.icon import Icon from reflex.components.props import NoExtrasAllowedProps, PropsBase -from reflex.event import ( - EventSpec, - call_script, -) +from reflex.event import EventSpec, run_script from reflex.style import Style, resolved_color_mode from reflex.utils import format from reflex.utils.imports import ImportVar @@ -260,7 +257,7 @@ class Toaster(Component): toast = f"{toast_command}(`{message}`)" toast_action = Var(_js_expr=toast) - return call_script(toast_action) + return run_script(toast_action) @staticmethod def toast_info(message: str = "", **kwargs): @@ -336,7 +333,7 @@ class Toaster(Component): dismiss_action = Var( _js_expr=dismiss, _var_data=VarData.merge(dismiss_var_data) ) - return call_script(dismiss_action) + return run_script(dismiss_action) @classmethod def create(cls, *children, **props) -> Component: diff --git a/reflex/components/suneditor/editor.py b/reflex/components/suneditor/editor.py index 3bca8a3f6..16d5689e2 100644 --- a/reflex/components/suneditor/editor.py +++ b/reflex/components/suneditor/editor.py @@ -7,7 +7,7 @@ from typing import Dict, List, Literal, Optional, Tuple, Union from reflex.base import Base from reflex.components.component import Component, NoSSRComponent -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.utils.format import to_camel_case from reflex.utils.imports import ImportDict, ImportVar from reflex.vars.base import Var @@ -207,31 +207,31 @@ class Editor(NoSSRComponent): disable_toolbar: Var[bool] # Fired when the editor content changes. - on_change: EventHandler[identity_event(str)] + on_change: EventHandler[passthrough_event_spec(str)] # Fired when the something is inputted in the editor. - on_input: EventHandler[empty_event] + on_input: EventHandler[no_args_event_spec] # Fired when the editor loses focus. on_blur: EventHandler[on_blur_spec] # Fired when the editor is loaded. - on_load: EventHandler[identity_event(bool)] + on_load: EventHandler[passthrough_event_spec(bool)] # Fired when the editor content is copied. - on_copy: EventHandler[empty_event] + on_copy: EventHandler[no_args_event_spec] # Fired when the editor content is cut. - on_cut: EventHandler[empty_event] + on_cut: EventHandler[no_args_event_spec] # Fired when the editor content is pasted. on_paste: EventHandler[on_paste_spec] # Fired when the code view is toggled. - toggle_code_view: EventHandler[identity_event(bool)] + toggle_code_view: EventHandler[passthrough_event_spec(bool)] # Fired when the full screen mode is toggled. - toggle_full_screen: EventHandler[identity_event(bool)] + toggle_full_screen: EventHandler[passthrough_event_spec(bool)] def add_imports(self) -> ImportDict: """Add imports for the Editor component. diff --git a/reflex/event.py b/reflex/event.py index c2e6955f6..245937a44 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -466,7 +466,7 @@ def key_event(e: Var[JavasciptKeyboardEvent]) -> Tuple[Var[str]]: return (e.key,) -def empty_event() -> Tuple[()]: +def no_args_event_spec() -> Tuple[()]: """Empty event handler. Returns: @@ -476,43 +476,14 @@ def empty_event() -> Tuple[()]: # 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 +stop_propagation = EventChain(events=[], args_spec=no_args_event_spec).stop_propagation +prevent_default = EventChain(events=[], args_spec=no_args_event_spec).prevent_default T = TypeVar("T") U = TypeVar("U") -# def identity_event(event_type: Type[T]) -> Callable[[Var[T]], Tuple[Var[T]]]: -# """A helper function that returns the input event as output. - -# Args: -# event_type: The type of the event. - -# Returns: -# A function that returns the input event as output. -# """ - -# def inner(ev: Var[T]) -> Tuple[Var[T]]: -# return (ev,) - -# inner.__signature__ = inspect.signature(inner).replace( # type: ignore -# parameters=[ -# inspect.Parameter( -# "ev", -# kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, -# annotation=Var[event_type], -# ) -# ], -# return_annotation=Tuple[Var[event_type]], -# ) -# inner.__annotations__["ev"] = Var[event_type] -# inner.__annotations__["return"] = Tuple[Var[event_type]] - -# return inner - - class IdentityEventReturn(Generic[T], Protocol): """Protocol for an identity event return.""" @@ -529,20 +500,22 @@ class IdentityEventReturn(Generic[T], Protocol): @overload -def identity_event(event_type: Type[T], /) -> Callable[[Var[T]], Tuple[Var[T]]]: ... # type: ignore +def passthrough_event_spec( + event_type: Type[T], / +) -> Callable[[Var[T]], Tuple[Var[T]]]: ... # type: ignore @overload -def identity_event( +def passthrough_event_spec( event_type_1: Type[T], event_type2: Type[U], / ) -> Callable[[Var[T], Var[U]], Tuple[Var[T], Var[U]]]: ... @overload -def identity_event(*event_types: Type[T]) -> IdentityEventReturn[T]: ... +def passthrough_event_spec(*event_types: Type[T]) -> IdentityEventReturn[T]: ... -def identity_event(*event_types: Type[T]) -> IdentityEventReturn[T]: # type: ignore +def passthrough_event_spec(*event_types: Type[T]) -> IdentityEventReturn[T]: # type: ignore """A helper function that returns the input event as output. Args: @@ -733,7 +706,16 @@ def console_log(message: str | Var[str]) -> EventSpec: Returns: An event to log the message. """ - return server_side("_console", get_fn_signature(console_log), message=message) + return run_script(Var("console").to(dict).log.to(FunctionVar).call(message)) + + +def noop() -> EventSpec: + """Do nothing. + + Returns: + An event to do nothing. + """ + return run_script(Var.create(None)) def back() -> EventSpec: @@ -742,7 +724,9 @@ def back() -> EventSpec: Returns: An event to go back one page. """ - return call_script("window.history.back()") + return run_script( + Var("window").to(dict).history.to(dict).back.to(FunctionVar).call() + ) def window_alert(message: str | Var[str]) -> EventSpec: @@ -754,7 +738,7 @@ def window_alert(message: str | Var[str]) -> EventSpec: Returns: An event to alert the message. """ - return server_side("_alert", get_fn_signature(window_alert), message=message) + return run_script(Var("window").to(dict).alert.to(FunctionVar).call(message)) def set_focus(ref: str) -> EventSpec: @@ -785,12 +769,12 @@ def scroll_to(elem_id: str, align_to_top: bool | Var[bool] = True) -> EventSpec: """ get_element_by_id = FunctionStringVar.create("document.getElementById") - return call_script( + return run_script( get_element_by_id(elem_id) .call(elem_id) .to(ObjectVar) .scrollIntoView.to(FunctionVar) - .call(align_to_top) + .call(align_to_top), ) @@ -897,10 +881,12 @@ def set_clipboard(content: str) -> EventSpec: Returns: EventSpec: An event to set some content in the clipboard. """ - return server_side( - "_set_clipboard", - get_fn_signature(set_clipboard), - content=content, + return run_script( + Var("navigator") + .to(dict) + .clipboard.to(dict) + .writeText.to(FunctionVar) + .call(content) ) @@ -987,13 +973,7 @@ 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: EventType | None = None, ) -> EventSpec: """Create an event handler that executes arbitrary javascript code. @@ -1007,12 +987,10 @@ def call_script( callback_kwargs = {} if callback is not None: callback_kwargs = { - "callback": str( - format.format_queue_events( - callback, - args_spec=lambda result: [result], - ), - ), + "callback": format.format_queue_events( + callback, + args_spec=lambda result: [result], + )._js_expr, } if isinstance(javascript_code, str): # When there is VarData, include it and eval the JS code inline on the client. @@ -1032,6 +1010,62 @@ def call_script( ) +def call_function( + javascript_code: str | Var, + callback: EventType | None = None, +) -> EventSpec: + """Create an event handler that executes arbitrary javascript code. + + Args: + javascript_code: The code to execute. + callback: EventHandler that will receive the result of evaluating the javascript code. + + Returns: + EventSpec: An event that will execute the client side javascript. + """ + callback_kwargs = {} + if callback is not None: + callback_kwargs = { + "callback": format.format_queue_events( + callback, + args_spec=lambda result: [result], + ), + } + + javascript_code = ( + Var(javascript_code) if isinstance(javascript_code, str) else javascript_code + ) + + return server_side( + "_call_function", + get_fn_signature(call_function), + function=javascript_code, + **callback_kwargs, + ) + + +def run_script( + javascript_code: str | Var, + callback: EventType | None = None, +) -> EventSpec: + """Create an event handler that executes arbitrary javascript code. + + Args: + javascript_code: The code to execute. + callback: EventHandler that will receive the result of evaluating the javascript code. + + Returns: + EventSpec: An event that will execute the client side javascript. + """ + javascript_code = ( + Var(javascript_code) if isinstance(javascript_code, str) else javascript_code + ) + + return call_function( + ArgsFunctionOperation.create(tuple(), javascript_code), callback + ) + + def get_event(state, event): """Get the event from the given state. @@ -1822,13 +1856,14 @@ class EventNamespace(types.SimpleNamespace): check_fn_match_arg_spec = staticmethod(check_fn_match_arg_spec) resolve_annotation = staticmethod(resolve_annotation) parse_args_spec = staticmethod(parse_args_spec) - identity_event = staticmethod(identity_event) + passthrough_event_spec = staticmethod(passthrough_event_spec) input_event = staticmethod(input_event) key_event = staticmethod(key_event) - empty_event = staticmethod(empty_event) + no_args_event_spec = staticmethod(no_args_event_spec) server_side = staticmethod(server_side) redirect = staticmethod(redirect) console_log = staticmethod(console_log) + noop = staticmethod(noop) back = staticmethod(back) window_alert = staticmethod(window_alert) set_focus = staticmethod(set_focus) @@ -1842,6 +1877,8 @@ class EventNamespace(types.SimpleNamespace): set_clipboard = staticmethod(set_clipboard) download = staticmethod(download) call_script = staticmethod(call_script) + call_function = staticmethod(call_function) + run_script = staticmethod(run_script) event = EventNamespace() diff --git a/reflex/experimental/client_state.py b/reflex/experimental/client_state.py index 74a25c2cd..ca14b8d2a 100644 --- a/reflex/experimental/client_state.py +++ b/reflex/experimental/client_state.py @@ -8,7 +8,7 @@ import sys from typing import Any, Callable, Union from reflex import constants -from reflex.event import EventChain, EventHandler, EventSpec, call_script +from reflex.event import EventChain, EventHandler, EventSpec, run_script from reflex.utils.imports import ImportVar from reflex.vars import ( VarData, @@ -227,7 +227,7 @@ class ClientStateVar(Var): """ if not self._global_ref: raise ValueError("ClientStateVar must be global to retrieve the value.") - return call_script(_client_state_ref(self._getter_name), callback=callback) + return run_script(_client_state_ref(self._getter_name), callback=callback) def push(self, value: Any) -> EventSpec: """Push a value to the client state variable from the backend. @@ -245,4 +245,4 @@ class ClientStateVar(Var): """ if not self._global_ref: raise ValueError("ClientStateVar must be global to push the value.") - return call_script(f"{_client_state_ref(self._setter_name)}({value})") + return run_script(f"{_client_state_ref(self._setter_name)}({value})") diff --git a/reflex/experimental/layout.py b/reflex/experimental/layout.py index a3b76581a..d203ce714 100644 --- a/reflex/experimental/layout.py +++ b/reflex/experimental/layout.py @@ -12,7 +12,7 @@ from reflex.components.radix.themes.components.icon_button import IconButton from reflex.components.radix.themes.layout.box import Box from reflex.components.radix.themes.layout.container import Container from reflex.components.radix.themes.layout.stack import HStack -from reflex.event import call_script +from reflex.event import run_script from reflex.experimental import hooks from reflex.state import ComponentState from reflex.style import Style @@ -173,7 +173,7 @@ class SidebarTrigger(Fragment): else: open, toggle = ( Var(_js_expr="open"), - call_script(Var(_js_expr="setOpen(!open)")), + run_script("setOpen(!open)"), ) trigger_props["left"] = cond(open, f"calc({sidebar_width} - 32px)", "0") diff --git a/reflex/utils/format.py b/reflex/utils/format.py index a914a585c..c4fbff20b 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -6,7 +6,7 @@ import inspect import json import os import re -from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union +from typing import TYPE_CHECKING, Any, List, Optional, Union from reflex import constants from reflex.utils import exceptions @@ -14,7 +14,7 @@ from reflex.utils.console import deprecate if TYPE_CHECKING: from reflex.components.component import ComponentStyle - from reflex.event import ArgsSpec, EventChain, EventHandler, EventSpec + from reflex.event import ArgsSpec, EventChain, EventHandler, EventSpec, EventType WRAP_MAP = { "{": "}", @@ -533,13 +533,7 @@ def format_event_chain( def format_queue_events( - events: ( - EventSpec - | EventHandler - | Callable - | List[EventSpec | EventHandler | Callable] - | None - ) = None, + events: EventType | None = None, args_spec: Optional[ArgsSpec] = None, ) -> Var[EventChain]: """Format a list of event handler / event spec as a javascript callback. diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index a614fd715..0574c007b 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -19,10 +19,10 @@ from reflex.constants import EventTriggers from reflex.event import ( EventChain, EventHandler, - empty_event, - identity_event, input_event, + no_args_event_spec, parse_args_spec, + passthrough_event_spec, ) from reflex.state import BaseState from reflex.style import Style @@ -111,10 +111,10 @@ def component2() -> Type[Component]: """ return { **super().get_event_triggers(), - "on_open": identity_event(bool), - "on_close": identity_event(bool), - "on_user_visited_count_changed": identity_event(int), - "on_user_list_changed": identity_event(List[str]), + "on_open": passthrough_event_spec(bool), + "on_close": passthrough_event_spec(bool), + "on_user_visited_count_changed": passthrough_event_spec(int), + "on_user_list_changed": passthrough_event_spec(List[str]), } def _get_imports(self) -> ParsedImportDict: @@ -1821,8 +1821,8 @@ def test_custom_component_declare_event_handlers_in_fields(): class TestComponent(Component): on_a: EventHandler[lambda e0: [e0]] on_b: EventHandler[input_event] - on_c: EventHandler[empty_event] - on_d: EventHandler[empty_event] + on_c: EventHandler[no_args_event_spec] + on_d: EventHandler[no_args_event_spec] on_e: EventHandler on_f: EventHandler[lambda a, b, c: [c, b, a]] diff --git a/tests/units/components/test_component_future_annotations.py b/tests/units/components/test_component_future_annotations.py index 44ec52c16..0867a2d37 100644 --- a/tests/units/components/test_component_future_annotations.py +++ b/tests/units/components/test_component_future_annotations.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any from reflex.components.component import Component -from reflex.event import EventHandler, empty_event, input_event +from reflex.event import EventHandler, input_event, no_args_event_spec # This is a repeat of its namesake in test_component.py. @@ -26,8 +26,8 @@ def test_custom_component_declare_event_handlers_in_fields(): class TestComponent(Component): on_a: EventHandler[lambda e0: [e0]] on_b: EventHandler[input_event] - on_c: EventHandler[empty_event] - on_d: EventHandler[empty_event] + on_c: EventHandler[no_args_event_spec] + on_d: EventHandler[no_args_event_spec] custom_component = ReferenceComponent.create() test_component = TestComponent.create() diff --git a/tests/units/test_event.py b/tests/units/test_event.py index d7b7cf7a2..5cefa5883 100644 --- a/tests/units/test_event.py +++ b/tests/units/test_event.py @@ -1,4 +1,4 @@ -from typing import List +from typing import Callable, List import pytest @@ -216,24 +216,40 @@ def test_event_console_log(): """Test the event console log function.""" spec = event.console_log("message") assert isinstance(spec, EventSpec) - assert spec.handler.fn.__qualname__ == "_console" - assert spec.args[0][0].equals(Var(_js_expr="message")) - assert spec.args[0][1].equals(LiteralVar.create("message")) - assert format.format_event(spec) == 'Event("_console", {message:"message"})' + assert spec.handler.fn.__qualname__ == "_call_function" + assert spec.args[0][0].equals(Var(_js_expr="function")) + assert spec.args[0][1].equals( + Var('(() => ((console["log"]("message"))))', _var_type=Callable) + ) + assert ( + format.format_event(spec) + == 'Event("_call_function", {function:(() => ((console["log"]("message"))))})' + ) spec = event.console_log(Var(_js_expr="message")) - assert format.format_event(spec) == 'Event("_console", {message:message})' + assert ( + format.format_event(spec) + == 'Event("_call_function", {function:(() => ((console["log"](message))))})' + ) def test_event_window_alert(): """Test the event window alert function.""" spec = event.window_alert("message") assert isinstance(spec, EventSpec) - assert spec.handler.fn.__qualname__ == "_alert" - assert spec.args[0][0].equals(Var(_js_expr="message")) - assert spec.args[0][1].equals(LiteralVar.create("message")) - assert format.format_event(spec) == 'Event("_alert", {message:"message"})' + assert spec.handler.fn.__qualname__ == "_call_function" + assert spec.args[0][0].equals(Var(_js_expr="function")) + assert spec.args[0][1].equals( + Var('(() => ((window["alert"]("message"))))', _var_type=Callable) + ) + assert ( + format.format_event(spec) + == 'Event("_call_function", {function:(() => ((window["alert"]("message"))))})' + ) spec = event.window_alert(Var(_js_expr="message")) - assert format.format_event(spec) == 'Event("_alert", {message:message})' + assert ( + format.format_event(spec) + == 'Event("_call_function", {function:(() => ((window["alert"](message))))})' + ) def test_set_focus(): From 16ed266d116257c9c58d4cb398058f5ce94e5880 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 4 Nov 2024 11:01:14 -0800 Subject: [PATCH 22/27] move check of path to only check name (#4299) * move check of path to only check name * assert .name in other tests as well * get even more ones --- tests/integration/test_upload.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/integration/test_upload.py b/tests/integration/test_upload.py index 813313462..7eaabf6a1 100644 --- a/tests/integration/test_upload.py +++ b/tests/integration/test_upload.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import time +from pathlib import Path from typing import Generator import pytest @@ -205,11 +206,12 @@ async def test_upload_file( file_data = await AppHarness._poll_for_async(get_file_data) assert isinstance(file_data, dict) - assert file_data[exp_name] == exp_contents + normalized_file_data = {Path(k).name: v for k, v in file_data.items()} + assert normalized_file_data[Path(exp_name).name] == exp_contents # check that the selected files are displayed selected_files = driver.find_element(By.ID, f"selected_files{suffix}") - assert selected_files.text == exp_name + assert Path(selected_files.text).name == Path(exp_name).name state = await upload_file.get_state(substate_token) if secondary: @@ -256,7 +258,9 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver): # check that the selected files are displayed selected_files = driver.find_element(By.ID, "selected_files") - assert selected_files.text == "\n".join(exp_files) + assert [Path(name).name for name in selected_files.text.split("\n")] == [ + Path(name).name for name in exp_files + ] # do the upload upload_button.click() @@ -271,8 +275,9 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver): file_data = await AppHarness._poll_for_async(get_file_data) assert isinstance(file_data, dict) + normalized_file_data = {Path(k).name: v for k, v in file_data.items()} for exp_name, exp_contents in exp_files.items(): - assert file_data[exp_name] == exp_contents + assert normalized_file_data[Path(exp_name).name] == exp_contents @pytest.mark.parametrize("secondary", [False, True]) @@ -317,7 +322,9 @@ def test_clear_files( # check that the selected files are displayed selected_files = driver.find_element(By.ID, f"selected_files{suffix}") - assert selected_files.text == "\n".join(exp_files) + assert [Path(name).name for name in selected_files.text.split("\n")] == [ + Path(name).name for name in exp_files + ] clear_button = driver.find_element(By.ID, f"clear_button{suffix}") assert clear_button @@ -369,6 +376,9 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive # look up the backend state and assert on progress state = await upload_file.get_state(substate_token) assert state.substates[state_name].progress_dicts - assert exp_name not in state.substates[state_name]._file_data + file_data = state.substates[state_name]._file_data + assert isinstance(file_data, dict) + normalized_file_data = {Path(k).name: v for k, v in file_data.items()} + assert Path(exp_name).name not in normalized_file_data target_file.unlink() From 51b0f7d28ed52d30581a8d16d6780fa722ade3c5 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 4 Nov 2024 11:06:24 -0800 Subject: [PATCH 23/27] special case field in _isinstance (#4298) --- reflex/utils/types.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/reflex/utils/types.py b/reflex/utils/types.py index d58825ed5..bcb52464a 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -570,6 +570,12 @@ def _isinstance(obj: Any, cls: GenericType, nested: bool = False) -> bool: _isinstance(item, args[0]) for item in obj ) + if args: + from reflex.vars import Field + + if origin is Field: + return _isinstance(obj, args[0]) + return isinstance(obj, get_base_class(cls)) From 702808afa656f2cbb6ecae1dcc7c3caaaa8def04 Mon Sep 17 00:00:00 2001 From: graham Date: Mon, 4 Nov 2024 12:36:12 -0700 Subject: [PATCH 24/27] Bugfix/leave gitignore as is (#4291) * Refactor initialize_gitignore to support list type for files_to_ignore and improve current ignore handling. Dont sort the gitignore file. * more consistent list comprehension var --- reflex/utils/prerequisites.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 19d4966bb..aba7714af 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -441,7 +441,7 @@ def create_config(app_name: str): def initialize_gitignore( gitignore_file: Path = constants.GitIgnore.FILE, - files_to_ignore: set[str] = constants.GitIgnore.DEFAULTS, + files_to_ignore: set[str] | list[str] = constants.GitIgnore.DEFAULTS, ): """Initialize the template .gitignore file. @@ -450,23 +450,20 @@ def initialize_gitignore( files_to_ignore: The files to add to the .gitignore file. """ # Combine with the current ignored files. - current_ignore: set[str] = set() + current_ignore: list[str] = [] if gitignore_file.exists(): - current_ignore |= set( - line.strip() for line in gitignore_file.read_text().splitlines() - ) + current_ignore = [ln.strip() for ln in gitignore_file.read_text().splitlines()] if files_to_ignore == current_ignore: console.debug(f"{gitignore_file} already up to date.") return - files_to_ignore |= current_ignore + files_to_ignore = [ln for ln in files_to_ignore if ln not in current_ignore] + files_to_ignore += current_ignore # Write files to the .gitignore file. gitignore_file.touch(exist_ok=True) console.debug(f"Creating {gitignore_file}") - gitignore_file.write_text( - "\n".join(sorted(files_to_ignore)) + "\n", - ) + gitignore_file.write_text("\n".join(files_to_ignore) + "\n") def initialize_requirements_txt(): From b3c199870e146e03c9dada8cec9546de4ba0386c Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Mon, 4 Nov 2024 22:05:39 +0100 Subject: [PATCH 25/27] bypass pydantic runtime validation for state init (#4256) * bypass pydantic runtime validation for state init closes #4235 * cleanup --- reflex/state.py | 8 ++++---- tests/units/test_state.py | 7 +++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 44ccebc30..6be3d1638 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -356,7 +356,6 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): def __init__( self, - *args, parent_state: BaseState | None = None, init_substates: bool = True, _reflex_internal_init: bool = False, @@ -367,11 +366,10 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): DO NOT INSTANTIATE STATE CLASSES DIRECTLY! Use StateManager.get_state() instead. Args: - *args: The args to pass to the Pydantic init method. parent_state: The parent state. init_substates: Whether to initialize the substates in this instance. _reflex_internal_init: A flag to indicate that the state is being initialized by the framework. - **kwargs: The kwargs to pass to the Pydantic init method. + **kwargs: The kwargs to set as attributes on the state. Raises: ReflexRuntimeError: If the state is instantiated directly by end user. @@ -384,7 +382,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): "See https://reflex.dev/docs/state/ for further information." ) kwargs["parent_state"] = parent_state - super().__init__(*args, **kwargs) + super().__init__() + for name, value in kwargs.items(): + setattr(self, name, value) # Setup the substates (for memory state manager only). if init_substates: diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 271f2e794..3ff8d453c 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -3404,3 +3404,10 @@ def test_fallback_pickle(): state3._g = (i for i in range(10)) pk3 = state3._serialize() assert len(pk3) == 0 + + +def test_typed_state() -> None: + class TypedState(rx.State): + field: rx.Field[str] = rx.field("") + + _ = TypedState(field="str") From 1122cbf0b10e800480ceebf74c208849b0cbc7a1 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 4 Nov 2024 14:37:58 -0800 Subject: [PATCH 26/27] handle none case in state setattr (#4301) --- reflex/state.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 6be3d1638..ca2728708 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1288,16 +1288,19 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): fields = self.get_fields() - if name in fields and not _isinstance( - value, (field_type := fields[name].outer_type_) - ): - console.deprecate( - "mismatched-type-assignment", - f"Tried to assign value {value} of type {type(value)} to field {type(self).__name__}.{name} of type {field_type}." - " This might lead to unexpected behavior.", - "0.6.5", - "0.7.0", - ) + if name in fields: + field = fields[name] + field_type = field.outer_type_ + if field.allow_none: + field_type = Union[field_type, None] + if not _isinstance(value, field_type): + console.deprecate( + "mismatched-type-assignment", + f"Tried to assign value {value} of type {type(value)} to field {type(self).__name__}.{name} of type {field_type}." + " This might lead to unexpected behavior.", + "0.6.5", + "0.7.0", + ) # Set the attribute. super().__setattr__(name, value) From bb903b605ab75a233f4ff2348621513ef33a3112 Mon Sep 17 00:00:00 2001 From: abulvenz Date: Mon, 4 Nov 2024 23:20:02 +0000 Subject: [PATCH 27/27] Fix wrong hook (#4295) * fix: Typo in variable name. * fix: Using existing hook constant. --------- Co-authored-by: Benedikt Bartscher --- reflex/components/core/upload.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index a2d86b910..58465b618 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -15,7 +15,7 @@ from reflex.components.el.elements.forms import Input from reflex.components.radix.themes.layout.box import Box from reflex.config import environment from reflex.constants import Dirs -from reflex.constants.compiler import Imports +from reflex.constants.compiler import Hooks, Imports from reflex.event import ( CallableEventSpec, EventChain, @@ -285,20 +285,18 @@ class Upload(MemoizationLeaf): format.to_camel_case(key): value for key, value in upload_props.items() } - use_dropzone_arguements = { + use_dropzone_arguments = { "onDrop": event_var, **upload_props, } left_side = f"const {{getRootProps: {root_props_unique_name}, getInputProps: {input_props_unique_name}}} " - right_side = f"useDropzone({str(Var.create(use_dropzone_arguements))})" + right_side = f"useDropzone({str(Var.create(use_dropzone_arguments))})" var_data = VarData.merge( VarData( imports=Imports.EVENTS, - hooks={ - "const [addEvents, connectError] = useContext(EventLoopContext);": None - }, + hooks={Hooks.EVENTS: None}, ), event_var._get_all_var_data(), VarData(