From c103ab5e2872ca9496978360802d84c930e50b4e Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 21 Oct 2024 18:53:51 -0700 Subject: [PATCH] Add type hinting to dataeditor events (#4210) --- reflex/components/base/error_boundary.py | 4 +- reflex/components/base/error_boundary.pyi | 2 +- reflex/components/core/clipboard.pyi | 2 +- reflex/components/datadisplay/dataeditor.py | 88 +++++++++++++++++-- reflex/components/datadisplay/dataeditor.pyi | 78 +++++++++++++--- reflex/components/moment/moment.pyi | 2 +- reflex/components/radix/primitives/drawer.pyi | 4 +- reflex/components/radix/themes/color_mode.pyi | 2 +- .../radix/themes/components/alert_dialog.pyi | 2 +- .../radix/themes/components/checkbox.pyi | 6 +- .../radix/themes/components/context_menu.pyi | 2 +- .../radix/themes/components/dialog.pyi | 4 +- .../radix/themes/components/dropdown_menu.pyi | 4 +- .../radix/themes/components/hover_card.pyi | 4 +- .../radix/themes/components/popover.pyi | 2 +- .../radix/themes/components/radio_cards.pyi | 2 +- .../radix/themes/components/radio_group.pyi | 2 +- .../radix/themes/components/select.pyi | 12 +-- .../radix/themes/components/switch.pyi | 2 +- .../radix/themes/components/tabs.pyi | 4 +- .../radix/themes/components/tooltip.pyi | 2 +- reflex/components/react_player/audio.pyi | 4 +- .../components/react_player/react_player.pyi | 4 +- reflex/components/react_player/video.pyi | 4 +- reflex/components/suneditor/editor.pyi | 8 +- reflex/event.py | 82 +++++++++++++++-- reflex/experimental/layout.pyi | 2 +- reflex/utils/pyi_generator.py | 78 +++++++++++++++- tests/integration/test_lifespan.py | 1 + 29 files changed, 342 insertions(+), 71 deletions(-) diff --git a/reflex/components/base/error_boundary.py b/reflex/components/base/error_boundary.py index b35c69a60ee..83becc0346a 100644 --- a/reflex/components/base/error_boundary.py +++ b/reflex/components/base/error_boundary.py @@ -12,7 +12,9 @@ from reflex.vars.base import Var -def on_error_spec(error: Var, info: Var[Dict[str, str]]) -> Tuple[Var[str], Var[str]]: +def on_error_spec( + error: Var[Dict[str, str]], info: Var[Dict[str, str]] +) -> Tuple[Var[str], Var[str]]: """The spec for the on_error event handler. Args: diff --git a/reflex/components/base/error_boundary.pyi b/reflex/components/base/error_boundary.pyi index 94b129bdcfa..c8474285125 100644 --- a/reflex/components/base/error_boundary.pyi +++ b/reflex/components/base/error_boundary.pyi @@ -11,7 +11,7 @@ from reflex.style import Style from reflex.vars.base import Var def on_error_spec( - error: Var, info: Var[Dict[str, str]] + error: Var[Dict[str, str]], info: Var[Dict[str, str]] ) -> Tuple[Var[str], Var[str]]: ... class ErrorBoundary(Component): diff --git a/reflex/components/core/clipboard.pyi b/reflex/components/core/clipboard.pyi index e2f6afc8d49..489a9bcc53e 100644 --- a/reflex/components/core/clipboard.pyi +++ b/reflex/components/core/clipboard.pyi @@ -40,7 +40,7 @@ class Clipboard(Fragment): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_paste: Optional[EventType] = None, + on_paste: Optional[EventType[list[tuple[str, str]]]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, diff --git a/reflex/components/datadisplay/dataeditor.py b/reflex/components/datadisplay/dataeditor.py index 01255dd14e2..a192c7a45c2 100644 --- a/reflex/components/datadisplay/dataeditor.py +++ b/reflex/components/datadisplay/dataeditor.py @@ -5,6 +5,8 @@ from enum import Enum from typing import Any, Dict, List, Literal, Optional, Tuple, Union +from typing_extensions import TypedDict + from reflex.base import Base from reflex.components.component import Component, NoSSRComponent from reflex.components.literals import LiteralRowMarker @@ -120,6 +122,78 @@ def on_edit_spec(pos, data: dict[str, Any]): return [pos, data] +class Bounds(TypedDict): + """The bounds of the group header.""" + + x: int + y: int + width: int + height: int + + +class CompatSelection(TypedDict): + """The selection.""" + + items: list + + +class Rectangle(TypedDict): + """The bounds of the group header.""" + + x: int + y: int + width: int + height: int + + +class GridSelectionCurrent(TypedDict): + """The current selection.""" + + cell: list[int] + range: Rectangle + rangeStack: list[Rectangle] + + +class GridSelection(TypedDict): + """The grid selection.""" + + current: Optional[GridSelectionCurrent] + columns: CompatSelection + rows: CompatSelection + + +class GroupHeaderClickedEventArgs(TypedDict): + """The arguments for the group header clicked event.""" + + kind: str + group: str + location: list[int] + bounds: Bounds + isEdge: bool + shiftKey: bool + ctrlKey: bool + metaKey: bool + isTouch: bool + localEventX: int + localEventY: int + button: int + buttons: int + scrollEdge: list[int] + + +class GridCell(TypedDict): + """The grid cell.""" + + span: Optional[List[int]] + + +class GridColumn(TypedDict): + """The grid column.""" + + title: str + group: Optional[str] + + class DataEditor(NoSSRComponent): """The DataEditor Component.""" @@ -238,10 +312,12 @@ class DataEditor(NoSSRComponent): on_group_header_clicked: EventHandler[on_edit_spec] # Fired when a group header is right-clicked. - on_group_header_context_menu: EventHandler[lambda grp_idx, data: [grp_idx, data]] + on_group_header_context_menu: EventHandler[ + identity_event(int, GroupHeaderClickedEventArgs) + ] # Fired when a group header is renamed. - on_group_header_renamed: EventHandler[lambda idx, val: [idx, val]] + on_group_header_renamed: EventHandler[identity_event(str, str)] # Fired when a header is clicked. on_header_clicked: EventHandler[identity_event(Tuple[int, int])] @@ -250,16 +326,16 @@ class DataEditor(NoSSRComponent): on_header_context_menu: EventHandler[identity_event(Tuple[int, int])] # Fired when a header menu item is clicked. - on_header_menu_click: EventHandler[lambda col, pos: [col, pos]] + on_header_menu_click: EventHandler[identity_event(int, Rectangle)] # Fired when an item is hovered. on_item_hovered: EventHandler[identity_event(Tuple[int, int])] # Fired when a selection is deleted. - on_delete: EventHandler[lambda selection: [selection]] + on_delete: EventHandler[identity_event(GridSelection)] # Fired when editing is finished. - on_finished_editing: EventHandler[lambda new_value, movement: [new_value, movement]] + on_finished_editing: EventHandler[identity_event(Union[GridCell, None], list[int])] # Fired when a row is appended. on_row_appended: EventHandler[empty_event] @@ -268,7 +344,7 @@ class DataEditor(NoSSRComponent): on_selection_cleared: EventHandler[empty_event] # Fired when a column is resized. - on_column_resize: EventHandler[lambda col, width: [col, width]] + on_column_resize: EventHandler[identity_event(GridColumn, int)] def add_imports(self) -> ImportDict: """Add imports for the component. diff --git a/reflex/components/datadisplay/dataeditor.pyi b/reflex/components/datadisplay/dataeditor.pyi index 1b8fed2879d..aadd9666ee3 100644 --- a/reflex/components/datadisplay/dataeditor.pyi +++ b/reflex/components/datadisplay/dataeditor.pyi @@ -6,6 +6,8 @@ from enum import Enum from typing import Any, Dict, List, Literal, Optional, Union, overload +from typing_extensions import TypedDict + from reflex.base import Base from reflex.components.component import NoSSRComponent from reflex.event import EventType @@ -78,6 +80,54 @@ class DataEditorTheme(Base): def on_edit_spec(pos, data: dict[str, Any]): ... +class Bounds(TypedDict): + x: int + y: int + width: int + height: int + +class CompatSelection(TypedDict): + items: list + +class Rectangle(TypedDict): + x: int + y: int + width: int + height: int + +class GridSelectionCurrent(TypedDict): + cell: list[int] + range: Rectangle + rangeStack: list[Rectangle] + +class GridSelection(TypedDict): + current: Optional[GridSelectionCurrent] + columns: CompatSelection + rows: CompatSelection + +class GroupHeaderClickedEventArgs(TypedDict): + kind: str + group: str + location: list[int] + bounds: Bounds + isEdge: bool + shiftKey: bool + ctrlKey: bool + metaKey: bool + isTouch: bool + localEventX: int + localEventY: int + button: int + buttons: int + scrollEdge: list[int] + +class GridCell(TypedDict): + span: Optional[List[int]] + +class GridColumn(TypedDict): + title: str + group: Optional[str] + class DataEditor(NoSSRComponent): def add_imports(self) -> ImportDict: ... def add_hooks(self) -> list[str]: ... @@ -136,24 +186,28 @@ class DataEditor(NoSSRComponent): autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, on_blur: Optional[EventType[[]]] = None, - on_cell_activated: Optional[EventType] = None, - on_cell_clicked: Optional[EventType] = None, - on_cell_context_menu: Optional[EventType] = None, + on_cell_activated: Optional[EventType[tuple[int, int]]] = None, + on_cell_clicked: Optional[EventType[tuple[int, int]]] = None, + on_cell_context_menu: Optional[EventType[tuple[int, int]]] = None, on_cell_edited: Optional[EventType] = None, on_click: Optional[EventType[[]]] = None, - on_column_resize: Optional[EventType] = None, + on_column_resize: Optional[EventType[GridColumn, int]] = None, on_context_menu: Optional[EventType[[]]] = None, - on_delete: Optional[EventType] = None, + on_delete: Optional[EventType[GridSelection]] = None, on_double_click: Optional[EventType[[]]] = None, - on_finished_editing: Optional[EventType] = None, + on_finished_editing: Optional[ + EventType[Union[GridCell, None], list[int]] + ] = None, on_focus: Optional[EventType[[]]] = None, on_group_header_clicked: Optional[EventType] = None, - on_group_header_context_menu: Optional[EventType] = None, - on_group_header_renamed: Optional[EventType] = None, - on_header_clicked: Optional[EventType] = None, - on_header_context_menu: Optional[EventType] = None, - on_header_menu_click: Optional[EventType] = None, - on_item_hovered: Optional[EventType] = None, + on_group_header_context_menu: Optional[ + EventType[int, GroupHeaderClickedEventArgs] + ] = None, + on_group_header_renamed: Optional[EventType[str, str]] = None, + on_header_clicked: Optional[EventType[tuple[int, int]]] = None, + on_header_context_menu: Optional[EventType[tuple[int, int]]] = None, + on_header_menu_click: Optional[EventType[int, Rectangle]] = None, + on_item_hovered: Optional[EventType[tuple[int, int]]] = None, on_mount: Optional[EventType[[]]] = None, on_mouse_down: Optional[EventType[[]]] = None, on_mouse_enter: Optional[EventType[[]]] = None, diff --git a/reflex/components/moment/moment.pyi b/reflex/components/moment/moment.pyi index 4f58fda7d02..ccffbb8d1e6 100644 --- a/reflex/components/moment/moment.pyi +++ b/reflex/components/moment/moment.pyi @@ -58,7 +58,7 @@ class Moment(NoSSRComponent): autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, on_blur: Optional[EventType[[]]] = None, - on_change: Optional[EventType] = None, + on_change: Optional[EventType[str]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, diff --git a/reflex/components/radix/primitives/drawer.pyi b/reflex/components/radix/primitives/drawer.pyi index 9c5463e5687..c4493ee9ba0 100644 --- a/reflex/components/radix/primitives/drawer.pyi +++ b/reflex/components/radix/primitives/drawer.pyi @@ -101,7 +101,7 @@ class DrawerRoot(DrawerComponent): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_open_change: Optional[EventType] = None, + on_open_change: Optional[EventType[bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, @@ -511,7 +511,7 @@ class Drawer(ComponentNamespace): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_open_change: Optional[EventType] = None, + on_open_change: Optional[EventType[bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, diff --git a/reflex/components/radix/themes/color_mode.pyi b/reflex/components/radix/themes/color_mode.pyi index 43703dc3782..d856b0bbd23 100644 --- a/reflex/components/radix/themes/color_mode.pyi +++ b/reflex/components/radix/themes/color_mode.pyi @@ -383,7 +383,7 @@ class ColorModeSwitch(Switch): autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, on_blur: Optional[EventType[[]]] = None, - on_change: Optional[EventType] = None, + on_change: Optional[EventType[bool]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, diff --git a/reflex/components/radix/themes/components/alert_dialog.pyi b/reflex/components/radix/themes/components/alert_dialog.pyi index d63fcae53b9..771def7d911 100644 --- a/reflex/components/radix/themes/components/alert_dialog.pyi +++ b/reflex/components/radix/themes/components/alert_dialog.pyi @@ -42,7 +42,7 @@ class AlertDialogRoot(RadixThemesComponent): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_open_change: Optional[EventType] = None, + on_open_change: Optional[EventType[bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, diff --git a/reflex/components/radix/themes/components/checkbox.pyi b/reflex/components/radix/themes/components/checkbox.pyi index fad4f5210d9..90a9220efeb 100644 --- a/reflex/components/radix/themes/components/checkbox.pyi +++ b/reflex/components/radix/themes/components/checkbox.pyi @@ -116,7 +116,7 @@ class Checkbox(RadixThemesComponent): autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, on_blur: Optional[EventType[[]]] = None, - on_change: Optional[EventType] = None, + on_change: Optional[EventType[bool]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, @@ -263,7 +263,7 @@ class HighLevelCheckbox(RadixThemesComponent): autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, on_blur: Optional[EventType[[]]] = None, - on_change: Optional[EventType] = None, + on_change: Optional[EventType[bool]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, @@ -407,7 +407,7 @@ class CheckboxNamespace(ComponentNamespace): autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, on_blur: Optional[EventType[[]]] = None, - on_change: Optional[EventType] = None, + on_change: Optional[EventType[bool]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, diff --git a/reflex/components/radix/themes/components/context_menu.pyi b/reflex/components/radix/themes/components/context_menu.pyi index fbefa88de24..56d8200e0a3 100644 --- a/reflex/components/radix/themes/components/context_menu.pyi +++ b/reflex/components/radix/themes/components/context_menu.pyi @@ -39,7 +39,7 @@ class ContextMenuRoot(RadixThemesComponent): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_open_change: Optional[EventType] = None, + on_open_change: Optional[EventType[bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, diff --git a/reflex/components/radix/themes/components/dialog.pyi b/reflex/components/radix/themes/components/dialog.pyi index e3f17d7e8d5..0713461e972 100644 --- a/reflex/components/radix/themes/components/dialog.pyi +++ b/reflex/components/radix/themes/components/dialog.pyi @@ -40,7 +40,7 @@ class DialogRoot(RadixThemesComponent): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_open_change: Optional[EventType] = None, + on_open_change: Optional[EventType[bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, @@ -382,7 +382,7 @@ class Dialog(ComponentNamespace): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_open_change: Optional[EventType] = None, + on_open_change: Optional[EventType[bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, diff --git a/reflex/components/radix/themes/components/dropdown_menu.pyi b/reflex/components/radix/themes/components/dropdown_menu.pyi index dba619e7d95..8de273be90a 100644 --- a/reflex/components/radix/themes/components/dropdown_menu.pyi +++ b/reflex/components/radix/themes/components/dropdown_menu.pyi @@ -49,7 +49,7 @@ class DropdownMenuRoot(RadixThemesComponent): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_open_change: Optional[EventType] = None, + on_open_change: Optional[EventType[bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, @@ -363,7 +363,7 @@ class DropdownMenuSub(RadixThemesComponent): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_open_change: Optional[EventType] = None, + on_open_change: Optional[EventType[bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, diff --git a/reflex/components/radix/themes/components/hover_card.pyi b/reflex/components/radix/themes/components/hover_card.pyi index 8924ef1a8a3..fa169c852f9 100644 --- a/reflex/components/radix/themes/components/hover_card.pyi +++ b/reflex/components/radix/themes/components/hover_card.pyi @@ -43,7 +43,7 @@ class HoverCardRoot(RadixThemesComponent): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_open_change: Optional[EventType] = None, + on_open_change: Optional[EventType[bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, @@ -256,7 +256,7 @@ class HoverCard(ComponentNamespace): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_open_change: Optional[EventType] = None, + on_open_change: Optional[EventType[bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, diff --git a/reflex/components/radix/themes/components/popover.pyi b/reflex/components/radix/themes/components/popover.pyi index 984a139d003..21839251738 100644 --- a/reflex/components/radix/themes/components/popover.pyi +++ b/reflex/components/radix/themes/components/popover.pyi @@ -41,7 +41,7 @@ class PopoverRoot(RadixThemesComponent): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_open_change: Optional[EventType] = None, + on_open_change: Optional[EventType[bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, diff --git a/reflex/components/radix/themes/components/radio_cards.pyi b/reflex/components/radix/themes/components/radio_cards.pyi index d7344762203..d2f6a1425b5 100644 --- a/reflex/components/radix/themes/components/radio_cards.pyi +++ b/reflex/components/radix/themes/components/radio_cards.pyi @@ -177,7 +177,7 @@ class RadioCardsRoot(RadixThemesComponent): on_mouse_up: Optional[EventType[[]]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, - on_value_change: Optional[EventType] = None, + on_value_change: Optional[EventType[str]] = None, **props, ) -> "RadioCardsRoot": """Create a new component instance. diff --git a/reflex/components/radix/themes/components/radio_group.pyi b/reflex/components/radix/themes/components/radio_group.pyi index c984fa1f252..f928421c506 100644 --- a/reflex/components/radix/themes/components/radio_group.pyi +++ b/reflex/components/radix/themes/components/radio_group.pyi @@ -113,7 +113,7 @@ class RadioGroupRoot(RadixThemesComponent): autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, on_blur: Optional[EventType[[]]] = None, - on_change: Optional[EventType] = None, + on_change: Optional[EventType[str]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, diff --git a/reflex/components/radix/themes/components/select.pyi b/reflex/components/radix/themes/components/select.pyi index c43d58ada0e..e0e184482b5 100644 --- a/reflex/components/radix/themes/components/select.pyi +++ b/reflex/components/radix/themes/components/select.pyi @@ -44,7 +44,7 @@ class SelectRoot(RadixThemesComponent): autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, on_blur: Optional[EventType[[]]] = None, - on_change: Optional[EventType] = None, + on_change: Optional[EventType[str]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, @@ -57,7 +57,7 @@ class SelectRoot(RadixThemesComponent): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_open_change: Optional[EventType] = None, + on_open_change: Optional[EventType[bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, @@ -680,7 +680,7 @@ class HighLevelSelect(SelectRoot): autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, on_blur: Optional[EventType[[]]] = None, - on_change: Optional[EventType] = None, + on_change: Optional[EventType[str]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, @@ -693,7 +693,7 @@ class HighLevelSelect(SelectRoot): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_open_change: Optional[EventType] = None, + on_open_change: Optional[EventType[bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, @@ -854,7 +854,7 @@ class Select(ComponentNamespace): autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, on_blur: Optional[EventType[[]]] = None, - on_change: Optional[EventType] = None, + on_change: Optional[EventType[str]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, @@ -867,7 +867,7 @@ class Select(ComponentNamespace): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_open_change: Optional[EventType] = None, + on_open_change: Optional[EventType[bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, diff --git a/reflex/components/radix/themes/components/switch.pyi b/reflex/components/radix/themes/components/switch.pyi index ba9c2595eb1..f8871872ac2 100644 --- a/reflex/components/radix/themes/components/switch.pyi +++ b/reflex/components/radix/themes/components/switch.pyi @@ -119,7 +119,7 @@ class Switch(RadixThemesComponent): autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, on_blur: Optional[EventType[[]]] = None, - on_change: Optional[EventType] = None, + on_change: Optional[EventType[bool]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, diff --git a/reflex/components/radix/themes/components/tabs.pyi b/reflex/components/radix/themes/components/tabs.pyi index 7b67bad6e30..4272bf2a369 100644 --- a/reflex/components/radix/themes/components/tabs.pyi +++ b/reflex/components/radix/themes/components/tabs.pyi @@ -41,7 +41,7 @@ class TabsRoot(RadixThemesComponent): autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, on_blur: Optional[EventType[[]]] = None, - on_change: Optional[EventType] = None, + on_change: Optional[EventType[str]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, @@ -340,7 +340,7 @@ class Tabs(ComponentNamespace): autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, on_blur: Optional[EventType[[]]] = None, - on_change: Optional[EventType] = None, + on_change: Optional[EventType[str]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, diff --git a/reflex/components/radix/themes/components/tooltip.pyi b/reflex/components/radix/themes/components/tooltip.pyi index ad7c4402f52..ac2a363688f 100644 --- a/reflex/components/radix/themes/components/tooltip.pyi +++ b/reflex/components/radix/themes/components/tooltip.pyi @@ -76,7 +76,7 @@ class Tooltip(RadixThemesComponent): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_open_change: Optional[EventType] = None, + on_open_change: Optional[EventType[bool]] = None, on_pointer_down_outside: Optional[EventType[[]]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, diff --git a/reflex/components/react_player/audio.pyi b/reflex/components/react_player/audio.pyi index d1f29f508b6..2556c8e833a 100644 --- a/reflex/components/react_player/audio.pyi +++ b/reflex/components/react_player/audio.pyi @@ -41,7 +41,7 @@ class Audio(ReactPlayer): on_context_menu: Optional[EventType[[]]] = None, on_disable_pip: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, - on_duration: Optional[EventType] = None, + on_duration: Optional[EventType[float]] = None, on_enable_pip: Optional[EventType[[]]] = None, on_ended: Optional[EventType[[]]] = None, on_error: Optional[EventType[[]]] = None, @@ -61,7 +61,7 @@ class Audio(ReactPlayer): on_progress: Optional[EventType] = None, on_ready: Optional[EventType[[]]] = None, on_scroll: Optional[EventType[[]]] = None, - on_seek: Optional[EventType] = None, + on_seek: Optional[EventType[float]] = None, on_start: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, diff --git a/reflex/components/react_player/react_player.pyi b/reflex/components/react_player/react_player.pyi index 940b09e51e5..9a445c29492 100644 --- a/reflex/components/react_player/react_player.pyi +++ b/reflex/components/react_player/react_player.pyi @@ -39,7 +39,7 @@ class ReactPlayer(NoSSRComponent): on_context_menu: Optional[EventType[[]]] = None, on_disable_pip: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, - on_duration: Optional[EventType] = None, + on_duration: Optional[EventType[float]] = None, on_enable_pip: Optional[EventType[[]]] = None, on_ended: Optional[EventType[[]]] = None, on_error: Optional[EventType[[]]] = None, @@ -59,7 +59,7 @@ class ReactPlayer(NoSSRComponent): on_progress: Optional[EventType] = None, on_ready: Optional[EventType[[]]] = None, on_scroll: Optional[EventType[[]]] = None, - on_seek: Optional[EventType] = None, + on_seek: Optional[EventType[float]] = None, on_start: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, diff --git a/reflex/components/react_player/video.pyi b/reflex/components/react_player/video.pyi index a50ccf71f61..d46e2617df9 100644 --- a/reflex/components/react_player/video.pyi +++ b/reflex/components/react_player/video.pyi @@ -41,7 +41,7 @@ class Video(ReactPlayer): on_context_menu: Optional[EventType[[]]] = None, on_disable_pip: Optional[EventType[[]]] = None, on_double_click: Optional[EventType[[]]] = None, - on_duration: Optional[EventType] = None, + on_duration: Optional[EventType[float]] = None, on_enable_pip: Optional[EventType[[]]] = None, on_ended: Optional[EventType[[]]] = None, on_error: Optional[EventType[[]]] = None, @@ -61,7 +61,7 @@ class Video(ReactPlayer): on_progress: Optional[EventType] = None, on_ready: Optional[EventType[[]]] = None, on_scroll: Optional[EventType[[]]] = None, - on_seek: Optional[EventType] = None, + on_seek: Optional[EventType[float]] = None, on_start: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, diff --git a/reflex/components/suneditor/editor.pyi b/reflex/components/suneditor/editor.pyi index 5cc45dc982a..73dd38fdccb 100644 --- a/reflex/components/suneditor/editor.pyi +++ b/reflex/components/suneditor/editor.pyi @@ -128,7 +128,7 @@ class Editor(NoSSRComponent): autofocus: Optional[bool] = None, custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, on_blur: Optional[EventType[str]] = None, - on_change: Optional[EventType] = None, + on_change: Optional[EventType[str]] = None, on_click: Optional[EventType[[]]] = None, on_context_menu: Optional[EventType[[]]] = None, on_copy: Optional[EventType[[]]] = None, @@ -136,7 +136,7 @@ class Editor(NoSSRComponent): on_double_click: Optional[EventType[[]]] = None, on_focus: Optional[EventType[[]]] = None, on_input: Optional[EventType[[]]] = None, - on_load: Optional[EventType] = None, + on_load: Optional[EventType[bool]] = None, on_mount: Optional[EventType[[]]] = None, on_mouse_down: Optional[EventType[[]]] = None, on_mouse_enter: Optional[EventType[[]]] = None, @@ -148,8 +148,8 @@ class Editor(NoSSRComponent): on_paste: Optional[EventType[str, bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, - toggle_code_view: Optional[EventType] = None, - toggle_full_screen: Optional[EventType] = None, + toggle_code_view: Optional[EventType[bool]] = None, + toggle_full_screen: Optional[EventType[bool]] = None, **props, ) -> "Editor": """Create an instance of Editor. No children allowed. diff --git a/reflex/event.py b/reflex/event.py index 76a46573927..cfe40ef9a13 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -24,7 +24,7 @@ overload, ) -from typing_extensions import ParamSpec, get_args, get_origin +from typing_extensions import ParamSpec, Protocol, get_args, get_origin from reflex import constants from reflex.utils import console, format @@ -465,33 +465,97 @@ def empty_event() -> Tuple[()]: T = TypeVar("T") +U = TypeVar("U") -def identity_event(event_type: Type[T]) -> Callable[[Var[T]], Tuple[Var[T]]]: +# 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.""" + + def __call__(self, *values: Var[T]) -> Tuple[Var[T], ...]: + """Return the input values. + + Args: + *values: The values to return. + + Returns: + The input values. + """ + return values + + +@overload +def identity_event(event_type: Type[T], /) -> Callable[[Var[T]], Tuple[Var[T]]]: ... # type: ignore + + +@overload +def identity_event( + 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 identity_event(*event_types: Type[T]) -> IdentityEventReturn[T]: # type: ignore """A helper function that returns the input event as output. Args: - event_type: The type of the event. + *event_types: The types of the events. Returns: A function that returns the input event as output. """ - def inner(ev: Var[T]) -> Tuple[Var[T]]: - return (ev,) + def inner(*values: Var[T]) -> Tuple[Var[T], ...]: + return values + + inner_type = tuple(Var[event_type] for event_type in event_types) + return_annotation = Tuple[inner_type] # type: ignore inner.__signature__ = inspect.signature(inner).replace( # type: ignore parameters=[ inspect.Parameter( - "ev", + f"ev_{i}", kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Var[event_type], ) + for i, event_type in enumerate(event_types) ], - return_annotation=Tuple[Var[event_type]], + return_annotation=return_annotation, ) - inner.__annotations__["ev"] = Var[event_type] - inner.__annotations__["return"] = Tuple[Var[event_type]] + for i, event_type in enumerate(event_types): + inner.__annotations__[f"ev_{i}"] = Var[event_type] + inner.__annotations__["return"] = return_annotation return inner diff --git a/reflex/experimental/layout.pyi b/reflex/experimental/layout.pyi index e4c82b3518f..dcdac5b5d7e 100644 --- a/reflex/experimental/layout.pyi +++ b/reflex/experimental/layout.pyi @@ -129,7 +129,7 @@ class DrawerSidebar(DrawerRoot): on_mouse_out: Optional[EventType[[]]] = None, on_mouse_over: Optional[EventType[[]]] = None, on_mouse_up: Optional[EventType[[]]] = None, - on_open_change: Optional[EventType] = None, + on_open_change: Optional[EventType[bool]] = None, on_scroll: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None, **props, diff --git a/reflex/utils/pyi_generator.py b/reflex/utils/pyi_generator.py index fd76576b94c..026a53bca69 100644 --- a/reflex/utils/pyi_generator.py +++ b/reflex/utils/pyi_generator.py @@ -16,7 +16,7 @@ from multiprocessing import Pool, cpu_count from pathlib import Path from types import ModuleType, SimpleNamespace -from typing import Any, Callable, Iterable, Type, get_args +from typing import Any, Callable, Iterable, Type, get_args, get_origin from reflex.components.component import Component from reflex.utils import types as rx_types @@ -372,6 +372,53 @@ def _extract_class_props_as_ast_nodes( return kwargs +def type_to_ast(typ) -> ast.AST: + """Converts any type annotation into its AST representation. + Handles nested generic types, unions, etc. + + Args: + typ: The type annotation to convert. + + Returns: + The AST representation of the type annotation. + """ + if typ is type(None): + return ast.Name(id="None") + + origin = get_origin(typ) + + # Handle plain types (int, str, custom classes, etc.) + if origin is None: + if hasattr(typ, "__name__"): + return ast.Name(id=typ.__name__) + elif hasattr(typ, "_name"): + return ast.Name(id=typ._name) + return ast.Name(id=str(typ)) + + # Get the base type name (List, Dict, Optional, etc.) + base_name = origin._name if hasattr(origin, "_name") else origin.__name__ + + # Get type arguments + args = get_args(typ) + + # Handle empty type arguments + if not args: + return ast.Name(id=base_name) + + # Convert all type arguments recursively + arg_nodes = [type_to_ast(arg) for arg in args] + + # Special case for single-argument types (like List[T] or Optional[T]) + if len(arg_nodes) == 1: + slice_value = arg_nodes[0] + else: + slice_value = ast.Tuple(elts=arg_nodes, ctx=ast.Load()) + + return ast.Subscript( + value=ast.Name(id=base_name), slice=ast.Index(value=slice_value), ctx=ast.Load() + ) + + def _get_parent_imports(func): _imports = {"reflex.vars": ["Var"]} for type_hint in inspect.get_annotations(func).values(): @@ -430,13 +477,40 @@ def _generate_component_create_functiondef( def figure_out_return_type(annotation: Any): if inspect.isclass(annotation) and issubclass(annotation, inspect._empty): return ast.Name(id="Optional[EventType]") + + if not isinstance(annotation, str) and get_origin(annotation) is tuple: + arguments = get_args(annotation) + + arguments_without_var = [ + get_args(argument)[0] if get_origin(argument) == Var else argument + for argument in arguments + ] + + # Convert each argument type to its AST representation + type_args = [type_to_ast(arg) for arg in arguments_without_var] + + # Join the type arguments with commas for EventType + args_str = ", ".join(ast.unparse(arg) for arg in type_args) + + # Create EventType using the joined string + event_type = ast.Name(id=f"EventType[{args_str}]") + + # Wrap in Optional + optional_type = ast.Subscript( + value=ast.Name(id="Optional"), + slice=ast.Index(value=event_type), + ctx=ast.Load(), + ) + + return ast.Name(id=ast.unparse(optional_type)) + if isinstance(annotation, str) and annotation.startswith("Tuple["): inside_of_tuple = annotation.removeprefix("Tuple[").removesuffix("]") if inside_of_tuple == "()": return ast.Name(id="Optional[EventType[[]]]") - arguments: list[str] = [""] + arguments = [""] bracket_count = 0 diff --git a/tests/integration/test_lifespan.py b/tests/integration/test_lifespan.py index f8bcf239730..22c399c0742 100644 --- a/tests/integration/test_lifespan.py +++ b/tests/integration/test_lifespan.py @@ -51,6 +51,7 @@ def task_global(self) -> int: def context_global(self) -> int: return lifespan_context_global + @rx.event def tick(self, date): pass