diff --git a/news/+pyi-union-prop-optional.bugfix.md b/news/+pyi-union-prop-optional.bugfix.md new file mode 100644 index 00000000000..4c19ef1bf92 --- /dev/null +++ b/news/+pyi-union-prop-optional.bugfix.md @@ -0,0 +1 @@ +Generated `.pyi` stubs now type a prop declared as a union — `content: Var[str] | Component`, say — as optional, matching the `None` default that `create()` gives every prop. Type checkers previously reported the generated signature itself as an error. diff --git a/packages/reflex-base/news/+pyi-union-prop-optional.bugfix.md b/packages/reflex-base/news/+pyi-union-prop-optional.bugfix.md new file mode 100644 index 00000000000..4c19ef1bf92 --- /dev/null +++ b/packages/reflex-base/news/+pyi-union-prop-optional.bugfix.md @@ -0,0 +1 @@ +Generated `.pyi` stubs now type a prop declared as a union — `content: Var[str] | Component`, say — as optional, matching the `None` default that `create()` gives every prop. Type checkers previously reported the generated signature itself as an error. diff --git a/packages/reflex-base/src/reflex_base/utils/pyi_generator.py b/packages/reflex-base/src/reflex_base/utils/pyi_generator.py index b58f0e0dfbb..b5a528a04c2 100644 --- a/packages/reflex-base/src/reflex_base/utils/pyi_generator.py +++ b/packages/reflex-base/src/reflex_base/utils/pyi_generator.py @@ -210,7 +210,8 @@ def _get_type_hint( for arg in value.__args__ ] res_args.sort() - return f"{' | '.join(res_args)}" + res = f"{' | '.join(res_args)}" + return f"{res} | None" if is_optional else res if args: inner_container_type_args = ( @@ -257,11 +258,12 @@ def _get_type_hint( return _get_type_hint(ev, type_hint_globals, is_optional=False) if _is_union(ev): - res = [ + res_args = [ _get_type_hint(arg, type_hint_globals, _is_optional(arg)) for arg in ev.__args__ ] - return f"{' | '.join(res)}" + res = f"{' | '.join(res_args)}" + return f"{res} | None" if is_optional else res res = ( _get_type_hint(ev, type_hint_globals, is_optional=False) if ev.__name__ == "Var" diff --git a/pyi_hashes.json b/pyi_hashes.json index 603fc724e48..83ed821c04b 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -40,7 +40,7 @@ "packages/reflex-components-dataeditor/src/reflex_components_dataeditor/dataeditor.pyi": "8aedc1629118cd3c0075340a5a0c3356", "packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.pyi": "2ce1c076ecf5c2fa4945b4abdbf2f91d", "packages/reflex-components-lucide/src/reflex_components_lucide/icon.pyi": "1e331a3d6420b97e5b1ce7f63ad53de8", - "packages/reflex-components-markdown/src/reflex_components_markdown/markdown.pyi": "79d0a59b1ba12a2f2c4a09fa6b5c776f", + "packages/reflex-components-markdown/src/reflex_components_markdown/markdown.pyi": "6759daf6e242e209d9298045c7b4456a", "packages/reflex-components-moment/src/reflex_components_moment/moment.pyi": "13e39f8d2b46bfd76c50cb04062da8ff", "packages/reflex-components-plotly/src/reflex_components_plotly/plotly.pyi": "beb057e382e527224597c320dbb72385", "packages/reflex-components-radix/src/reflex_components_radix/__init__.pyi": "a77352f60fb6f4135b5d08a6e56efa6d", diff --git a/tests/units/reflex_base/utils/pyi_generator/dataset/var_types.py b/tests/units/reflex_base/utils/pyi_generator/dataset/var_types.py index fcd4e30cbee..435afa1e198 100644 --- a/tests/units/reflex_base/utils/pyi_generator/dataset/var_types.py +++ b/tests/units/reflex_base/utils/pyi_generator/dataset/var_types.py @@ -7,10 +7,11 @@ - Callable prop: Var[Callable[[], bool]] (should NOT expand inner type) - Component with no custom props (just inherited defaults) - Component with only event handlers (no data props) +- Union props without None: still optional in create() """ from collections.abc import Callable -from typing import Any +from typing import Any, Literal from reflex_base.components.component import Component, field from reflex_base.event import EventHandler, passthrough_event_spec @@ -48,3 +49,13 @@ class VarTypesComponent(Component): # Callable prop — the inner Callable type should not be expanded. on_check: Var[Callable[[], bool]] = field(doc="A callable that returns bool.") + + +class UnionPropsComponent(Component): + """A component with props annotated as unions that do not include None.""" + + # Union of a Var and another type; create() still defaults it to None. + content: Var[str] | Component = field(doc="The content to render.") + + # Union of a Literal and a Var. + color_scheme: Literal["red", "blue"] | Var[str] = field(doc="The color scheme.") diff --git a/tests/units/reflex_base/utils/pyi_generator/golden/var_types.pyi b/tests/units/reflex_base/utils/pyi_generator/golden/var_types.pyi index b4340b3461e..bf2ea421683 100644 --- a/tests/units/reflex_base/utils/pyi_generator/golden/var_types.pyi +++ b/tests/units/reflex_base/utils/pyi_generator/golden/var_types.pyi @@ -4,7 +4,7 @@ # This file was generated by `reflex/utils/pyi_generator.py`! # ------------------------------------------------------ from collections.abc import Callable, Mapping, Sequence -from typing import Any +from typing import Any, Literal from reflex_base.components.component import Component from reflex_base.event import EventType, PointerEventInfo @@ -225,3 +225,72 @@ class VarTypesComponent(Component): Returns: The component. """ + +class UnionPropsComponent(Component): + @classmethod + def create( + cls, + *children, + content: Component | Var[str] | str | None = None, + color_scheme: Literal["blue", "red"] | Var[str] | str | None = None, + style: Sequence[Mapping[str, Any]] + | Mapping[str, Any] + | Var[Mapping[str, Any]] + | Breakpoints + | None = None, + key: Any | None = None, + id: Any | None = None, + ref: Var | None = None, + class_name: Any | None = None, + custom_attrs: dict[str, Any | Var] | None = None, + on_blur: EventType[()] | None = None, + on_click: EventType[()] | EventType[PointerEventInfo] | None = None, + on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None, + on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None, + on_focus: EventType[()] | None = None, + on_mount: EventType[()] | None = None, + on_mouse_down: EventType[()] | None = None, + on_mouse_enter: EventType[()] | None = None, + on_mouse_leave: EventType[()] | None = None, + on_mouse_move: EventType[()] | None = None, + on_mouse_out: EventType[()] | None = None, + on_mouse_over: EventType[()] | None = None, + on_mouse_up: EventType[()] | None = None, + on_scroll: EventType[()] | None = None, + on_scroll_end: EventType[()] | None = None, + on_unmount: EventType[()] | None = None, + **props, + ) -> UnionPropsComponent: + """Create the component. + + Args: + *children: The children of the component. + content: The content to render. + color_scheme: The color scheme. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + ref: The Var to pass as the ref to the component. + class_name: The class name for the component. + custom_attrs: Attributes passed directly to the component. + on_focus: Fired when the element (or some element inside of it) receives focus. For example, it is called when the user clicks on a text input. + on_blur: Fired when focus has left the element (or left some element inside of it). For example, it is called when the user clicks outside of a focused text input. + on_click: Fired when the user clicks on an element. For example, it's called when the user clicks on a button. + on_context_menu: Fired when the user right-clicks on an element. + on_double_click: Fired when the user double-clicks on an element. + on_mouse_down: Fired when the user presses a mouse button on an element. + on_mouse_enter: Fired when the mouse pointer enters the element. + on_mouse_leave: Fired when the mouse pointer leaves the element. + on_mouse_move: Fired when the mouse pointer moves over the element. + on_mouse_out: Fired when the mouse pointer moves out of the element. + on_mouse_over: Fired when the mouse pointer moves onto the element. + on_mouse_up: Fired when the user releases a mouse button on an element. + on_scroll: Fired when the user scrolls the element. + on_scroll_end: Fired when scrolling ends on the element. + on_mount: Fired when the component is mounted to the page. + on_unmount: Fired when the component is removed from the page. Only called during navigation, not on page refresh. + **props: The props of the component. + + Returns: + The component. + """ diff --git a/tests/units/reflex_base/utils/pyi_generator/test_unit.py b/tests/units/reflex_base/utils/pyi_generator/test_unit.py index 4bbf5107b8a..8f0f67c7555 100644 --- a/tests/units/reflex_base/utils/pyi_generator/test_unit.py +++ b/tests/units/reflex_base/utils/pyi_generator/test_unit.py @@ -154,6 +154,18 @@ def test_get_type_hint_union_without_none(type_hint_globals): assert result == "int | str" +def test_get_type_hint_union_without_none_optional(type_hint_globals): + """A union prop still gets `| None`, since create() defaults it to None.""" + result = _get_type_hint(Union[str, int], type_hint_globals, is_optional=True) # noqa: UP007 + assert result == "int | str | None" + + +def test_get_type_hint_str_union_without_none_optional(type_hint_globals): + """A union given as a string annotation is treated the same way.""" + result = _get_type_hint("Union[str, int]", type_hint_globals, is_optional=True) + assert result == "str | int | None" + + def test_get_type_hint_union_with_none(type_hint_globals): result = _get_type_hint(Union[str, int, None], type_hint_globals) # noqa: UP007 assert result == "int | str | None"