Merge branch 'main' into lendemor/builtins_states

This commit is contained in:
Lendemor 2024-10-22 22:27:10 +02:00
commit 703a6da3d7
17 changed files with 257 additions and 192 deletions

36
poetry.lock generated
View File

@ -570,18 +570,18 @@ test = ["pytest (>=6)"]
[[package]]
name = "fastapi"
version = "0.115.2"
version = "0.115.3"
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.2-py3-none-any.whl", hash = "sha256:61704c71286579cc5a598763905928f24ee98bfcc07aabe84cfefb98812bbc86"},
{file = "fastapi-0.115.2.tar.gz", hash = "sha256:3995739e0b09fa12f984bce8fa9ae197b35d433750d3d312422d846e283697ee"},
{file = "fastapi-0.115.3-py3-none-any.whl", hash = "sha256:8035e8f9a2b0aa89cea03b6c77721178ed5358e1aea4cd8570d9466895c0638c"},
{file = "fastapi-0.115.3.tar.gz", hash = "sha256:c091c6a35599c036d676fa24bd4a6e19fa30058d93d950216cdc672881f6f7db"},
]
[package.dependencies]
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
starlette = ">=0.37.2,<0.41.0"
starlette = ">=0.40.0,<0.42.0"
typing-extensions = ">=4.8.0"
[package.extras]
@ -1977,6 +1977,20 @@ files = [
[package.dependencies]
six = ">=1.5"
[[package]]
name = "python-dotenv"
version = "1.0.1"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.8"
files = [
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
]
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "python-engineio"
version = "4.10.1"
@ -2253,13 +2267,13 @@ idna2008 = ["idna"]
[[package]]
name = "rich"
version = "13.9.2"
version = "13.9.3"
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.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"},
{file = "rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c"},
{file = "rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283"},
{file = "rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e"},
]
[package.dependencies]
@ -2525,13 +2539,13 @@ SQLAlchemy = ">=2.0.14,<2.1.0"
[[package]]
name = "starlette"
version = "0.40.0"
version = "0.41.0"
description = "The little ASGI library that shines."
optional = false
python-versions = ">=3.8"
files = [
{file = "starlette-0.40.0-py3-none-any.whl", hash = "sha256:c494a22fae73805376ea6bf88439783ecfba9aac88a43911b48c653437e784c4"},
{file = "starlette-0.40.0.tar.gz", hash = "sha256:1a3139688fb298ce5e2d661d37046a66ad996ce94be4d4983be019a23a04ea35"},
{file = "starlette-0.41.0-py3-none-any.whl", hash = "sha256:a0193a3c413ebc9c78bff1c3546a45bb8c8bcb4a84cae8747d650a65bd37210a"},
{file = "starlette-0.41.0.tar.gz", hash = "sha256:39cbd8768b107d68bfe1ff1672b38a2c38b49777de46d2a592841d58e3bf7c2a"},
]
[package.dependencies]
@ -3033,4 +3047,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "8090ccaeca173bd8612e17a0b8d157d7492618e49450abd1c8373e2976349db0"
content-hash = "c5da15520cef58124f6699007c81158036840469d4f9972592d72bd456c45e7e"

View File

@ -33,6 +33,7 @@ jinja2 = ">=3.1.2,<4.0"
psutil = ">=5.9.4,<7.0"
pydantic = ">=1.10.2,<3.0"
python-multipart = ">=0.0.5,<0.1"
python-dotenv = ">=1.0.1"
python-socketio = ">=5.7.0,<6.0"
redis = ">=4.3.5,<6.0"
rich = ">=13.0.0,<14.0"

View File

