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 5923e33898..e350be5152 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -1389,7 +1389,7 @@ async def upload_file(request: Request, files: List[UploadFile]): 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 86620e65de..aa366e3bb0 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -83,7 +83,7 @@ def substate_token(self) -> str: 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 @@ def create( V4 = TypeVar("V4") V5 = TypeVar("V5") +background_event_decorator = background + if sys.version_info >= (3, 10): from typing import Concatenate @@ -1557,32 +1566,12 @@ def __get__(self, instance, owner) -> Callable: # type: ignore 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 e3d2371538..a2a5a0615d 100644 --- a/reflex/experimental/misc.py +++ b/reflex/experimental/misc.py @@ -7,7 +7,7 @@ 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 6e229b97dc..2704d58f24 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 @@ async def set_state( 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 a445112f3b..87aa1459bd 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 @@ class State(rx.State): _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 @@ async def handle_event(self): 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 @@ async def handle_event_yield_only(self): 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 reset_counter(self): 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 @@ async def racy_task(self): 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 @@ async def triple_count(self): 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 @@ async def yield_in_async_with_self(self): 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 f81e9f235b..338025bcdb 100644 --- a/tests/units/states/upload.py +++ b/tests/units/states/upload.py @@ -71,7 +71,7 @@ async def multi_handle_upload(self, files: List[rx.UploadFile]): 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 @@ async def multi_handle_upload(self, files: List[rx.UploadFile]): 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 @@ async def multi_handle_upload(self, files: List[rx.UploadFile]): 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 6bb81522f0..7fba7ba1d4 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 89dd1fd3d5..8397954cfd 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -1965,7 +1965,7 @@ def computed_order(self) -> List[str]: """ 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 @@ async def background_task(self): 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 @@ async def background_task_reset(self): 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 | 50 +++++++++++++++++++++++++++++++---------- 3 files changed, 104 insertions(+), 15 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 2704d58f24..7bdbcdc2bf 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -91,7 +91,7 @@ ) 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 @@ def _evaluate( 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 @@ def __setattr__(self, name: str, value: Any): 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 3d79920112..baedcc5a01 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 2f26e9170d..78862aa17e 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -63,7 +63,14 @@ 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 @@ def __init__( "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 @@ def __get__(self, instance: BaseState | None, owner): ) if not self._cache: - return self.fget(instance) - - # 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) + 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) + + 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 908482d240..624e3bee86 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 e577df67d0..7d76b080ac 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 ada511ef2b..c70b4c844c 100644 --- a/reflex/components/base/bare.py +++ b/reflex/components/base/bare.py @@ -4,10 +4,11 @@ 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 @@ def create(cls, contents: Any) -> 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 5c62347492..399becee92 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 @@ 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 @@ def create(cls, *children, **props) -> 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 c454cba7d8..787cca9d08 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -5,11 +5,17 @@ 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 @@ 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 @@ def create(cls, *children, **props) -> Component: # 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 @@ def create(cls, *children, **props) -> Component: 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 @@ def create(cls, *children, **props) -> Component: ), ) 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 @@ def _update_arg_tuple_for_on_drop(cls, arg_value: tuple[Var, Var]): 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 43ce3b63d5..e5c73bd147 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 e391c9fd01..c0e172224c 100644 --- a/reflex/components/dynamic.py +++ b/reflex/components/dynamic.py @@ -63,6 +63,9 @@ def make_component(component: Component) -> str: """ # 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 make_component(component: Component) -> str: 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 d577abc6e1..0587c61eda 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 a020a7e15e..3bfcf6e8c0 100644 --- a/tests/integration/test_form_submit.py +++ b/tests/integration/test_form_submit.py @@ -121,7 +121,7 @@ def index(): 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 919a39f3b3..cae56e1a8d 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 3b2ee014f2..0000000000 --- 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 b7b721a924..c2d73aca52 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 78862aa17e..b06e7b7c97 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -619,10 +619,6 @@ def to( """ 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 25d5eaac41..d428c16e68 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 0d99eda27d..bf0e5c454a 100644 --- a/reflex/components/radix/themes/components/slider.py +++ b/reflex/components/radix/themes/components/slider.py @@ -16,8 +16,8 @@ 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 dec8368350..8939ea23e5 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 e350be5152..88fc9d4734 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -929,6 +929,8 @@ def get_compilation_time() -> str: ) 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 7bdbcdc2bf..cc9dda05be 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -105,6 +105,15 @@ # 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 @@ def _serialize(self) -> bytes: """ 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 @@ def _serialize(self) -> bytes: 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 8397954cfd..fe2f652aca 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -8,6 +8,7 @@ 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 @@ class DillState(BaseState): 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 88fc9d4734..5367ef20b4 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -46,7 +46,6 @@ 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.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 @@ def add_page( 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 @@ def _compile_page(self, route: str): 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 @@ def add_custom_404_page( 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 ef882a2ead..52bf0be1db 100644 --- a/reflex/app_mixins/lifespan.py +++ b/reflex/app_mixins/lifespan.py @@ -4,6 +4,7 @@ import asyncio import contextlib +import dataclasses import functools import inspect from typing import Callable, Coroutine, Set, Union @@ -16,11 +17,14 @@ 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 1e42faf180..30593d9aec 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 .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 ed301c4959..23207a4629 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 52f0c8efcc..87a8c49c22 100644 --- a/reflex/page.py +++ b/reflex/page.py @@ -6,6 +6,7 @@ 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 7fba7ba1d4..1e34a67c39 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 fe2f652aca..271f2e7941 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 c847428512..154ec0d13f 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 2d13180bcd..be4517f44a 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 489a9bcc53..1284b8050b 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 e5c73bd147..a63854ef22 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 6402aaf423..558333cec8 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 2f889bc382..bc9bc9689a 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 415d0f049d..16221ff63f 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 5d72584a9a..405f8ac528 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 29464415d6..186b78a688 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 b975cfebe1..abf7283c64 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 6fe4d10dd2..d478dc171f 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 @@ def create(cls, *children: Any, **props: Any) -> Component: """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 c4493ee9ba..ea2dd8dcf4 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 2139f0c23a..c4dce0a364 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 3d57fbe5ca..03a30f164b 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 d856b0bbd2..76bffcdf59 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 771def7d91..f4b674ce2b 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 90a9220efe..8f69a0f731 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 56d8200e0a..bfb88b3039 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 0713461e97..a277fc7759 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 8de273be90..ccb2ef3471 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 fa169c852f..50c646971c 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 2183925173..35f6854f60 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 d2f6a1425b..ac6a645a96 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 f928421c50..5ff5d6e7c6 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 cb1990a7c8..d74c26be99 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 e0e184482b..d43ac73275 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 8939ea23e5..5ac3c275f9 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 f8871872ac..aebeb761e6 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 4272bf2a36..ac61092094 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 f234d91503..099a1700a0 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 7ea0860b59..ffe827aff2 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 e78dd926d8..42875e1e53 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 1841829af9..5c8cc85df2 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 e4027cf401..ebae93f713 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 a05e3747bd..4a6bb81b30 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 9772be792b..983de30e3c 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 f0b494ff85..2c6d7fffa0 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 4a8f61c5e2..affe362bbe 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 c025960064..025f8443de 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 73dd38fdcc..3734d2f16e 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 1fc17341b8..667015768e 100644 --- a/reflex/utils/pyi_generator.py +++ b/reflex/utils/pyi_generator.py @@ -568,6 +568,7 @@ def figure_out_return_type(annotation: Any): ) 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 figure_out_return_type(annotation: Any): 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 65c0f049bf..a914a585c6 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 | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/reflex/event.py b/reflex/event.py index aa366e3bb0..14249091eb 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();" - - return call_script(js_code) + get_element_by_id = FunctionStringVar.create("document.getElementById") + + 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 c655c3a607..22b3693b4b 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 6ce6916c66..07f09c6f6e 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.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 @@ def copy_script() -> Any: "pascal", "perl", "php", + "plain", "plsql", "po", "postcss", @@ -369,10 +371,11 @@ def copy_script() -> Any: "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 @@ def copy_script() -> Any: ] +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 @@ def create( # 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 @@ def create( 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 bcf2719e92..e2475aed5e 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 92dbe8144e..adce134fcd 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 @@ def dict(self, *args, **kwargs): 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 65f1157d37..175c68f630 100644 --- a/reflex/components/sonner/toast.py +++ b/reflex/components/sonner/toast.py @@ -4,12 +4,10 @@ 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 @@ def dict(self, *args, **kwargs) -> dict[str, Any]: ) 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 @@ def send_toast(message: str = "", level: str | None = None, **props) -> EventSpe 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 976f9f3104..10168257eb 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 f163385138..6d4a0bbc7c 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 0000000000..8ab07f1355 --- /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 1caf053b5f..27ca62d934 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 @@ def create(cls, *children, **props) -> Component: 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 558333cec8..b1ff93c386 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 399becee92..85db3906dc 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -480,6 +480,7 @@ def __init__(self, *args, **kwargs): 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 @@ def _create_event_chain( 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 @@ def _create_event_chain( 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 @@ def _create_event_chain( 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 @@ def _create_event_chain( 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 @@ def __init__(self, *args, **kwargs): 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 7cb776ee9a..4caf14b414 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 bc9bc9689a..a8e9b6174a 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 c4dce0a364..72595a9338 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 bf0e5c454a..bb017ea736 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 @@ 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 5ac3c275f9..b2f155fe6b 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 14249091eb..c2e6955f61 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -29,8 +29,12 @@ 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 @@ def create( 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 6d4a0bbc7c..661f290952 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 667015768e..342277cadd 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 figure_out_return_type(annotation: Any): # 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 figure_out_return_type(annotation: Any): 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 figure_out_return_type(annotation: Any): ( 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 baedcc5a01..d58825ed55 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -774,3 +774,69 @@ def wrapper(*args, **kwargs): # 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 c2d73aca52..a614fd7152 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -20,13 +20,17 @@ 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 do_something(self): 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 get_event_triggers(self) -> Dict[str, Any]: """ 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 81579acc77..dd88138bfd 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -2,7 +2,7 @@ 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 9c965b00f0..527cca9809 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 3b4fa39d1b..53761284a7 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 @@ def _get_custom_code(self) -> Optional[str]: 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 @@ def create( """ # 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 @@ def _render(self): 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 0bf3dd9561..1e4f7110c4 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 bbcecb1d8c..2722747232 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 @@ def add_style(self) -> dict[str, Any] | None: }, } + 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 abf7283c64..c0047442bb 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 b1083ba94d..a01d40e075 100644 --- a/reflex/components/radix/themes/color_mode.py +++ b/reflex/components/radix/themes/color_mode.py @@ -17,7 +17,7 @@ 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 @@ def color_mode_item(_color_mode): 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 76bffcdf59..a5b7b31ecc 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 bb017ea736..4f456cdca3 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 b2f155fe6b..f77573d442 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 699028380b..d83fd168b1 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 @@ def add_style(self) -> dict[str, Any] | None: "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 b72afbaa76..1ce6359a46 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 94bba4fb67..d11c3488bc 100644 --- a/reflex/components/radix/themes/layout/stack.py +++ b/reflex/components/radix/themes/layout/stack.py @@ -12,20 +12,22 @@ 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 @@ def create( 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 5eed3db460..c4b475218c 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 d478dc171f..dca6bb7e13 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 @@ def _get_style(self) -> dict: 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 ea2dd8dcf4..3f43401a40 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 f3c8ec319f..12ac64b90d 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 f4b674ce2b..543497f4b4 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 3eb54a4577..b3f55f8ba5 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 @@ 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, 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 aligns preferences to prevent collisions with boundary edges. + # 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 bfb88b3039..f7eb4682a4 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 e8da506edf..840a50ecbe 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 a277fc7759..a6d8e24f71 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 885e8df357..ee90405018 100644 --- a/reflex/components/radix/themes/components/dropdown_menu.py +++ b/reflex/components/radix/themes/components/dropdown_menu.py @@ -23,7 +23,6 @@ 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 ccb2ef3471..980a33bb5e 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 e76184795c..10002c7e6f 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 50c646971c..e8b5b43ee4 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 2535a8a220..6997ec5c5b 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 35f6854f60..dfe7a3a68c 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 dcdac5b5d7..f0e94a62d1 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 cc9dda05be..ee0aa3e93d 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -104,6 +104,8 @@ # 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 @@ def __getstate__(self): 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 @@ def _serialize(self) -> bytes: 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 @@ async def get_state( 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 @@ async def set_state( # 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 ee0aa3e93d..44ccebc308 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -8,6 +8,7 @@ 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 bdc9be4aed..467c5fa2ce 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 d70e7706f7..19d4966bb4 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 7d76b080ac..66df09ee11 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 ffc4426f9d..acba5936aa 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -303,10 +303,13 @@ "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 aa1c92b727..a0f60ea57b 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 eb37d53e7b..15145ecbfe 100644 --- a/reflex/components/base/script.py +++ b/reflex/components/base/script.py @@ -8,7 +8,7 @@ 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 85db3906dc..470ba1145a 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -47,8 +47,8 @@ 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 @@ def get_event_triggers(self) -> Dict[str, Any]: """ 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 @@ def get_event_triggers(self) -> Dict[str, Any]: 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 @@ def __init__(self, *args, **kwargs): 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 cce0af4a72..6d6a38acce 100644 --- a/reflex/components/core/clipboard.py +++ b/reflex/components/core/clipboard.py @@ -6,7 +6,7 @@ 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 1284b8050b..e51bad112b 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 86efb7dcdd..12cc94426f 100644 --- a/reflex/components/core/debounce.py +++ b/reflex/components/core/debounce.py @@ -6,7 +6,7 @@ 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 787cca9d08..a2d86b9102 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -22,8 +22,8 @@ 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 27ca62d934..860f75007d 100644 --- a/reflex/components/datadisplay/dataeditor.py +++ b/reflex/components/datadisplay/dataeditor.py @@ -10,7 +10,7 @@ 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 07f09c6f6e..21f22424da 100644 --- a/reflex/components/datadisplay/shiki_code_block.py +++ b/reflex/components/datadisplay/shiki_code_block.py @@ -14,7 +14,7 @@ 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 @@ def copy_script() -> Any: }} else {{ // console.error('Parent element not found.'); }} -}}); +}}) """ ) diff --git a/reflex/components/moment/moment.py b/reflex/components/moment/moment.py index 4ac835b35d..d5d6d8f7cf 100644 --- a/reflex/components/moment/moment.py +++ b/reflex/components/moment/moment.py @@ -4,7 +4,7 @@ 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 fe74b09356..237c308ce4 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 dca6bb7e13..f99342a587 100644 --- a/reflex/components/radix/primitives/drawer.py +++ b/reflex/components/radix/primitives/drawer.py @@ -10,7 +10,7 @@ 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 @@ def _get_style(self) -> dict: 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 4d4be7e40f..f2b8201ad5 100644 --- a/reflex/components/radix/primitives/form.py +++ b/reflex/components/radix/primitives/form.py @@ -8,7 +8,7 @@ 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 12ac64b90d..36d38532c2 100644 --- a/reflex/components/radix/themes/components/alert_dialog.py +++ b/reflex/components/radix/themes/components/alert_dialog.py @@ -5,7 +5,7 @@ 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 2944b1f11d..1460382f5a 100644 --- a/reflex/components/radix/themes/components/checkbox.py +++ b/reflex/components/radix/themes/components/checkbox.py @@ -6,7 +6,7 @@ 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 b3f55f8ba5..7b2032a428 100644 --- a/reflex/components/radix/themes/components/context_menu.py +++ b/reflex/components/radix/themes/components/context_menu.py @@ -4,7 +4,7 @@ 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 840a50ecbe..5d33cbc5f1 100644 --- a/reflex/components/radix/themes/components/dialog.py +++ b/reflex/components/radix/themes/components/dialog.py @@ -5,7 +5,7 @@ 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 ee90405018..0d2ac38e2e 100644 --- a/reflex/components/radix/themes/components/dropdown_menu.py +++ b/reflex/components/radix/themes/components/dropdown_menu.py @@ -4,7 +4,7 @@ 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 10002c7e6f..6babb10d2e 100644 --- a/reflex/components/radix/themes/components/hover_card.py +++ b/reflex/components/radix/themes/components/hover_card.py @@ -5,7 +5,7 @@ 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 6997ec5c5b..bcafe6d385 100644 --- a/reflex/components/radix/themes/components/popover.py +++ b/reflex/components/radix/themes/components/popover.py @@ -5,7 +5,7 @@ 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 e0aa2a7490..e075a1ba28 100644 --- a/reflex/components/radix/themes/components/radio_cards.py +++ b/reflex/components/radix/themes/components/radio_cards.py @@ -4,7 +4,7 @@ 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 a55ca3a413..df3843cf90 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.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 47a1eaf3f2..f71276c8b1 100644 --- a/reflex/components/radix/themes/components/select.py +++ b/reflex/components/radix/themes/components/select.py @@ -5,7 +5,7 @@ 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 4f456cdca3..d833ef7820 100644 --- a/reflex/components/radix/themes/components/slider.py +++ b/reflex/components/radix/themes/components/slider.py @@ -6,7 +6,7 @@ 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 @@ ) 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 f77573d442..0d1b17e5f6 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 13be32d835..dea4913aa7 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 12359b5282..ac04c1a3db 100644 --- a/reflex/components/radix/themes/components/tabs.py +++ b/reflex/components/radix/themes/components/tabs.py @@ -7,7 +7,7 @@ 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 ac35c86d12..3bc61f545f 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 b2c58b754e..fb0319ceb3 100644 --- a/reflex/components/react_player/react_player.py +++ b/reflex/components/react_player/react_player.py @@ -5,7 +5,7 @@ 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 865b50a325..028bcb4e4c 100644 --- a/reflex/components/recharts/cartesian.py +++ b/reflex/components/recharts/cartesian.py @@ -6,7 +6,7 @@ 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 @@ def get_event_triggers(self) -> dict[str, Union[Var, Any]]: 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 d7e07b2d83..13f1252136 100644 --- a/reflex/components/recharts/charts.py +++ b/reflex/components/recharts/charts.py @@ -8,7 +8,7 @@ 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 @@ def get_event_triggers(self) -> dict[str, Union[Var, Any]]: 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 @@ def get_event_triggers(self) -> dict[str, Union[Var, Any]]: 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 641e1562ae..124ef744de 100644 --- a/reflex/components/recharts/general.py +++ b/reflex/components/recharts/general.py @@ -6,7 +6,7 @@ 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 ccb96f1806..0aedf48931 100644 --- a/reflex/components/recharts/polar.py +++ b/reflex/components/recharts/polar.py @@ -6,7 +6,7 @@ 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 @@ def get_event_triggers(self) -> dict[str, Union[Var, Any]]: 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 @@ def get_event_triggers(self) -> dict[str, Union[Var, Any]]: 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 @@ def get_event_triggers(self) -> dict[str, Union[Var, Any]]: 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 @@ def get_event_triggers(self) -> dict[str, Union[Var, Any]]: 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 175c68f630..c226c7bd03 100644 --- a/reflex/components/sonner/toast.py +++ b/reflex/components/sonner/toast.py @@ -8,10 +8,7 @@ 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 @@ def send_toast(message: str = "", level: str | None = None, **props) -> EventSpe 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 @@ def toast_dismiss(id: Var | str | None = None): 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 3bca8a3f65..16d5689e21 100644 --- a/reflex/components/suneditor/editor.py +++ b/reflex/components/suneditor/editor.py @@ -7,7 +7,7 @@ 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 c2e6955f61..245937a44e 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 @@ def __call__(self, *values: Var[T]) -> Tuple[Var[T], ...]: @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 @@ def wrapper(func: Callable[P, T]) -> Callable[P, T]: 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 @@ def wrapper(func: Callable[P, T]) -> Callable[P, T]: 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 74a25c2cdd..ca14b8d2ad 100644 --- a/reflex/experimental/client_state.py +++ b/reflex/experimental/client_state.py @@ -8,7 +8,7 @@ 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 @@ def retrieve( """ 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 @@ def push(self, value: Any) -> EventSpec: """ 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 a3b76581a9..d203ce7147 100644 --- a/reflex/experimental/layout.py +++ b/reflex/experimental/layout.py @@ -12,7 +12,7 @@ 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 @@ def create(cls, sidebar: Component, **props): 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 a914a585c6..c4fbff20bb 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -6,7 +6,7 @@ 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 @@ 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 a614fd7152..0574c007b4 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -19,10 +19,10 @@ 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 get_event_triggers(self) -> Dict[str, Any]: """ 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 get_event_triggers(self) -> Dict[str, Any]: 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 44ec52c16e..0867a2d378 100644 --- a/tests/units/components/test_component_future_annotations.py +++ b/tests/units/components/test_component_future_annotations.py @@ -3,7 +3,7 @@ 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 get_event_triggers(self) -> dict[str, Any]: 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 d7b7cf7a2c..5cefa58834 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 813313462e..7eaabf6a19 100644 --- a/tests/integration/test_upload.py +++ b/tests/integration/test_upload.py @@ -4,6 +4,7 @@ import asyncio import time +from pathlib import Path from typing import Generator import pytest @@ -205,11 +206,12 @@ async def get_file_data(): 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 get_file_data(): 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 d58825ed55..bcb52464a1 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 19d4966bb4..aba7714afe 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 44ccebc308..6be3d16385 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 @@ def __init__( 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 @@ def __init__( "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 271f2e7941..3ff8d453cc 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -3404,3 +3404,10 @@ class DillState(BaseState): 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 6be3d16385..ca2728708c 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1288,16 +1288,19 @@ def __setattr__(self, name: str, value: Any): 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 a2d86b9102..58465b618b 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -15,7 +15,7 @@ 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 @@ def create(cls, *children, **props) -> Component: 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(