From 0d9fc53a7df8720790cc5db83476b40eaa0d7e04 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 4 Nov 2024 10:11:04 -0800 Subject: [PATCH 01/18] [REF-3961] move "warn_if_too_large" logic into BaseState (#4284) Check for too large serialized state whenever `BaseState._serialize` is used, so it can apply to all state managers, not just `StateManagerRedis`. --- reflex/state.py | 54 ++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index cc9dda05b..ee0aa3e93 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -104,6 +104,8 @@ var = computed_var # If the state is this large, it's considered a performance issue. TOO_LARGE_SERIALIZED_STATE = 100 * 1024 # 100kb +# Only warn about each state class size once. +_WARNED_ABOUT_STATE_SIZE: Set[str] = set() # Errors caught during pickling of state HANDLED_PICKLE_ERRORS = ( @@ -2046,6 +2048,27 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): state["__dict__"].pop(inherited_var_name, None) return state + def _warn_if_too_large( + self, + pickle_state_size: int, + ): + """Print a warning when the state is too large. + + Args: + pickle_state_size: The size of the pickled state. + """ + state_full_name = self.get_full_name() + if ( + state_full_name not in _WARNED_ABOUT_STATE_SIZE + and pickle_state_size > TOO_LARGE_SERIALIZED_STATE + and self.substates + ): + console.warn( + f"State {state_full_name} serializes to {pickle_state_size} bytes " + "which may present performance issues. Consider reducing the size of this state." + ) + _WARNED_ABOUT_STATE_SIZE.add(state_full_name) + @classmethod @functools.lru_cache() def _to_schema(cls) -> str: @@ -2084,7 +2107,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): The serialized state. """ try: - return pickle.dumps((self._to_schema(), self)) + pickle_state = pickle.dumps((self._to_schema(), self)) + self._warn_if_too_large(len(pickle_state)) + return pickle_state except HANDLED_PICKLE_ERRORS as og_pickle_error: error = ( f"Failed to serialize state {self.get_full_name()} due to unpicklable object. " @@ -3075,9 +3100,6 @@ class StateManagerRedis(StateManager): b"evicted", } - # Only warn about each state class size once. - _warned_about_state_size: ClassVar[Set[str]] = set() - async def _get_parent_state( self, token: str, state: BaseState | None = None ) -> BaseState | None: @@ -3221,29 +3243,6 @@ class StateManagerRedis(StateManager): return state._get_root_state() return state - def _warn_if_too_large( - self, - state: BaseState, - pickle_state_size: int, - ): - """Print a warning when the state is too large. - - Args: - state: The state to check. - pickle_state_size: The size of the pickled state. - """ - state_full_name = state.get_full_name() - if ( - state_full_name not in self._warned_about_state_size - and pickle_state_size > TOO_LARGE_SERIALIZED_STATE - and state.substates - ): - console.warn( - f"State {state_full_name} serializes to {pickle_state_size} bytes " - "which may present performance issues. Consider reducing the size of this state." - ) - self._warned_about_state_size.add(state_full_name) - @override async def set_state( self, @@ -3294,7 +3293,6 @@ class StateManagerRedis(StateManager): # Persist only the given state (parents or substates are excluded by BaseState.__getstate__). if state._get_was_touched(): pickle_state = state._serialize() - self._warn_if_too_large(state, len(pickle_state)) if pickle_state: await self.redis.set( _substate_key(client_token, state), From 163acf70a280e63c59237856b6ccf1811bd0e34b Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 4 Nov 2024 10:11:51 -0800 Subject: [PATCH 02/18] [ENG-759] [ENG-1104] patch `json.dumps` to handle `__wrapped__` objects (#4166) * [ENG-1104] patch `json.dumps` to handle `__wrapped__` objects Unwrap proxied objects if the default serializer doesn't work. * pre-commit fixup * Skip default fallback logic when `cls` is specified `cls` will provide its own default serialization mechanism, passing a `cls` Encoder class is now also a way to opt-out of our patching shenanigans and just use your own code. This will work, provided the library doing the JSON encoding isn't also using their own custom class. * Override JSONEncoder.default instead of json.dumps Many libraries (like httpx, used by openai), will use `from json import dumps`, and if they do that before `reflex.state` gets imported, then they will get the original dumps function instead of our patched one. To workaround this, monkeypatch the `JSONEncoder.default` function instead. This is also nicer behavior for custom subclasses of JSONEncoder; if someone wants to opt-out of our monkeypatching, they can simply not call `super().default(o)` in their subclass, which by default only raises a TypeError. --------- Co-authored-by: Nikhil Rao --- reflex/state.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/reflex/state.py b/reflex/state.py index ee0aa3e93..44ccebc30 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -8,6 +8,7 @@ import copy import dataclasses import functools import inspect +import json import pickle import sys import uuid @@ -3713,6 +3714,29 @@ def serialize_mutable_proxy(mp: MutableProxy): return mp.__wrapped__ +_orig_json_JSONEncoder_default = json.JSONEncoder.default + + +def _json_JSONEncoder_default_wrapper(self: json.JSONEncoder, o: Any) -> Any: + """Wrap JSONEncoder.default to handle MutableProxy objects. + + Args: + self: the JSONEncoder instance. + o: the object to serialize. + + Returns: + A JSON-able object. + """ + try: + return o.__wrapped__ + except AttributeError: + pass + return _orig_json_JSONEncoder_default(self, o) + + +json.JSONEncoder.default = _json_JSONEncoder_default_wrapper + + class ImmutableMutableProxy(MutableProxy): """A proxy for a mutable object that tracks changes. From 6394a6dfc5eda88fd8a8d47b63ca8fc9aa0150c4 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 4 Nov 2024 10:31:24 -0800 Subject: [PATCH 03/18] raise error when get package manager is not found (#4289) * raise error when get package manager is not found * add escape hatch * handle installing frontend packages more gracefully * fix no return * dang it darglint --- reflex/utils/exec.py | 6 ++-- reflex/utils/prerequisites.py | 56 +++++++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index bdc9be4ae..467c5fa2c 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -467,9 +467,11 @@ def output_system_info(): console.debug(f"{dep}") console.debug( - f"Using package installer at: {prerequisites.get_install_package_manager()}" # type: ignore + f"Using package installer at: {prerequisites.get_install_package_manager(on_failure_return_none=True)}" # type: ignore ) - console.debug(f"Using package executer at: {prerequisites.get_package_manager()}") # type: ignore + console.debug( + f"Using package executer at: {prerequisites.get_package_manager(on_failure_return_none=True)}" + ) # type: ignore if system != "Windows": console.debug(f"Unzip path: {path_ops.which('unzip')}") diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index d70e7706f..19d4966bb 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -204,10 +204,13 @@ def get_bun_version() -> version.Version | None: return None -def get_install_package_manager() -> str | None: +def get_install_package_manager(on_failure_return_none: bool = False) -> str | None: """Get the package manager executable for installation. Currently, bun is used for installation only. + Args: + on_failure_return_none: Whether to return None on failure. + Returns: The path to the package manager. """ @@ -217,21 +220,29 @@ def get_install_package_manager() -> str | None: or windows_check_onedrive_in_path() or windows_npm_escape_hatch() ): - return get_package_manager() + return get_package_manager(on_failure_return_none) return str(get_config().bun_path) -def get_package_manager() -> str | None: +def get_package_manager(on_failure_return_none: bool = False) -> str | None: """Get the package manager executable for running app. Currently on unix systems, npm is used for running the app only. + Args: + on_failure_return_none: Whether to return None on failure. + Returns: The path to the package manager. + + Raises: + FileNotFoundError: If the package manager is not found. """ npm_path = path_ops.get_npm_path() if npm_path is not None: - npm_path = str(Path(npm_path).resolve()) - return npm_path + return str(Path(npm_path).resolve()) + if on_failure_return_none: + return None + raise FileNotFoundError("NPM not found. You may need to run `reflex init`.") def windows_check_onedrive_in_path() -> bool: @@ -920,20 +931,39 @@ def install_frontend_packages(packages: set[str], config: Config): packages: A list of package names to be installed. config: The config object. + Raises: + FileNotFoundError: If the package manager is not found. + Example: >>> install_frontend_packages(["react", "react-dom"], get_config()) """ # unsupported archs(arm and 32bit machines) will use npm anyway. so we dont have to run npm twice fallback_command = ( - get_package_manager() - if not constants.IS_WINDOWS - or constants.IS_WINDOWS - and is_windows_bun_supported() - and not windows_check_onedrive_in_path() + get_package_manager(on_failure_return_none=True) + if ( + not constants.IS_WINDOWS + or constants.IS_WINDOWS + and is_windows_bun_supported() + and not windows_check_onedrive_in_path() + ) else None ) + + install_package_manager = ( + get_install_package_manager(on_failure_return_none=True) or fallback_command + ) + + if install_package_manager is None: + raise FileNotFoundError( + "Could not find a package manager to install frontend packages. You may need to run `reflex init`." + ) + + fallback_command = ( + fallback_command if fallback_command is not install_package_manager else None + ) + processes.run_process_with_fallback( - [get_install_package_manager(), "install"], # type: ignore + [install_package_manager, "install"], # type: ignore fallback=fallback_command, analytics_enabled=True, show_status_message="Installing base frontend packages", @@ -944,7 +974,7 @@ def install_frontend_packages(packages: set[str], config: Config): if config.tailwind is not None: processes.run_process_with_fallback( [ - get_install_package_manager(), + install_package_manager, "add", "-d", constants.Tailwind.VERSION, @@ -960,7 +990,7 @@ def install_frontend_packages(packages: set[str], config: Config): # Install custom packages defined in frontend_packages if len(packages) > 0: processes.run_process_with_fallback( - [get_install_package_manager(), "add", *packages], + [install_package_manager, "add", *packages], fallback=fallback_command, analytics_enabled=True, show_status_message="Installing frontend packages from config and components", From ca81e623db25fb94c70ee5a2d9bbe47ede7d78e8 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 4 Nov 2024 10:33:07 -0800 Subject: [PATCH 04/18] add noop event (#4288) * add noop event * fix pyi * get it right pyright * why * remove silly events * fix tests * remove semi colon * errors in merging --- reflex/.templates/web/utils/state.js | 35 ++-- reflex/__init__.py | 3 + reflex/__init__.pyi | 3 + reflex/components/base/script.py | 8 +- reflex/components/component.py | 36 ++-- reflex/components/core/clipboard.py | 4 +- reflex/components/core/clipboard.pyi | 4 +- reflex/components/core/debounce.py | 4 +- reflex/components/core/upload.py | 8 +- reflex/components/datadisplay/dataeditor.py | 36 ++-- .../datadisplay/shiki_code_block.py | 6 +- reflex/components/moment/moment.py | 4 +- reflex/components/next/image.py | 6 +- reflex/components/radix/primitives/drawer.py | 16 +- reflex/components/radix/primitives/form.py | 4 +- .../radix/themes/components/alert_dialog.py | 10 +- .../radix/themes/components/checkbox.py | 6 +- .../radix/themes/components/context_menu.py | 26 +-- .../radix/themes/components/dialog.py | 14 +- .../radix/themes/components/dropdown_menu.py | 26 +-- .../radix/themes/components/hover_card.py | 4 +- .../radix/themes/components/popover.py | 16 +- .../radix/themes/components/radio_cards.py | 4 +- .../radix/themes/components/radio_group.py | 4 +- .../radix/themes/components/select.py | 12 +- .../radix/themes/components/slider.py | 8 +- .../radix/themes/components/slider.pyi | 8 +- .../radix/themes/components/switch.py | 4 +- .../radix/themes/components/tabs.py | 4 +- .../radix/themes/components/tooltip.py | 8 +- .../components/react_player/react_player.py | 34 ++-- reflex/components/recharts/cartesian.py | 90 +++++----- reflex/components/recharts/charts.py | 44 ++--- reflex/components/recharts/general.py | 20 +-- reflex/components/recharts/polar.py | 66 ++++---- reflex/components/sonner/toast.py | 9 +- reflex/components/suneditor/editor.py | 16 +- reflex/event.py | 157 +++++++++++------- reflex/experimental/client_state.py | 6 +- reflex/experimental/layout.py | 4 +- reflex/utils/format.py | 12 +- tests/units/components/test_component.py | 16 +- .../test_component_future_annotations.py | 6 +- tests/units/test_event.py | 38 +++-- 44 files changed, 453 insertions(+), 396 deletions(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 7d76b080a..66df09ee1 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -178,11 +178,6 @@ export const applyEvent = async (event, socket) => { return false; } - if (event.name == "_console") { - console.log(event.payload.message); - return false; - } - if (event.name == "_remove_cookie") { cookies.remove(event.payload.key, { ...event.payload.options }); queueEventIfSocketExists(initialEvents(), socket); @@ -213,12 +208,6 @@ export const applyEvent = async (event, socket) => { return false; } - if (event.name == "_set_clipboard") { - const content = event.payload.content; - navigator.clipboard.writeText(content); - return false; - } - if (event.name == "_download") { const a = document.createElement("a"); a.hidden = true; @@ -233,11 +222,6 @@ export const applyEvent = async (event, socket) => { return false; } - if (event.name == "_alert") { - alert(event.payload.message); - return false; - } - if (event.name == "_set_focus") { const ref = event.payload.ref in refs ? refs[event.payload.ref] : event.payload.ref; @@ -254,6 +238,25 @@ export const applyEvent = async (event, socket) => { return false; } + if (event.name == "_call_function") { + try { + const eval_result = event.payload.function(); + if (event.payload.callback) { + if (!!eval_result && typeof eval_result.then === "function") { + event.payload.callback(await eval_result); + } else { + event.payload.callback(eval_result); + } + } + } catch (e) { + console.log("_call_function", e); + if (window && window?.onerror) { + window.onerror(e.message, null, null, null, e); + } + } + return false; + } + if (event.name == "_call_script") { try { const eval_result = eval(event.payload.javascript_code); diff --git a/reflex/__init__.py b/reflex/__init__.py index ffc4426f9..acba5936a 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -303,10 +303,13 @@ _MAPPING: dict = { "EventHandler", "background", "call_script", + "call_function", + "run_script", "clear_local_storage", "clear_session_storage", "console_log", "download", + "noop", "prevent_default", "redirect", "remove_cookie", diff --git a/reflex/__init__.pyi b/reflex/__init__.pyi index aa1c92b72..a0f60ea57 100644 --- a/reflex/__init__.pyi +++ b/reflex/__init__.pyi @@ -155,17 +155,20 @@ from .constants import Env as Env from .event import EventChain as EventChain from .event import EventHandler as EventHandler from .event import background as background +from .event import call_function as call_function from .event import call_script as call_script from .event import clear_local_storage as clear_local_storage from .event import clear_session_storage as clear_session_storage from .event import console_log as console_log from .event import download as download from .event import event as event +from .event import noop as noop from .event import prevent_default as prevent_default from .event import redirect as redirect from .event import remove_cookie as remove_cookie from .event import remove_local_storage as remove_local_storage from .event import remove_session_storage as remove_session_storage +from .event import run_script as run_script from .event import scroll_to as scroll_to from .event import set_clipboard as set_clipboard from .event import set_focus as set_focus diff --git a/reflex/components/base/script.py b/reflex/components/base/script.py index eb37d53e7..15145ecbf 100644 --- a/reflex/components/base/script.py +++ b/reflex/components/base/script.py @@ -8,7 +8,7 @@ from __future__ import annotations from typing import Literal from reflex.components.component import Component -from reflex.event import EventHandler, empty_event +from reflex.event import EventHandler, no_args_event_spec from reflex.vars.base import LiteralVar, Var @@ -35,13 +35,13 @@ class Script(Component): ) # Triggered when the script is loading - on_load: EventHandler[empty_event] + on_load: EventHandler[no_args_event_spec] # Triggered when the script has loaded - on_ready: EventHandler[empty_event] + on_ready: EventHandler[no_args_event_spec] # Triggered when the script has errored - on_error: EventHandler[empty_event] + on_error: EventHandler[no_args_event_spec] @classmethod def create(cls, *children, **props) -> Component: diff --git a/reflex/components/component.py b/reflex/components/component.py index 85db3906d..470ba1145 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -47,8 +47,8 @@ from reflex.event import ( EventVar, call_event_fn, call_event_handler, - empty_event, get_handler_args, + no_args_event_spec, ) from reflex.style import Style, format_as_emotion from reflex.utils import format, imports, types @@ -637,21 +637,21 @@ class Component(BaseComponent, ABC): """ default_triggers = { - EventTriggers.ON_FOCUS: empty_event, - EventTriggers.ON_BLUR: empty_event, - EventTriggers.ON_CLICK: empty_event, - EventTriggers.ON_CONTEXT_MENU: empty_event, - EventTriggers.ON_DOUBLE_CLICK: empty_event, - EventTriggers.ON_MOUSE_DOWN: empty_event, - EventTriggers.ON_MOUSE_ENTER: empty_event, - EventTriggers.ON_MOUSE_LEAVE: empty_event, - EventTriggers.ON_MOUSE_MOVE: empty_event, - EventTriggers.ON_MOUSE_OUT: empty_event, - EventTriggers.ON_MOUSE_OVER: empty_event, - EventTriggers.ON_MOUSE_UP: empty_event, - EventTriggers.ON_SCROLL: empty_event, - EventTriggers.ON_MOUNT: empty_event, - EventTriggers.ON_UNMOUNT: empty_event, + EventTriggers.ON_FOCUS: no_args_event_spec, + EventTriggers.ON_BLUR: no_args_event_spec, + EventTriggers.ON_CLICK: no_args_event_spec, + EventTriggers.ON_CONTEXT_MENU: no_args_event_spec, + EventTriggers.ON_DOUBLE_CLICK: no_args_event_spec, + EventTriggers.ON_MOUSE_DOWN: no_args_event_spec, + EventTriggers.ON_MOUSE_ENTER: no_args_event_spec, + EventTriggers.ON_MOUSE_LEAVE: no_args_event_spec, + EventTriggers.ON_MOUSE_MOVE: no_args_event_spec, + EventTriggers.ON_MOUSE_OUT: no_args_event_spec, + EventTriggers.ON_MOUSE_OVER: no_args_event_spec, + EventTriggers.ON_MOUSE_UP: no_args_event_spec, + EventTriggers.ON_SCROLL: no_args_event_spec, + EventTriggers.ON_MOUNT: no_args_event_spec, + EventTriggers.ON_UNMOUNT: no_args_event_spec, } # Look for component specific triggers, @@ -662,7 +662,7 @@ class Component(BaseComponent, ABC): annotation = field.annotation if (metadata := getattr(annotation, "__metadata__", None)) is not None: args_spec = metadata[0] - default_triggers[field.name] = args_spec or (empty_event) # type: ignore + default_triggers[field.name] = args_spec or (no_args_event_spec) # type: ignore return default_triggers def __repr__(self) -> str: @@ -1723,7 +1723,7 @@ class CustomComponent(Component): value = self._create_event_chain( value=value, args_spec=event_triggers_in_component_declaration.get( - key, empty_event + key, no_args_event_spec ), key=key, ) diff --git a/reflex/components/core/clipboard.py b/reflex/components/core/clipboard.py index cce0af4a7..6d6a38acc 100644 --- a/reflex/components/core/clipboard.py +++ b/reflex/components/core/clipboard.py @@ -6,7 +6,7 @@ from typing import Dict, List, Tuple, Union from reflex.components.base.fragment import Fragment from reflex.components.tags.tag import Tag -from reflex.event import EventChain, EventHandler, identity_event +from reflex.event import EventChain, EventHandler, passthrough_event_spec from reflex.utils.format import format_prop, wrap from reflex.utils.imports import ImportVar from reflex.vars import get_unique_variable_name @@ -20,7 +20,7 @@ class Clipboard(Fragment): targets: Var[List[str]] # Called when the user pastes data into the document. Data is a list of tuples of (mime_type, data). Binary types will be base64 encoded as a data uri. - on_paste: EventHandler[identity_event(List[Tuple[str, str]])] + on_paste: EventHandler[passthrough_event_spec(List[Tuple[str, str]])] # Save the original event actions for the on_paste event. on_paste_event_actions: Var[Dict[str, Union[bool, int]]] diff --git a/reflex/components/core/clipboard.pyi b/reflex/components/core/clipboard.pyi index 1284b8050..e51bad112 100644 --- a/reflex/components/core/clipboard.pyi +++ b/reflex/components/core/clipboard.pyi @@ -6,7 +6,9 @@ from typing import Any, Dict, List, Optional, Union, overload from reflex.components.base.fragment import Fragment -from reflex.event import EventType +from reflex.event import ( + EventType, +) from reflex.style import Style from reflex.utils.imports import ImportVar from reflex.vars.base import Var diff --git a/reflex/components/core/debounce.py b/reflex/components/core/debounce.py index 86efb7dcd..12cc94426 100644 --- a/reflex/components/core/debounce.py +++ b/reflex/components/core/debounce.py @@ -6,7 +6,7 @@ from typing import Any, Type, Union from reflex.components.component import Component from reflex.constants import EventTriggers -from reflex.event import EventHandler, empty_event +from reflex.event import EventHandler, no_args_event_spec from reflex.vars import VarData from reflex.vars.base import Var @@ -46,7 +46,7 @@ class DebounceInput(Component): element: Var[Type[Component]] # Fired when the input value changes - on_change: EventHandler[empty_event] + on_change: EventHandler[no_args_event_spec] @classmethod def create(cls, *children: Component, **props: Any) -> Component: diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index 787cca9d0..a2d86b910 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -22,8 +22,8 @@ from reflex.event import ( EventHandler, EventSpec, call_event_fn, - call_script, parse_args_spec, + run_script, ) from reflex.utils import format from reflex.utils.imports import ImportVar @@ -106,8 +106,8 @@ def clear_selected_files(id_: str = DEFAULT_UPLOAD_ID) -> EventSpec: """ # UploadFilesProvider assigns a special function to clear selected files # into the shared global refs object to make it accessible outside a React - # component via `call_script` (otherwise backend could never clear files). - return call_script(f"refs['__clear_selected_files']({id_!r})") + # component via `run_script` (otherwise backend could never clear files). + return run_script(f"refs['__clear_selected_files']({id_!r})") def cancel_upload(upload_id: str) -> EventSpec: @@ -119,7 +119,7 @@ def cancel_upload(upload_id: str) -> EventSpec: Returns: An event spec that cancels the upload when triggered. """ - return call_script( + return run_script( f"upload_controllers[{str(LiteralVar.create(upload_id))}]?.abort()" ) diff --git a/reflex/components/datadisplay/dataeditor.py b/reflex/components/datadisplay/dataeditor.py index 27ca62d93..860f75007 100644 --- a/reflex/components/datadisplay/dataeditor.py +++ b/reflex/components/datadisplay/dataeditor.py @@ -10,7 +10,7 @@ from typing_extensions import TypedDict from reflex.base import Base from reflex.components.component import Component, NoSSRComponent from reflex.components.literals import LiteralRowMarker -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.utils import console, format, types from reflex.utils.imports import ImportDict, ImportVar from reflex.utils.serializers import serializer @@ -284,56 +284,58 @@ class DataEditor(NoSSRComponent): theme: Var[Union[DataEditorTheme, Dict]] # Fired when a cell is activated. - on_cell_activated: EventHandler[identity_event(Tuple[int, int])] + on_cell_activated: EventHandler[passthrough_event_spec(Tuple[int, int])] # Fired when a cell is clicked. - on_cell_clicked: EventHandler[identity_event(Tuple[int, int])] + on_cell_clicked: EventHandler[passthrough_event_spec(Tuple[int, int])] # Fired when a cell is right-clicked. - on_cell_context_menu: EventHandler[identity_event(Tuple[int, int])] + on_cell_context_menu: EventHandler[passthrough_event_spec(Tuple[int, int])] # Fired when a cell is edited. - on_cell_edited: EventHandler[identity_event(Tuple[int, int], GridCell)] + on_cell_edited: EventHandler[passthrough_event_spec(Tuple[int, int], GridCell)] # Fired when a group header is clicked. - on_group_header_clicked: EventHandler[identity_event(Tuple[int, int], GridCell)] + on_group_header_clicked: EventHandler[ + passthrough_event_spec(Tuple[int, int], GridCell) + ] # Fired when a group header is right-clicked. on_group_header_context_menu: EventHandler[ - identity_event(int, GroupHeaderClickedEventArgs) + passthrough_event_spec(int, GroupHeaderClickedEventArgs) ] # Fired when a group header is renamed. - on_group_header_renamed: EventHandler[identity_event(str, str)] + on_group_header_renamed: EventHandler[passthrough_event_spec(str, str)] # Fired when a header is clicked. - on_header_clicked: EventHandler[identity_event(Tuple[int, int])] + on_header_clicked: EventHandler[passthrough_event_spec(Tuple[int, int])] # Fired when a header is right-clicked. - on_header_context_menu: EventHandler[identity_event(Tuple[int, int])] + on_header_context_menu: EventHandler[passthrough_event_spec(Tuple[int, int])] # Fired when a header menu item is clicked. - on_header_menu_click: EventHandler[identity_event(int, Rectangle)] + on_header_menu_click: EventHandler[passthrough_event_spec(int, Rectangle)] # Fired when an item is hovered. - on_item_hovered: EventHandler[identity_event(Tuple[int, int])] + on_item_hovered: EventHandler[passthrough_event_spec(Tuple[int, int])] # Fired when a selection is deleted. - on_delete: EventHandler[identity_event(GridSelection)] + on_delete: EventHandler[passthrough_event_spec(GridSelection)] # Fired when editing is finished. on_finished_editing: EventHandler[ - identity_event(Union[GridCell, None], tuple[int, int]) + passthrough_event_spec(Union[GridCell, None], tuple[int, int]) ] # Fired when a row is appended. - on_row_appended: EventHandler[empty_event] + on_row_appended: EventHandler[no_args_event_spec] # Fired when the selection is cleared. - on_selection_cleared: EventHandler[empty_event] + on_selection_cleared: EventHandler[no_args_event_spec] # Fired when a column is resized. - on_column_resize: EventHandler[identity_event(GridColumn, int)] + on_column_resize: EventHandler[passthrough_event_spec(GridColumn, int)] def add_imports(self) -> ImportDict: """Add imports for the component. diff --git a/reflex/components/datadisplay/shiki_code_block.py b/reflex/components/datadisplay/shiki_code_block.py index 07f09c6f6..21f22424d 100644 --- a/reflex/components/datadisplay/shiki_code_block.py +++ b/reflex/components/datadisplay/shiki_code_block.py @@ -14,7 +14,7 @@ from reflex.components.el.elements.forms import Button from reflex.components.lucide.icon import Icon from reflex.components.props import NoExtrasAllowedProps from reflex.components.radix.themes.layout.box import Box -from reflex.event import call_script, set_clipboard +from reflex.event import run_script, set_clipboard from reflex.style import Style from reflex.utils.exceptions import VarTypeError from reflex.utils.imports import ImportVar @@ -30,7 +30,7 @@ def copy_script() -> Any: Returns: Any: The result of calling the script. """ - return call_script( + return run_script( f""" // Event listener for the parent click document.addEventListener('click', function(event) {{ @@ -68,7 +68,7 @@ document.addEventListener('click', function(event) {{ }} else {{ // console.error('Parent element not found.'); }} -}}); +}}) """ ) diff --git a/reflex/components/moment/moment.py b/reflex/components/moment/moment.py index 4ac835b35..d5d6d8f7c 100644 --- a/reflex/components/moment/moment.py +++ b/reflex/components/moment/moment.py @@ -4,7 +4,7 @@ import dataclasses from typing import List, Optional from reflex.components.component import NoSSRComponent -from reflex.event import EventHandler, identity_event +from reflex.event import EventHandler, passthrough_event_spec from reflex.utils.imports import ImportDict from reflex.vars.base import LiteralVar, Var @@ -96,7 +96,7 @@ class Moment(NoSSRComponent): locale: Var[str] # Fires when the date changes. - on_change: EventHandler[identity_event(str)] + on_change: EventHandler[passthrough_event_spec(str)] def add_imports(self) -> ImportDict: """Add the imports for the Moment component. diff --git a/reflex/components/next/image.py b/reflex/components/next/image.py index fe74b0935..237c308ce 100644 --- a/reflex/components/next/image.py +++ b/reflex/components/next/image.py @@ -2,7 +2,7 @@ from typing import Any, Literal, Optional, Union -from reflex.event import EventHandler, empty_event +from reflex.event import EventHandler, no_args_event_spec from reflex.utils import types from reflex.vars.base import Var @@ -56,10 +56,10 @@ class Image(NextComponent): blurDataURL: Var[str] # Fires when the image has loaded. - on_load: EventHandler[empty_event] + on_load: EventHandler[no_args_event_spec] # Fires when the image has an error. - on_error: EventHandler[empty_event] + on_error: EventHandler[no_args_event_spec] @classmethod def create( diff --git a/reflex/components/radix/primitives/drawer.py b/reflex/components/radix/primitives/drawer.py index dca6bb7e1..f99342a58 100644 --- a/reflex/components/radix/primitives/drawer.py +++ b/reflex/components/radix/primitives/drawer.py @@ -10,7 +10,7 @@ from reflex.components.component import Component, ComponentNamespace from reflex.components.radix.primitives.base import RadixPrimitiveComponent from reflex.components.radix.themes.base import Theme from reflex.components.radix.themes.layout.flex import Flex -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.utils import console from reflex.vars.base import Var @@ -40,7 +40,7 @@ class DrawerRoot(DrawerComponent): open: Var[bool] # Fires when the drawer is opened or closed. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] # When `False`, it allows interaction with elements outside of the drawer without closing it. Defaults to `True`. modal: Var[bool] @@ -49,7 +49,7 @@ class DrawerRoot(DrawerComponent): direction: Var[LiteralDirectionType] # Gets triggered after the open or close animation ends, it receives an open argument with the open state of the drawer by the time the function was triggered. - on_animation_end: EventHandler[identity_event(bool)] + on_animation_end: EventHandler[passthrough_event_spec(bool)] # When `False`, dragging, clicking outside, pressing esc, etc. will not close the drawer. Use this in combination with the open prop, otherwise you won't be able to open/close the drawer. dismissible: Var[bool] @@ -141,19 +141,19 @@ class DrawerContent(DrawerComponent): return {"css": base_style} # Fired when the drawer content is opened. Deprecated. - on_open_auto_focus: EventHandler[empty_event] + on_open_auto_focus: EventHandler[no_args_event_spec] # Fired when the drawer content is closed. Deprecated. - on_close_auto_focus: EventHandler[empty_event] + on_close_auto_focus: EventHandler[no_args_event_spec] # Fired when the escape key is pressed. Deprecated. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] # Fired when the pointer is down outside the drawer content. Deprecated. - on_pointer_down_outside: EventHandler[empty_event] + on_pointer_down_outside: EventHandler[no_args_event_spec] # Fired when interacting outside the drawer content. Deprecated. - on_interact_outside: EventHandler[empty_event] + on_interact_outside: EventHandler[no_args_event_spec] @classmethod def create(cls, *children, **props): diff --git a/reflex/components/radix/primitives/form.py b/reflex/components/radix/primitives/form.py index 4d4be7e40..f2b8201ad 100644 --- a/reflex/components/radix/primitives/form.py +++ b/reflex/components/radix/primitives/form.py @@ -8,7 +8,7 @@ from reflex.components.component import ComponentNamespace from reflex.components.core.debounce import DebounceInput from reflex.components.el.elements.forms import Form as HTMLForm from reflex.components.radix.themes.components.text_field import TextFieldRoot -from reflex.event import EventHandler, empty_event +from reflex.event import EventHandler, no_args_event_spec from reflex.vars.base import Var from .base import RadixPrimitiveComponentWithClassName @@ -28,7 +28,7 @@ class FormRoot(FormComponent, HTMLForm): alias = "RadixFormRoot" # Fired when the errors are cleared. - on_clear_server_errors: EventHandler[empty_event] + on_clear_server_errors: EventHandler[no_args_event_spec] def add_style(self) -> dict[str, Any] | None: """Add style to the component. diff --git a/reflex/components/radix/themes/components/alert_dialog.py b/reflex/components/radix/themes/components/alert_dialog.py index 12ac64b90..36d38532c 100644 --- a/reflex/components/radix/themes/components/alert_dialog.py +++ b/reflex/components/radix/themes/components/alert_dialog.py @@ -5,7 +5,7 @@ from typing import Literal from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.el import elements -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var from ..base import RadixThemesComponent, RadixThemesTriggerComponent @@ -22,7 +22,7 @@ class AlertDialogRoot(RadixThemesComponent): open: Var[bool] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] # The open state of the dialog when it is initially rendered. Use when you do not need to control its open state. default_open: Var[bool] @@ -46,13 +46,13 @@ class AlertDialogContent(elements.Div, RadixThemesComponent): force_mount: Var[bool] # Fired when the dialog is opened. - on_open_auto_focus: EventHandler[empty_event] + on_open_auto_focus: EventHandler[no_args_event_spec] # Fired when the dialog is closed. - on_close_auto_focus: EventHandler[empty_event] + on_close_auto_focus: EventHandler[no_args_event_spec] # Fired when the escape key is pressed. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] class AlertDialogTitle(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/checkbox.py b/reflex/components/radix/themes/components/checkbox.py index 2944b1f11..1460382f5 100644 --- a/reflex/components/radix/themes/components/checkbox.py +++ b/reflex/components/radix/themes/components/checkbox.py @@ -6,7 +6,7 @@ from reflex.components.component import Component, ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.radix.themes.layout.flex import Flex from reflex.components.radix.themes.typography.text import Text -from reflex.event import EventHandler, identity_event +from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import LiteralVar, Var from ..base import ( @@ -61,7 +61,7 @@ class Checkbox(RadixThemesComponent): _rename_props = {"onChange": "onCheckedChange"} # Fired when the checkbox is checked or unchecked. - on_change: EventHandler[identity_event(bool)] + on_change: EventHandler[passthrough_event_spec(bool)] class HighLevelCheckbox(RadixThemesComponent): @@ -112,7 +112,7 @@ class HighLevelCheckbox(RadixThemesComponent): _rename_props = {"onChange": "onCheckedChange"} # Fired when the checkbox is checked or unchecked. - on_change: EventHandler[identity_event(bool)] + on_change: EventHandler[passthrough_event_spec(bool)] @classmethod def create(cls, text: Var[str] = LiteralVar.create(""), **props) -> Component: diff --git a/reflex/components/radix/themes/components/context_menu.py b/reflex/components/radix/themes/components/context_menu.py index b3f55f8ba..7b2032a42 100644 --- a/reflex/components/radix/themes/components/context_menu.py +++ b/reflex/components/radix/themes/components/context_menu.py @@ -4,7 +4,7 @@ from typing import Dict, List, Literal, Union from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -39,7 +39,7 @@ class ContextMenuRoot(RadixThemesComponent): _invalid_children: List[str] = ["ContextMenuItem"] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] # The reading direction of submenus when applicable. If omitted, inherits globally from DirectionProvider or assumes LTR (left-to-right) reading mode. dir: Var[LiteralDirType] @@ -109,19 +109,19 @@ class ContextMenuContent(RadixThemesComponent): hide_when_detached: Var[bool] # Fired when focus moves back after closing. - on_close_auto_focus: EventHandler[empty_event] + on_close_auto_focus: EventHandler[no_args_event_spec] # Fired when the escape key is pressed. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] # Fired when a pointer down event happens outside the context menu. - on_pointer_down_outside: EventHandler[empty_event] + on_pointer_down_outside: EventHandler[no_args_event_spec] # Fired when focus moves outside the context menu. - on_focus_outside: EventHandler[empty_event] + on_focus_outside: EventHandler[no_args_event_spec] # Fired when the pointer interacts outside the context menu. - on_interact_outside: EventHandler[empty_event] + on_interact_outside: EventHandler[no_args_event_spec] class ContextMenuSub(RadixThemesComponent): @@ -136,7 +136,7 @@ class ContextMenuSub(RadixThemesComponent): default_open: Var[bool] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] class ContextMenuSubTrigger(RadixThemesComponent): @@ -191,16 +191,16 @@ class ContextMenuSubContent(RadixThemesComponent): _valid_parents: List[str] = ["ContextMenuSub"] # Fired when the escape key is pressed. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] # Fired when a pointer down event happens outside the context menu. - on_pointer_down_outside: EventHandler[empty_event] + on_pointer_down_outside: EventHandler[no_args_event_spec] # Fired when focus moves outside the context menu. - on_focus_outside: EventHandler[empty_event] + on_focus_outside: EventHandler[no_args_event_spec] # Fired when interacting outside the context menu. - on_interact_outside: EventHandler[empty_event] + on_interact_outside: EventHandler[no_args_event_spec] class ContextMenuItem(RadixThemesComponent): @@ -226,7 +226,7 @@ class ContextMenuItem(RadixThemesComponent): _valid_parents: List[str] = ["ContextMenuContent", "ContextMenuSubContent"] # Fired when the item is selected. - on_select: EventHandler[empty_event] + on_select: EventHandler[no_args_event_spec] class ContextMenuSeparator(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/dialog.py b/reflex/components/radix/themes/components/dialog.py index 840a50ecb..5d33cbc5f 100644 --- a/reflex/components/radix/themes/components/dialog.py +++ b/reflex/components/radix/themes/components/dialog.py @@ -5,7 +5,7 @@ from typing import Literal from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.el import elements -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -23,7 +23,7 @@ class DialogRoot(RadixThemesComponent): open: Var[bool] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] # The open state of the dialog when it is initially rendered. Use when you do not need to control its open state. default_open: Var[bool] @@ -50,19 +50,19 @@ class DialogContent(elements.Div, RadixThemesComponent): size: Var[Responsive[Literal["1", "2", "3", "4"]]] # Fired when the dialog is opened. - on_open_auto_focus: EventHandler[empty_event] + on_open_auto_focus: EventHandler[no_args_event_spec] # Fired when the dialog is closed. - on_close_auto_focus: EventHandler[empty_event] + on_close_auto_focus: EventHandler[no_args_event_spec] # Fired when the escape key is pressed. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] # Fired when the pointer is down outside the dialog. - on_pointer_down_outside: EventHandler[empty_event] + on_pointer_down_outside: EventHandler[no_args_event_spec] # Fired when the pointer interacts outside the dialog. - on_interact_outside: EventHandler[empty_event] + on_interact_outside: EventHandler[no_args_event_spec] class DialogDescription(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/dropdown_menu.py b/reflex/components/radix/themes/components/dropdown_menu.py index ee9040501..0d2ac38e2 100644 --- a/reflex/components/radix/themes/components/dropdown_menu.py +++ b/reflex/components/radix/themes/components/dropdown_menu.py @@ -4,7 +4,7 @@ from typing import Dict, List, Literal, Union from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -49,7 +49,7 @@ class DropdownMenuRoot(RadixThemesComponent): _invalid_children: List[str] = ["DropdownMenuItem"] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] class DropdownMenuTrigger(RadixThemesTriggerComponent): @@ -116,19 +116,19 @@ class DropdownMenuContent(RadixThemesComponent): hide_when_detached: Var[bool] # Fired when the dialog is closed. - on_close_auto_focus: EventHandler[empty_event] + on_close_auto_focus: EventHandler[no_args_event_spec] # Fired when the escape key is pressed. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] # Fired when the pointer is down outside the dialog. - on_pointer_down_outside: EventHandler[empty_event] + on_pointer_down_outside: EventHandler[no_args_event_spec] # Fired when focus moves outside the dialog. - on_focus_outside: EventHandler[empty_event] + on_focus_outside: EventHandler[no_args_event_spec] # Fired when the pointer interacts outside the dialog. - on_interact_outside: EventHandler[empty_event] + on_interact_outside: EventHandler[no_args_event_spec] class DropdownMenuSubTrigger(RadixThemesTriggerComponent): @@ -160,7 +160,7 @@ class DropdownMenuSub(RadixThemesComponent): default_open: Var[bool] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] class DropdownMenuSubContent(RadixThemesComponent): @@ -198,16 +198,16 @@ class DropdownMenuSubContent(RadixThemesComponent): _valid_parents: List[str] = ["DropdownMenuSub"] # Fired when the escape key is pressed. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] # Fired when the pointer is down outside the dialog. - on_pointer_down_outside: EventHandler[empty_event] + on_pointer_down_outside: EventHandler[no_args_event_spec] # Fired when focus moves outside the dialog. - on_focus_outside: EventHandler[empty_event] + on_focus_outside: EventHandler[no_args_event_spec] # Fired when the pointer interacts outside the dialog. - on_interact_outside: EventHandler[empty_event] + on_interact_outside: EventHandler[no_args_event_spec] class DropdownMenuItem(RadixThemesComponent): @@ -233,7 +233,7 @@ class DropdownMenuItem(RadixThemesComponent): _valid_parents: List[str] = ["DropdownMenuContent", "DropdownMenuSubContent"] # Fired when the item is selected. - on_select: EventHandler[empty_event] + on_select: EventHandler[no_args_event_spec] class DropdownMenuSeparator(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/hover_card.py b/reflex/components/radix/themes/components/hover_card.py index 10002c7e6..6babb10d2 100644 --- a/reflex/components/radix/themes/components/hover_card.py +++ b/reflex/components/radix/themes/components/hover_card.py @@ -5,7 +5,7 @@ from typing import Dict, Literal, Union from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.el import elements -from reflex.event import EventHandler, identity_event +from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -32,7 +32,7 @@ class HoverCardRoot(RadixThemesComponent): close_delay: Var[int] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] class HoverCardTrigger(RadixThemesTriggerComponent): diff --git a/reflex/components/radix/themes/components/popover.py b/reflex/components/radix/themes/components/popover.py index 6997ec5c5..bcafe6d38 100644 --- a/reflex/components/radix/themes/components/popover.py +++ b/reflex/components/radix/themes/components/popover.py @@ -5,7 +5,7 @@ from typing import Dict, Literal, Union from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.el import elements -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -26,7 +26,7 @@ class PopoverRoot(RadixThemesComponent): modal: Var[bool] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] # The open state of the popover when it is initially rendered. Use when you do not need to control its open state. default_open: Var[bool] @@ -71,22 +71,22 @@ class PopoverContent(elements.Div, RadixThemesComponent): hide_when_detached: Var[bool] # Fired when the dialog is opened. - on_open_auto_focus: EventHandler[empty_event] + on_open_auto_focus: EventHandler[no_args_event_spec] # Fired when the dialog is closed. - on_close_auto_focus: EventHandler[empty_event] + on_close_auto_focus: EventHandler[no_args_event_spec] # Fired when the escape key is pressed. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] # Fired when the pointer is down outside the dialog. - on_pointer_down_outside: EventHandler[empty_event] + on_pointer_down_outside: EventHandler[no_args_event_spec] # Fired when focus moves outside the dialog. - on_focus_outside: EventHandler[empty_event] + on_focus_outside: EventHandler[no_args_event_spec] # Fired when the pointer interacts outside the dialog. - on_interact_outside: EventHandler[empty_event] + on_interact_outside: EventHandler[no_args_event_spec] class PopoverClose(RadixThemesTriggerComponent): diff --git a/reflex/components/radix/themes/components/radio_cards.py b/reflex/components/radix/themes/components/radio_cards.py index e0aa2a749..e075a1ba2 100644 --- a/reflex/components/radix/themes/components/radio_cards.py +++ b/reflex/components/radix/themes/components/radio_cards.py @@ -4,7 +4,7 @@ from types import SimpleNamespace from typing import Literal, Union from reflex.components.core.breakpoints import Responsive -from reflex.event import EventHandler, identity_event +from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var from ..base import LiteralAccentColor, RadixThemesComponent @@ -65,7 +65,7 @@ class RadioCardsRoot(RadixThemesComponent): loop: Var[bool] # Event handler called when the value changes. - on_value_change: EventHandler[identity_event(str)] + on_value_change: EventHandler[passthrough_event_spec(str)] class RadioCardsItem(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/radio_group.py b/reflex/components/radix/themes/components/radio_group.py index a55ca3a41..df3843cf9 100644 --- a/reflex/components/radix/themes/components/radio_group.py +++ b/reflex/components/radix/themes/components/radio_group.py @@ -9,7 +9,7 @@ from reflex.components.component import Component, ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.radix.themes.layout.flex import Flex from reflex.components.radix.themes.typography.text import Text -from reflex.event import EventHandler, identity_event +from reflex.event import EventHandler, passthrough_event_spec from reflex.utils import types from reflex.vars.base import LiteralVar, Var from reflex.vars.sequence import StringVar @@ -59,7 +59,7 @@ class RadioGroupRoot(RadixThemesComponent): _rename_props = {"onChange": "onValueChange"} # Fired when the value of the radio group changes. - on_change: EventHandler[identity_event(str)] + on_change: EventHandler[passthrough_event_spec(str)] class RadioGroupItem(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/select.py b/reflex/components/radix/themes/components/select.py index 47a1eaf3f..f71276c8b 100644 --- a/reflex/components/radix/themes/components/select.py +++ b/reflex/components/radix/themes/components/select.py @@ -5,7 +5,7 @@ from typing import List, Literal, Union import reflex as rx from reflex.components.component import Component, ComponentNamespace from reflex.components.core.breakpoints import Responsive -from reflex.event import empty_event, identity_event +from reflex.event import no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -48,10 +48,10 @@ class SelectRoot(RadixThemesComponent): _rename_props = {"onChange": "onValueChange"} # Fired when the value of the select changes. - on_change: rx.EventHandler[identity_event(str)] + on_change: rx.EventHandler[passthrough_event_spec(str)] # Fired when the select is opened or closed. - on_open_change: rx.EventHandler[identity_event(bool)] + on_open_change: rx.EventHandler[passthrough_event_spec(bool)] class SelectTrigger(RadixThemesComponent): @@ -104,13 +104,13 @@ class SelectContent(RadixThemesComponent): align_offset: Var[int] # Fired when the select content is closed. - on_close_auto_focus: rx.EventHandler[empty_event] + on_close_auto_focus: rx.EventHandler[no_args_event_spec] # Fired when the escape key is pressed. - on_escape_key_down: rx.EventHandler[empty_event] + on_escape_key_down: rx.EventHandler[no_args_event_spec] # Fired when a pointer down event happens outside the select content. - on_pointer_down_outside: rx.EventHandler[empty_event] + on_pointer_down_outside: rx.EventHandler[no_args_event_spec] class SelectGroup(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/slider.py b/reflex/components/radix/themes/components/slider.py index 4f456cdca..d833ef782 100644 --- a/reflex/components/radix/themes/components/slider.py +++ b/reflex/components/radix/themes/components/slider.py @@ -6,7 +6,7 @@ from typing import List, Literal, Optional, Union from reflex.components.component import Component from reflex.components.core.breakpoints import Responsive -from reflex.event import EventHandler, identity_event +from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -15,9 +15,9 @@ from ..base import ( ) on_value_event_spec = ( - identity_event(list[Union[int, float]]), - identity_event(list[int]), - identity_event(list[float]), + passthrough_event_spec(list[Union[int, float]]), + passthrough_event_spec(list[int]), + passthrough_event_spec(list[float]), ) diff --git a/reflex/components/radix/themes/components/slider.pyi b/reflex/components/radix/themes/components/slider.pyi index f77573d44..0d1b17e5f 100644 --- a/reflex/components/radix/themes/components/slider.pyi +++ b/reflex/components/radix/themes/components/slider.pyi @@ -6,16 +6,16 @@ from typing import Any, Dict, List, Literal, Optional, Union, overload from reflex.components.core.breakpoints import Breakpoints -from reflex.event import EventType, identity_event +from reflex.event import EventType, passthrough_event_spec from reflex.style import Style from reflex.vars.base import Var from ..base import RadixThemesComponent on_value_event_spec = ( - identity_event(list[Union[int, float]]), - identity_event(list[int]), - identity_event(list[float]), + passthrough_event_spec(list[Union[int, float]]), + passthrough_event_spec(list[int]), + passthrough_event_spec(list[float]), ) class Slider(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/switch.py b/reflex/components/radix/themes/components/switch.py index 13be32d83..dea4913aa 100644 --- a/reflex/components/radix/themes/components/switch.py +++ b/reflex/components/radix/themes/components/switch.py @@ -3,7 +3,7 @@ from typing import Literal from reflex.components.core.breakpoints import Responsive -from reflex.event import EventHandler, identity_event +from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -59,7 +59,7 @@ class Switch(RadixThemesComponent): _rename_props = {"onChange": "onCheckedChange"} # Fired when the value of the switch changes - on_change: EventHandler[identity_event(bool)] + on_change: EventHandler[passthrough_event_spec(bool)] switch = Switch.create diff --git a/reflex/components/radix/themes/components/tabs.py b/reflex/components/radix/themes/components/tabs.py index 12359b528..ac04c1a3d 100644 --- a/reflex/components/radix/themes/components/tabs.py +++ b/reflex/components/radix/themes/components/tabs.py @@ -7,7 +7,7 @@ from typing import Any, Dict, List, Literal from reflex.components.component import Component, ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.core.colors import color -from reflex.event import EventHandler, identity_event +from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var from ..base import ( @@ -42,7 +42,7 @@ class TabsRoot(RadixThemesComponent): _rename_props = {"onChange": "onValueChange"} # Fired when the value of the tabs changes. - on_change: EventHandler[identity_event(str)] + on_change: EventHandler[passthrough_event_spec(str)] def add_style(self) -> Dict[str, Any] | None: """Add style for the component. diff --git a/reflex/components/radix/themes/components/tooltip.py b/reflex/components/radix/themes/components/tooltip.py index ac35c86d1..3bc61f545 100644 --- a/reflex/components/radix/themes/components/tooltip.py +++ b/reflex/components/radix/themes/components/tooltip.py @@ -3,7 +3,7 @@ from typing import Dict, Literal, Union from reflex.components.component import Component -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.utils import format from reflex.vars.base import Var @@ -85,13 +85,13 @@ class Tooltip(RadixThemesComponent): aria_label: Var[str] # Fired when the open state changes. - on_open_change: EventHandler[identity_event(bool)] + on_open_change: EventHandler[passthrough_event_spec(bool)] # Fired when the escape key is pressed. - on_escape_key_down: EventHandler[empty_event] + on_escape_key_down: EventHandler[no_args_event_spec] # Fired when the pointer is down outside the tooltip. - on_pointer_down_outside: EventHandler[empty_event] + on_pointer_down_outside: EventHandler[no_args_event_spec] @classmethod def create(cls, *children, **props) -> Component: diff --git a/reflex/components/react_player/react_player.py b/reflex/components/react_player/react_player.py index b2c58b754..fb0319ceb 100644 --- a/reflex/components/react_player/react_player.py +++ b/reflex/components/react_player/react_player.py @@ -5,7 +5,7 @@ from __future__ import annotations from typing_extensions import TypedDict from reflex.components.component import NoSSRComponent -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var @@ -57,49 +57,49 @@ class ReactPlayer(NoSSRComponent): height: Var[str] # Called when media is loaded and ready to play. If playing is set to true, media will play immediately. - on_ready: EventHandler[empty_event] + on_ready: EventHandler[no_args_event_spec] # Called when media starts playing. - on_start: EventHandler[empty_event] + on_start: EventHandler[no_args_event_spec] # Called when media starts or resumes playing after pausing or buffering. - on_play: EventHandler[empty_event] + on_play: EventHandler[no_args_event_spec] # Callback containing played and loaded progress as a fraction, and playedSeconds and loadedSeconds in seconds. eg { played: 0.12, playedSeconds: 11.3, loaded: 0.34, loadedSeconds: 16.7 } - on_progress: EventHandler[identity_event(Progress)] + on_progress: EventHandler[passthrough_event_spec(Progress)] # Callback containing duration of the media, in seconds. - on_duration: EventHandler[identity_event(float)] + on_duration: EventHandler[passthrough_event_spec(float)] # Called when media is paused. - on_pause: EventHandler[empty_event] + on_pause: EventHandler[no_args_event_spec] # Called when media starts buffering. - on_buffer: EventHandler[empty_event] + on_buffer: EventHandler[no_args_event_spec] # Called when media has finished buffering. Works for files, YouTube and Facebook. - on_buffer_end: EventHandler[empty_event] + on_buffer_end: EventHandler[no_args_event_spec] # Called when media seeks with seconds parameter. - on_seek: EventHandler[identity_event(float)] + on_seek: EventHandler[passthrough_event_spec(float)] # Called when playback rate of the player changed. Only supported by YouTube, Vimeo (if enabled), Wistia, and file paths. - on_playback_rate_change: EventHandler[empty_event] + on_playback_rate_change: EventHandler[no_args_event_spec] # Called when playback quality of the player changed. Only supported by YouTube (if enabled). - on_playback_quality_change: EventHandler[empty_event] + on_playback_quality_change: EventHandler[no_args_event_spec] # Called when media finishes playing. Does not fire when loop is set to true. - on_ended: EventHandler[empty_event] + on_ended: EventHandler[no_args_event_spec] # Called when an error occurs whilst attempting to play media. - on_error: EventHandler[empty_event] + on_error: EventHandler[no_args_event_spec] # Called when user clicks the light mode preview. - on_click_preview: EventHandler[empty_event] + on_click_preview: EventHandler[no_args_event_spec] # Called when picture-in-picture mode is enabled. - on_enable_pip: EventHandler[empty_event] + on_enable_pip: EventHandler[no_args_event_spec] # Called when picture-in-picture mode is disabled. - on_disable_pip: EventHandler[empty_event] + on_disable_pip: EventHandler[no_args_event_spec] diff --git a/reflex/components/recharts/cartesian.py b/reflex/components/recharts/cartesian.py index 865b50a32..028bcb4e4 100644 --- a/reflex/components/recharts/cartesian.py +++ b/reflex/components/recharts/cartesian.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List, Union from reflex.constants import EventTriggers from reflex.constants.colors import Color -from reflex.event import EventHandler, empty_event +from reflex.event import EventHandler, no_args_event_spec from reflex.vars.base import LiteralVar, Var from .recharts import ( @@ -109,25 +109,25 @@ class Axis(Recharts): text_anchor: Var[LiteralTextAnchor] # The customized event handler of click on the ticks of this axis - on_click: EventHandler[empty_event] + on_click: EventHandler[no_args_event_spec] # The customized event handler of mousedown on the ticks of this axis - on_mouse_down: EventHandler[empty_event] + on_mouse_down: EventHandler[no_args_event_spec] # The customized event handler of mouseup on the ticks of this axis - on_mouse_up: EventHandler[empty_event] + on_mouse_up: EventHandler[no_args_event_spec] # The customized event handler of mousemove on the ticks of this axis - on_mouse_move: EventHandler[empty_event] + on_mouse_move: EventHandler[no_args_event_spec] # The customized event handler of mouseout on the ticks of this axis - on_mouse_out: EventHandler[empty_event] + on_mouse_out: EventHandler[no_args_event_spec] # The customized event handler of mouseenter on the ticks of this axis - on_mouse_enter: EventHandler[empty_event] + on_mouse_enter: EventHandler[no_args_event_spec] # The customized event handler of mouseleave on the ticks of this axis - on_mouse_leave: EventHandler[empty_event] + on_mouse_leave: EventHandler[no_args_event_spec] class XAxis(Axis): @@ -252,7 +252,7 @@ class Brush(Recharts): A dict mapping the event trigger to the var that is passed to the handler. """ return { - EventTriggers.ON_CHANGE: empty_event, + EventTriggers.ON_CHANGE: no_args_event_spec, } @@ -293,34 +293,34 @@ class Cartesian(Recharts): name: Var[Union[str, int]] # The customized event handler of animation start - on_animation_start: EventHandler[empty_event] + on_animation_start: EventHandler[no_args_event_spec] # The customized event handler of animation end - on_animation_end: EventHandler[empty_event] + on_animation_end: EventHandler[no_args_event_spec] # The customized event handler of click on the component in this group - on_click: EventHandler[empty_event] + on_click: EventHandler[no_args_event_spec] # The customized event handler of mousedown on the component in this group - on_mouse_down: EventHandler[empty_event] + on_mouse_down: EventHandler[no_args_event_spec] # The customized event handler of mouseup on the component in this group - on_mouse_up: EventHandler[empty_event] + on_mouse_up: EventHandler[no_args_event_spec] # The customized event handler of mousemove on the component in this group - on_mouse_move: EventHandler[empty_event] + on_mouse_move: EventHandler[no_args_event_spec] # The customized event handler of mouseover on the component in this group - on_mouse_over: EventHandler[empty_event] + on_mouse_over: EventHandler[no_args_event_spec] # The customized event handler of mouseout on the component in this group - on_mouse_out: EventHandler[empty_event] + on_mouse_out: EventHandler[no_args_event_spec] # The customized event handler of mouseenter on the component in this group - on_mouse_enter: EventHandler[empty_event] + on_mouse_enter: EventHandler[no_args_event_spec] # The customized event handler of mouseleave on the component in this group - on_mouse_leave: EventHandler[empty_event] + on_mouse_leave: EventHandler[no_args_event_spec] class Area(Cartesian): @@ -526,28 +526,28 @@ class Scatter(Recharts): animation_easing: Var[LiteralAnimationEasing] # The customized event handler of click on the component in this group - on_click: EventHandler[empty_event] + on_click: EventHandler[no_args_event_spec] # The customized event handler of mousedown on the component in this group - on_mouse_down: EventHandler[empty_event] + on_mouse_down: EventHandler[no_args_event_spec] # The customized event handler of mouseup on the component in this group - on_mouse_up: EventHandler[empty_event] + on_mouse_up: EventHandler[no_args_event_spec] # The customized event handler of mousemove on the component in this group - on_mouse_move: EventHandler[empty_event] + on_mouse_move: EventHandler[no_args_event_spec] # The customized event handler of mouseover on the component in this group - on_mouse_over: EventHandler[empty_event] + on_mouse_over: EventHandler[no_args_event_spec] # The customized event handler of mouseout on the component in this group - on_mouse_out: EventHandler[empty_event] + on_mouse_out: EventHandler[no_args_event_spec] # The customized event handler of mouseenter on the component in this group - on_mouse_enter: EventHandler[empty_event] + on_mouse_enter: EventHandler[no_args_event_spec] # The customized event handler of mouseleave on the component in this group - on_mouse_leave: EventHandler[empty_event] + on_mouse_leave: EventHandler[no_args_event_spec] class Funnel(Recharts): @@ -591,34 +591,34 @@ class Funnel(Recharts): _valid_children: List[str] = ["LabelList", "Cell"] # The customized event handler of animation start - on_animation_start: EventHandler[empty_event] + on_animation_start: EventHandler[no_args_event_spec] # The customized event handler of animation end - on_animation_end: EventHandler[empty_event] + on_animation_end: EventHandler[no_args_event_spec] # The customized event handler of click on the component in this group - on_click: EventHandler[empty_event] + on_click: EventHandler[no_args_event_spec] # The customized event handler of mousedown on the component in this group - on_mouse_down: EventHandler[empty_event] + on_mouse_down: EventHandler[no_args_event_spec] # The customized event handler of mouseup on the component in this group - on_mouse_up: EventHandler[empty_event] + on_mouse_up: EventHandler[no_args_event_spec] # The customized event handler of mousemove on the component in this group - on_mouse_move: EventHandler[empty_event] + on_mouse_move: EventHandler[no_args_event_spec] # The customized event handler of mouseover on the component in this group - on_mouse_over: EventHandler[empty_event] + on_mouse_over: EventHandler[no_args_event_spec] # The customized event handler of mouseout on the component in this group - on_mouse_out: EventHandler[empty_event] + on_mouse_out: EventHandler[no_args_event_spec] # The customized event handler of mouseenter on the component in this group - on_mouse_enter: EventHandler[empty_event] + on_mouse_enter: EventHandler[no_args_event_spec] # The customized event handler of mouseleave on the component in this group - on_mouse_leave: EventHandler[empty_event] + on_mouse_leave: EventHandler[no_args_event_spec] class ErrorBar(Recharts): @@ -715,28 +715,28 @@ class ReferenceDot(Reference): _valid_children: List[str] = ["Label"] # The customized event handler of click on the component in this chart - on_click: EventHandler[empty_event] + on_click: EventHandler[no_args_event_spec] # The customized event handler of mousedown on the component in this chart - on_mouse_down: EventHandler[empty_event] + on_mouse_down: EventHandler[no_args_event_spec] # The customized event handler of mouseup on the component in this chart - on_mouse_up: EventHandler[empty_event] + on_mouse_up: EventHandler[no_args_event_spec] # The customized event handler of mouseover on the component in this chart - on_mouse_over: EventHandler[empty_event] + on_mouse_over: EventHandler[no_args_event_spec] # The customized event handler of mouseout on the component in this chart - on_mouse_out: EventHandler[empty_event] + on_mouse_out: EventHandler[no_args_event_spec] # The customized event handler of mouseenter on the component in this chart - on_mouse_enter: EventHandler[empty_event] + on_mouse_enter: EventHandler[no_args_event_spec] # The customized event handler of mousemove on the component in this chart - on_mouse_move: EventHandler[empty_event] + on_mouse_move: EventHandler[no_args_event_spec] # The customized event handler of mouseleave on the component in this chart - on_mouse_leave: EventHandler[empty_event] + on_mouse_leave: EventHandler[no_args_event_spec] class ReferenceArea(Recharts): diff --git a/reflex/components/recharts/charts.py b/reflex/components/recharts/charts.py index d7e07b2d8..13f125213 100644 --- a/reflex/components/recharts/charts.py +++ b/reflex/components/recharts/charts.py @@ -8,7 +8,7 @@ from reflex.components.component import Component from reflex.components.recharts.general import ResponsiveContainer from reflex.constants import EventTriggers from reflex.constants.colors import Color -from reflex.event import EventHandler, empty_event +from reflex.event import EventHandler, no_args_event_spec from reflex.vars.base import Var from .recharts import ( @@ -31,16 +31,16 @@ class ChartBase(RechartsCharts): height: Var[Union[str, int]] = "100%" # type: ignore # The customized event handler of click on the component in this chart - on_click: EventHandler[empty_event] + on_click: EventHandler[no_args_event_spec] # The customized event handler of mouseenter on the component in this chart - on_mouse_enter: EventHandler[empty_event] + on_mouse_enter: EventHandler[no_args_event_spec] # The customized event handler of mousemove on the component in this chart - on_mouse_move: EventHandler[empty_event] + on_mouse_move: EventHandler[no_args_event_spec] # The customized event handler of mouseleave on the component in this chart - on_mouse_leave: EventHandler[empty_event] + on_mouse_leave: EventHandler[no_args_event_spec] @staticmethod def _ensure_valid_dimension(name: str, value: Any) -> None: @@ -270,16 +270,16 @@ class PieChart(ChartBase): ] # The customized event handler of mousedown on the sectors in this group - on_mouse_down: EventHandler[empty_event] + on_mouse_down: EventHandler[no_args_event_spec] # The customized event handler of mouseup on the sectors in this group - on_mouse_up: EventHandler[empty_event] + on_mouse_up: EventHandler[no_args_event_spec] # The customized event handler of mouseover on the sectors in this group - on_mouse_over: EventHandler[empty_event] + on_mouse_over: EventHandler[no_args_event_spec] # The customized event handler of mouseout on the sectors in this group - on_mouse_out: EventHandler[empty_event] + on_mouse_out: EventHandler[no_args_event_spec] class RadarChart(ChartBase): @@ -330,9 +330,9 @@ class RadarChart(ChartBase): A dict mapping the event trigger to the var that is passed to the handler. """ return { - EventTriggers.ON_CLICK: empty_event, - EventTriggers.ON_MOUSE_ENTER: empty_event, - EventTriggers.ON_MOUSE_LEAVE: empty_event, + EventTriggers.ON_CLICK: no_args_event_spec, + EventTriggers.ON_MOUSE_ENTER: no_args_event_spec, + EventTriggers.ON_MOUSE_LEAVE: no_args_event_spec, } @@ -419,14 +419,14 @@ class ScatterChart(ChartBase): A dict mapping the event trigger to the var that is passed to the handler. """ return { - EventTriggers.ON_CLICK: empty_event, - EventTriggers.ON_MOUSE_DOWN: empty_event, - EventTriggers.ON_MOUSE_UP: empty_event, - EventTriggers.ON_MOUSE_MOVE: empty_event, - EventTriggers.ON_MOUSE_OVER: empty_event, - EventTriggers.ON_MOUSE_OUT: empty_event, - EventTriggers.ON_MOUSE_ENTER: empty_event, - EventTriggers.ON_MOUSE_LEAVE: empty_event, + EventTriggers.ON_CLICK: no_args_event_spec, + EventTriggers.ON_MOUSE_DOWN: no_args_event_spec, + EventTriggers.ON_MOUSE_UP: no_args_event_spec, + EventTriggers.ON_MOUSE_MOVE: no_args_event_spec, + EventTriggers.ON_MOUSE_OVER: no_args_event_spec, + EventTriggers.ON_MOUSE_OUT: no_args_event_spec, + EventTriggers.ON_MOUSE_ENTER: no_args_event_spec, + EventTriggers.ON_MOUSE_LEAVE: no_args_event_spec, } @@ -488,10 +488,10 @@ class Treemap(RechartsCharts): animation_easing: Var[LiteralAnimationEasing] # The customized event handler of animation start - on_animation_start: EventHandler[empty_event] + on_animation_start: EventHandler[no_args_event_spec] # The customized event handler of animation end - on_animation_end: EventHandler[empty_event] + on_animation_end: EventHandler[no_args_event_spec] @classmethod def create(cls, *children, **props) -> Component: diff --git a/reflex/components/recharts/general.py b/reflex/components/recharts/general.py index 641e1562a..124ef744d 100644 --- a/reflex/components/recharts/general.py +++ b/reflex/components/recharts/general.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List, Union from reflex.components.component import MemoizationLeaf from reflex.constants.colors import Color -from reflex.event import EventHandler, empty_event +from reflex.event import EventHandler, no_args_event_spec from reflex.vars.base import LiteralVar, Var from .recharts import ( @@ -46,7 +46,7 @@ class ResponsiveContainer(Recharts, MemoizationLeaf): debounce: Var[int] # If specified provides a callback providing the updated chart width and height values. - on_resize: EventHandler[empty_event] + on_resize: EventHandler[no_args_event_spec] # Valid children components _valid_children: List[str] = [ @@ -104,28 +104,28 @@ class Legend(Recharts): margin: Var[Dict[str, Any]] # The customized event handler of click on the items in this group - on_click: EventHandler[empty_event] + on_click: EventHandler[no_args_event_spec] # The customized event handler of mousedown on the items in this group - on_mouse_down: EventHandler[empty_event] + on_mouse_down: EventHandler[no_args_event_spec] # The customized event handler of mouseup on the items in this group - on_mouse_up: EventHandler[empty_event] + on_mouse_up: EventHandler[no_args_event_spec] # The customized event handler of mousemove on the items in this group - on_mouse_move: EventHandler[empty_event] + on_mouse_move: EventHandler[no_args_event_spec] # The customized event handler of mouseover on the items in this group - on_mouse_over: EventHandler[empty_event] + on_mouse_over: EventHandler[no_args_event_spec] # The customized event handler of mouseout on the items in this group - on_mouse_out: EventHandler[empty_event] + on_mouse_out: EventHandler[no_args_event_spec] # The customized event handler of mouseenter on the items in this group - on_mouse_enter: EventHandler[empty_event] + on_mouse_enter: EventHandler[no_args_event_spec] # The customized event handler of mouseleave on the items in this group - on_mouse_leave: EventHandler[empty_event] + on_mouse_leave: EventHandler[no_args_event_spec] class GraphingTooltip(Recharts): diff --git a/reflex/components/recharts/polar.py b/reflex/components/recharts/polar.py index ccb96f180..0aedf4893 100644 --- a/reflex/components/recharts/polar.py +++ b/reflex/components/recharts/polar.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List, Union from reflex.constants import EventTriggers from reflex.constants.colors import Color -from reflex.event import EventHandler, empty_event +from reflex.event import EventHandler, no_args_event_spec from reflex.vars.base import LiteralVar, Var from .recharts import ( @@ -103,14 +103,14 @@ class Pie(Recharts): A dict mapping the event trigger to the var that is passed to the handler. """ return { - EventTriggers.ON_ANIMATION_START: empty_event, - EventTriggers.ON_ANIMATION_END: empty_event, - EventTriggers.ON_CLICK: empty_event, - EventTriggers.ON_MOUSE_MOVE: empty_event, - EventTriggers.ON_MOUSE_OVER: empty_event, - EventTriggers.ON_MOUSE_OUT: empty_event, - EventTriggers.ON_MOUSE_ENTER: empty_event, - EventTriggers.ON_MOUSE_LEAVE: empty_event, + EventTriggers.ON_ANIMATION_START: no_args_event_spec, + EventTriggers.ON_ANIMATION_END: no_args_event_spec, + EventTriggers.ON_CLICK: no_args_event_spec, + EventTriggers.ON_MOUSE_MOVE: no_args_event_spec, + EventTriggers.ON_MOUSE_OVER: no_args_event_spec, + EventTriggers.ON_MOUSE_OUT: no_args_event_spec, + EventTriggers.ON_MOUSE_ENTER: no_args_event_spec, + EventTriggers.ON_MOUSE_LEAVE: no_args_event_spec, } @@ -167,8 +167,8 @@ class Radar(Recharts): A dict mapping the event trigger to the var that is passed to the handler. """ return { - EventTriggers.ON_ANIMATION_START: empty_event, - EventTriggers.ON_ANIMATION_END: empty_event, + EventTriggers.ON_ANIMATION_START: no_args_event_spec, + EventTriggers.ON_ANIMATION_END: no_args_event_spec, } @@ -219,14 +219,14 @@ class RadialBar(Recharts): A dict mapping the event trigger to the var that is passed to the handler. """ return { - EventTriggers.ON_CLICK: empty_event, - EventTriggers.ON_MOUSE_MOVE: empty_event, - EventTriggers.ON_MOUSE_OVER: empty_event, - EventTriggers.ON_MOUSE_OUT: empty_event, - EventTriggers.ON_MOUSE_ENTER: empty_event, - EventTriggers.ON_MOUSE_LEAVE: empty_event, - EventTriggers.ON_ANIMATION_START: empty_event, - EventTriggers.ON_ANIMATION_END: empty_event, + EventTriggers.ON_CLICK: no_args_event_spec, + EventTriggers.ON_MOUSE_MOVE: no_args_event_spec, + EventTriggers.ON_MOUSE_OVER: no_args_event_spec, + EventTriggers.ON_MOUSE_OUT: no_args_event_spec, + EventTriggers.ON_MOUSE_ENTER: no_args_event_spec, + EventTriggers.ON_MOUSE_LEAVE: no_args_event_spec, + EventTriggers.ON_ANIMATION_START: no_args_event_spec, + EventTriggers.ON_ANIMATION_END: no_args_event_spec, } @@ -277,28 +277,28 @@ class PolarAngleAxis(Recharts): _valid_children: List[str] = ["Label"] # The customized event handler of click on the ticks of this axis. - on_click: EventHandler[empty_event] + on_click: EventHandler[no_args_event_spec] # The customized event handler of mousedown on the the ticks of this axis. - on_mouse_down: EventHandler[empty_event] + on_mouse_down: EventHandler[no_args_event_spec] # The customized event handler of mouseup on the ticks of this axis. - on_mouse_up: EventHandler[empty_event] + on_mouse_up: EventHandler[no_args_event_spec] # The customized event handler of mousemove on the ticks of this axis. - on_mouse_move: EventHandler[empty_event] + on_mouse_move: EventHandler[no_args_event_spec] # The customized event handler of mouseover on the ticks of this axis. - on_mouse_over: EventHandler[empty_event] + on_mouse_over: EventHandler[no_args_event_spec] # The customized event handler of mouseout on the ticks of this axis. - on_mouse_out: EventHandler[empty_event] + on_mouse_out: EventHandler[no_args_event_spec] # The customized event handler of moustenter on the ticks of this axis. - on_mouse_enter: EventHandler[empty_event] + on_mouse_enter: EventHandler[no_args_event_spec] # The customized event handler of mouseleave on the ticks of this axis. - on_mouse_leave: EventHandler[empty_event] + on_mouse_leave: EventHandler[no_args_event_spec] class PolarGrid(Recharts): @@ -392,12 +392,12 @@ class PolarRadiusAxis(Recharts): A dict mapping the event trigger to the var that is passed to the handler. """ return { - EventTriggers.ON_CLICK: empty_event, - EventTriggers.ON_MOUSE_MOVE: empty_event, - EventTriggers.ON_MOUSE_OVER: empty_event, - EventTriggers.ON_MOUSE_OUT: empty_event, - EventTriggers.ON_MOUSE_ENTER: empty_event, - EventTriggers.ON_MOUSE_LEAVE: empty_event, + EventTriggers.ON_CLICK: no_args_event_spec, + EventTriggers.ON_MOUSE_MOVE: no_args_event_spec, + EventTriggers.ON_MOUSE_OVER: no_args_event_spec, + EventTriggers.ON_MOUSE_OUT: no_args_event_spec, + EventTriggers.ON_MOUSE_ENTER: no_args_event_spec, + EventTriggers.ON_MOUSE_LEAVE: no_args_event_spec, } diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py index 175c68f63..c226c7bd0 100644 --- a/reflex/components/sonner/toast.py +++ b/reflex/components/sonner/toast.py @@ -8,10 +8,7 @@ from reflex.base import Base from reflex.components.component import Component, ComponentNamespace from reflex.components.lucide.icon import Icon from reflex.components.props import NoExtrasAllowedProps, PropsBase -from reflex.event import ( - EventSpec, - call_script, -) +from reflex.event import EventSpec, run_script from reflex.style import Style, resolved_color_mode from reflex.utils import format from reflex.utils.imports import ImportVar @@ -260,7 +257,7 @@ class Toaster(Component): toast = f"{toast_command}(`{message}`)" toast_action = Var(_js_expr=toast) - return call_script(toast_action) + return run_script(toast_action) @staticmethod def toast_info(message: str = "", **kwargs): @@ -336,7 +333,7 @@ class Toaster(Component): dismiss_action = Var( _js_expr=dismiss, _var_data=VarData.merge(dismiss_var_data) ) - return call_script(dismiss_action) + return run_script(dismiss_action) @classmethod def create(cls, *children, **props) -> Component: diff --git a/reflex/components/suneditor/editor.py b/reflex/components/suneditor/editor.py index 3bca8a3f6..16d5689e2 100644 --- a/reflex/components/suneditor/editor.py +++ b/reflex/components/suneditor/editor.py @@ -7,7 +7,7 @@ from typing import Dict, List, Literal, Optional, Tuple, Union from reflex.base import Base from reflex.components.component import Component, NoSSRComponent -from reflex.event import EventHandler, empty_event, identity_event +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.utils.format import to_camel_case from reflex.utils.imports import ImportDict, ImportVar from reflex.vars.base import Var @@ -207,31 +207,31 @@ class Editor(NoSSRComponent): disable_toolbar: Var[bool] # Fired when the editor content changes. - on_change: EventHandler[identity_event(str)] + on_change: EventHandler[passthrough_event_spec(str)] # Fired when the something is inputted in the editor. - on_input: EventHandler[empty_event] + on_input: EventHandler[no_args_event_spec] # Fired when the editor loses focus. on_blur: EventHandler[on_blur_spec] # Fired when the editor is loaded. - on_load: EventHandler[identity_event(bool)] + on_load: EventHandler[passthrough_event_spec(bool)] # Fired when the editor content is copied. - on_copy: EventHandler[empty_event] + on_copy: EventHandler[no_args_event_spec] # Fired when the editor content is cut. - on_cut: EventHandler[empty_event] + on_cut: EventHandler[no_args_event_spec] # Fired when the editor content is pasted. on_paste: EventHandler[on_paste_spec] # Fired when the code view is toggled. - toggle_code_view: EventHandler[identity_event(bool)] + toggle_code_view: EventHandler[passthrough_event_spec(bool)] # Fired when the full screen mode is toggled. - toggle_full_screen: EventHandler[identity_event(bool)] + toggle_full_screen: EventHandler[passthrough_event_spec(bool)] def add_imports(self) -> ImportDict: """Add imports for the Editor component. diff --git a/reflex/event.py b/reflex/event.py index c2e6955f6..245937a44 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -466,7 +466,7 @@ def key_event(e: Var[JavasciptKeyboardEvent]) -> Tuple[Var[str]]: return (e.key,) -def empty_event() -> Tuple[()]: +def no_args_event_spec() -> Tuple[()]: """Empty event handler. Returns: @@ -476,43 +476,14 @@ def empty_event() -> Tuple[()]: # These chains can be used for their side effects when no other events are desired. -stop_propagation = EventChain(events=[], args_spec=empty_event).stop_propagation -prevent_default = EventChain(events=[], args_spec=empty_event).prevent_default +stop_propagation = EventChain(events=[], args_spec=no_args_event_spec).stop_propagation +prevent_default = EventChain(events=[], args_spec=no_args_event_spec).prevent_default T = TypeVar("T") U = TypeVar("U") -# def identity_event(event_type: Type[T]) -> Callable[[Var[T]], Tuple[Var[T]]]: -# """A helper function that returns the input event as output. - -# Args: -# event_type: The type of the event. - -# Returns: -# A function that returns the input event as output. -# """ - -# def inner(ev: Var[T]) -> Tuple[Var[T]]: -# return (ev,) - -# inner.__signature__ = inspect.signature(inner).replace( # type: ignore -# parameters=[ -# inspect.Parameter( -# "ev", -# kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, -# annotation=Var[event_type], -# ) -# ], -# return_annotation=Tuple[Var[event_type]], -# ) -# inner.__annotations__["ev"] = Var[event_type] -# inner.__annotations__["return"] = Tuple[Var[event_type]] - -# return inner - - class IdentityEventReturn(Generic[T], Protocol): """Protocol for an identity event return.""" @@ -529,20 +500,22 @@ class IdentityEventReturn(Generic[T], Protocol): @overload -def identity_event(event_type: Type[T], /) -> Callable[[Var[T]], Tuple[Var[T]]]: ... # type: ignore +def passthrough_event_spec( + event_type: Type[T], / +) -> Callable[[Var[T]], Tuple[Var[T]]]: ... # type: ignore @overload -def identity_event( +def passthrough_event_spec( event_type_1: Type[T], event_type2: Type[U], / ) -> Callable[[Var[T], Var[U]], Tuple[Var[T], Var[U]]]: ... @overload -def identity_event(*event_types: Type[T]) -> IdentityEventReturn[T]: ... +def passthrough_event_spec(*event_types: Type[T]) -> IdentityEventReturn[T]: ... -def identity_event(*event_types: Type[T]) -> IdentityEventReturn[T]: # type: ignore +def passthrough_event_spec(*event_types: Type[T]) -> IdentityEventReturn[T]: # type: ignore """A helper function that returns the input event as output. Args: @@ -733,7 +706,16 @@ def console_log(message: str | Var[str]) -> EventSpec: Returns: An event to log the message. """ - return server_side("_console", get_fn_signature(console_log), message=message) + return run_script(Var("console").to(dict).log.to(FunctionVar).call(message)) + + +def noop() -> EventSpec: + """Do nothing. + + Returns: + An event to do nothing. + """ + return run_script(Var.create(None)) def back() -> EventSpec: @@ -742,7 +724,9 @@ def back() -> EventSpec: Returns: An event to go back one page. """ - return call_script("window.history.back()") + return run_script( + Var("window").to(dict).history.to(dict).back.to(FunctionVar).call() + ) def window_alert(message: str | Var[str]) -> EventSpec: @@ -754,7 +738,7 @@ def window_alert(message: str | Var[str]) -> EventSpec: Returns: An event to alert the message. """ - return server_side("_alert", get_fn_signature(window_alert), message=message) + return run_script(Var("window").to(dict).alert.to(FunctionVar).call(message)) def set_focus(ref: str) -> EventSpec: @@ -785,12 +769,12 @@ def scroll_to(elem_id: str, align_to_top: bool | Var[bool] = True) -> EventSpec: """ get_element_by_id = FunctionStringVar.create("document.getElementById") - return call_script( + return run_script( get_element_by_id(elem_id) .call(elem_id) .to(ObjectVar) .scrollIntoView.to(FunctionVar) - .call(align_to_top) + .call(align_to_top), ) @@ -897,10 +881,12 @@ def set_clipboard(content: str) -> EventSpec: Returns: EventSpec: An event to set some content in the clipboard. """ - return server_side( - "_set_clipboard", - get_fn_signature(set_clipboard), - content=content, + return run_script( + Var("navigator") + .to(dict) + .clipboard.to(dict) + .writeText.to(FunctionVar) + .call(content) ) @@ -987,13 +973,7 @@ def _callback_arg_spec(eval_result): def call_script( javascript_code: str | Var[str], - callback: ( - EventSpec - | EventHandler - | Callable - | List[EventSpec | EventHandler | Callable] - | None - ) = None, + callback: EventType | None = None, ) -> EventSpec: """Create an event handler that executes arbitrary javascript code. @@ -1007,12 +987,10 @@ def call_script( callback_kwargs = {} if callback is not None: callback_kwargs = { - "callback": str( - format.format_queue_events( - callback, - args_spec=lambda result: [result], - ), - ), + "callback": format.format_queue_events( + callback, + args_spec=lambda result: [result], + )._js_expr, } if isinstance(javascript_code, str): # When there is VarData, include it and eval the JS code inline on the client. @@ -1032,6 +1010,62 @@ def call_script( ) +def call_function( + javascript_code: str | Var, + callback: EventType | None = None, +) -> EventSpec: + """Create an event handler that executes arbitrary javascript code. + + Args: + javascript_code: The code to execute. + callback: EventHandler that will receive the result of evaluating the javascript code. + + Returns: + EventSpec: An event that will execute the client side javascript. + """ + callback_kwargs = {} + if callback is not None: + callback_kwargs = { + "callback": format.format_queue_events( + callback, + args_spec=lambda result: [result], + ), + } + + javascript_code = ( + Var(javascript_code) if isinstance(javascript_code, str) else javascript_code + ) + + return server_side( + "_call_function", + get_fn_signature(call_function), + function=javascript_code, + **callback_kwargs, + ) + + +def run_script( + javascript_code: str | Var, + callback: EventType | None = None, +) -> EventSpec: + """Create an event handler that executes arbitrary javascript code. + + Args: + javascript_code: The code to execute. + callback: EventHandler that will receive the result of evaluating the javascript code. + + Returns: + EventSpec: An event that will execute the client side javascript. + """ + javascript_code = ( + Var(javascript_code) if isinstance(javascript_code, str) else javascript_code + ) + + return call_function( + ArgsFunctionOperation.create(tuple(), javascript_code), callback + ) + + def get_event(state, event): """Get the event from the given state. @@ -1822,13 +1856,14 @@ class EventNamespace(types.SimpleNamespace): check_fn_match_arg_spec = staticmethod(check_fn_match_arg_spec) resolve_annotation = staticmethod(resolve_annotation) parse_args_spec = staticmethod(parse_args_spec) - identity_event = staticmethod(identity_event) + passthrough_event_spec = staticmethod(passthrough_event_spec) input_event = staticmethod(input_event) key_event = staticmethod(key_event) - empty_event = staticmethod(empty_event) + no_args_event_spec = staticmethod(no_args_event_spec) server_side = staticmethod(server_side) redirect = staticmethod(redirect) console_log = staticmethod(console_log) + noop = staticmethod(noop) back = staticmethod(back) window_alert = staticmethod(window_alert) set_focus = staticmethod(set_focus) @@ -1842,6 +1877,8 @@ class EventNamespace(types.SimpleNamespace): set_clipboard = staticmethod(set_clipboard) download = staticmethod(download) call_script = staticmethod(call_script) + call_function = staticmethod(call_function) + run_script = staticmethod(run_script) event = EventNamespace() diff --git a/reflex/experimental/client_state.py b/reflex/experimental/client_state.py index 74a25c2cd..ca14b8d2a 100644 --- a/reflex/experimental/client_state.py +++ b/reflex/experimental/client_state.py @@ -8,7 +8,7 @@ import sys from typing import Any, Callable, Union from reflex import constants -from reflex.event import EventChain, EventHandler, EventSpec, call_script +from reflex.event import EventChain, EventHandler, EventSpec, run_script from reflex.utils.imports import ImportVar from reflex.vars import ( VarData, @@ -227,7 +227,7 @@ class ClientStateVar(Var): """ if not self._global_ref: raise ValueError("ClientStateVar must be global to retrieve the value.") - return call_script(_client_state_ref(self._getter_name), callback=callback) + return run_script(_client_state_ref(self._getter_name), callback=callback) def push(self, value: Any) -> EventSpec: """Push a value to the client state variable from the backend. @@ -245,4 +245,4 @@ class ClientStateVar(Var): """ if not self._global_ref: raise ValueError("ClientStateVar must be global to push the value.") - return call_script(f"{_client_state_ref(self._setter_name)}({value})") + return run_script(f"{_client_state_ref(self._setter_name)}({value})") diff --git a/reflex/experimental/layout.py b/reflex/experimental/layout.py index a3b76581a..d203ce714 100644 --- a/reflex/experimental/layout.py +++ b/reflex/experimental/layout.py @@ -12,7 +12,7 @@ from reflex.components.radix.themes.components.icon_button import IconButton from reflex.components.radix.themes.layout.box import Box from reflex.components.radix.themes.layout.container import Container from reflex.components.radix.themes.layout.stack import HStack -from reflex.event import call_script +from reflex.event import run_script from reflex.experimental import hooks from reflex.state import ComponentState from reflex.style import Style @@ -173,7 +173,7 @@ class SidebarTrigger(Fragment): else: open, toggle = ( Var(_js_expr="open"), - call_script(Var(_js_expr="setOpen(!open)")), + run_script("setOpen(!open)"), ) trigger_props["left"] = cond(open, f"calc({sidebar_width} - 32px)", "0") diff --git a/reflex/utils/format.py b/reflex/utils/format.py index a914a585c..c4fbff20b 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -6,7 +6,7 @@ import inspect import json import os import re -from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union +from typing import TYPE_CHECKING, Any, List, Optional, Union from reflex import constants from reflex.utils import exceptions @@ -14,7 +14,7 @@ from reflex.utils.console import deprecate if TYPE_CHECKING: from reflex.components.component import ComponentStyle - from reflex.event import ArgsSpec, EventChain, EventHandler, EventSpec + from reflex.event import ArgsSpec, EventChain, EventHandler, EventSpec, EventType WRAP_MAP = { "{": "}", @@ -533,13 +533,7 @@ def format_event_chain( def format_queue_events( - events: ( - EventSpec - | EventHandler - | Callable - | List[EventSpec | EventHandler | Callable] - | None - ) = None, + events: EventType | None = None, args_spec: Optional[ArgsSpec] = None, ) -> Var[EventChain]: """Format a list of event handler / event spec as a javascript callback. diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index a614fd715..0574c007b 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -19,10 +19,10 @@ from reflex.constants import EventTriggers from reflex.event import ( EventChain, EventHandler, - empty_event, - identity_event, input_event, + no_args_event_spec, parse_args_spec, + passthrough_event_spec, ) from reflex.state import BaseState from reflex.style import Style @@ -111,10 +111,10 @@ def component2() -> Type[Component]: """ return { **super().get_event_triggers(), - "on_open": identity_event(bool), - "on_close": identity_event(bool), - "on_user_visited_count_changed": identity_event(int), - "on_user_list_changed": identity_event(List[str]), + "on_open": passthrough_event_spec(bool), + "on_close": passthrough_event_spec(bool), + "on_user_visited_count_changed": passthrough_event_spec(int), + "on_user_list_changed": passthrough_event_spec(List[str]), } def _get_imports(self) -> ParsedImportDict: @@ -1821,8 +1821,8 @@ def test_custom_component_declare_event_handlers_in_fields(): class TestComponent(Component): on_a: EventHandler[lambda e0: [e0]] on_b: EventHandler[input_event] - on_c: EventHandler[empty_event] - on_d: EventHandler[empty_event] + on_c: EventHandler[no_args_event_spec] + on_d: EventHandler[no_args_event_spec] on_e: EventHandler on_f: EventHandler[lambda a, b, c: [c, b, a]] diff --git a/tests/units/components/test_component_future_annotations.py b/tests/units/components/test_component_future_annotations.py index 44ec52c16..0867a2d37 100644 --- a/tests/units/components/test_component_future_annotations.py +++ b/tests/units/components/test_component_future_annotations.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any from reflex.components.component import Component -from reflex.event import EventHandler, empty_event, input_event +from reflex.event import EventHandler, input_event, no_args_event_spec # This is a repeat of its namesake in test_component.py. @@ -26,8 +26,8 @@ def test_custom_component_declare_event_handlers_in_fields(): class TestComponent(Component): on_a: EventHandler[lambda e0: [e0]] on_b: EventHandler[input_event] - on_c: EventHandler[empty_event] - on_d: EventHandler[empty_event] + on_c: EventHandler[no_args_event_spec] + on_d: EventHandler[no_args_event_spec] custom_component = ReferenceComponent.create() test_component = TestComponent.create() diff --git a/tests/units/test_event.py b/tests/units/test_event.py index d7b7cf7a2..5cefa5883 100644 --- a/tests/units/test_event.py +++ b/tests/units/test_event.py @@ -1,4 +1,4 @@ -from typing import List +from typing import Callable, List import pytest @@ -216,24 +216,40 @@ def test_event_console_log(): """Test the event console log function.""" spec = event.console_log("message") assert isinstance(spec, EventSpec) - assert spec.handler.fn.__qualname__ == "_console" - assert spec.args[0][0].equals(Var(_js_expr="message")) - assert spec.args[0][1].equals(LiteralVar.create("message")) - assert format.format_event(spec) == 'Event("_console", {message:"message"})' + assert spec.handler.fn.__qualname__ == "_call_function" + assert spec.args[0][0].equals(Var(_js_expr="function")) + assert spec.args[0][1].equals( + Var('(() => ((console["log"]("message"))))', _var_type=Callable) + ) + assert ( + format.format_event(spec) + == 'Event("_call_function", {function:(() => ((console["log"]("message"))))})' + ) spec = event.console_log(Var(_js_expr="message")) - assert format.format_event(spec) == 'Event("_console", {message:message})' + assert ( + format.format_event(spec) + == 'Event("_call_function", {function:(() => ((console["log"](message))))})' + ) def test_event_window_alert(): """Test the event window alert function.""" spec = event.window_alert("message") assert isinstance(spec, EventSpec) - assert spec.handler.fn.__qualname__ == "_alert" - assert spec.args[0][0].equals(Var(_js_expr="message")) - assert spec.args[0][1].equals(LiteralVar.create("message")) - assert format.format_event(spec) == 'Event("_alert", {message:"message"})' + assert spec.handler.fn.__qualname__ == "_call_function" + assert spec.args[0][0].equals(Var(_js_expr="function")) + assert spec.args[0][1].equals( + Var('(() => ((window["alert"]("message"))))', _var_type=Callable) + ) + assert ( + format.format_event(spec) + == 'Event("_call_function", {function:(() => ((window["alert"]("message"))))})' + ) spec = event.window_alert(Var(_js_expr="message")) - assert format.format_event(spec) == 'Event("_alert", {message:message})' + assert ( + format.format_event(spec) + == 'Event("_call_function", {function:(() => ((window["alert"](message))))})' + ) def test_set_focus(): From 16ed266d116257c9c58d4cb398058f5ce94e5880 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 4 Nov 2024 11:01:14 -0800 Subject: [PATCH 05/18] move check of path to only check name (#4299) * move check of path to only check name * assert .name in other tests as well * get even more ones --- tests/integration/test_upload.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/integration/test_upload.py b/tests/integration/test_upload.py index 813313462..7eaabf6a1 100644 --- a/tests/integration/test_upload.py +++ b/tests/integration/test_upload.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import time +from pathlib import Path from typing import Generator import pytest @@ -205,11 +206,12 @@ async def test_upload_file( file_data = await AppHarness._poll_for_async(get_file_data) assert isinstance(file_data, dict) - assert file_data[exp_name] == exp_contents + normalized_file_data = {Path(k).name: v for k, v in file_data.items()} + assert normalized_file_data[Path(exp_name).name] == exp_contents # check that the selected files are displayed selected_files = driver.find_element(By.ID, f"selected_files{suffix}") - assert selected_files.text == exp_name + assert Path(selected_files.text).name == Path(exp_name).name state = await upload_file.get_state(substate_token) if secondary: @@ -256,7 +258,9 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver): # check that the selected files are displayed selected_files = driver.find_element(By.ID, "selected_files") - assert selected_files.text == "\n".join(exp_files) + assert [Path(name).name for name in selected_files.text.split("\n")] == [ + Path(name).name for name in exp_files + ] # do the upload upload_button.click() @@ -271,8 +275,9 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver): file_data = await AppHarness._poll_for_async(get_file_data) assert isinstance(file_data, dict) + normalized_file_data = {Path(k).name: v for k, v in file_data.items()} for exp_name, exp_contents in exp_files.items(): - assert file_data[exp_name] == exp_contents + assert normalized_file_data[Path(exp_name).name] == exp_contents @pytest.mark.parametrize("secondary", [False, True]) @@ -317,7 +322,9 @@ def test_clear_files( # check that the selected files are displayed selected_files = driver.find_element(By.ID, f"selected_files{suffix}") - assert selected_files.text == "\n".join(exp_files) + assert [Path(name).name for name in selected_files.text.split("\n")] == [ + Path(name).name for name in exp_files + ] clear_button = driver.find_element(By.ID, f"clear_button{suffix}") assert clear_button @@ -369,6 +376,9 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive # look up the backend state and assert on progress state = await upload_file.get_state(substate_token) assert state.substates[state_name].progress_dicts - assert exp_name not in state.substates[state_name]._file_data + file_data = state.substates[state_name]._file_data + assert isinstance(file_data, dict) + normalized_file_data = {Path(k).name: v for k, v in file_data.items()} + assert Path(exp_name).name not in normalized_file_data target_file.unlink() From 51b0f7d28ed52d30581a8d16d6780fa722ade3c5 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 4 Nov 2024 11:06:24 -0800 Subject: [PATCH 06/18] special case field in _isinstance (#4298) --- reflex/utils/types.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/reflex/utils/types.py b/reflex/utils/types.py index d58825ed5..bcb52464a 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -570,6 +570,12 @@ def _isinstance(obj: Any, cls: GenericType, nested: bool = False) -> bool: _isinstance(item, args[0]) for item in obj ) + if args: + from reflex.vars import Field + + if origin is Field: + return _isinstance(obj, args[0]) + return isinstance(obj, get_base_class(cls)) From 702808afa656f2cbb6ecae1dcc7c3caaaa8def04 Mon Sep 17 00:00:00 2001 From: graham Date: Mon, 4 Nov 2024 12:36:12 -0700 Subject: [PATCH 07/18] Bugfix/leave gitignore as is (#4291) * Refactor initialize_gitignore to support list type for files_to_ignore and improve current ignore handling. Dont sort the gitignore file. * more consistent list comprehension var --- reflex/utils/prerequisites.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 19d4966bb..aba7714af 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -441,7 +441,7 @@ def create_config(app_name: str): def initialize_gitignore( gitignore_file: Path = constants.GitIgnore.FILE, - files_to_ignore: set[str] = constants.GitIgnore.DEFAULTS, + files_to_ignore: set[str] | list[str] = constants.GitIgnore.DEFAULTS, ): """Initialize the template .gitignore file. @@ -450,23 +450,20 @@ def initialize_gitignore( files_to_ignore: The files to add to the .gitignore file. """ # Combine with the current ignored files. - current_ignore: set[str] = set() + current_ignore: list[str] = [] if gitignore_file.exists(): - current_ignore |= set( - line.strip() for line in gitignore_file.read_text().splitlines() - ) + current_ignore = [ln.strip() for ln in gitignore_file.read_text().splitlines()] if files_to_ignore == current_ignore: console.debug(f"{gitignore_file} already up to date.") return - files_to_ignore |= current_ignore + files_to_ignore = [ln for ln in files_to_ignore if ln not in current_ignore] + files_to_ignore += current_ignore # Write files to the .gitignore file. gitignore_file.touch(exist_ok=True) console.debug(f"Creating {gitignore_file}") - gitignore_file.write_text( - "\n".join(sorted(files_to_ignore)) + "\n", - ) + gitignore_file.write_text("\n".join(files_to_ignore) + "\n") def initialize_requirements_txt(): From b3c199870e146e03c9dada8cec9546de4ba0386c Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Mon, 4 Nov 2024 22:05:39 +0100 Subject: [PATCH 08/18] bypass pydantic runtime validation for state init (#4256) * bypass pydantic runtime validation for state init closes #4235 * cleanup --- reflex/state.py | 8 ++++---- tests/units/test_state.py | 7 +++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 44ccebc30..6be3d1638 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -356,7 +356,6 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): def __init__( self, - *args, parent_state: BaseState | None = None, init_substates: bool = True, _reflex_internal_init: bool = False, @@ -367,11 +366,10 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): DO NOT INSTANTIATE STATE CLASSES DIRECTLY! Use StateManager.get_state() instead. Args: - *args: The args to pass to the Pydantic init method. parent_state: The parent state. init_substates: Whether to initialize the substates in this instance. _reflex_internal_init: A flag to indicate that the state is being initialized by the framework. - **kwargs: The kwargs to pass to the Pydantic init method. + **kwargs: The kwargs to set as attributes on the state. Raises: ReflexRuntimeError: If the state is instantiated directly by end user. @@ -384,7 +382,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): "See https://reflex.dev/docs/state/ for further information." ) kwargs["parent_state"] = parent_state - super().__init__(*args, **kwargs) + super().__init__() + for name, value in kwargs.items(): + setattr(self, name, value) # Setup the substates (for memory state manager only). if init_substates: diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 271f2e794..3ff8d453c 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -3404,3 +3404,10 @@ def test_fallback_pickle(): state3._g = (i for i in range(10)) pk3 = state3._serialize() assert len(pk3) == 0 + + +def test_typed_state() -> None: + class TypedState(rx.State): + field: rx.Field[str] = rx.field("") + + _ = TypedState(field="str") From 1122cbf0b10e800480ceebf74c208849b0cbc7a1 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 4 Nov 2024 14:37:58 -0800 Subject: [PATCH 09/18] handle none case in state setattr (#4301) --- reflex/state.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 6be3d1638..ca2728708 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1288,16 +1288,19 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): fields = self.get_fields() - if name in fields and not _isinstance( - value, (field_type := fields[name].outer_type_) - ): - console.deprecate( - "mismatched-type-assignment", - f"Tried to assign value {value} of type {type(value)} to field {type(self).__name__}.{name} of type {field_type}." - " This might lead to unexpected behavior.", - "0.6.5", - "0.7.0", - ) + if name in fields: + field = fields[name] + field_type = field.outer_type_ + if field.allow_none: + field_type = Union[field_type, None] + if not _isinstance(value, field_type): + console.deprecate( + "mismatched-type-assignment", + f"Tried to assign value {value} of type {type(value)} to field {type(self).__name__}.{name} of type {field_type}." + " This might lead to unexpected behavior.", + "0.6.5", + "0.7.0", + ) # Set the attribute. super().__setattr__(name, value) From bb903b605ab75a233f4ff2348621513ef33a3112 Mon Sep 17 00:00:00 2001 From: abulvenz Date: Mon, 4 Nov 2024 23:20:02 +0000 Subject: [PATCH 10/18] Fix wrong hook (#4295) * fix: Typo in variable name. * fix: Using existing hook constant. --------- Co-authored-by: Benedikt Bartscher --- reflex/components/core/upload.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index a2d86b910..58465b618 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -15,7 +15,7 @@ from reflex.components.el.elements.forms import Input from reflex.components.radix.themes.layout.box import Box from reflex.config import environment from reflex.constants import Dirs -from reflex.constants.compiler import Imports +from reflex.constants.compiler import Hooks, Imports from reflex.event import ( CallableEventSpec, EventChain, @@ -285,20 +285,18 @@ class Upload(MemoizationLeaf): format.to_camel_case(key): value for key, value in upload_props.items() } - use_dropzone_arguements = { + use_dropzone_arguments = { "onDrop": event_var, **upload_props, } left_side = f"const {{getRootProps: {root_props_unique_name}, getInputProps: {input_props_unique_name}}} " - right_side = f"useDropzone({str(Var.create(use_dropzone_arguements))})" + right_side = f"useDropzone({str(Var.create(use_dropzone_arguments))})" var_data = VarData.merge( VarData( imports=Imports.EVENTS, - hooks={ - "const [addEvents, connectError] = useContext(EventLoopContext);": None - }, + hooks={Hooks.EVENTS: None}, ), event_var._get_all_var_data(), VarData( From 0ed7c5d96994337a7e2dbfde1b0e741d9bf108ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Tue, 5 Nov 2024 07:21:59 -0800 Subject: [PATCH 11/18] expose rx.get_state() to get instance of state from anywhere (#3959) * expose rx.get_state() to get instance of state from anywhere * fix circular import and add read-only proxy --- reflex/__init__.py | 1 + reflex/__init__.pyi | 1 + reflex/istate/__init__.py | 1 + reflex/istate/proxy.py | 33 +++++++++++++++++++++++++++++++++ reflex/istate/wrappers.py | 31 +++++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+) create mode 100644 reflex/istate/__init__.py create mode 100644 reflex/istate/proxy.py create mode 100644 reflex/istate/wrappers.py diff --git a/reflex/__init__.py b/reflex/__init__.py index acba5936a..cfb971a99 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -336,6 +336,7 @@ _MAPPING: dict = { "State", "dynamic", ], + "istate.wrappers": ["get_state"], "style": ["Style", "toggle_color_mode"], "utils.imports": ["ImportVar"], "utils.serializers": ["serializer"], diff --git a/reflex/__init__.pyi b/reflex/__init__.pyi index a0f60ea57..2d22fe497 100644 --- a/reflex/__init__.pyi +++ b/reflex/__init__.pyi @@ -180,6 +180,7 @@ from .experimental import _x as _x from .istate.storage import Cookie as Cookie from .istate.storage import LocalStorage as LocalStorage from .istate.storage import SessionStorage as SessionStorage +from .istate.wrappers import get_state as get_state from .middleware import Middleware as Middleware from .middleware import middleware as middleware from .model import Model as Model diff --git a/reflex/istate/__init__.py b/reflex/istate/__init__.py new file mode 100644 index 000000000..d71d038f8 --- /dev/null +++ b/reflex/istate/__init__.py @@ -0,0 +1 @@ +"""This module will provide interfaces for the state.""" diff --git a/reflex/istate/proxy.py b/reflex/istate/proxy.py new file mode 100644 index 000000000..8d6051cf2 --- /dev/null +++ b/reflex/istate/proxy.py @@ -0,0 +1,33 @@ +"""A module to hold state proxy classes.""" + +from typing import Any + +from reflex.state import StateProxy + + +class ReadOnlyStateProxy(StateProxy): + """A read-only proxy for a state.""" + + def __setattr__(self, name: str, value: Any) -> None: + """Prevent setting attributes on the state for read-only proxy. + + Args: + name: The attribute name. + value: The attribute value. + + Raises: + NotImplementedError: Always raised when trying to set an attribute on proxied state. + """ + if name.startswith("_self_"): + # Special case attributes of the proxy itself, not applied to the wrapped object. + super().__setattr__(name, value) + return + raise NotImplementedError("This is a read-only state proxy.") + + def mark_dirty(self): + """Mark the state as dirty. + + Raises: + NotImplementedError: Always raised when trying to mark the proxied state as dirty. + """ + raise NotImplementedError("This is a read-only state proxy.") diff --git a/reflex/istate/wrappers.py b/reflex/istate/wrappers.py new file mode 100644 index 000000000..7f010eb9e --- /dev/null +++ b/reflex/istate/wrappers.py @@ -0,0 +1,31 @@ +"""Wrappers for the state manager.""" + +from typing import Any + +from reflex.istate.proxy import ReadOnlyStateProxy +from reflex.state import ( + _split_substate_key, + _substate_key, + get_state_manager, +) + + +async def get_state(token, state_cls: Any | None = None) -> ReadOnlyStateProxy: + """Get the instance of a state for a token. + + Args: + token: The token for the state. + state_cls: The class of the state. + + Returns: + A read-only proxy of the state instance. + """ + mng = get_state_manager() + if state_cls is not None: + root_state = await mng.get_state(_substate_key(token, state_cls)) + else: + root_state = await mng.get_state(token) + _, state_path = _split_substate_key(token) + state_cls = root_state.get_class_substate(tuple(state_path.split("."))) + instance = await root_state.get_state(state_cls) + return ReadOnlyStateProxy(instance) From b5d1e03de13e284556679e0cee70c870e62bb40d Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 5 Nov 2024 09:56:10 -0800 Subject: [PATCH 12/18] improve object var symantics (#4290) * improve object var symantics * add case for serializers * check against serializer with to = dict * add tests * fix typing issues * remove default value * older version of python doesn't have assert type * add base to rx field cases * get it from typing_extension --- reflex/utils/serializers.py | 29 +++++++-- reflex/vars/base.py | 82 ++++++++++++++----------- reflex/vars/number.py | 4 +- reflex/vars/object.py | 54 +++++++++-------- reflex/vars/sequence.py | 14 ++--- tests/units/test_state.py | 14 ++--- tests/units/vars/test_object.py | 102 ++++++++++++++++++++++++++++++++ 7 files changed, 222 insertions(+), 77 deletions(-) create mode 100644 tests/units/vars/test_object.py diff --git a/reflex/utils/serializers.py b/reflex/utils/serializers.py index 614257181..d3dbb1d4c 100644 --- a/reflex/utils/serializers.py +++ b/reflex/utils/serializers.py @@ -78,7 +78,7 @@ def serializer( ) # Apply type transformation if requested - if to is not None: + if to is not None or ((to := type_hints.get("return")) is not None): SERIALIZER_TYPES[type_] = to get_serializer_type.cache_clear() @@ -189,16 +189,37 @@ def get_serializer_type(type_: Type) -> Optional[Type]: return None -def has_serializer(type_: Type) -> bool: +def has_serializer(type_: Type, into_type: Type | None = None) -> bool: """Check if there is a serializer for the type. Args: type_: The type to check. + into_type: The type to serialize into. Returns: Whether there is a serializer for the type. """ - return get_serializer(type_) is not None + serializer_for_type = get_serializer(type_) + return serializer_for_type is not None and ( + into_type is None or get_serializer_type(type_) == into_type + ) + + +def can_serialize(type_: Type, into_type: Type | None = None) -> bool: + """Check if there is a serializer for the type. + + Args: + type_: The type to check. + into_type: The type to serialize into. + + Returns: + Whether there is a serializer for the type. + """ + return has_serializer(type_, into_type) or ( + isinstance(type_, type) + and dataclasses.is_dataclass(type_) + and (into_type is None or into_type is dict) + ) @serializer(to=str) @@ -214,7 +235,7 @@ def serialize_type(value: type) -> str: return value.__name__ -@serializer +@serializer(to=dict) def serialize_base(value: Base) -> dict: """Serialize a Base instance. diff --git a/reflex/vars/base.py b/reflex/vars/base.py index b06e7b7c9..0e6bbaec7 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -75,7 +75,6 @@ from reflex.utils.types import ( if TYPE_CHECKING: from reflex.state import BaseState - from .function import FunctionVar from .number import ( BooleanVar, NumberVar, @@ -279,6 +278,24 @@ def _decode_var_immutable(value: str) -> tuple[VarData | None, str]: return VarData.merge(*var_datas) if var_datas else None, value +def can_use_in_object_var(cls: GenericType) -> bool: + """Check if the class can be used in an ObjectVar. + + Args: + cls: The class to check. + + Returns: + Whether the class can be used in an ObjectVar. + """ + if types.is_union(cls): + return all(can_use_in_object_var(t) for t in types.get_args(cls)) + return ( + inspect.isclass(cls) + and not issubclass(cls, Var) + and serializers.can_serialize(cls, dict) + ) + + @dataclasses.dataclass( eq=False, frozen=True, @@ -565,36 +582,33 @@ class Var(Generic[VAR_TYPE]): # Encode the _var_data into the formatted output for tracking purposes. return f"{constants.REFLEX_VAR_OPENING_TAG}{hashed_var}{constants.REFLEX_VAR_CLOSING_TAG}{self._js_expr}" - @overload - def to(self, output: Type[StringVar]) -> StringVar: ... - @overload def to(self, output: Type[str]) -> StringVar: ... @overload - def to(self, output: Type[BooleanVar]) -> BooleanVar: ... + def to(self, output: Type[bool]) -> BooleanVar: ... @overload - def to( - self, output: Type[NumberVar], var_type: type[int] | type[float] = float - ) -> NumberVar: ... + def to(self, output: type[int] | type[float]) -> NumberVar: ... @overload def to( self, - output: Type[ArrayVar], - var_type: type[list] | type[tuple] | type[set] = list, + output: type[list] | type[tuple] | type[set], ) -> ArrayVar: ... @overload def to( - self, output: Type[ObjectVar], var_type: types.GenericType = dict - ) -> ObjectVar: ... + self, output: Type[ObjectVar], var_type: Type[VAR_INSIDE] + ) -> ObjectVar[VAR_INSIDE]: ... @overload def to( - self, output: Type[FunctionVar], var_type: Type[Callable] = Callable - ) -> FunctionVar: ... + self, output: Type[ObjectVar], var_type: None = None + ) -> ObjectVar[VAR_TYPE]: ... + + @overload + def to(self, output: VAR_SUBCLASS, var_type: None = None) -> VAR_SUBCLASS: ... @overload def to( @@ -630,21 +644,19 @@ class Var(Generic[VAR_TYPE]): return get_to_operation(NoneVar).create(self) # type: ignore # Handle fixed_output_type being Base or a dataclass. - try: - if issubclass(fixed_output_type, Base): - return self.to(ObjectVar, output) - except TypeError: - pass - if dataclasses.is_dataclass(fixed_output_type) and not issubclass( - fixed_output_type, Var - ): + if can_use_in_object_var(fixed_output_type): return self.to(ObjectVar, output) if inspect.isclass(output): for var_subclass in _var_subclasses[::-1]: if issubclass(output, var_subclass.var_subclass): + current_var_type = self._var_type + if current_var_type is Any: + new_var_type = var_type + else: + new_var_type = var_type or current_var_type to_operation_return = var_subclass.to_var_subclass.create( - value=self, _var_type=var_type + value=self, _var_type=new_var_type ) return to_operation_return # type: ignore @@ -707,11 +719,7 @@ class Var(Generic[VAR_TYPE]): ): return self.to(NumberVar, self._var_type) - if all( - inspect.isclass(t) - and (issubclass(t, Base) or dataclasses.is_dataclass(t)) - for t in inner_types - ): + if can_use_in_object_var(var_type): return self.to(ObjectVar, self._var_type) return self @@ -730,13 +738,9 @@ class Var(Generic[VAR_TYPE]): if issubclass(fixed_type, var_subclass.python_types): return self.to(var_subclass.var_subclass, self._var_type) - try: - if issubclass(fixed_type, Base): - return self.to(ObjectVar, self._var_type) - except TypeError: - pass - if dataclasses.is_dataclass(fixed_type): + if can_use_in_object_var(fixed_type): return self.to(ObjectVar, self._var_type) + return self def get_default_value(self) -> Any: @@ -1181,6 +1185,9 @@ class Var(Generic[VAR_TYPE]): OUTPUT = TypeVar("OUTPUT", bound=Var) +VAR_SUBCLASS = TypeVar("VAR_SUBCLASS", bound=Var) +VAR_INSIDE = TypeVar("VAR_INSIDE") + class ToOperation: """A var operation that converts a var to another type.""" @@ -2888,6 +2895,8 @@ def dispatch( V = TypeVar("V") +BASE_TYPE = TypeVar("BASE_TYPE", bound=Base) + class Field(Generic[T]): """Shadow class for Var to allow for type hinting in the IDE.""" @@ -2924,6 +2933,11 @@ class Field(Generic[T]): self: Field[Dict[str, V]], instance: None, owner ) -> ObjectVar[Dict[str, V]]: ... + @overload + def __get__( + self: Field[BASE_TYPE], instance: None, owner + ) -> ObjectVar[BASE_TYPE]: ... + @overload def __get__(self, instance: None, owner) -> Var[T]: ... diff --git a/reflex/vars/number.py b/reflex/vars/number.py index e403e63e4..a762796e2 100644 --- a/reflex/vars/number.py +++ b/reflex/vars/number.py @@ -1116,7 +1116,9 @@ U = TypeVar("U") @var_operation -def ternary_operation(condition: BooleanVar, if_true: Var[T], if_false: Var[U]): +def ternary_operation( + condition: BooleanVar, if_true: Var[T], if_false: Var[U] +) -> CustomVarOperationReturn[Union[T, U]]: """Create a ternary operation. Args: diff --git a/reflex/vars/object.py b/reflex/vars/object.py index 56f3535d8..e60ea09e3 100644 --- a/reflex/vars/object.py +++ b/reflex/vars/object.py @@ -36,7 +36,7 @@ from .base import ( from .number import BooleanVar, NumberVar, raise_unsupported_operand_types from .sequence import ArrayVar, StringVar -OBJECT_TYPE = TypeVar("OBJECT_TYPE", bound=Dict) +OBJECT_TYPE = TypeVar("OBJECT_TYPE") KEY_TYPE = TypeVar("KEY_TYPE") VALUE_TYPE = TypeVar("VALUE_TYPE") @@ -59,7 +59,7 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=dict): @overload def _value_type( - self: ObjectVar[Dict[KEY_TYPE, VALUE_TYPE]], + self: ObjectVar[Dict[Any, VALUE_TYPE]], ) -> Type[VALUE_TYPE]: ... @overload @@ -87,7 +87,7 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=dict): @overload def values( - self: ObjectVar[Dict[KEY_TYPE, VALUE_TYPE]], + self: ObjectVar[Dict[Any, VALUE_TYPE]], ) -> ArrayVar[List[VALUE_TYPE]]: ... @overload @@ -103,7 +103,7 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=dict): @overload def entries( - self: ObjectVar[Dict[KEY_TYPE, VALUE_TYPE]], + self: ObjectVar[Dict[Any, VALUE_TYPE]], ) -> ArrayVar[List[Tuple[str, VALUE_TYPE]]]: ... @overload @@ -133,47 +133,47 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=dict): # NoReturn is used here to catch when key value is Any @overload def __getitem__( - self: ObjectVar[Dict[KEY_TYPE, NoReturn]], + self: ObjectVar[Dict[Any, NoReturn]], key: Var | Any, ) -> Var: ... @overload def __getitem__( self: ( - ObjectVar[Dict[KEY_TYPE, int]] - | ObjectVar[Dict[KEY_TYPE, float]] - | ObjectVar[Dict[KEY_TYPE, int | float]] + ObjectVar[Dict[Any, int]] + | ObjectVar[Dict[Any, float]] + | ObjectVar[Dict[Any, int | float]] ), key: Var | Any, ) -> NumberVar: ... @overload def __getitem__( - self: ObjectVar[Dict[KEY_TYPE, str]], + self: ObjectVar[Dict[Any, str]], key: Var | Any, ) -> StringVar: ... @overload def __getitem__( - self: ObjectVar[Dict[KEY_TYPE, list[ARRAY_INNER_TYPE]]], + self: ObjectVar[Dict[Any, list[ARRAY_INNER_TYPE]]], key: Var | Any, ) -> ArrayVar[list[ARRAY_INNER_TYPE]]: ... @overload def __getitem__( - self: ObjectVar[Dict[KEY_TYPE, set[ARRAY_INNER_TYPE]]], + self: ObjectVar[Dict[Any, set[ARRAY_INNER_TYPE]]], key: Var | Any, ) -> ArrayVar[set[ARRAY_INNER_TYPE]]: ... @overload def __getitem__( - self: ObjectVar[Dict[KEY_TYPE, tuple[ARRAY_INNER_TYPE, ...]]], + self: ObjectVar[Dict[Any, tuple[ARRAY_INNER_TYPE, ...]]], key: Var | Any, ) -> ArrayVar[tuple[ARRAY_INNER_TYPE, ...]]: ... @overload def __getitem__( - self: ObjectVar[Dict[KEY_TYPE, dict[OTHER_KEY_TYPE, VALUE_TYPE]]], + self: ObjectVar[Dict[Any, dict[OTHER_KEY_TYPE, VALUE_TYPE]]], key: Var | Any, ) -> ObjectVar[dict[OTHER_KEY_TYPE, VALUE_TYPE]]: ... @@ -195,50 +195,56 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=dict): # NoReturn is used here to catch when key value is Any @overload def __getattr__( - self: ObjectVar[Dict[KEY_TYPE, NoReturn]], + self: ObjectVar[Dict[Any, NoReturn]], name: str, ) -> Var: ... @overload def __getattr__( self: ( - ObjectVar[Dict[KEY_TYPE, int]] - | ObjectVar[Dict[KEY_TYPE, float]] - | ObjectVar[Dict[KEY_TYPE, int | float]] + ObjectVar[Dict[Any, int]] + | ObjectVar[Dict[Any, float]] + | ObjectVar[Dict[Any, int | float]] ), name: str, ) -> NumberVar: ... @overload def __getattr__( - self: ObjectVar[Dict[KEY_TYPE, str]], + self: ObjectVar[Dict[Any, str]], name: str, ) -> StringVar: ... @overload def __getattr__( - self: ObjectVar[Dict[KEY_TYPE, list[ARRAY_INNER_TYPE]]], + self: ObjectVar[Dict[Any, list[ARRAY_INNER_TYPE]]], name: str, ) -> ArrayVar[list[ARRAY_INNER_TYPE]]: ... @overload def __getattr__( - self: ObjectVar[Dict[KEY_TYPE, set[ARRAY_INNER_TYPE]]], + self: ObjectVar[Dict[Any, set[ARRAY_INNER_TYPE]]], name: str, ) -> ArrayVar[set[ARRAY_INNER_TYPE]]: ... @overload def __getattr__( - self: ObjectVar[Dict[KEY_TYPE, tuple[ARRAY_INNER_TYPE, ...]]], + self: ObjectVar[Dict[Any, tuple[ARRAY_INNER_TYPE, ...]]], name: str, ) -> ArrayVar[tuple[ARRAY_INNER_TYPE, ...]]: ... @overload def __getattr__( - self: ObjectVar[Dict[KEY_TYPE, dict[OTHER_KEY_TYPE, VALUE_TYPE]]], + self: ObjectVar[Dict[Any, dict[OTHER_KEY_TYPE, VALUE_TYPE]]], name: str, ) -> ObjectVar[dict[OTHER_KEY_TYPE, VALUE_TYPE]]: ... + @overload + def __getattr__( + self: ObjectVar, + name: str, + ) -> ObjectItemOperation: ... + def __getattr__(self, name) -> Var: """Get an attribute of the var. @@ -377,8 +383,8 @@ class LiteralObjectVar(CachedVarOperation, ObjectVar[OBJECT_TYPE], LiteralVar): @classmethod def create( cls, - _var_value: OBJECT_TYPE, - _var_type: GenericType | None = None, + _var_value: dict, + _var_type: Type[OBJECT_TYPE] | None = None, _var_data: VarData | None = None, ) -> LiteralObjectVar[OBJECT_TYPE]: """Create the literal object var. diff --git a/reflex/vars/sequence.py b/reflex/vars/sequence.py index 39139ce3f..08429883f 100644 --- a/reflex/vars/sequence.py +++ b/reflex/vars/sequence.py @@ -853,31 +853,31 @@ class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(list, tuple, set)): @overload def __getitem__( self: ( - ArrayVar[Tuple[OTHER_TUPLE, int]] - | ArrayVar[Tuple[OTHER_TUPLE, float]] - | ArrayVar[Tuple[OTHER_TUPLE, int | float]] + ArrayVar[Tuple[Any, int]] + | ArrayVar[Tuple[Any, float]] + | ArrayVar[Tuple[Any, int | float]] ), i: Literal[1, -1], ) -> NumberVar: ... @overload def __getitem__( - self: ArrayVar[Tuple[str, OTHER_TUPLE]], i: Literal[0, -2] + self: ArrayVar[Tuple[str, Any]], i: Literal[0, -2] ) -> StringVar: ... @overload def __getitem__( - self: ArrayVar[Tuple[OTHER_TUPLE, str]], i: Literal[1, -1] + self: ArrayVar[Tuple[Any, str]], i: Literal[1, -1] ) -> StringVar: ... @overload def __getitem__( - self: ArrayVar[Tuple[bool, OTHER_TUPLE]], i: Literal[0, -2] + self: ArrayVar[Tuple[bool, Any]], i: Literal[0, -2] ) -> BooleanVar: ... @overload def __getitem__( - self: ArrayVar[Tuple[OTHER_TUPLE, bool]], i: Literal[1, -1] + self: ArrayVar[Tuple[Any, bool]], i: Literal[1, -1] ) -> BooleanVar: ... @overload diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 3ff8d453c..83e348cd2 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -45,7 +45,7 @@ from reflex.testing import chdir from reflex.utils import format, prerequisites, types from reflex.utils.exceptions import SetUndefinedStateVarError from reflex.utils.format import json_dumps -from reflex.vars.base import ComputedVar, Var +from reflex.vars.base import Var, computed_var from tests.units.states.mutation import MutableSQLAModel, MutableTestState from .states import GenState @@ -109,7 +109,7 @@ class TestState(BaseState): _backend: int = 0 asynctest: int = 0 - @ComputedVar + @computed_var def sum(self) -> float: """Dynamically sum the numbers. @@ -118,7 +118,7 @@ class TestState(BaseState): """ return self.num1 + self.num2 - @ComputedVar + @computed_var def upper(self) -> str: """Uppercase the key. @@ -1124,7 +1124,7 @@ def test_child_state(): v: int = 2 class ChildState(MainState): - @ComputedVar + @computed_var def rendered_var(self): return self.v @@ -1143,7 +1143,7 @@ def test_conditional_computed_vars(): t1: str = "a" t2: str = "b" - @ComputedVar + @computed_var def rendered_var(self) -> str: if self.flag: return self.t1 @@ -3095,12 +3095,12 @@ def test_potentially_dirty_substates(): """ class State(RxState): - @ComputedVar + @computed_var def foo(self) -> str: return "" class C1(State): - @ComputedVar + @computed_var def bar(self) -> str: return "" diff --git a/tests/units/vars/test_object.py b/tests/units/vars/test_object.py new file mode 100644 index 000000000..efcb21166 --- /dev/null +++ b/tests/units/vars/test_object.py @@ -0,0 +1,102 @@ +import pytest +from typing_extensions import assert_type + +import reflex as rx +from reflex.utils.types import GenericType +from reflex.vars.base import Var +from reflex.vars.object import LiteralObjectVar, ObjectVar + + +class Bare: + """A bare class with a single attribute.""" + + quantity: int = 0 + + +@rx.serializer +def serialize_bare(obj: Bare) -> dict: + """A serializer for the bare class. + + Args: + obj: The object to serialize. + + Returns: + A dictionary with the quantity attribute. + """ + return {"quantity": obj.quantity} + + +class Base(rx.Base): + """A reflex base class with a single attribute.""" + + quantity: int = 0 + + +class ObjectState(rx.State): + """A reflex state with bare and base objects.""" + + bare: rx.Field[Bare] = rx.field(Bare()) + base: rx.Field[Base] = rx.field(Base()) + + +@pytest.mark.parametrize("type_", [Base, Bare]) +def test_var_create(type_: GenericType) -> None: + my_object = type_() + var = Var.create(my_object) + assert var._var_type is type_ + + quantity = var.quantity + assert quantity._var_type is int + + +@pytest.mark.parametrize("type_", [Base, Bare]) +def test_literal_create(type_: GenericType) -> None: + my_object = type_() + var = LiteralObjectVar.create(my_object) + assert var._var_type is type_ + + quantity = var.quantity + assert quantity._var_type is int + + +@pytest.mark.parametrize("type_", [Base, Bare]) +def test_guess(type_: GenericType) -> None: + my_object = type_() + var = Var.create(my_object) + var = var.guess_type() + assert var._var_type is type_ + + quantity = var.quantity + assert quantity._var_type is int + + +@pytest.mark.parametrize("type_", [Base, Bare]) +def test_state(type_: GenericType) -> None: + attr_name = type_.__name__.lower() + var = getattr(ObjectState, attr_name) + assert var._var_type is type_ + + quantity = var.quantity + assert quantity._var_type is int + + +@pytest.mark.parametrize("type_", [Base, Bare]) +def test_state_to_operation(type_: GenericType) -> None: + attr_name = type_.__name__.lower() + original_var = getattr(ObjectState, attr_name) + + var = original_var.to(ObjectVar, type_) + assert var._var_type is type_ + + var = original_var.to(ObjectVar) + assert var._var_type is type_ + + +def test_typing() -> None: + # Bare + var = ObjectState.bare.to(ObjectVar) + _ = assert_type(var, ObjectVar[Bare]) + + # Base + var = ObjectState.base + _ = assert_type(var, ObjectVar[Base]) From ce22ca5f71c581e5310a363899b125f3f5e68c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Tue, 5 Nov 2024 10:50:32 -0800 Subject: [PATCH 13/18] rollback to 14.2.16 until v15 is more stable (#4297) --- reflex/constants/installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index f720218e3..4b4716fc9 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -177,7 +177,7 @@ class PackageJson(SimpleNamespace): "@emotion/react": "11.13.3", "axios": "1.7.7", "json5": "2.2.3", - "next": "15.0.1", + "next": "14.2.16", "next-sitemap": "4.2.3", "next-themes": "0.3.0", "react": "18.3.1", From 1c4f4100523a8cf228ccfb24aa532f11a8f0b752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Tue, 5 Nov 2024 10:50:43 -0800 Subject: [PATCH 14/18] bump python packages version (#4302) --- .pre-commit-config.yaml | 2 +- poetry.lock | 246 ++++++++++++++++++++-------------------- pyproject.toml | 6 +- 3 files changed, 128 insertions(+), 126 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 60cbec00f..41372d4e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ fail_fast: true repos: - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.7.1 + rev: v0.7.2 hooks: - id: ruff-format args: [reflex, tests] diff --git a/poetry.lock b/poetry.lock index baf9ebb69..15f8b951d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "alembic" -version = "1.13.3" +version = "1.14.0" description = "A database migration tool for SQLAlchemy." optional = false python-versions = ">=3.8" files = [ - {file = "alembic-1.13.3-py3-none-any.whl", hash = "sha256:908e905976d15235fae59c9ac42c4c5b75cfcefe3d27c0fbf7ae15a37715d80e"}, - {file = "alembic-1.13.3.tar.gz", hash = "sha256:203503117415561e203aa14541740643a611f641517f0209fcae63e9fa09f1a2"}, + {file = "alembic-1.14.0-py3-none-any.whl", hash = "sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25"}, + {file = "alembic-1.14.0.tar.gz", hash = "sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b"}, ] [package.dependencies] @@ -54,13 +54,13 @@ trio = ["trio (>=0.26.1)"] [[package]] name = "async-timeout" -version = "4.0.3" +version = "5.0.0" description = "Timeout context manager for asyncio programs" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, + {file = "async_timeout-5.0.0-py3-none-any.whl", hash = "sha256:904719a4bd6e0520047d0ddae220aabee67b877f7ca17bf8cea20f67f6247ae0"}, + {file = "async_timeout-5.0.0.tar.gz", hash = "sha256:49675ec889daacfe65ff66d2dde7dd1447a6f4b2f23721022e4ba121f8772a85"}, ] [[package]] @@ -585,13 +585,13 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.115.3" +version = "0.115.4" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi-0.115.3-py3-none-any.whl", hash = "sha256:8035e8f9a2b0aa89cea03b6c77721178ed5358e1aea4cd8570d9466895c0638c"}, - {file = "fastapi-0.115.3.tar.gz", hash = "sha256:c091c6a35599c036d676fa24bd4a6e19fa30058d93d950216cdc672881f6f7db"}, + {file = "fastapi-0.115.4-py3-none-any.whl", hash = "sha256:0b504a063ffb3cf96a5e27dc1bc32c80ca743a2528574f9cdc77daa2d31b4742"}, + {file = "fastapi-0.115.4.tar.gz", hash = "sha256:db653475586b091cb8b2fec2ac54a680ac6a158e07406e1abae31679e8826349"}, ] [package.dependencies] @@ -937,13 +937,13 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "keyring" -version = "25.4.1" +version = "25.5.0" description = "Store and access your passwords safely." optional = false python-versions = ">=3.8" files = [ - {file = "keyring-25.4.1-py3-none-any.whl", hash = "sha256:5426f817cf7f6f007ba5ec722b1bcad95a75b27d780343772ad76b17cb47b0bf"}, - {file = "keyring-25.4.1.tar.gz", hash = "sha256:b07ebc55f3e8ed86ac81dd31ef14e81ace9dd9c3d4b5d77a6e9a2016d0d71a1b"}, + {file = "keyring-25.5.0-py3-none-any.whl", hash = "sha256:e67f8ac32b04be4714b42fe84ce7dad9c40985b9ca827c592cc303e7c26d9741"}, + {file = "keyring-25.5.0.tar.gz", hash = "sha256:4c753b3ec91717fe713c4edd522d625889d8973a349b0e582622f49766de58e6"}, ] [package.dependencies] @@ -1210,64 +1210,66 @@ files = [ [[package]] name = "numpy" -version = "2.1.2" +version = "2.1.3" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" files = [ - {file = "numpy-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30d53720b726ec36a7f88dc873f0eec8447fbc93d93a8f079dfac2629598d6ee"}, - {file = "numpy-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d3ca0a72dd8846eb6f7dfe8f19088060fcb76931ed592d29128e0219652884"}, - {file = "numpy-2.1.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:fc44e3c68ff00fd991b59092a54350e6e4911152682b4782f68070985aa9e648"}, - {file = "numpy-2.1.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:7c1c60328bd964b53f8b835df69ae8198659e2b9302ff9ebb7de4e5a5994db3d"}, - {file = "numpy-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cdb606a7478f9ad91c6283e238544451e3a95f30fb5467fbf715964341a8a86"}, - {file = "numpy-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d666cb72687559689e9906197e3bec7b736764df6a2e58ee265e360663e9baf7"}, - {file = "numpy-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6eef7a2dbd0abfb0d9eaf78b73017dbfd0b54051102ff4e6a7b2980d5ac1a03"}, - {file = "numpy-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:12edb90831ff481f7ef5f6bc6431a9d74dc0e5ff401559a71e5e4611d4f2d466"}, - {file = "numpy-2.1.2-cp310-cp310-win32.whl", hash = "sha256:a65acfdb9c6ebb8368490dbafe83c03c7e277b37e6857f0caeadbbc56e12f4fb"}, - {file = "numpy-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:860ec6e63e2c5c2ee5e9121808145c7bf86c96cca9ad396c0bd3e0f2798ccbe2"}, - {file = "numpy-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b42a1a511c81cc78cbc4539675713bbcf9d9c3913386243ceff0e9429ca892fe"}, - {file = "numpy-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:faa88bc527d0f097abdc2c663cddf37c05a1c2f113716601555249805cf573f1"}, - {file = "numpy-2.1.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c82af4b2ddd2ee72d1fc0c6695048d457e00b3582ccde72d8a1c991b808bb20f"}, - {file = "numpy-2.1.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:13602b3174432a35b16c4cfb5de9a12d229727c3dd47a6ce35111f2ebdf66ff4"}, - {file = "numpy-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ebec5fd716c5a5b3d8dfcc439be82a8407b7b24b230d0ad28a81b61c2f4659a"}, - {file = "numpy-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2b49c3c0804e8ecb05d59af8386ec2f74877f7ca8fd9c1e00be2672e4d399b1"}, - {file = "numpy-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2cbba4b30bf31ddbe97f1c7205ef976909a93a66bb1583e983adbd155ba72ac2"}, - {file = "numpy-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8e00ea6fc82e8a804433d3e9cedaa1051a1422cb6e443011590c14d2dea59146"}, - {file = "numpy-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5006b13a06e0b38d561fab5ccc37581f23c9511879be7693bd33c7cd15ca227c"}, - {file = "numpy-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:f1eb068ead09f4994dec71c24b2844f1e4e4e013b9629f812f292f04bd1510d9"}, - {file = "numpy-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7bf0a4f9f15b32b5ba53147369e94296f5fffb783db5aacc1be15b4bf72f43b"}, - {file = "numpy-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b1d0fcae4f0949f215d4632be684a539859b295e2d0cb14f78ec231915d644db"}, - {file = "numpy-2.1.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f751ed0a2f250541e19dfca9f1eafa31a392c71c832b6bb9e113b10d050cb0f1"}, - {file = "numpy-2.1.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:bd33f82e95ba7ad632bc57837ee99dba3d7e006536200c4e9124089e1bf42426"}, - {file = "numpy-2.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b8cde4f11f0a975d1fd59373b32e2f5a562ade7cde4f85b7137f3de8fbb29a0"}, - {file = "numpy-2.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d95f286b8244b3649b477ac066c6906fbb2905f8ac19b170e2175d3d799f4df"}, - {file = "numpy-2.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ab4754d432e3ac42d33a269c8567413bdb541689b02d93788af4131018cbf366"}, - {file = "numpy-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e585c8ae871fd38ac50598f4763d73ec5497b0de9a0ab4ef5b69f01c6a046142"}, - {file = "numpy-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9c6c754df29ce6a89ed23afb25550d1c2d5fdb9901d9c67a16e0b16eaf7e2550"}, - {file = "numpy-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:456e3b11cb79ac9946c822a56346ec80275eaf2950314b249b512896c0d2505e"}, - {file = "numpy-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a84498e0d0a1174f2b3ed769b67b656aa5460c92c9554039e11f20a05650f00d"}, - {file = "numpy-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4d6ec0d4222e8ffdab1744da2560f07856421b367928026fb540e1945f2eeeaf"}, - {file = "numpy-2.1.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:259ec80d54999cc34cd1eb8ded513cb053c3bf4829152a2e00de2371bd406f5e"}, - {file = "numpy-2.1.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:675c741d4739af2dc20cd6c6a5c4b7355c728167845e3c6b0e824e4e5d36a6c3"}, - {file = "numpy-2.1.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b2d4e667895cc55e3ff2b56077e4c8a5604361fc21a042845ea3ad67465aa8"}, - {file = "numpy-2.1.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43cca367bf94a14aca50b89e9bc2061683116cfe864e56740e083392f533ce7a"}, - {file = "numpy-2.1.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:76322dcdb16fccf2ac56f99048af32259dcc488d9b7e25b51e5eca5147a3fb98"}, - {file = "numpy-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:32e16a03138cabe0cb28e1007ee82264296ac0983714094380b408097a418cfe"}, - {file = "numpy-2.1.2-cp313-cp313-win32.whl", hash = "sha256:242b39d00e4944431a3cd2db2f5377e15b5785920421993770cddb89992c3f3a"}, - {file = "numpy-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f2ded8d9b6f68cc26f8425eda5d3877b47343e68ca23d0d0846f4d312ecaa445"}, - {file = "numpy-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ffef621c14ebb0188a8633348504a35c13680d6da93ab5cb86f4e54b7e922b5"}, - {file = "numpy-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad369ed238b1959dfbade9018a740fb9392c5ac4f9b5173f420bd4f37ba1f7a0"}, - {file = "numpy-2.1.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d82075752f40c0ddf57e6e02673a17f6cb0f8eb3f587f63ca1eaab5594da5b17"}, - {file = "numpy-2.1.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:1600068c262af1ca9580a527d43dc9d959b0b1d8e56f8a05d830eea39b7c8af6"}, - {file = "numpy-2.1.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a26ae94658d3ba3781d5e103ac07a876b3e9b29db53f68ed7df432fd033358a8"}, - {file = "numpy-2.1.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13311c2db4c5f7609b462bc0f43d3c465424d25c626d95040f073e30f7570e35"}, - {file = "numpy-2.1.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:2abbf905a0b568706391ec6fa15161fad0fb5d8b68d73c461b3c1bab6064dd62"}, - {file = "numpy-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ef444c57d664d35cac4e18c298c47d7b504c66b17c2ea91312e979fcfbdfb08a"}, - {file = "numpy-2.1.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bdd407c40483463898b84490770199d5714dcc9dd9b792f6c6caccc523c00952"}, - {file = "numpy-2.1.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:da65fb46d4cbb75cb417cddf6ba5e7582eb7bb0b47db4b99c9fe5787ce5d91f5"}, - {file = "numpy-2.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c193d0b0238638e6fc5f10f1b074a6993cb13b0b431f64079a509d63d3aa8b7"}, - {file = "numpy-2.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a7d80b2e904faa63068ead63107189164ca443b42dd1930299e0d1cb041cec2e"}, - {file = "numpy-2.1.2.tar.gz", hash = "sha256:13532a088217fa624c99b843eeb54640de23b3414b14aa66d023805eb731066c"}, + {file = "numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff"}, + {file = "numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5"}, + {file = "numpy-2.1.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1"}, + {file = "numpy-2.1.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd"}, + {file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3"}, + {file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098"}, + {file = "numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c"}, + {file = "numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4"}, + {file = "numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23"}, + {file = "numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0"}, + {file = "numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d"}, + {file = "numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41"}, + {file = "numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9"}, + {file = "numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09"}, + {file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a"}, + {file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b"}, + {file = "numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee"}, + {file = "numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0"}, + {file = "numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9"}, + {file = "numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2"}, + {file = "numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e"}, + {file = "numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958"}, + {file = "numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8"}, + {file = "numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564"}, + {file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512"}, + {file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b"}, + {file = "numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc"}, + {file = "numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0"}, + {file = "numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9"}, + {file = "numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a"}, + {file = "numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f"}, + {file = "numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598"}, + {file = "numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57"}, + {file = "numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe"}, + {file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43"}, + {file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56"}, + {file = "numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a"}, + {file = "numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef"}, + {file = "numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f"}, + {file = "numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed"}, + {file = "numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f"}, + {file = "numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4"}, + {file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e"}, + {file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0"}, + {file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408"}, + {file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6"}, + {file = "numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f"}, + {file = "numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17"}, + {file = "numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48"}, + {file = "numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4"}, + {file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f"}, + {file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4"}, + {file = "numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d"}, + {file = "numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb"}, + {file = "numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761"}, ] [[package]] @@ -1475,13 +1477,13 @@ xmp = ["defusedxml"] [[package]] name = "pip" -version = "24.2" +version = "24.3.1" description = "The PyPA recommended tool for installing Python packages." optional = false python-versions = ">=3.8" files = [ - {file = "pip-24.2-py3-none-any.whl", hash = "sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2"}, - {file = "pip-24.2.tar.gz", hash = "sha256:5b5e490b5e9cb275c879595064adce9ebd31b854e3e803740b72f9ccf34a45b8"}, + {file = "pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed"}, + {file = "pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99"}, ] [[package]] @@ -1908,37 +1910,37 @@ test = ["black (>=22.1.0)", "flake8 (>=4.0.1)", "pre-commit (>=2.17.0)", "pytest [[package]] name = "pytest-benchmark" -version = "4.0.0" +version = "5.1.0" description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1"}, - {file = "pytest_benchmark-4.0.0-py3-none-any.whl", hash = "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6"}, + {file = "pytest-benchmark-5.1.0.tar.gz", hash = "sha256:9ea661cdc292e8231f7cd4c10b0319e56a2118e2c09d9f50e1b3d150d2aca105"}, + {file = "pytest_benchmark-5.1.0-py3-none-any.whl", hash = "sha256:922de2dfa3033c227c96da942d1878191afa135a29485fb942e85dff1c592c89"}, ] [package.dependencies] py-cpuinfo = "*" -pytest = ">=3.8" +pytest = ">=8.1" [package.extras] aspect = ["aspectlib"] elasticsearch = ["elasticsearch"] -histogram = ["pygal", "pygaljs"] +histogram = ["pygal", "pygaljs", "setuptools"] [[package]] name = "pytest-cov" -version = "5.0.0" +version = "6.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, ] [package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} +coverage = {version = ">=7.5", extras = ["toml"]} pytest = ">=4.6" [package.extras] @@ -2013,13 +2015,13 @@ docs = ["sphinx"] [[package]] name = "python-multipart" -version = "0.0.12" +version = "0.0.17" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.8" files = [ - {file = "python_multipart-0.0.12-py3-none-any.whl", hash = "sha256:43dcf96cf65888a9cd3423544dd0d75ac10f7aa0c3c28a175bbcd00c9ce1aebf"}, - {file = "python_multipart-0.0.12.tar.gz", hash = "sha256:045e1f98d719c1ce085ed7f7e1ef9d8ccc8c02ba02b5566d5f7521410ced58cb"}, + {file = "python_multipart-0.0.17-py3-none-any.whl", hash = "sha256:15dc4f487e0a9476cc1201261188ee0940165cffc94429b6fc565c4d3045cb5d"}, + {file = "python_multipart-0.0.17.tar.gz", hash = "sha256:41330d831cae6e2f22902704ead2826ea038d0419530eadff3ea80175aec5538"}, ] [[package]] @@ -2268,13 +2270,13 @@ idna2008 = ["idna"] [[package]] name = "rich" -version = "13.9.3" +version = "13.9.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" files = [ - {file = "rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283"}, - {file = "rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e"}, + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, ] [package.dependencies] @@ -2287,29 +2289,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.7.1" +version = "0.7.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.7.1-py3-none-linux_armv6l.whl", hash = "sha256:cb1bc5ed9403daa7da05475d615739cc0212e861b7306f314379d958592aaa89"}, - {file = "ruff-0.7.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27c1c52a8d199a257ff1e5582d078eab7145129aa02721815ca8fa4f9612dc35"}, - {file = "ruff-0.7.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:588a34e1ef2ea55b4ddfec26bbe76bc866e92523d8c6cdec5e8aceefeff02d99"}, - {file = "ruff-0.7.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94fc32f9cdf72dc75c451e5f072758b118ab8100727168a3df58502b43a599ca"}, - {file = "ruff-0.7.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:985818742b833bffa543a84d1cc11b5e6871de1b4e0ac3060a59a2bae3969250"}, - {file = "ruff-0.7.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32f1e8a192e261366c702c5fb2ece9f68d26625f198a25c408861c16dc2dea9c"}, - {file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:699085bf05819588551b11751eff33e9ca58b1b86a6843e1b082a7de40da1565"}, - {file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:344cc2b0814047dc8c3a8ff2cd1f3d808bb23c6658db830d25147339d9bf9ea7"}, - {file = "ruff-0.7.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4316bbf69d5a859cc937890c7ac7a6551252b6a01b1d2c97e8fc96e45a7c8b4a"}, - {file = "ruff-0.7.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79d3af9dca4c56043e738a4d6dd1e9444b6d6c10598ac52d146e331eb155a8ad"}, - {file = "ruff-0.7.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5c121b46abde94a505175524e51891f829414e093cd8326d6e741ecfc0a9112"}, - {file = "ruff-0.7.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8422104078324ea250886954e48f1373a8fe7de59283d747c3a7eca050b4e378"}, - {file = "ruff-0.7.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:56aad830af8a9db644e80098fe4984a948e2b6fc2e73891538f43bbe478461b8"}, - {file = "ruff-0.7.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:658304f02f68d3a83c998ad8bf91f9b4f53e93e5412b8f2388359d55869727fd"}, - {file = "ruff-0.7.1-py3-none-win32.whl", hash = "sha256:b517a2011333eb7ce2d402652ecaa0ac1a30c114fbbd55c6b8ee466a7f600ee9"}, - {file = "ruff-0.7.1-py3-none-win_amd64.whl", hash = "sha256:f38c41fcde1728736b4eb2b18850f6d1e3eedd9678c914dede554a70d5241307"}, - {file = "ruff-0.7.1-py3-none-win_arm64.whl", hash = "sha256:19aa200ec824c0f36d0c9114c8ec0087082021732979a359d6f3c390a6ff2a37"}, - {file = "ruff-0.7.1.tar.gz", hash = "sha256:9d8a41d4aa2dad1575adb98a82870cf5db5f76b2938cf2206c22c940034a36f4"}, + {file = "ruff-0.7.2-py3-none-linux_armv6l.whl", hash = "sha256:b73f873b5f52092e63ed540adefc3c36f1f803790ecf2590e1df8bf0a9f72cb8"}, + {file = "ruff-0.7.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5b813ef26db1015953daf476202585512afd6a6862a02cde63f3bafb53d0b2d4"}, + {file = "ruff-0.7.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:853277dbd9675810c6826dad7a428d52a11760744508340e66bf46f8be9701d9"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21aae53ab1490a52bf4e3bf520c10ce120987b047c494cacf4edad0ba0888da2"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccc7e0fc6e0cb3168443eeadb6445285abaae75142ee22b2b72c27d790ab60ba"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd77877a4e43b3a98e5ef4715ba3862105e299af0c48942cc6d51ba3d97dc859"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e00163fb897d35523c70d71a46fbaa43bf7bf9af0f4534c53ea5b96b2e03397b"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3c54b538633482dc342e9b634d91168fe8cc56b30a4b4f99287f4e339103e88"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b792468e9804a204be221b14257566669d1db5c00d6bb335996e5cd7004ba80"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dba53ed84ac19ae4bfb4ea4bf0172550a2285fa27fbb13e3746f04c80f7fa088"}, + {file = "ruff-0.7.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b19fafe261bf741bca2764c14cbb4ee1819b67adb63ebc2db6401dcd652e3748"}, + {file = "ruff-0.7.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28bd8220f4d8f79d590db9e2f6a0674f75ddbc3847277dd44ac1f8d30684b828"}, + {file = "ruff-0.7.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9fd67094e77efbea932e62b5d2483006154794040abb3a5072e659096415ae1e"}, + {file = "ruff-0.7.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:576305393998b7bd6c46018f8104ea3a9cb3fa7908c21d8580e3274a3b04b691"}, + {file = "ruff-0.7.2-py3-none-win32.whl", hash = "sha256:fa993cfc9f0ff11187e82de874dfc3611df80852540331bc85c75809c93253a8"}, + {file = "ruff-0.7.2-py3-none-win_amd64.whl", hash = "sha256:dd8800cbe0254e06b8fec585e97554047fb82c894973f7ff18558eee33d1cb88"}, + {file = "ruff-0.7.2-py3-none-win_arm64.whl", hash = "sha256:bb8368cd45bba3f57bb29cbb8d64b4a33f8415d0149d2655c5c8539452ce7760"}, + {file = "ruff-0.7.2.tar.gz", hash = "sha256:2b14e77293380e475b4e3a7a368e14549288ed2931fce259a6f99978669e844f"}, ] [[package]] @@ -2329,13 +2331,13 @@ jeepney = ">=0.6" [[package]] name = "selenium" -version = "4.25.0" +version = "4.26.1" description = "Official Python bindings for Selenium WebDriver" optional = false python-versions = ">=3.8" files = [ - {file = "selenium-4.25.0-py3-none-any.whl", hash = "sha256:3798d2d12b4a570bc5790163ba57fef10b2afee958bf1d80f2a3cf07c4141f33"}, - {file = "selenium-4.25.0.tar.gz", hash = "sha256:95d08d3b82fb353f3c474895154516604c7f0e6a9a565ae6498ef36c9bac6921"}, + {file = "selenium-4.26.1-py3-none-any.whl", hash = "sha256:1db3f3a0cd5bb07624fa8a3905a6fdde1595a42185a0617077c361dc53d104fb"}, + {file = "selenium-4.26.1.tar.gz", hash = "sha256:7640f3f08ae7f4e450f895678e8a10a55eb4e4ca18311ed675ecc4684b96b683"}, ] [package.dependencies] @@ -2348,23 +2350,23 @@ websocket-client = ">=1.8,<2.0" [[package]] name = "setuptools" -version = "75.2.0" +version = "75.3.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-75.2.0-py3-none-any.whl", hash = "sha256:a7fcb66f68b4d9e8e66b42f9876150a3371558f98fa32222ffaa5bced76406f8"}, - {file = "setuptools-75.2.0.tar.gz", hash = "sha256:753bb6ebf1f465a1912e19ed1d41f403a79173a9acf66a42e7e6aec45c3c16ec"}, + {file = "setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd"}, + {file = "setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.12.*)", "pytest-mypy"] [[package]] name = "shellingham" @@ -2540,13 +2542,13 @@ SQLAlchemy = ">=2.0.14,<2.1.0" [[package]] name = "starlette" -version = "0.41.0" +version = "0.41.2" description = "The little ASGI library that shines." optional = false python-versions = ">=3.8" files = [ - {file = "starlette-0.41.0-py3-none-any.whl", hash = "sha256:a0193a3c413ebc9c78bff1c3546a45bb8c8bcb4a84cae8747d650a65bd37210a"}, - {file = "starlette-0.41.0.tar.gz", hash = "sha256:39cbd8768b107d68bfe1ff1672b38a2c38b49777de46d2a592841d58e3bf7c2a"}, + {file = "starlette-0.41.2-py3-none-any.whl", hash = "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d"}, + {file = "starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62"}, ] [package.dependencies] @@ -2790,13 +2792,13 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", [[package]] name = "virtualenv" -version = "20.27.0" +version = "20.27.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" files = [ - {file = "virtualenv-20.27.0-py3-none-any.whl", hash = "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655"}, - {file = "virtualenv-20.27.0.tar.gz", hash = "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2"}, + {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, + {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, ] [package.dependencies] @@ -3048,4 +3050,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "547fdabf7a030c2a7c8d63eb5b2a3c5e821afa86390f08b895db038d30013904" +content-hash = "664c8d3c78923d39d1d59227cb43416228ac396a7004344c058377886421c086" diff --git a/pyproject.toml b/pyproject.toml index 511ac9a7d..a3e3a17f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,15 +68,15 @@ darglint = ">=1.8.1,<2.0" dill = ">=0.3.8" toml = ">=0.10.2,<1.0" pytest-asyncio = ">=0.24.0" -pytest-cov = ">=4.0.0,<6.0" -ruff = "0.7.1" +pytest-cov = ">=4.0.0,<7.0" +ruff = "0.7.2" pandas = ">=2.1.1,<3.0" pillow = ">=10.0.0,<12.0" plotly = ">=5.13.0,<6.0" asynctest = ">=0.13.0,<1.0" pre-commit = ">=3.2.1" selenium = ">=4.11.0,<5.0" -pytest-benchmark = ">=4.0.0,<5.0" +pytest-benchmark = ">=4.0.0,<6.0" playwright = ">=1.46.0" pytest-playwright = ">=0.5.1" From 4a6c16e9dc31650caa192201fbd5005f381f2ea6 Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Tue, 5 Nov 2024 21:25:13 +0100 Subject: [PATCH 15/18] More env var cleanup (#4248) * fix and test bug in config env loading * streamline env var interpretation with @adhami3310 * improve error messages, fix invalid value for TELEMETRY_ENABLED * just a small hint * ruffing * fix typo from review * refactor - ruff broke the imports.. * cleanup imports * more * add internal and enum env var support * ruff cleanup * more global imports * revert telemetry, it lives in rx.Config * minor fixes/cleanup * i missed some refs * fix darglint * reload config is internal * fix EnvVar name * add test for EnvVar + minor typing improvement * bool tests * was this broken? * retain old behavior * migrate APP_HARNESS_HEADLESS to new env var system * migrate more APP_HARNESS env vars to new config system * migrate SCREENSHOT_DIR to new env var system * refactor EnvVar.get to be a method * readd deleted functions and deprecate them * improve EnvVar api, cleanup RELOAD_CONFIG question * move is_prod_mode back to where it was --- reflex/app.py | 11 +- reflex/base.py | 6 +- reflex/compiler/compiler.py | 2 +- reflex/components/core/upload.py | 2 +- reflex/config.py | 230 +++++++++++++++--- reflex/constants/__init__.py | 7 - reflex/constants/base.py | 17 +- reflex/constants/installer.py | 4 +- reflex/custom_components/custom_components.py | 4 +- reflex/model.py | 14 +- reflex/reflex.py | 14 +- reflex/state.py | 2 +- reflex/testing.py | 12 +- reflex/utils/exec.py | 39 ++- reflex/utils/net.py | 2 +- reflex/utils/path_ops.py | 4 +- reflex/utils/prerequisites.py | 19 +- reflex/utils/registry.py | 2 +- reflex/utils/telemetry.py | 5 +- tests/integration/conftest.py | 5 +- tests/units/test_config.py | 40 ++- tests/units/utils/test_utils.py | 9 + 22 files changed, 331 insertions(+), 119 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 5367ef20b..ae3f904c0 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -12,7 +12,6 @@ import inspect import io import json import multiprocessing -import os import platform import sys import traceback @@ -96,7 +95,7 @@ from reflex.state import ( code_uses_state_contexts, ) from reflex.utils import codespaces, console, exceptions, format, prerequisites, types -from reflex.utils.exec import is_prod_mode, is_testing_env, should_skip_compile +from reflex.utils.exec import is_prod_mode, is_testing_env from reflex.utils.imports import ImportVar if TYPE_CHECKING: @@ -507,7 +506,7 @@ class App(MiddlewareMixin, LifespanMixin): # Check if the route given is valid verify_route_validity(route) - if route in self.unevaluated_pages and os.getenv(constants.RELOAD_CONFIG): + if route in self.unevaluated_pages and environment.RELOAD_CONFIG.is_set(): # when the app is reloaded(typically for app harness tests), we should maintain # the latest render function of a route.This applies typically to decorated pages # since they are only added when app._compile is called. @@ -724,7 +723,7 @@ class App(MiddlewareMixin, LifespanMixin): Whether the app should be compiled. """ # Check the environment variable. - if should_skip_compile(): + if environment.REFLEX_SKIP_COMPILE.get(): return False nocompile = prerequisites.get_web_dir() / constants.NOCOMPILE_FILE @@ -947,7 +946,7 @@ class App(MiddlewareMixin, LifespanMixin): executor = None if ( platform.system() in ("Linux", "Darwin") - and (number_of_processes := environment.REFLEX_COMPILE_PROCESSES) + and (number_of_processes := environment.REFLEX_COMPILE_PROCESSES.get()) is not None ): executor = concurrent.futures.ProcessPoolExecutor( @@ -956,7 +955,7 @@ class App(MiddlewareMixin, LifespanMixin): ) else: executor = concurrent.futures.ThreadPoolExecutor( - max_workers=environment.REFLEX_COMPILE_THREADS + max_workers=environment.REFLEX_COMPILE_THREADS.get() ) for route, component in zip(self.pages, page_components): diff --git a/reflex/base.py b/reflex/base.py index c334ddf56..e661d3ade 100644 --- a/reflex/base.py +++ b/reflex/base.py @@ -16,9 +16,6 @@ except ModuleNotFoundError: from pydantic.fields import ModelField # type: ignore -from reflex import constants - - def validate_field_name(bases: List[Type["BaseModel"]], field_name: str) -> None: """Ensure that the field's name does not shadow an existing attribute of the model. @@ -31,7 +28,8 @@ def validate_field_name(bases: List[Type["BaseModel"]], field_name: str) -> None """ from reflex.utils.exceptions import VarNameError - reload = os.getenv(constants.RELOAD_CONFIG) == "True" + # can't use reflex.config.environment here cause of circular import + reload = os.getenv("__RELOAD_CONFIG", "").lower() == "true" for base in bases: try: if not reload and getattr(base, field_name, None): diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index e9d56b7e7..9f81f319d 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -527,7 +527,7 @@ def remove_tailwind_from_postcss() -> tuple[str, str]: def purge_web_pages_dir(): """Empty out .web/pages directory.""" - if not is_prod_mode() and environment.REFLEX_PERSIST_WEB_DIR: + if not is_prod_mode() and environment.REFLEX_PERSIST_WEB_DIR.get(): # Skip purging the web directory in dev mode if REFLEX_PERSIST_WEB_DIR is set. return diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index 58465b618..fe8845e8f 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -132,7 +132,7 @@ def get_upload_dir() -> Path: """ Upload.is_used = True - uploaded_files_dir = environment.REFLEX_UPLOADED_FILES_DIR + uploaded_files_dir = environment.REFLEX_UPLOADED_FILES_DIR.get() uploaded_files_dir.mkdir(parents=True, exist_ok=True) return uploaded_files_dir diff --git a/reflex/config.py b/reflex/config.py index 12cc0916a..049cc2e83 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -10,7 +10,17 @@ import os import sys import urllib.parse from pathlib import Path -from typing import Any, Dict, List, Optional, Set +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Generic, + List, + Optional, + Set, + TypeVar, + get_args, +) from typing_extensions import Annotated, get_type_hints @@ -300,6 +310,141 @@ def interpret_env_var_value( ) +T = TypeVar("T") + + +class EnvVar(Generic[T]): + """Environment variable.""" + + name: str + default: Any + type_: T + + def __init__(self, name: str, default: Any, type_: T) -> None: + """Initialize the environment variable. + + Args: + name: The environment variable name. + default: The default value. + type_: The type of the value. + """ + self.name = name + self.default = default + self.type_ = type_ + + def interpret(self, value: str) -> T: + """Interpret the environment variable value. + + Args: + value: The environment variable value. + + Returns: + The interpreted value. + """ + return interpret_env_var_value(value, self.type_, self.name) + + def getenv(self) -> Optional[T]: + """Get the interpreted environment variable value. + + Returns: + The environment variable value. + """ + env_value = os.getenv(self.name, None) + if env_value is not None: + return self.interpret(env_value) + return None + + def is_set(self) -> bool: + """Check if the environment variable is set. + + Returns: + True if the environment variable is set. + """ + return self.name in os.environ + + def get(self) -> T: + """Get the interpreted environment variable value or the default value if not set. + + Returns: + The interpreted value. + """ + env_value = self.getenv() + if env_value is not None: + return env_value + return self.default + + def set(self, value: T | None) -> None: + """Set the environment variable. None unsets the variable. + + Args: + value: The value to set. + """ + if value is None: + _ = os.environ.pop(self.name, None) + else: + if isinstance(value, enum.Enum): + value = value.value + os.environ[self.name] = str(value) + + +class env_var: # type: ignore + """Descriptor for environment variables.""" + + name: str + default: Any + internal: bool = False + + def __init__(self, default: Any, internal: bool = False) -> None: + """Initialize the descriptor. + + Args: + default: The default value. + internal: Whether the environment variable is reflex internal. + """ + self.default = default + self.internal = internal + + def __set_name__(self, owner, name): + """Set the name of the descriptor. + + Args: + owner: The owner class. + name: The name of the descriptor. + """ + self.name = name + + def __get__(self, instance, owner): + """Get the EnvVar instance. + + Args: + instance: The instance. + owner: The owner class. + + Returns: + The EnvVar instance. + """ + type_ = get_args(get_type_hints(owner)[self.name])[0] + env_name = self.name + if self.internal: + env_name = f"__{env_name}" + return EnvVar(name=env_name, default=self.default, type_=type_) + + +if TYPE_CHECKING: + + def env_var(default, internal=False) -> EnvVar: + """Typing helper for the env_var descriptor. + + Args: + default: The default value. + internal: Whether the environment variable is reflex internal. + + Returns: + The EnvVar instance. + """ + return default + + class PathExistsFlag: """Flag to indicate that a path must exist.""" @@ -307,83 +452,98 @@ class PathExistsFlag: ExistingPath = Annotated[Path, PathExistsFlag] -@dataclasses.dataclass(init=False) class EnvironmentVariables: """Environment variables class to instantiate environment variables.""" # Whether to use npm over bun to install frontend packages. - REFLEX_USE_NPM: bool = False + REFLEX_USE_NPM: EnvVar[bool] = env_var(False) # The npm registry to use. - NPM_CONFIG_REGISTRY: Optional[str] = None + NPM_CONFIG_REGISTRY: EnvVar[Optional[str]] = env_var(None) # Whether to use Granian for the backend. Otherwise, use Uvicorn. - REFLEX_USE_GRANIAN: bool = False + REFLEX_USE_GRANIAN: EnvVar[bool] = env_var(False) # The username to use for authentication on python package repository. Username and password must both be provided. - TWINE_USERNAME: Optional[str] = None + TWINE_USERNAME: EnvVar[Optional[str]] = env_var(None) # The password to use for authentication on python package repository. Username and password must both be provided. - TWINE_PASSWORD: Optional[str] = None + TWINE_PASSWORD: EnvVar[Optional[str]] = env_var(None) # Whether to use the system installed bun. If set to false, bun will be bundled with the app. - REFLEX_USE_SYSTEM_BUN: bool = False + REFLEX_USE_SYSTEM_BUN: EnvVar[bool] = env_var(False) # Whether to use the system installed node and npm. If set to false, node and npm will be bundled with the app. - REFLEX_USE_SYSTEM_NODE: bool = False + REFLEX_USE_SYSTEM_NODE: EnvVar[bool] = env_var(False) # The working directory for the next.js commands. - REFLEX_WEB_WORKDIR: Path = Path(constants.Dirs.WEB) + REFLEX_WEB_WORKDIR: EnvVar[Path] = env_var(Path(constants.Dirs.WEB)) # Path to the alembic config file - ALEMBIC_CONFIG: ExistingPath = Path(constants.ALEMBIC_CONFIG) + ALEMBIC_CONFIG: EnvVar[ExistingPath] = env_var(Path(constants.ALEMBIC_CONFIG)) # Disable SSL verification for HTTPX requests. - SSL_NO_VERIFY: bool = False + SSL_NO_VERIFY: EnvVar[bool] = env_var(False) # The directory to store uploaded files. - REFLEX_UPLOADED_FILES_DIR: Path = Path(constants.Dirs.UPLOADED_FILES) + REFLEX_UPLOADED_FILES_DIR: EnvVar[Path] = env_var( + Path(constants.Dirs.UPLOADED_FILES) + ) - # Whether to use seperate processes to compile the frontend and how many. If not set, defaults to thread executor. - REFLEX_COMPILE_PROCESSES: Optional[int] = None + # Whether to use separate processes to compile the frontend and how many. If not set, defaults to thread executor. + REFLEX_COMPILE_PROCESSES: EnvVar[Optional[int]] = env_var(None) - # Whether to use seperate threads to compile the frontend and how many. Defaults to `min(32, os.cpu_count() + 4)`. - REFLEX_COMPILE_THREADS: Optional[int] = None + # Whether to use separate threads to compile the frontend and how many. Defaults to `min(32, os.cpu_count() + 4)`. + REFLEX_COMPILE_THREADS: EnvVar[Optional[int]] = env_var(None) # The directory to store reflex dependencies. - REFLEX_DIR: Path = Path(constants.Reflex.DIR) + REFLEX_DIR: EnvVar[Path] = env_var(Path(constants.Reflex.DIR)) # Whether to print the SQL queries if the log level is INFO or lower. - SQLALCHEMY_ECHO: bool = False + SQLALCHEMY_ECHO: EnvVar[bool] = env_var(False) # Whether to ignore the redis config error. Some redis servers only allow out-of-band configuration. - REFLEX_IGNORE_REDIS_CONFIG_ERROR: bool = False + REFLEX_IGNORE_REDIS_CONFIG_ERROR: EnvVar[bool] = env_var(False) # Whether to skip purging the web directory in dev mode. - REFLEX_PERSIST_WEB_DIR: bool = False + REFLEX_PERSIST_WEB_DIR: EnvVar[bool] = env_var(False) # The reflex.build frontend host. - REFLEX_BUILD_FRONTEND: str = constants.Templates.REFLEX_BUILD_FRONTEND + REFLEX_BUILD_FRONTEND: EnvVar[str] = env_var( + constants.Templates.REFLEX_BUILD_FRONTEND + ) # The reflex.build backend host. - REFLEX_BUILD_BACKEND: str = constants.Templates.REFLEX_BUILD_BACKEND + REFLEX_BUILD_BACKEND: EnvVar[str] = env_var( + constants.Templates.REFLEX_BUILD_BACKEND + ) - def __init__(self): - """Initialize the environment variables.""" - type_hints = get_type_hints(type(self)) + # This env var stores the execution mode of the app + REFLEX_ENV_MODE: EnvVar[constants.Env] = env_var(constants.Env.DEV) - for field in dataclasses.fields(self): - raw_value = os.getenv(field.name, None) + # Whether to run the backend only. Exclusive with REFLEX_FRONTEND_ONLY. + REFLEX_BACKEND_ONLY: EnvVar[bool] = env_var(False) - field.type = type_hints.get(field.name) or field.type + # Whether to run the frontend only. Exclusive with REFLEX_BACKEND_ONLY. + REFLEX_FRONTEND_ONLY: EnvVar[bool] = env_var(False) - value = ( - interpret_env_var_value(raw_value, field.type, field.name) - if raw_value is not None - else get_default_value_for_field(field) - ) + # Reflex internal env to reload the config. + RELOAD_CONFIG: EnvVar[bool] = env_var(False, internal=True) - setattr(self, field.name, value) + # If this env var is set to "yes", App.compile will be a no-op + REFLEX_SKIP_COMPILE: EnvVar[bool] = env_var(False, internal=True) + + # Whether to run app harness tests in headless mode. + APP_HARNESS_HEADLESS: EnvVar[bool] = env_var(False) + + # Which app harness driver to use. + APP_HARNESS_DRIVER: EnvVar[str] = env_var("Chrome") + + # Arguments to pass to the app harness driver. + APP_HARNESS_DRIVER_ARGS: EnvVar[str] = env_var("") + + # Where to save screenshots when tests fail. + SCREENSHOT_DIR: EnvVar[Optional[Path]] = env_var(None) environment = EnvironmentVariables() diff --git a/reflex/constants/__init__.py b/reflex/constants/__init__.py index a540805b5..a1233a3fa 100644 --- a/reflex/constants/__init__.py +++ b/reflex/constants/__init__.py @@ -2,18 +2,13 @@ from .base import ( COOKIES, - ENV_BACKEND_ONLY_ENV_VAR, - ENV_FRONTEND_ONLY_ENV_VAR, - ENV_MODE_ENV_VAR, IS_WINDOWS, LOCAL_STORAGE, POLLING_MAX_HTTP_BUFFER_SIZE, PYTEST_CURRENT_TEST, REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG, - RELOAD_CONFIG, SESSION_STORAGE, - SKIP_COMPILE_ENV_VAR, ColorMode, Dirs, Env, @@ -106,7 +101,6 @@ __ALL__ = [ POLLING_MAX_HTTP_BUFFER_SIZE, PYTEST_CURRENT_TEST, Reflex, - RELOAD_CONFIG, RequirementsTxt, RouteArgType, RouteRegex, @@ -116,7 +110,6 @@ __ALL__ = [ ROUTER_DATA_INCLUDE, ROUTE_NOT_FOUND, SETTER_PREFIX, - SKIP_COMPILE_ENV_VAR, SocketEvent, StateManagerMode, Tailwind, diff --git a/reflex/constants/base.py b/reflex/constants/base.py index bba53d625..6ec73cdf0 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -112,7 +112,7 @@ class Templates(SimpleNamespace): from reflex.config import environment return ( - environment.REFLEX_BUILD_FRONTEND + environment.REFLEX_BUILD_FRONTEND.get() + "/gen?reflex_init_token={reflex_init_token}" ) @@ -126,7 +126,7 @@ class Templates(SimpleNamespace): """ from reflex.config import environment - return environment.REFLEX_BUILD_BACKEND + "/api/init/{reflex_init_token}" + return environment.REFLEX_BUILD_BACKEND.get() + "/api/init/{reflex_init_token}" @classproperty @classmethod @@ -139,7 +139,8 @@ class Templates(SimpleNamespace): from reflex.config import environment return ( - environment.REFLEX_BUILD_BACKEND + "/api/gen/{generation_hash}/refactored" + environment.REFLEX_BUILD_BACKEND.get() + + "/api/gen/{generation_hash}/refactored" ) class Dirs(SimpleNamespace): @@ -239,19 +240,9 @@ COOKIES = "cookies" LOCAL_STORAGE = "local_storage" SESSION_STORAGE = "session_storage" -# If this env var is set to "yes", App.compile will be a no-op -SKIP_COMPILE_ENV_VAR = "__REFLEX_SKIP_COMPILE" - -# This env var stores the execution mode of the app -ENV_MODE_ENV_VAR = "REFLEX_ENV_MODE" - -ENV_BACKEND_ONLY_ENV_VAR = "REFLEX_BACKEND_ONLY" -ENV_FRONTEND_ONLY_ENV_VAR = "REFLEX_FRONTEND_ONLY" - # Testing variables. # Testing os env set by pytest when running a test case. PYTEST_CURRENT_TEST = "PYTEST_CURRENT_TEST" -RELOAD_CONFIG = "__REFLEX_RELOAD_CONFIG" REFLEX_VAR_OPENING_TAG = "" REFLEX_VAR_CLOSING_TAG = "" diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index 4b4716fc9..26a53f2d8 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -63,7 +63,7 @@ class Bun(SimpleNamespace): """ from reflex.config import environment - return environment.REFLEX_DIR / "bun" + return environment.REFLEX_DIR.get() / "bun" @classproperty @classmethod @@ -100,7 +100,7 @@ class Fnm(SimpleNamespace): """ from reflex.config import environment - return environment.REFLEX_DIR / "fnm" + return environment.REFLEX_DIR.get() / "fnm" @classproperty @classmethod diff --git a/reflex/custom_components/custom_components.py b/reflex/custom_components/custom_components.py index ddda3de56..7681caebc 100644 --- a/reflex/custom_components/custom_components.py +++ b/reflex/custom_components/custom_components.py @@ -609,14 +609,14 @@ def publish( help="The API token to use for authentication on python package repository. If token is provided, no username/password should be provided at the same time", ), username: Optional[str] = typer.Option( - environment.TWINE_USERNAME, + environment.TWINE_USERNAME.get(), "-u", "--username", show_default="TWINE_USERNAME environment variable value if set", help="The username to use for authentication on python package repository. Username and password must both be provided.", ), password: Optional[str] = typer.Option( - environment.TWINE_PASSWORD, + environment.TWINE_PASSWORD.get(), "-p", "--password", show_default="TWINE_PASSWORD environment variable value if set", diff --git a/reflex/model.py b/reflex/model.py index 5f5e8647d..4b070ec67 100644 --- a/reflex/model.py +++ b/reflex/model.py @@ -38,12 +38,12 @@ def get_engine(url: str | None = None) -> sqlalchemy.engine.Engine: url = url or conf.db_url if url is None: raise ValueError("No database url configured") - if not environment.ALEMBIC_CONFIG.exists(): + if not environment.ALEMBIC_CONFIG.get().exists(): console.warn( "Database is not initialized, run [bold]reflex db init[/bold] first." ) # Print the SQL queries if the log level is INFO or lower. - echo_db_query = environment.SQLALCHEMY_ECHO + echo_db_query = environment.SQLALCHEMY_ECHO.get() # Needed for the admin dash on sqlite. connect_args = {"check_same_thread": False} if url.startswith("sqlite") else {} return sqlmodel.create_engine(url, echo=echo_db_query, connect_args=connect_args) @@ -231,7 +231,7 @@ class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssue Returns: tuple of (config, script_directory) """ - config = alembic.config.Config(environment.ALEMBIC_CONFIG) + config = alembic.config.Config(environment.ALEMBIC_CONFIG.get()) return config, alembic.script.ScriptDirectory( config.get_main_option("script_location", default="version"), ) @@ -266,8 +266,8 @@ class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssue def alembic_init(cls): """Initialize alembic for the project.""" alembic.command.init( - config=alembic.config.Config(environment.ALEMBIC_CONFIG), - directory=str(environment.ALEMBIC_CONFIG.parent / "alembic"), + config=alembic.config.Config(environment.ALEMBIC_CONFIG.get()), + directory=str(environment.ALEMBIC_CONFIG.get().parent / "alembic"), ) @classmethod @@ -287,7 +287,7 @@ class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssue Returns: True when changes have been detected. """ - if not environment.ALEMBIC_CONFIG.exists(): + if not environment.ALEMBIC_CONFIG.get().exists(): return False config, script_directory = cls._alembic_config() @@ -388,7 +388,7 @@ class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssue True - indicating the process was successful. None - indicating the process was skipped. """ - if not environment.ALEMBIC_CONFIG.exists(): + if not environment.ALEMBIC_CONFIG.get().exists(): return with cls.get_db_engine().connect() as connection: diff --git a/reflex/reflex.py b/reflex/reflex.py index 7bef8b7e5..6ccba01d3 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -160,7 +160,7 @@ def _run( console.set_log_level(loglevel) # Set env mode in the environment - os.environ[constants.ENV_MODE_ENV_VAR] = env.value + environment.REFLEX_ENV_MODE.set(env) # Show system info exec.output_system_info() @@ -277,13 +277,13 @@ def run( False, "--frontend-only", help="Execute only frontend.", - envvar=constants.ENV_FRONTEND_ONLY_ENV_VAR, + envvar=environment.REFLEX_FRONTEND_ONLY.name, ), backend: bool = typer.Option( False, "--backend-only", help="Execute only backend.", - envvar=constants.ENV_BACKEND_ONLY_ENV_VAR, + envvar=environment.REFLEX_BACKEND_ONLY.name, ), frontend_port: str = typer.Option( config.frontend_port, help="Specify a different frontend port." @@ -302,8 +302,8 @@ def run( if frontend and backend: console.error("Cannot use both --frontend-only and --backend-only options.") raise typer.Exit(1) - os.environ[constants.ENV_BACKEND_ONLY_ENV_VAR] = str(backend).lower() - os.environ[constants.ENV_FRONTEND_ONLY_ENV_VAR] = str(frontend).lower() + environment.REFLEX_BACKEND_ONLY.set(backend) + environment.REFLEX_FRONTEND_ONLY.set(frontend) _run(env, frontend, backend, frontend_port, backend_port, backend_host, loglevel) @@ -405,7 +405,7 @@ script_cli = typer.Typer() def _skip_compile(): """Skip the compile step.""" - os.environ[constants.SKIP_COMPILE_ENV_VAR] = "yes" + environment.REFLEX_SKIP_COMPILE.set(True) @db_cli.command(name="init") @@ -420,7 +420,7 @@ def db_init(): return # Check the alembic config. - if environment.ALEMBIC_CONFIG.exists(): + if environment.ALEMBIC_CONFIG.get().exists(): console.error( "Database is already initialized. Use " "[bold]reflex db makemigrations[/bold] to create schema change " diff --git a/reflex/state.py b/reflex/state.py index ca2728708..a53df7b6f 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -3377,7 +3377,7 @@ class StateManagerRedis(StateManager): ) except ResponseError: # Some redis servers only allow out-of-band configuration, so ignore errors here. - if not environment.REFLEX_IGNORE_REDIS_CONFIG_ERROR: + if not environment.REFLEX_IGNORE_REDIS_CONFIG_ERROR.get(): raise async with self.redis.pubsub() as pubsub: await pubsub.psubscribe(lock_key_channel) diff --git a/reflex/testing.py b/reflex/testing.py index b41e56884..bb7ead2d9 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -43,6 +43,7 @@ import reflex.utils.exec import reflex.utils.format import reflex.utils.prerequisites import reflex.utils.processes +from reflex.config import environment from reflex.state import ( BaseState, StateManager, @@ -250,6 +251,7 @@ class AppHarness: def _initialize_app(self): # disable telemetry reporting for tests + os.environ["TELEMETRY_ENABLED"] = "false" self.app_path.mkdir(parents=True, exist_ok=True) if self.app_source is not None: @@ -615,10 +617,10 @@ class AppHarness: if self.frontend_url is None: raise RuntimeError("Frontend is not running.") want_headless = False - if os.environ.get("APP_HARNESS_HEADLESS"): + if environment.APP_HARNESS_HEADLESS.get(): want_headless = True if driver_clz is None: - requested_driver = os.environ.get("APP_HARNESS_DRIVER", "Chrome") + requested_driver = environment.APP_HARNESS_DRIVER.get() driver_clz = getattr(webdriver, requested_driver) if driver_options is None: driver_options = getattr(webdriver, f"{requested_driver}Options")() @@ -640,7 +642,7 @@ class AppHarness: driver_options.add_argument("headless") if driver_options is None: raise RuntimeError(f"Could not determine options for {driver_clz}") - if args := os.environ.get("APP_HARNESS_DRIVER_ARGS"): + if args := environment.APP_HARNESS_DRIVER_ARGS.get(): for arg in args.split(","): driver_options.add_argument(arg) if driver_option_args is not None: @@ -944,7 +946,7 @@ class AppHarnessProd(AppHarness): def _start_backend(self): if self.app_instance is None: raise RuntimeError("App was not initialized.") - os.environ[reflex.constants.SKIP_COMPILE_ENV_VAR] = "yes" + environment.REFLEX_SKIP_COMPILE.set(True) self.backend = uvicorn.Server( uvicorn.Config( app=self.app_instance, @@ -961,7 +963,7 @@ class AppHarnessProd(AppHarness): try: return super()._poll_for_servers(timeout) finally: - os.environ.pop(reflex.constants.SKIP_COMPILE_ENV_VAR, None) + environment.REFLEX_SKIP_COMPILE.set(None) def stop(self): """Stop the frontend python webserver.""" diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index 467c5fa2c..fb613810a 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -184,7 +184,7 @@ def should_use_granian(): Returns: True if Granian should be used. """ - return environment.REFLEX_USE_GRANIAN + return environment.REFLEX_USE_GRANIAN.get() def get_app_module(): @@ -369,7 +369,9 @@ def run_uvicorn_backend_prod(host, port, loglevel): command, run=True, show_logs=True, - env={constants.SKIP_COMPILE_ENV_VAR: "yes"}, # skip compile for prod backend + env={ + environment.REFLEX_SKIP_COMPILE.name: "true" + }, # skip compile for prod backend ) @@ -405,7 +407,7 @@ def run_granian_backend_prod(host, port, loglevel): run=True, show_logs=True, env={ - constants.SKIP_COMPILE_ENV_VAR: "yes" + environment.REFLEX_SKIP_COMPILE.name: "true" }, # skip compile for prod backend ) except ImportError: @@ -491,11 +493,8 @@ def is_prod_mode() -> bool: Returns: True if the app is running in production mode or False if running in dev mode. """ - current_mode = os.environ.get( - constants.ENV_MODE_ENV_VAR, - constants.Env.DEV.value, - ) - return current_mode == constants.Env.PROD.value + current_mode = environment.REFLEX_ENV_MODE.get() + return current_mode == constants.Env.PROD def is_frontend_only() -> bool: @@ -504,7 +503,13 @@ def is_frontend_only() -> bool: Returns: True if the app is running in frontend-only mode. """ - return os.environ.get(constants.ENV_FRONTEND_ONLY_ENV_VAR, "").lower() == "true" + console.deprecate( + "is_frontend_only() is deprecated and will be removed in a future release.", + reason="Use `environment.REFLEX_FRONTEND_ONLY.get()` instead.", + deprecation_version="0.6.5", + removal_version="0.7.0", + ) + return environment.REFLEX_FRONTEND_ONLY.get() def is_backend_only() -> bool: @@ -513,7 +518,13 @@ def is_backend_only() -> bool: Returns: True if the app is running in backend-only mode. """ - return os.environ.get(constants.ENV_BACKEND_ONLY_ENV_VAR, "").lower() == "true" + console.deprecate( + "is_backend_only() is deprecated and will be removed in a future release.", + reason="Use `environment.REFLEX_BACKEND_ONLY.get()` instead.", + deprecation_version="0.6.5", + removal_version="0.7.0", + ) + return environment.REFLEX_BACKEND_ONLY.get() def should_skip_compile() -> bool: @@ -522,4 +533,10 @@ def should_skip_compile() -> bool: Returns: True if the app should skip compile. """ - return os.environ.get(constants.SKIP_COMPILE_ENV_VAR) == "yes" + console.deprecate( + "should_skip_compile() is deprecated and will be removed in a future release.", + reason="Use `environment.REFLEX_SKIP_COMPILE.get()` instead.", + deprecation_version="0.6.5", + removal_version="0.7.0", + ) + return environment.REFLEX_SKIP_COMPILE.get() diff --git a/reflex/utils/net.py b/reflex/utils/net.py index 2c6f22764..acc202912 100644 --- a/reflex/utils/net.py +++ b/reflex/utils/net.py @@ -12,7 +12,7 @@ def _httpx_verify_kwarg() -> bool: Returns: True if SSL verification is enabled, False otherwise """ - return not environment.SSL_NO_VERIFY + return not environment.SSL_NO_VERIFY.get() def get(url: str, **kwargs) -> httpx.Response: diff --git a/reflex/utils/path_ops.py b/reflex/utils/path_ops.py index ee93b24cf..a2ba2b151 100644 --- a/reflex/utils/path_ops.py +++ b/reflex/utils/path_ops.py @@ -136,7 +136,7 @@ def use_system_node() -> bool: Returns: Whether the system node should be used. """ - return environment.REFLEX_USE_SYSTEM_NODE + return environment.REFLEX_USE_SYSTEM_NODE.get() def use_system_bun() -> bool: @@ -145,7 +145,7 @@ def use_system_bun() -> bool: Returns: Whether the system bun should be used. """ - return environment.REFLEX_USE_SYSTEM_BUN + return environment.REFLEX_USE_SYSTEM_BUN.get() def get_node_bin_path() -> Path | None: diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index aba7714af..7124d46c4 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -69,7 +69,7 @@ def get_web_dir() -> Path: Returns: The working directory. """ - return environment.REFLEX_WEB_WORKDIR + return environment.REFLEX_WEB_WORKDIR.get() def _python_version_check(): @@ -260,7 +260,7 @@ def windows_npm_escape_hatch() -> bool: Returns: If the user has set REFLEX_USE_NPM. """ - return environment.REFLEX_USE_NPM + return environment.REFLEX_USE_NPM.get() def get_app(reload: bool = False) -> ModuleType: @@ -278,7 +278,7 @@ def get_app(reload: bool = False) -> ModuleType: from reflex.utils import telemetry try: - os.environ[constants.RELOAD_CONFIG] = str(reload) + environment.RELOAD_CONFIG.set(reload) config = get_config() if not config.app_name: raise RuntimeError( @@ -1019,7 +1019,7 @@ def needs_reinit(frontend: bool = True) -> bool: return False # Make sure the .reflex directory exists. - if not environment.REFLEX_DIR.exists(): + if not environment.REFLEX_DIR.get().exists(): return True # Make sure the .web directory exists in frontend mode. @@ -1124,7 +1124,7 @@ def ensure_reflex_installation_id() -> Optional[int]: """ try: initialize_reflex_user_directory() - installation_id_file = environment.REFLEX_DIR / "installation_id" + installation_id_file = environment.REFLEX_DIR.get() / "installation_id" installation_id = None if installation_id_file.exists(): @@ -1149,7 +1149,7 @@ def ensure_reflex_installation_id() -> Optional[int]: def initialize_reflex_user_directory(): """Initialize the reflex user directory.""" # Create the reflex directory. - path_ops.mkdir(environment.REFLEX_DIR) + path_ops.mkdir(environment.REFLEX_DIR.get()) def initialize_frontend_dependencies(): @@ -1172,7 +1172,10 @@ def check_db_initialized() -> bool: Returns: True if alembic is initialized (or if database is not used). """ - if get_config().db_url is not None and not environment.ALEMBIC_CONFIG.exists(): + if ( + get_config().db_url is not None + and not environment.ALEMBIC_CONFIG.get().exists() + ): console.error( "Database is not initialized. Run [bold]reflex db init[/bold] first." ) @@ -1182,7 +1185,7 @@ def check_db_initialized() -> bool: def check_schema_up_to_date(): """Check if the sqlmodel metadata matches the current database schema.""" - if get_config().db_url is None or not environment.ALEMBIC_CONFIG.exists(): + if get_config().db_url is None or not environment.ALEMBIC_CONFIG.get().exists(): return with model.Model.get_db_engine().connect() as connection: try: diff --git a/reflex/utils/registry.py b/reflex/utils/registry.py index 77c3d31cd..d98178c61 100644 --- a/reflex/utils/registry.py +++ b/reflex/utils/registry.py @@ -55,4 +55,4 @@ def _get_npm_registry() -> str: Returns: str: """ - return environment.NPM_CONFIG_REGISTRY or get_best_registry() + return environment.NPM_CONFIG_REGISTRY.get() or get_best_registry() diff --git a/reflex/utils/telemetry.py b/reflex/utils/telemetry.py index 9ae165ea2..806b916fc 100644 --- a/reflex/utils/telemetry.py +++ b/reflex/utils/telemetry.py @@ -8,6 +8,8 @@ import multiprocessing import platform import warnings +from reflex.config import environment + try: from datetime import UTC, datetime except ImportError: @@ -20,7 +22,6 @@ import psutil from reflex import constants from reflex.utils import console -from reflex.utils.exec import should_skip_compile from reflex.utils.prerequisites import ensure_reflex_installation_id, get_project_hash POSTHOG_API_URL: str = "https://app.posthog.com/capture/" @@ -94,7 +95,7 @@ def _raise_on_missing_project_hash() -> bool: False when compilation should be skipped (i.e. no .web directory is required). Otherwise return True. """ - return not should_skip_compile() + return not environment.REFLEX_SKIP_COMPILE.get() def _prepare_event(event: str, **kwargs) -> dict: diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 212ac9981..f7b825f16 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -6,6 +6,7 @@ from pathlib import Path import pytest +from reflex.config import environment from reflex.testing import AppHarness, AppHarnessProd DISPLAY = None @@ -21,7 +22,7 @@ def xvfb(): Yields: the pyvirtualdisplay object that the browser will be open on """ - if os.environ.get("GITHUB_ACTIONS") and not os.environ.get("APP_HARNESS_HEADLESS"): + if os.environ.get("GITHUB_ACTIONS") and not environment.APP_HARNESS_HEADLESS.get(): from pyvirtualdisplay.smartdisplay import ( # pyright: ignore [reportMissingImports] SmartDisplay, ) @@ -42,7 +43,7 @@ def pytest_exception_interact(node, call, report): call: The pytest call describing when/where the test was invoked. report: The pytest log report object. """ - screenshot_dir = os.environ.get("SCREENSHOT_DIR") + screenshot_dir = environment.SCREENSHOT_DIR.get() if DISPLAY is None or screenshot_dir is None: return diff --git a/tests/units/test_config.py b/tests/units/test_config.py index 0c63abc96..e5d4622bd 100644 --- a/tests/units/test_config.py +++ b/tests/units/test_config.py @@ -8,6 +8,8 @@ import pytest import reflex as rx import reflex.config from reflex.config import ( + EnvVar, + env_var, environment, interpret_boolean_env, interpret_enum_env, @@ -214,7 +216,7 @@ def test_replace_defaults( def reflex_dir_constant() -> Path: - return environment.REFLEX_DIR + return environment.REFLEX_DIR.get() def test_reflex_dir_env_var(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: @@ -227,6 +229,7 @@ def test_reflex_dir_env_var(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> monkeypatch.setenv("REFLEX_DIR", str(tmp_path)) mp_ctx = multiprocessing.get_context(method="spawn") + assert reflex_dir_constant() == tmp_path with mp_ctx.Pool(processes=1) as pool: assert pool.apply(reflex_dir_constant) == tmp_path @@ -242,3 +245,38 @@ def test_interpret_int_env() -> None: @pytest.mark.parametrize("value, expected", [("true", True), ("false", False)]) def test_interpret_bool_env(value: str, expected: bool) -> None: assert interpret_boolean_env(value, "TELEMETRY_ENABLED") == expected + + +def test_env_var(): + class TestEnv: + BLUBB: EnvVar[str] = env_var("default") + INTERNAL: EnvVar[str] = env_var("default", internal=True) + BOOLEAN: EnvVar[bool] = env_var(False) + + assert TestEnv.BLUBB.get() == "default" + assert TestEnv.BLUBB.name == "BLUBB" + TestEnv.BLUBB.set("new") + assert os.environ.get("BLUBB") == "new" + assert TestEnv.BLUBB.get() == "new" + TestEnv.BLUBB.set(None) + assert "BLUBB" not in os.environ + + assert TestEnv.INTERNAL.get() == "default" + assert TestEnv.INTERNAL.name == "__INTERNAL" + TestEnv.INTERNAL.set("new") + assert os.environ.get("__INTERNAL") == "new" + assert TestEnv.INTERNAL.get() == "new" + assert TestEnv.INTERNAL.getenv() == "new" + TestEnv.INTERNAL.set(None) + assert "__INTERNAL" not in os.environ + + assert TestEnv.BOOLEAN.get() is False + assert TestEnv.BOOLEAN.name == "BOOLEAN" + TestEnv.BOOLEAN.set(True) + assert os.environ.get("BOOLEAN") == "True" + assert TestEnv.BOOLEAN.get() is True + TestEnv.BOOLEAN.set(False) + assert os.environ.get("BOOLEAN") == "False" + assert TestEnv.BOOLEAN.get() is False + TestEnv.BOOLEAN.set(None) + assert "BOOLEAN" not in os.environ diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index dd88138bf..cc98c3ace 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -10,6 +10,7 @@ from packaging import version from reflex import constants from reflex.base import Base +from reflex.config import environment from reflex.event import EventHandler from reflex.state import BaseState from reflex.utils import ( @@ -593,3 +594,11 @@ def test_style_prop_with_event_handler_value(callable): rx.box( style=style, # type: ignore ) + + +def test_is_prod_mode() -> None: + """Test that the prod mode is correctly determined.""" + environment.REFLEX_ENV_MODE.set(constants.Env.PROD) + assert utils_exec.is_prod_mode() + environment.REFLEX_ENV_MODE.set(None) + assert not utils_exec.is_prod_mode() From 01e3844ac45268fd59a85a3c6f54e581649acb3b Mon Sep 17 00:00:00 2001 From: Carlos <36110765+carlosabadia@users.noreply.github.com> Date: Tue, 5 Nov 2024 23:03:07 +0100 Subject: [PATCH 16/18] default props comment for GraphinTooltip (#4101) * default props comment for GraphinTooltip * update --- reflex/components/recharts/general.py | 33 +++++++++++++------------- reflex/components/recharts/general.pyi | 28 +++++++++++----------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/reflex/components/recharts/general.py b/reflex/components/recharts/general.py index 124ef744d..1769ea125 100644 --- a/reflex/components/recharts/general.py +++ b/reflex/components/recharts/general.py @@ -135,16 +135,16 @@ class GraphingTooltip(Recharts): alias = "RechartsTooltip" - # The separator between name and value. + # The separator between name and value. Default: ":" separator: Var[str] - # The offset size of tooltip. Number + # The offset size of tooltip. Number. Default: 10 offset: Var[int] - # When an item of the payload has value null or undefined, this item won't be displayed. + # When an item of the payload has value null or undefined, this item won't be displayed. Default: True filter_null: Var[bool] - # If set false, no cursor will be drawn when tooltip is active. + # If set false, no cursor will be drawn when tooltip is active. Default: {"strokeWidth": 1, "fill": rx.color("gray", 3)} cursor: Var[Union[Dict[str, Any], bool]] = LiteralVar.create( { "strokeWidth": 1, @@ -155,16 +155,17 @@ class GraphingTooltip(Recharts): # The box of viewing area, which has the shape of {x: someVal, y: someVal, width: someVal, height: someVal}, usually calculated internally. view_box: Var[Dict[str, Any]] - # The style of default tooltip content item which is a li element. DEFAULT: {} + # The style of default tooltip content item which is a li element. Default: {"color": rx.color("gray", 12)} item_style: Var[Dict[str, Any]] = LiteralVar.create( { "color": Color("gray", 12), } ) - # The style of tooltip wrapper which is a dom element. DEFAULT: {} + # The style of tooltip wrapper which is a dom element. Default: {} wrapper_style: Var[Dict[str, Any]] - # The style of tooltip content which is a dom element. DEFAULT: {} + + # The style of tooltip content which is a dom element. Default: {"background": rx.color("gray", 1), "borderColor": rx.color("gray", 4), "borderRadius": "8px"} content_style: Var[Dict[str, Any]] = LiteralVar.create( { "background": Color("gray", 1), @@ -173,30 +174,28 @@ class GraphingTooltip(Recharts): } ) - # The style of default tooltip label which is a p element. DEFAULT: {} + # The style of default tooltip label which is a p element. Default: {"color": rx.color("gray", 11)} label_style: Var[Dict[str, Any]] = LiteralVar.create({"color": Color("gray", 11)}) - # This option allows the tooltip to extend beyond the viewBox of the chart itself. DEFAULT: { x: false, y: false } - allow_escape_view_box: Var[Dict[str, bool]] = LiteralVar.create( - {"x": False, "y": False} - ) + # This option allows the tooltip to extend beyond the viewBox of the chart itself. Default: {"x": False, "y": False} + allow_escape_view_box: Var[Dict[str, bool]] - # If set true, the tooltip is displayed. If set false, the tooltip is hidden, usually calculated internally. + # If set true, the tooltip is displayed. If set false, the tooltip is hidden, usually calculated internally. Default: False active: Var[bool] # If this field is set, the tooltip position will be fixed and will not move anymore. position: Var[Dict[str, Any]] - # The coordinate of tooltip which is usually calculated internally. + # The coordinate of tooltip which is usually calculated internally. Default: {"x": 0, "y": 0} coordinate: Var[Dict[str, Any]] - # If set false, animation of tooltip will be disabled. DEFAULT: true in CSR, and false in SSR + # If set false, animation of tooltip will be disabled. Default: True is_animation_active: Var[bool] - # Specifies the duration of animation, the unit of this option is ms. DEFAULT: 1500 + # Specifies the duration of animation, the unit of this option is ms. Default: 1500 animation_duration: Var[int] - # The type of easing function. DEFAULT: 'ease' + # The type of easing function. Default: "ease" animation_easing: Var[LiteralAnimationEasing] diff --git a/reflex/components/recharts/general.pyi b/reflex/components/recharts/general.pyi index affe362bb..c99d24b04 100644 --- a/reflex/components/recharts/general.pyi +++ b/reflex/components/recharts/general.pyi @@ -255,22 +255,22 @@ class GraphingTooltip(Recharts): Args: *children: The children of the component. - separator: The separator between name and value. - offset: The offset size of tooltip. Number - filter_null: When an item of the payload has value null or undefined, this item won't be displayed. - cursor: If set false, no cursor will be drawn when tooltip is active. + separator: The separator between name and value. Default: ":" + offset: The offset size of tooltip. Number. Default: 10 + filter_null: When an item of the payload has value null or undefined, this item won't be displayed. Default: True + cursor: If set false, no cursor will be drawn when tooltip is active. Default: {"strokeWidth": 1, "fill": rx.color("gray", 3)} view_box: The box of viewing area, which has the shape of {x: someVal, y: someVal, width: someVal, height: someVal}, usually calculated internally. - item_style: The style of default tooltip content item which is a li element. DEFAULT: {} - wrapper_style: The style of tooltip wrapper which is a dom element. DEFAULT: {} - content_style: The style of tooltip content which is a dom element. DEFAULT: {} - label_style: The style of default tooltip label which is a p element. DEFAULT: {} - allow_escape_view_box: This option allows the tooltip to extend beyond the viewBox of the chart itself. DEFAULT: { x: false, y: false } - active: If set true, the tooltip is displayed. If set false, the tooltip is hidden, usually calculated internally. + item_style: The style of default tooltip content item which is a li element. Default: {"color": rx.color("gray", 12)} + wrapper_style: The style of tooltip wrapper which is a dom element. Default: {} + content_style: The style of tooltip content which is a dom element. Default: {"background": rx.color("gray", 1), "borderColor": rx.color("gray", 4), "borderRadius": "8px"} + label_style: The style of default tooltip label which is a p element. Default: {"color": rx.color("gray", 11)} + allow_escape_view_box: This option allows the tooltip to extend beyond the viewBox of the chart itself. Default: {"x": False, "y": False} + active: If set true, the tooltip is displayed. If set false, the tooltip is hidden, usually calculated internally. Default: False position: If this field is set, the tooltip position will be fixed and will not move anymore. - coordinate: The coordinate of tooltip which is usually calculated internally. - is_animation_active: If set false, animation of tooltip will be disabled. DEFAULT: true in CSR, and false in SSR - 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' + coordinate: The coordinate of tooltip which is usually calculated internally. Default: {"x": 0, "y": 0} + is_animation_active: If set false, animation of tooltip will be disabled. Default: True + 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" style: The style of the component. key: A unique key for the component. id: The id for the component. From c216eeafebcf22d32c8a17042988171521e6d902 Mon Sep 17 00:00:00 2001 From: simon Date: Tue, 5 Nov 2024 18:23:13 -0800 Subject: [PATCH 17/18] add v1 support --- reflex/reflex.py | 118 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 112 insertions(+), 6 deletions(-) diff --git a/reflex/reflex.py b/reflex/reflex.py index 6ccba01d3..c7109a747 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -10,6 +10,7 @@ from typing import List, Optional import typer import typer.core from reflex_cli.deployments import deployments_cli +from reflex_cli.v2.deployments import hosting_cli from reflex_cli.utils import dependency from reflex import constants @@ -383,6 +384,13 @@ def login( _login() +@cli.command() +def loginv2(loglevel: constants.LogLevel = typer.Option(config.loglevel)): + from reflex_cli.v2 import cli as hosting_cli + + hosting_cli.login() + + @cli.command() def logout( loglevel: constants.LogLevel = typer.Option( @@ -569,12 +577,7 @@ def deploy( hosting_cli.deploy( app_name=app_name, - export_fn=lambda zip_dest_dir, - api_url, - deploy_url, - frontend, - backend, - zipping: export_utils.export( + export_fn=lambda zip_dest_dir, api_url, deploy_url, frontend, backend, zipping: export_utils.export( zip_dest_dir=zip_dest_dir, api_url=api_url, deploy_url=deploy_url, @@ -599,6 +602,104 @@ def deploy( ) +@cli.command() +def deployv2( + app_name: str = typer.Option( + config.app_name, + "--app-name", + help="The name of the App to deploy under.", + hidden=True, + ), + regions: List[str] = typer.Option( + list(), + "-r", + "--region", + help="The regions to deploy to. For multiple envs, repeat this option, e.g. --region sjc --region iad", + ), + envs: List[str] = typer.Option( + list(), + "--env", + help="The environment variables to set: =. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.", + ), + vmtype: Optional[str] = typer.Option( + None, + "--vmtype", + help="Vm type id. Run reflex apps vmtypes list to get options.", + ), + hostname: Optional[str] = typer.Option( + None, + "--hostname", + help="The hostname of the frontend.", + hidden=True, + ), + interactive: bool = typer.Option( + True, + help="Whether to list configuration options and ask for confirmation.", + ), + envfile: Optional[str] = typer.Option( + None, + "--envfile", + help="The path to an env file to use. Will override any envs set manually.", + hidden=True, + ), + loglevel: constants.LogLevel = typer.Option( + config.loglevel, help="The log level to use." + ), + project: Optional[str] = typer.Option( + None, + "--project", + help="project to deploy to", + hidden=True, + ), + token: Optional[str] = typer.Option( + None, + "--token", + help="token to use for auth", + hidden=True, + ), +): + """Deploy the app to the Reflex hosting service.""" + from reflex_cli.v2 import cli as hosting_cli + + from reflex.utils import prerequisites + from reflex.utils import export as export_utils + from reflex_cli.v2.utils import dependency + + # Set the log level. + console.set_log_level(loglevel) + + # Only check requirements if interactive. There is user interaction for requirements update. + if interactive: + dependency.check_requirements() + + # Check if we are set up. + if prerequisites.needs_reinit(frontend=True): + _init(name=config.app_name, loglevel=loglevel) + prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME) + + hosting_cli.deploy( + app_name=app_name, + export_fn=lambda zip_dest_dir, api_url, deploy_url, frontend, backend, zipping: export_utils.export( + zip_dest_dir=zip_dest_dir, + api_url=api_url, + deploy_url=deploy_url, + frontend=frontend, + backend=backend, + zipping=zipping, + loglevel=loglevel.subprocess_level(), + ), + regions=regions, + envs=envs, + vmtype=vmtype, + envfile=envfile, + hostname=hostname, + interactive=interactive, + loglevel=loglevel.subprocess_level(), + token=token, + project=project, + ) + + cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.") cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.") cli.add_typer( @@ -606,6 +707,11 @@ cli.add_typer( name="deployments", help="Subcommands for managing the Deployments.", ) +cli.add_typer( + hosting_cli, + name="apps", + help="Subcommands for managing the Deployments.", +) cli.add_typer( custom_components_cli, name="component", From d9ab3a0f1c1af9b66e7768407c417252de3dd7a6 Mon Sep 17 00:00:00 2001 From: simon Date: Tue, 5 Nov 2024 18:28:16 -0800 Subject: [PATCH 18/18] Revert "add v1 support" This reverts commit c216eeafebcf22d32c8a17042988171521e6d902. --- reflex/reflex.py | 118 +++-------------------------------------------- 1 file changed, 6 insertions(+), 112 deletions(-) diff --git a/reflex/reflex.py b/reflex/reflex.py index c7109a747..6ccba01d3 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -10,7 +10,6 @@ from typing import List, Optional import typer import typer.core from reflex_cli.deployments import deployments_cli -from reflex_cli.v2.deployments import hosting_cli from reflex_cli.utils import dependency from reflex import constants @@ -384,13 +383,6 @@ def login( _login() -@cli.command() -def loginv2(loglevel: constants.LogLevel = typer.Option(config.loglevel)): - from reflex_cli.v2 import cli as hosting_cli - - hosting_cli.login() - - @cli.command() def logout( loglevel: constants.LogLevel = typer.Option( @@ -577,7 +569,12 @@ def deploy( hosting_cli.deploy( app_name=app_name, - export_fn=lambda zip_dest_dir, api_url, deploy_url, frontend, backend, zipping: export_utils.export( + export_fn=lambda zip_dest_dir, + api_url, + deploy_url, + frontend, + backend, + zipping: export_utils.export( zip_dest_dir=zip_dest_dir, api_url=api_url, deploy_url=deploy_url, @@ -602,104 +599,6 @@ def deploy( ) -@cli.command() -def deployv2( - app_name: str = typer.Option( - config.app_name, - "--app-name", - help="The name of the App to deploy under.", - hidden=True, - ), - regions: List[str] = typer.Option( - list(), - "-r", - "--region", - help="The regions to deploy to. For multiple envs, repeat this option, e.g. --region sjc --region iad", - ), - envs: List[str] = typer.Option( - list(), - "--env", - help="The environment variables to set: =. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.", - ), - vmtype: Optional[str] = typer.Option( - None, - "--vmtype", - help="Vm type id. Run reflex apps vmtypes list to get options.", - ), - hostname: Optional[str] = typer.Option( - None, - "--hostname", - help="The hostname of the frontend.", - hidden=True, - ), - interactive: bool = typer.Option( - True, - help="Whether to list configuration options and ask for confirmation.", - ), - envfile: Optional[str] = typer.Option( - None, - "--envfile", - help="The path to an env file to use. Will override any envs set manually.", - hidden=True, - ), - loglevel: constants.LogLevel = typer.Option( - config.loglevel, help="The log level to use." - ), - project: Optional[str] = typer.Option( - None, - "--project", - help="project to deploy to", - hidden=True, - ), - token: Optional[str] = typer.Option( - None, - "--token", - help="token to use for auth", - hidden=True, - ), -): - """Deploy the app to the Reflex hosting service.""" - from reflex_cli.v2 import cli as hosting_cli - - from reflex.utils import prerequisites - from reflex.utils import export as export_utils - from reflex_cli.v2.utils import dependency - - # Set the log level. - console.set_log_level(loglevel) - - # Only check requirements if interactive. There is user interaction for requirements update. - if interactive: - dependency.check_requirements() - - # Check if we are set up. - if prerequisites.needs_reinit(frontend=True): - _init(name=config.app_name, loglevel=loglevel) - prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME) - - hosting_cli.deploy( - app_name=app_name, - export_fn=lambda zip_dest_dir, api_url, deploy_url, frontend, backend, zipping: export_utils.export( - zip_dest_dir=zip_dest_dir, - api_url=api_url, - deploy_url=deploy_url, - frontend=frontend, - backend=backend, - zipping=zipping, - loglevel=loglevel.subprocess_level(), - ), - regions=regions, - envs=envs, - vmtype=vmtype, - envfile=envfile, - hostname=hostname, - interactive=interactive, - loglevel=loglevel.subprocess_level(), - token=token, - project=project, - ) - - cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.") cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.") cli.add_typer( @@ -707,11 +606,6 @@ cli.add_typer( name="deployments", help="Subcommands for managing the Deployments.", ) -cli.add_typer( - hosting_cli, - name="apps", - help="Subcommands for managing the Deployments.", -) cli.add_typer( custom_components_cli, name="component",