From 1817c30e2270b5c9525425a92253c5473c818b98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Fri, 3 May 2024 21:09:11 +0200 Subject: [PATCH] add toast component (#3186) --- reflex/__init__.py | 1 + reflex/__init__.pyi | 1 + reflex/components/__init__.py | 1 + reflex/components/chakra/datadisplay/list.py | 4 +- reflex/components/chakra/datadisplay/list.pyi | 6 +- reflex/components/chakra/forms/pininput.pyi | 2 +- .../components/radix/primitives/accordion.py | 4 +- .../components/radix/primitives/accordion.pyi | 4 +- reflex/components/radix/themes/base.pyi | 2 +- reflex/components/radix/themes/color_mode.pyi | 4 +- reflex/components/radix/themes/layout/list.py | 4 +- .../components/radix/themes/layout/list.pyi | 4 +- reflex/components/sonner/__init__.py | 3 + reflex/components/sonner/toast.py | 267 ++++++++++++++++++ reflex/components/sonner/toast.pyi | 205 ++++++++++++++ reflex/utils/pyi_generator.py | 94 +++++- reflex/utils/types.py | 12 + 17 files changed, 600 insertions(+), 18 deletions(-) create mode 100644 reflex/components/sonner/__init__.py create mode 100644 reflex/components/sonner/toast.py create mode 100644 reflex/components/sonner/toast.pyi diff --git a/reflex/__init__.py b/reflex/__init__.py index f395ba52b..d6aa5a837 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -111,6 +111,7 @@ _ALL_COMPONENTS = [ "ordered_list", "moment", "logo", + "toast", ] _MAPPING = { diff --git a/reflex/__init__.pyi b/reflex/__init__.pyi index 76c47f5b1..2fbb9bfca 100644 --- a/reflex/__init__.pyi +++ b/reflex/__init__.pyi @@ -98,6 +98,7 @@ from reflex.components import unordered_list as unordered_list from reflex.components import ordered_list as ordered_list from reflex.components import moment as moment from reflex.components import logo as logo +from reflex.components import toast as toast from reflex.components.component import Component as Component from reflex.components.component import NoSSRComponent as NoSSRComponent from reflex.components.component import memo as memo diff --git a/reflex/components/__init__.py b/reflex/components/__init__.py index f136b2420..a5a7fa5d0 100644 --- a/reflex/components/__init__.py +++ b/reflex/components/__init__.py @@ -15,6 +15,7 @@ from .next import NextLink, next_link from .plotly import * from .radix import * from .react_player import * +from .sonner import * from .suneditor import * icon = lucide.icon diff --git a/reflex/components/chakra/datadisplay/list.py b/reflex/components/chakra/datadisplay/list.py index 4c78b56e3..0bfe155fa 100644 --- a/reflex/components/chakra/datadisplay/list.py +++ b/reflex/components/chakra/datadisplay/list.py @@ -23,9 +23,7 @@ class List(ChakraComponent): style_type: Var[str] @classmethod - def create( - cls, *children, items: list | Var[list] | None = None, **props - ) -> Component: + def create(cls, *children, items: Var[list] | None = None, **props) -> Component: """Create a list component. Args: diff --git a/reflex/components/chakra/datadisplay/list.pyi b/reflex/components/chakra/datadisplay/list.pyi index 542435e4e..344a64688 100644 --- a/reflex/components/chakra/datadisplay/list.pyi +++ b/reflex/components/chakra/datadisplay/list.pyi @@ -18,7 +18,7 @@ class List(ChakraComponent): def create( # type: ignore cls, *children, - items: Optional[list | Var[list] | None] = None, + items: Optional[Union[Var[list], list]] = None, spacing: Optional[Union[Var[str], str]] = None, style_position: Optional[Union[Var[str], str]] = None, style_type: Optional[Union[Var[str], str]] = None, @@ -178,7 +178,7 @@ class OrderedList(List): def create( # type: ignore cls, *children, - items: Optional[list | Var[list] | None] = None, + items: Optional[Union[Var[list], list]] = None, spacing: Optional[Union[Var[str], str]] = None, style_position: Optional[Union[Var[str], str]] = None, style_type: Optional[Union[Var[str], str]] = None, @@ -262,7 +262,7 @@ class UnorderedList(List): def create( # type: ignore cls, *children, - items: Optional[list | Var[list] | None] = None, + items: Optional[Union[Var[list], list]] = None, spacing: Optional[Union[Var[str], str]] = None, style_position: Optional[Union[Var[str], str]] = None, style_type: Optional[Union[Var[str], str]] = None, diff --git a/reflex/components/chakra/forms/pininput.pyi b/reflex/components/chakra/forms/pininput.pyi index 42f940fee..a3f76c8ab 100644 --- a/reflex/components/chakra/forms/pininput.pyi +++ b/reflex/components/chakra/forms/pininput.pyi @@ -147,7 +147,7 @@ class PinInputField(ChakraComponent): def create( # type: ignore cls, *children, - index: Optional[Var[int]] = None, + index: Optional[Union[Var[int], int]] = None, name: Optional[Union[Var[str], str]] = None, style: Optional[Style] = None, key: Optional[Any] = None, diff --git a/reflex/components/radix/primitives/accordion.py b/reflex/components/radix/primitives/accordion.py index 2cf37c34f..40f2d3495 100644 --- a/reflex/components/radix/primitives/accordion.py +++ b/reflex/components/radix/primitives/accordion.py @@ -314,10 +314,10 @@ class AccordionRoot(AccordionComponent): type: Var[LiteralAccordionType] # The value of the item to expand. - value: Var[Optional[Union[str, List[str]]]] + value: Var[Union[str, List[str]]] # The default value of the item to expand. - default_value: Var[Optional[Union[str, List[str]]]] + default_value: Var[Union[str, List[str]]] # Whether or not the accordion is collapsible. collapsible: Var[bool] diff --git a/reflex/components/radix/primitives/accordion.pyi b/reflex/components/radix/primitives/accordion.pyi index 3b7a3c5e2..dc2f6b853 100644 --- a/reflex/components/radix/primitives/accordion.pyi +++ b/reflex/components/radix/primitives/accordion.pyi @@ -303,8 +303,8 @@ class AccordionItem(AccordionComponent): def create( # type: ignore cls, *children, - header: Optional[Component | Var] = None, - content: Optional[Component | Var] = None, + header: Optional[Union[Component, Var]] = None, + content: Optional[Union[Component, Var]] = None, value: Optional[Union[Var[str], str]] = None, disabled: Optional[Union[Var[bool], bool]] = None, as_child: Optional[Union[Var[bool], bool]] = None, diff --git a/reflex/components/radix/themes/base.pyi b/reflex/components/radix/themes/base.pyi index fd843a2eb..d7ee29801 100644 --- a/reflex/components/radix/themes/base.pyi +++ b/reflex/components/radix/themes/base.pyi @@ -409,7 +409,7 @@ class Theme(RadixThemesComponent): def create( # type: ignore cls, *children, - color_mode: Optional[LiteralAppearance | None] = None, + color_mode: Optional[Literal["inherit", "light", "dark"]] = None, theme_panel: Optional[bool] = False, has_background: Optional[Union[Var[bool], bool]] = None, appearance: Optional[ diff --git a/reflex/components/radix/themes/color_mode.pyi b/reflex/components/radix/themes/color_mode.pyi index 51a208212..1c6174366 100644 --- a/reflex/components/radix/themes/color_mode.pyi +++ b/reflex/components/radix/themes/color_mode.pyi @@ -109,7 +109,9 @@ class ColorModeIconButton(IconButton): def create( # type: ignore cls, *children, - position: Optional[LiteralPosition | None] = None, + position: Optional[ + Literal["top-left", "top-right", "bottom-left", "bottom-right"] + ] = None, as_child: Optional[Union[Var[bool], bool]] = None, size: Optional[ Union[Var[Literal["1", "2", "3", "4"]], Literal["1", "2", "3", "4"]] diff --git a/reflex/components/radix/themes/layout/list.py b/reflex/components/radix/themes/layout/list.py index 958740ea3..2bc80b6a4 100644 --- a/reflex/components/radix/themes/layout/list.py +++ b/reflex/components/radix/themes/layout/list.py @@ -49,7 +49,7 @@ class BaseList(Component): def create( cls, *children, - items: Optional[Union[Var[Iterable], Iterable]] = None, + items: Optional[Var[Iterable]] = None, **props, ): """Create a list component. @@ -68,7 +68,7 @@ class BaseList(Component): if isinstance(items, Var): children = [Foreach.create(items, ListItem.create)] else: - children = [ListItem.create(item) for item in items] + children = [ListItem.create(item) for item in items] # type: ignore props["list_style_position"] = "outside" props["direction"] = "column" style = props.setdefault("style", {}) diff --git a/reflex/components/radix/themes/layout/list.pyi b/reflex/components/radix/themes/layout/list.pyi index df76a96a6..bd9c1c633 100644 --- a/reflex/components/radix/themes/layout/list.pyi +++ b/reflex/components/radix/themes/layout/list.pyi @@ -40,7 +40,7 @@ class BaseList(Component): def create( # type: ignore cls, *children, - items: Optional[Union[Union[Var[Iterable], Iterable], Iterable]] = None, + items: Optional[Union[Var[Iterable], Iterable]] = None, list_style_type: Optional[ Union[ Var[ @@ -600,7 +600,7 @@ class List(ComponentNamespace): @staticmethod def __call__( *children, - items: Optional[Union[Union[Var[Iterable], Iterable], Iterable]] = None, + items: Optional[Union[Var[Iterable], Iterable]] = None, list_style_type: Optional[ Union[ Var[ diff --git a/reflex/components/sonner/__init__.py b/reflex/components/sonner/__init__.py new file mode 100644 index 000000000..46d6d534e --- /dev/null +++ b/reflex/components/sonner/__init__.py @@ -0,0 +1,3 @@ +"""Init file for the sonner component.""" + +from .toast import toast diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py new file mode 100644 index 000000000..8a6c59a54 --- /dev/null +++ b/reflex/components/sonner/toast.py @@ -0,0 +1,267 @@ +"""Sonner toast component.""" + +from __future__ import annotations + +from typing import Literal + +from reflex.base import Base +from reflex.components.component import Component, ComponentNamespace +from reflex.components.lucide.icon import Icon +from reflex.event import EventSpec, call_script +from reflex.style import Style, color_mode +from reflex.utils import format +from reflex.utils.imports import ImportVar +from reflex.utils.serializers import serialize +from reflex.vars import Var, VarData + +LiteralPosition = Literal[ + "top-left", + "top-center", + "top-right", + "bottom-left", + "bottom-center", + "bottom-right", +] + + +toast_ref = Var.create_safe("refs['__toast']") + + +class PropsBase(Base): + """Base class for all props classes.""" + + def json(self) -> str: + """Convert the object to a json string. + + Returns: + The object as a json string. + """ + from reflex.utils.serializers import serialize + + return self.__config__.json_dumps( + {format.to_camel_case(key): value for key, value in self.dict().items()}, + default=serialize, + ) + + +class ToastProps(PropsBase): + """Props for the toast component.""" + + # Toast's description, renders underneath the title. + description: str = "" + + # Whether to show the close button. + close_button: bool = False + + # Dark toast in light mode and vice versa. + invert: bool = False + + # Control the sensitivity of the toast for screen readers + important: bool = False + + # Time in milliseconds that should elapse before automatically closing the toast. + duration: int = 4000 + + # Position of the toast. + position: LiteralPosition = "bottom-right" + + # If false, it'll prevent the user from dismissing the toast. + dismissible: bool = True + + # TODO: fix serialization of icons for toast? (might not be possible yet) + # Icon displayed in front of toast's text, aligned vertically. + # icon: Optional[Icon] = None + + # TODO: fix implementation for action / cancel buttons + # Renders a primary button, clicking it will close the toast. + # action: str = "" + + # Renders a secondary button, clicking it will close the toast. + # cancel: str = "" + + # Custom id for the toast. + id: str = "" + + # Removes the default styling, which allows for easier customization. + unstyled: bool = False + + # Custom style for the toast. + style: Style = Style() + + # Custom style for the toast primary button. + # action_button_styles: Style = Style() + + # Custom style for the toast secondary button. + # cancel_button_styles: Style = Style() + + +class Toaster(Component): + """A Toaster Component for displaying toast notifications.""" + + library = "sonner@1.4.41" + + tag = "Toaster" + + # the theme of the toast + theme: Var[str] = color_mode + + # whether to show rich colors + rich_colors: Var[bool] = Var.create_safe(True) + + # whether to expand the toast + expand: Var[bool] = Var.create_safe(True) + + # the number of toasts that are currently visible + visible_toasts: Var[int] + + # the position of the toast + position: Var[LiteralPosition] = Var.create_safe("bottom-right") + + # whether to show the close button + close_button: Var[bool] = Var.create_safe(False) + + # offset of the toast + offset: Var[str] + + # directionality of the toast (default: ltr) + dir: Var[str] + + # Keyboard shortcut that will move focus to the toaster area. + hotkey: Var[str] + + # Dark toasts in light mode and vice versa. + invert: Var[bool] + + # These will act as default options for all toasts. See toast() for all available options. + toast_options: Var[ToastProps] + + # Gap between toasts when expanded + gap: Var[int] + + # Changes the default loading icon + loading_icon: Var[Icon] + + # Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked. + pause_when_page_is_hidden: Var[bool] + + def _get_hooks(self) -> Var[str]: + hook = Var.create_safe(f"{toast_ref} = toast", _var_is_local=True) + hook._var_data = VarData( # type: ignore + imports={ + "/utils/state": [ImportVar(tag="refs")], + self.library: [ImportVar(tag="toast", install=False)], + } + ) + return hook + + @staticmethod + def send_toast(message: str, level: str | None = None, **props) -> EventSpec: + """Send a toast message. + + Args: + message: The message to display. + level: The level of the toast. + **props: The options for the toast. + + Returns: + The toast event. + """ + toast_command = f"{toast_ref}.{level}" if level is not None else toast_ref + if props: + args = serialize(ToastProps(**props)) + toast = f"{toast_command}(`{message}`, {args})" + else: + toast = f"{toast_command}(`{message}`)" + + toast_action = Var.create(toast, _var_is_string=False, _var_is_local=True) + return call_script(toast_action) # type: ignore + + @staticmethod + def toast_info(message: str, **kwargs): + """Display an info toast message. + + Args: + message: The message to display. + kwargs: Additional toast props. + + Returns: + The toast event. + """ + return Toaster.send_toast(message, level="info", **kwargs) + + @staticmethod + def toast_warning(message: str, **kwargs): + """Display a warning toast message. + + Args: + message: The message to display. + kwargs: Additional toast props. + + Returns: + The toast event. + """ + return Toaster.send_toast(message, level="warning", **kwargs) + + @staticmethod + def toast_error(message: str, **kwargs): + """Display an error toast message. + + Args: + message: The message to display. + kwargs: Additional toast props. + + Returns: + The toast event. + """ + return Toaster.send_toast(message, level="error", **kwargs) + + @staticmethod + def toast_success(message: str, **kwargs): + """Display a success toast message. + + Args: + message: The message to display. + kwargs: Additional toast props. + + Returns: + The toast event. + """ + return Toaster.send_toast(message, level="success", **kwargs) + + def toast_dismiss(self, id: str | None): + """Dismiss a toast. + + Args: + id: The id of the toast to dismiss. + + Returns: + The toast dismiss event. + """ + if id is None: + dismiss = f"{toast_ref}.dismiss()" + else: + dismiss = f"{toast_ref}.dismiss({id})" + dismiss_action = Var.create(dismiss, _var_is_string=False, _var_is_local=True) + return call_script(dismiss_action) # type: ignore + + +# TODO: figure out why loading toast stay open forever +# def toast_loading(message: str, **kwargs): +# return _toast(message, level="loading", **kwargs) + + +class ToastNamespace(ComponentNamespace): + """Namespace for toast components.""" + + provider = staticmethod(Toaster.create) + options = staticmethod(ToastProps) + info = staticmethod(Toaster.toast_info) + warning = staticmethod(Toaster.toast_warning) + error = staticmethod(Toaster.toast_error) + success = staticmethod(Toaster.toast_success) + dismiss = staticmethod(Toaster.toast_dismiss) + # loading = staticmethod(toast_loading) + __call__ = staticmethod(Toaster.send_toast) + + +toast = ToastNamespace() diff --git a/reflex/components/sonner/toast.pyi b/reflex/components/sonner/toast.pyi new file mode 100644 index 000000000..2bf937703 --- /dev/null +++ b/reflex/components/sonner/toast.pyi @@ -0,0 +1,205 @@ +"""Stub file for reflex/components/sonner/toast.py""" +# ------------------- DO NOT EDIT ---------------------- +# This file was generated by `reflex/utils/pyi_generator.py`! +# ------------------------------------------------------ + +from typing import Any, Dict, Literal, Optional, Union, overload +from reflex.vars import Var, BaseVar, ComputedVar +from reflex.event import EventChain, EventHandler, EventSpec +from reflex.style import Style +from typing import Literal +from reflex.base import Base +from reflex.components.component import Component, ComponentNamespace +from reflex.components.lucide.icon import Icon +from reflex.event import EventSpec, call_script +from reflex.style import Style, color_mode +from reflex.utils import format +from reflex.utils.imports import ImportVar +from reflex.utils.serializers import serialize +from reflex.vars import Var, VarData + +LiteralPosition = Literal[ + "top-left", + "top-center", + "top-right", + "bottom-left", + "bottom-center", + "bottom-right", +] +toast_ref = Var.create_safe("refs['__toast']") + +class PropsBase(Base): + def json(self) -> str: ... + +class ToastProps(PropsBase): + description: str + close_button: bool + invert: bool + important: bool + duration: int + position: LiteralPosition + dismissible: bool + id: str + unstyled: bool + style: Style + +class Toaster(Component): + @staticmethod + def send_toast(message: str, level: str | None = None, **props) -> EventSpec: ... + @staticmethod + def toast_info(message: str, **kwargs): ... + @staticmethod + def toast_warning(message: str, **kwargs): ... + @staticmethod + def toast_error(message: str, **kwargs): ... + @staticmethod + def toast_success(message: str, **kwargs): ... + def toast_dismiss(self, id: str | None): ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + theme: Optional[Union[Var[str], str]] = None, + rich_colors: Optional[Union[Var[bool], bool]] = None, + expand: Optional[Union[Var[bool], bool]] = None, + visible_toasts: Optional[Union[Var[int], int]] = None, + position: Optional[ + Union[ + Var[ + Literal[ + "top-left", + "top-center", + "top-right", + "bottom-left", + "bottom-center", + "bottom-right", + ] + ], + Literal[ + "top-left", + "top-center", + "top-right", + "bottom-left", + "bottom-center", + "bottom-right", + ], + ] + ] = None, + close_button: Optional[Union[Var[bool], bool]] = None, + offset: Optional[Union[Var[str], str]] = None, + dir: Optional[Union[Var[str], str]] = None, + hotkey: Optional[Union[Var[str], str]] = None, + invert: Optional[Union[Var[bool], bool]] = None, + toast_options: Optional[Union[Var[ToastProps], ToastProps]] = None, + gap: Optional[Union[Var[int], int]] = None, + loading_icon: Optional[Union[Var[Icon], Icon]] = None, + pause_when_page_is_hidden: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, + on_blur: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_click: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_context_menu: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_double_click: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_focus: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mount: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_down: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_enter: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_leave: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_move: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_out: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_over: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_up: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_scroll: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_unmount: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + **props + ) -> "Toaster": + """Create the component. + + Args: + *children: The children of the component. + theme: the theme of the toast + rich_colors: whether to show rich colors + expand: whether to expand the toast + visible_toasts: the number of toasts that are currently visible + position: the position of the toast + close_button: whether to show the close button + offset: offset of the toast + dir: directionality of the toast (default: ltr) + hotkey: Keyboard shortcut that will move focus to the toaster area. + invert: Dark toasts in light mode and vice versa. + toast_options: These will act as default options for all toasts. See toast() for all available options. + gap: Gap between toasts when expanded + loading_icon: Changes the default loading icon + pause_when_page_is_hidden: Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The props of the component. + + Returns: + The component. + """ + ... + +class ToastNamespace(ComponentNamespace): + provider = staticmethod(Toaster.create) + options = staticmethod(ToastProps) + info = staticmethod(Toaster.toast_info) + warning = staticmethod(Toaster.toast_warning) + error = staticmethod(Toaster.toast_error) + success = staticmethod(Toaster.toast_success) + dismiss = staticmethod(Toaster.toast_dismiss) + + @staticmethod + def __call__(message: str, level: Optional[str], **props) -> "Optional[EventSpec]": + """Send a toast message. + + Args: + message: The message to display. + level: The level of the toast. + **props: The options for the toast. + + Returns: + The toast event. + """ + ... + +toast = ToastNamespace() diff --git a/reflex/utils/pyi_generator.py b/reflex/utils/pyi_generator.py index e4ba068cf..d14a8af4b 100644 --- a/reflex/utils/pyi_generator.py +++ b/reflex/utils/pyi_generator.py @@ -117,6 +117,29 @@ def _get_type_hint(value, type_hint_globals, is_optional=True) -> str: """ res = "" args = get_args(value) + + if value is type(None): + return "None" + + if rx_types.is_union(value): + if type(None) in value.__args__: + res_args = [ + _get_type_hint(arg, type_hint_globals, rx_types.is_optional(arg)) + for arg in value.__args__ + if arg is not type(None) + ] + if len(res_args) == 1: + return f"Optional[{res_args[0]}]" + else: + res = f"Union[{', '.join(res_args)}]" + return f"Optional[{res}]" + + res_args = [ + _get_type_hint(arg, type_hint_globals, rx_types.is_optional(arg)) + for arg in value.__args__ + ] + return f"Union[{', '.join(res_args)}]" + if args: inner_container_type_args = ( [repr(arg) for arg in args] @@ -141,6 +164,20 @@ def _get_type_hint(value, type_hint_globals, is_optional=True) -> str: res = f"Union[{res}]" elif isinstance(value, str): ev = eval(value, type_hint_globals) + if rx_types.is_optional(ev): + # hints = { + # _get_type_hint(arg, type_hint_globals, is_optional=False) + # for arg in ev.__args__ + # } + return _get_type_hint(ev, type_hint_globals, is_optional=False) + # return f"Optional[{', '.join(hints)}]" + + if rx_types.is_union(ev): + res = [ + _get_type_hint(arg, type_hint_globals, rx_types.is_optional(arg)) + for arg in ev.__args__ + ] + return f"Union[{', '.join(res)}]" res = ( _get_type_hint(ev, type_hint_globals, is_optional=False) if ev.__name__ == "Var" @@ -424,7 +461,58 @@ def _generate_component_create_functiondef( return definition +def _generate_staticmethod_call_functiondef( + node: ast.FunctionDef | None, + clz: type[Component] | type[SimpleNamespace], + type_hint_globals: dict[str, Any], +) -> ast.FunctionDef | None: + ... + + fullspec = getfullargspec(clz.__call__) + + call_args = ast.arguments( + args=[ + ast.arg( + name, + annotation=ast.Name( + id=_get_type_hint( + anno := fullspec.annotations[name], + type_hint_globals, + is_optional=rx_types.is_optional(anno), + ) + ), + ) + for name in fullspec.args + ], + posonlyargs=[], + kwonlyargs=[], + kw_defaults=[], + kwarg=ast.arg(arg="props"), + defaults=[], + ) + definition = ast.FunctionDef( + name="__call__", + args=call_args, + body=[ + ast.Expr(value=ast.Constant(value=clz.__call__.__doc__)), + ast.Expr( + value=ast.Constant(...), + ), + ], + decorator_list=[ast.Name(id="staticmethod")], + lineno=node.lineno if node is not None else None, + returns=ast.Constant( + value=_get_type_hint( + typing.get_type_hints(clz.__call__).get("return", None), + type_hint_globals, + ) + ), + ) + return definition + + def _generate_namespace_call_functiondef( + node: ast.ClassDef | None, clz_name: str, classes: dict[str, type[Component] | type[SimpleNamespace]], type_hint_globals: dict[str, Any], @@ -432,6 +520,7 @@ def _generate_namespace_call_functiondef( """Generate the __call__ function definition for a SimpleNamespace. Args: + node: The existing __call__ classdef parent node from the ast clz_name: The name of the SimpleNamespace class to generate the __call__ functiondef for. classes: Map name to actual class definition. type_hint_globals: The globals to use to resolving a type hint str. @@ -446,10 +535,12 @@ def _generate_namespace_call_functiondef( clz = classes[clz_name] + if not hasattr(clz.__call__, "__self__"): + return _generate_staticmethod_call_functiondef(node, clz, type_hint_globals) # type: ignore + # Determine which class is wrapped by the namespace __call__ method component_clz = clz.__call__.__self__ - # Only generate for create functions if clz.__call__.__func__.__name__ != "create": return None @@ -603,6 +694,7 @@ class StubGenerator(ast.NodeTransformer): if not child.targets[:]: node.body.remove(child) call_definition = _generate_namespace_call_functiondef( + node, self.current_class, self.classes, type_hint_globals=self.type_hint_globals, diff --git a/reflex/utils/types.py b/reflex/utils/types.py index 32d3e406c..8214e0765 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -126,6 +126,18 @@ def is_generic_alias(cls: GenericType) -> bool: return isinstance(cls, GenericAliasTypes) +def is_none(cls: GenericType) -> bool: + """Check if a class is None. + + Args: + cls: The class to check. + + Returns: + Whether the class is None. + """ + return cls is type(None) or cls is None + + def is_union(cls: GenericType) -> bool: """Check if a class is a Union.