@ -321,14 +321,14 @@ _MAPPING: dict = {
"window_alert",
],
"istate.builtins": ["ComponentState", "State"],
"middleware": ["middleware", "Middleware"],
"model": ["session", "Model"],
"state": [
"var",
"istate.storage": [
"Cookie",
"LocalStorage",
"SessionStorage",
],
"middleware": ["middleware", "Middleware"],
"model": ["session", "Model"],
"state": ["var"],
"style": ["Style", "toggle_color_mode"],
"utils.imports": ["ImportVar"],
"utils.serializers": ["serializer"],

View File

@ -176,14 +176,14 @@ from .event import window_alert as window_alert
from .experimental import _x as _x
from .istate.builtins import ComponentState as ComponentState
from .istate.builtins import State as State
from .istate.storage import Cookie as Cookie
from .istate.storage import LocalStorage as LocalStorage
from .istate.storage import SessionStorage as SessionStorage
from .middleware import Middleware as Middleware
from .middleware import middleware as middleware
from .model import Model as Model
from .model import session as session
from .page import page as page
from .state import Cookie as Cookie
from .state import LocalStorage as LocalStorage
from .state import SessionStorage as SessionStorage
from .state import var as var
from .style import Style as Style
from .style import toggle_color_mode as toggle_color_mode

View File

@ -28,7 +28,8 @@ from reflex.components.base import (
Title,
)
from reflex.components.component import Component, ComponentStyle, CustomComponent
from reflex.state import BaseState, Cookie, LocalStorage, SessionStorage
from reflex.istate.storage import Cookie, LocalStorage, SessionStorage
from reflex.state import BaseState
from reflex.style import Style
from reflex.utils import console, format, imports, path_ops
from reflex.utils.imports import ImportVar, ParsedImportDict

View File

@ -109,19 +109,6 @@ class DataEditorTheme(Base):
text_medium: Optional[str] = None
def on_edit_spec(pos, data: dict[str, Any]):
"""The on edit spec function.
Args:
pos: The position of the edit event.
data: The data of the edit event.
Returns:
The position and data.
"""
return [pos, data]
class Bounds(TypedDict):
"""The bounds of the group header."""
@ -149,7 +136,7 @@ class Rectangle(TypedDict):
class GridSelectionCurrent(TypedDict):
"""The current selection."""
cell: list[int]
cell: tuple[int, int]
range: Rectangle
rangeStack: list[Rectangle]
@ -167,7 +154,7 @@ class GroupHeaderClickedEventArgs(TypedDict):
kind: str
group: str
location: list[int]
location: tuple[int, int]
bounds: Bounds
isEdge: bool
shiftKey: bool
@ -178,7 +165,7 @@ class GroupHeaderClickedEventArgs(TypedDict):
localEventY: int
button: int
buttons: int
scrollEdge: list[int]
scrollEdge: tuple[int, int]
class GridCell(TypedDict):
@ -306,10 +293,10 @@ class DataEditor(NoSSRComponent):
on_cell_context_menu: EventHandler[identity_event(Tuple[int, int])]
# Fired when a cell is edited.
on_cell_edited: EventHandler[on_edit_spec]
on_cell_edited: EventHandler[identity_event(Tuple[int, int], GridCell)]
# Fired when a group header is clicked.
on_group_header_clicked: EventHandler[on_edit_spec]
on_group_header_clicked: EventHandler[identity_event(Tuple[int, int], GridCell)]
# Fired when a group header is right-clicked.
on_group_header_context_menu: EventHandler[
@ -335,7 +322,9 @@ class DataEditor(NoSSRComponent):
on_delete: EventHandler[identity_event(GridSelection)]
# Fired when editing is finished.
on_finished_editing: EventHandler[identity_event(Union[GridCell, None], list[int])]
on_finished_editing: EventHandler[
identity_event(Union[GridCell, None], tuple[int, int])
]
# Fired when a row is appended.
on_row_appended: EventHandler[empty_event]

View File

@ -78,8 +78,6 @@ class DataEditorTheme(Base):
text_light: Optional[str]
text_medium: Optional[str]
def on_edit_spec(pos, data: dict[str, Any]): ...
class Bounds(TypedDict):
x: int
y: int
@ -96,7 +94,7 @@ class Rectangle(TypedDict):
height: int
class GridSelectionCurrent(TypedDict):
cell: list[int]
cell: tuple[int, int]
range: Rectangle
rangeStack: list[Rectangle]
@ -108,7 +106,7 @@ class GridSelection(TypedDict):
class GroupHeaderClickedEventArgs(TypedDict):
kind: str
group: str
location: list[int]
location: tuple[int, int]
bounds: Bounds
isEdge: bool
shiftKey: bool
@ -119,7 +117,7 @@ class GroupHeaderClickedEventArgs(TypedDict):
localEventY: int
button: int
buttons: int
scrollEdge: list[int]
scrollEdge: tuple[int, int]
class GridCell(TypedDict):
span: Optional[List[int]]
@ -189,17 +187,17 @@ class DataEditor(NoSSRComponent):
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_cell_edited: Optional[EventType[tuple[int, int], GridCell]] = None,
on_click: Optional[EventType[[]]] = None,
on_column_resize: Optional[EventType[GridColumn, int]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_delete: Optional[EventType[GridSelection]] = None,
on_double_click: Optional[EventType[[]]] = None,
on_finished_editing: Optional[
EventType[Union[GridCell, None], list[int]]
EventType[Union[GridCell, None], tuple[int, int]]
] = None,
on_focus: Optional[EventType[[]]] = None,
on_group_header_clicked: Optional[EventType] = None,
on_group_header_clicked: Optional[EventType[tuple[int, int], GridCell]] = None,
on_group_header_context_menu: Optional[
EventType[int, GroupHeaderClickedEventArgs]
] = None,

View File

@ -5,7 +5,9 @@
# ------------------------------------------------------
from typing import Any, Dict, Literal, Optional, Union, overload
from reflex.event import EventType
from reflex.event import (
EventType,
)
from reflex.style import Style
from reflex.vars.base import Var

View File

@ -1,5 +1,6 @@
"""React Player component for audio and video."""
from . import react_player
from .audio import Audio
from .video import Video

View File

@ -5,6 +5,7 @@
# ------------------------------------------------------
from typing import Any, Dict, Optional, Union, overload
import reflex
from reflex.components.react_player.react_player import ReactPlayer
from reflex.event import EventType
from reflex.style import Style
@ -58,7 +59,9 @@ class Audio(ReactPlayer):
on_play: Optional[EventType[[]]] = None,
on_playback_quality_change: Optional[EventType[[]]] = None,
on_playback_rate_change: Optional[EventType[[]]] = None,
on_progress: Optional[EventType] = None,
on_progress: Optional[
EventType[reflex.components.react_player.react_player.Progress]
] = None,
on_ready: Optional[EventType[[]]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_seek: Optional[EventType[float]] = None,

View File

@ -2,11 +2,22 @@
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.vars.base import Var
class Progress(TypedDict):
"""Callback containing played and loaded progress as a fraction, and playedSeconds and loadedSeconds in seconds."""
played: float
playedSeconds: float
loaded: float
loadedSeconds: float
class ReactPlayer(NoSSRComponent):
"""Using react-player and not implement all props and callback yet.
reference: https://github.com/cookpete/react-player.
@ -55,7 +66,7 @@ class ReactPlayer(NoSSRComponent):
on_play: EventHandler[empty_event]
# 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[lambda progress: [progress]]
on_progress: EventHandler[identity_event(Progress)]
# Callback containing duration of the media, in seconds.
on_duration: EventHandler[identity_event(float)]

View File

@ -5,11 +5,19 @@
# ------------------------------------------------------
from typing import Any, Dict, Optional, Union, overload
from typing_extensions import TypedDict
from reflex.components.component import NoSSRComponent
from reflex.event import EventType
from reflex.style import Style
from reflex.vars.base import Var
class Progress(TypedDict):
played: float
playedSeconds: float
loaded: float
loadedSeconds: float
class ReactPlayer(NoSSRComponent):
@overload
@classmethod
@ -56,7 +64,7 @@ class ReactPlayer(NoSSRComponent):
on_play: Optional[EventType[[]]] = None,
on_playback_quality_change: Optional[EventType[[]]] = None,
on_playback_rate_change: Optional[EventType[[]]] = None,
on_progress: Optional[EventType] = None,
on_progress: Optional[EventType[Progress]] = None,
on_ready: Optional[EventType[[]]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_seek: Optional[EventType[float]] = None,

View File

@ -5,6 +5,7 @@
# ------------------------------------------------------
from typing import Any, Dict, Optional, Union, overload
import reflex
from reflex.components.react_player.react_player import ReactPlayer
from reflex.event import EventType
from reflex.style import Style
@ -58,7 +59,9 @@ class Video(ReactPlayer):
on_play: Optional[EventType[[]]] = None,
on_playback_quality_change: Optional[EventType[[]]] = None,
on_playback_rate_change: Optional[EventType[[]]] = None,
on_progress: Optional[EventType] = None,
on_progress: Optional[
EventType[reflex.components.react_player.react_player.Progress]
] = None,
on_ready: Optional[EventType[[]]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_seek: Optional[EventType[float]] = None,

View File

@ -436,6 +436,9 @@ class Config(Base):
# Attributes that were explicitly set by the user.
_non_default_attributes: Set[str] = pydantic.PrivateAttr(set())
# Path to file containing key-values pairs to override in the environment; Dotenv format.
env_file: Optional[str] = None
def __init__(self, *args, **kwargs):
"""Initialize the config values.
@ -477,6 +480,7 @@ class Config(Base):
def update_from_env(self) -> dict[str, Any]:
"""Update the config values based on set environment variables.
If there is a set env_file, it is loaded first.
Returns:
The updated config values.
@ -486,6 +490,12 @@ class Config(Base):
"""
from reflex.utils.exceptions import EnvVarValueError
if self.env_file:
from dotenv import load_dotenv
# load env file if exists
load_dotenv(self.env_file, override=True)
updated_values = {}
# Iterate over the fields.
for key, field in self.__fields__.items():

144
reflex/istate/storage.py Normal file
View File

@ -0,0 +1,144 @@
"""Client-side storage classes for reflex state variables."""
from __future__ import annotations
from typing import Any
from reflex.utils import format
class ClientStorageBase:
"""Base class for client-side storage."""
def options(self) -> dict[str, Any]:
"""Get the options for the storage.
Returns:
All set options for the storage (not None).
"""
return {
format.to_camel_case(k): v for k, v in vars(self).items() if v is not None
}
class Cookie(ClientStorageBase, str):
"""Represents a state Var that is stored as a cookie in the browser."""
name: str | None
path: str
max_age: int | None
domain: str | None
secure: bool | None
same_site: str
def __new__(
cls,
object: Any = "",
encoding: str | None = None,
errors: str | None = None,
/,
name: str | None = None,
path: str = "/",
max_age: int | None = None,
domain: str | None = None,
secure: bool | None = None,
same_site: str = "lax",
):
"""Create a client-side Cookie (str).
Args:
object: The initial object.
encoding: The encoding to use.
errors: The error handling scheme to use.
name: The name of the cookie on the client side.
path: Cookie path. Use / as the path if the cookie should be accessible on all pages.
max_age: Relative max age of the cookie in seconds from when the client receives it.
domain: Domain for the cookie (sub.domain.com or .allsubdomains.com).
secure: Is the cookie only accessible through HTTPS?
same_site: Whether the cookie is sent with third party requests.
One of (true|false|none|lax|strict)
Returns:
The client-side Cookie object.
Note: expires (absolute Date) is not supported at this time.
"""
if encoding or errors:
inst = super().__new__(cls, object, encoding or "utf-8", errors or "strict")
else:
inst = super().__new__(cls, object)
inst.name = name
inst.path = path
inst.max_age = max_age
inst.domain = domain
inst.secure = secure
inst.same_site = same_site
return inst
class LocalStorage(ClientStorageBase, str):
"""Represents a state Var that is stored in localStorage in the browser."""
name: str | None
sync: bool = False
def __new__(
cls,
object: Any = "",
encoding: str | None = None,
errors: str | None = None,
/,
name: str | None = None,
sync: bool = False,
) -> "LocalStorage":
"""Create a client-side localStorage (str).
Args:
object: The initial object.
encoding: The encoding to use.
errors: The error handling scheme to use.
name: The name of the storage key on the client side.
sync: Whether changes should be propagated to other tabs.
Returns:
The client-side localStorage object.
"""
if encoding or errors:
inst = super().__new__(cls, object, encoding or "utf-8", errors or "strict")
else:
inst = super().__new__(cls, object)
inst.name = name
inst.sync = sync
return inst
class SessionStorage(ClientStorageBase, str):
"""Represents a state Var that is stored in sessionStorage in the browser."""
name: str | None
def __new__(
cls,
object: Any = "",
encoding: str | None = None,
errors: str | None = None,
/,
name: str | None = None,
) -> "SessionStorage":
"""Create a client-side sessionStorage (str).
Args:
object: The initial object.
encoding: The encoding to use.
errors: The error handling scheme to use
name: The name of the storage on the client side
Returns:
The client-side sessionStorage object.
"""
if encoding or errors:
inst = super().__new__(cls, object, encoding or "utf-8", errors or "strict")
else:
inst = super().__new__(cls, object)
inst.name = name
return inst

View File

@ -41,6 +41,9 @@ from typing_extensions import Self
from reflex.config import get_config
from reflex.istate.data import RouterData
from reflex.istate.storage import (
ClientStorageBase,
)
from reflex.vars.base import (
ComputedVar,
DynamicRouteVar,
@ -3176,143 +3179,6 @@ def get_state_manager() -> StateManager:
return app.state_manager
class ClientStorageBase:
"""Base class for client-side storage."""
def options(self) -> dict[str, Any]:
"""Get the options for the storage.
Returns:
All set options for the storage (not None).
"""
return {
format.to_camel_case(k): v for k, v in vars(self).items() if v is not None
}
class Cookie(ClientStorageBase, str):
"""Represents a state Var that is stored as a cookie in the browser."""
name: str | None
path: str
max_age: int | None
domain: str | None
secure: bool | None
same_site: str
def __new__(
cls,
object: Any = "",
encoding: str | None = None,
errors: str | None = None,
/,
name: str | None = None,
path: str = "/",
max_age: int | None = None,
domain: str | None = None,
secure: bool | None = None,
same_site: str = "lax",
):
"""Create a client-side Cookie (str).
Args:
object: The initial object.
encoding: The encoding to use.
errors: The error handling scheme to use.
name: The name of the cookie on the client side.
path: Cookie path. Use / as the path if the cookie should be accessible on all pages.
max_age: Relative max age of the cookie in seconds from when the client receives it.
domain: Domain for the cookie (sub.domain.com or .allsubdomains.com).
secure: Is the cookie only accessible through HTTPS?
same_site: Whether the cookie is sent with third party requests.
One of (true|false|none|lax|strict)
Returns:
The client-side Cookie object.
Note: expires (absolute Date) is not supported at this time.
"""
if encoding or errors:
inst = super().__new__(cls, object, encoding or "utf-8", errors or "strict")
else:
inst = super().__new__(cls, object)
inst.name = name
inst.path = path
inst.max_age = max_age
inst.domain = domain
inst.secure = secure
inst.same_site = same_site
return inst
class LocalStorage(ClientStorageBase, str):
"""Represents a state Var that is stored in localStorage in the browser."""
name: str | None
sync: bool = False
def __new__(
cls,
object: Any = "",
encoding: str | None = None,
errors: str | None = None,
/,
name: str | None = None,
sync: bool = False,
) -> "LocalStorage":
"""Create a client-side localStorage (str).
Args:
object: The initial object.
encoding: The encoding to use.
errors: The error handling scheme to use.
name: The name of the storage key on the client side.
sync: Whether changes should be propagated to other tabs.
Returns:
The client-side localStorage object.
"""
if encoding or errors:
inst = super().__new__(cls, object, encoding or "utf-8", errors or "strict")
else:
inst = super().__new__(cls, object)
inst.name = name
inst.sync = sync
return inst
class SessionStorage(ClientStorageBase, str):
"""Represents a state Var that is stored in sessionStorage in the browser."""
name: str | None
def __new__(
cls,
object: Any = "",
encoding: str | None = None,
errors: str | None = None,
/,
name: str | None = None,
) -> "SessionStorage":
"""Create a client-side sessionStorage (str).
Args:
object: The initial object.
encoding: The encoding to use.
errors: The error handling scheme to use
name: The name of the storage on the client side
Returns:
The client-side sessionStorage object.
"""
if encoding or errors:
inst = super().__new__(cls, object, encoding or "utf-8", errors or "strict")
else:
inst = super().__new__(cls, object)
inst.name = name
return inst
class MutableProxy(wrapt.ObjectProxy):
"""A proxy for a mutable object that tracks changes."""

View File

@ -214,7 +214,9 @@ def _get_type_hint(value, type_hint_globals, is_optional=True) -> str:
return res
def _generate_imports(typing_imports: Iterable[str]) -> list[ast.ImportFrom]:
def _generate_imports(
typing_imports: Iterable[str],
) -> list[ast.ImportFrom | ast.Import]:
"""Generate the import statements for the stub file.
Args:
@ -228,6 +230,7 @@ def _generate_imports(typing_imports: Iterable[str]) -> list[ast.ImportFrom]:
ast.ImportFrom(module=name, names=[ast.alias(name=val) for val in values])
for name, values in DEFAULT_IMPORTS.items()
],
ast.Import([ast.alias("reflex")]),
]
@ -372,12 +375,13 @@ def _extract_class_props_as_ast_nodes(
return kwargs
def type_to_ast(typ) -> ast.AST:
def type_to_ast(typ, cls: type) -> ast.AST:
"""Converts any type annotation into its AST representation.
Handles nested generic types, unions, etc.
Args:
typ: The type annotation to convert.
cls: The class where the type annotation is used.
Returns:
The AST representation of the type annotation.
@ -390,6 +394,16 @@ def type_to_ast(typ) -> ast.AST:
# Handle plain types (int, str, custom classes, etc.)
if origin is None:
if hasattr(typ, "__name__"):
if typ.__module__.startswith("reflex."):
typ_parts = typ.__module__.split(".")
cls_parts = cls.__module__.split(".")
zipped = list(zip(typ_parts, cls_parts, strict=False))
if all(a == b for a, b in zipped) and len(typ_parts) == len(cls_parts):
return ast.Name(id=typ.__name__)
return ast.Name(id=typ.__module__ + "." + typ.__name__)
return ast.Name(id=typ.__name__)
elif hasattr(typ, "_name"):
return ast.Name(id=typ._name)
@ -406,7 +420,7 @@ def type_to_ast(typ) -> ast.AST:
return ast.Name(id=base_name)
# Convert all type arguments recursively
arg_nodes = [type_to_ast(arg) for arg in args]
arg_nodes = [type_to_ast(arg, cls) for arg in args]
# Special case for single-argument types (like List[T] or Optional[T])
if len(arg_nodes) == 1:
@ -487,7 +501,7 @@ def _generate_component_create_functiondef(
]
# Convert each argument type to its AST representation
type_args = [type_to_ast(arg) for arg in arguments_without_var]
type_args = [type_to_ast(arg, cls=clz) 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)