From 13591793de2a5b870c7181c99b132fbc463ffc3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Tue, 22 Oct 2024 12:17:31 -0700 Subject: [PATCH 1/3] move client storage classes to their own file (#4216) * move client storage classes to their own file * fix 3.9 annotations --- reflex/__init__.py | 8 ++- reflex/__init__.pyi | 6 +- reflex/compiler/utils.py | 3 +- reflex/istate/storage.py | 144 +++++++++++++++++++++++++++++++++++++++ reflex/state.py | 140 +------------------------------------ 5 files changed, 157 insertions(+), 144 deletions(-) create mode 100644 reflex/istate/storage.py diff --git a/reflex/__init__.py b/reflex/__init__.py index ad51d2cf4..979e5499a 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -320,13 +320,15 @@ _MAPPING: dict = { "upload_files", "window_alert", ], + "istate.storage": [ + "Cookie", + "LocalStorage", + "SessionStorage", + ], "middleware": ["middleware", "Middleware"], "model": ["session", "Model"], "state": [ "var", - "Cookie", - "LocalStorage", - "SessionStorage", "ComponentState", "State", ], diff --git a/reflex/__init__.pyi b/reflex/__init__.pyi index d928778d8..aeebaadf8 100644 --- a/reflex/__init__.pyi +++ b/reflex/__init__.pyi @@ -174,15 +174,15 @@ from .event import stop_propagation as stop_propagation from .event import upload_files as upload_files from .event import window_alert as window_alert from .experimental import _x as _x +from .istate.storage import Cookie as Cookie +from .istate.storage import LocalStorage as LocalStorage +from .istate.storage import SessionStorage as SessionStorage from .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 ComponentState as ComponentState -from .state import Cookie as Cookie -from .state import LocalStorage as LocalStorage -from .state import SessionStorage as SessionStorage from .state import State as State from .state import var as var from .style import Style as Style diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index 6f4fa2d1b..40640faa6 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -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 diff --git a/reflex/istate/storage.py b/reflex/istate/storage.py new file mode 100644 index 000000000..85e21ffa7 --- /dev/null +++ b/reflex/istate/storage.py @@ -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 diff --git a/reflex/state.py b/reflex/state.py index 3422d1ba7..dcd576b3a 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -42,6 +42,9 @@ from typing_extensions import Self from reflex import event 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, @@ -3349,143 +3352,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.""" From 227fb2cb75176147356435de487097c4e8e574f5 Mon Sep 17 00:00:00 2001 From: Simon Young <40179067+Kastier1@users.noreply.github.com> Date: Tue, 22 Oct 2024 12:37:17 -0700 Subject: [PATCH 2/3] HOS-93: add support for .env file (#4219) * HOS-93: add support for .env file * HOS-93: remove stray print * HOS-93: poetry lock * HOS-93: update comment --------- Co-authored-by: simon --- poetry.lock | 36 +++++++++++++++++++++++++----------- pyproject.toml | 1 + reflex/config.py | 10 ++++++++++ 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/poetry.lock b/poetry.lock index bbd2735ac..7161e6af7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index 93f3c5d50..3fe6041f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/reflex/config.py b/reflex/config.py index 00f33b653..eb319c5dd 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -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(): From a65fc2e90b27af682181a47e8e2dc73820d1732a Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 22 Oct 2024 13:09:14 -0700 Subject: [PATCH 3/3] Add on progress typing to react player (#4211) * add on progress typing to react player * fix pyi file * have the pyi here as well * more pyi changes * fix imports * run pyi * for some reason it want event on three lines no clue why * simplify case for when type is in the same module * run pyi * remove last missing type for datadisplay --- reflex/components/datadisplay/dataeditor.py | 27 ++++++------------- reflex/components/datadisplay/dataeditor.pyi | 14 +++++----- .../radix/themes/components/tooltip.pyi | 4 ++- reflex/components/react_player/__init__.py | 1 + reflex/components/react_player/audio.pyi | 5 +++- .../components/react_player/react_player.py | 13 ++++++++- .../components/react_player/react_player.pyi | 10 ++++++- reflex/components/react_player/video.pyi | 5 +++- reflex/utils/pyi_generator.py | 22 ++++++++++++--- 9 files changed, 65 insertions(+), 36 deletions(-) diff --git a/reflex/components/datadisplay/dataeditor.py b/reflex/components/datadisplay/dataeditor.py index a192c7a45..b9bd4cebf 100644 --- a/reflex/components/datadisplay/dataeditor.py +++ b/reflex/components/datadisplay/dataeditor.py @@ -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] diff --git a/reflex/components/datadisplay/dataeditor.pyi b/reflex/components/datadisplay/dataeditor.pyi index aadd9666e..6402aaf42 100644 --- a/reflex/components/datadisplay/dataeditor.pyi +++ b/reflex/components/datadisplay/dataeditor.pyi @@ -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, diff --git a/reflex/components/radix/themes/components/tooltip.pyi b/reflex/components/radix/themes/components/tooltip.pyi index ac2a36368..e78dd926d 100644 --- a/reflex/components/radix/themes/components/tooltip.pyi +++ b/reflex/components/radix/themes/components/tooltip.pyi @@ -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 diff --git a/reflex/components/react_player/__init__.py b/reflex/components/react_player/__init__.py index 8c4a4486f..3f807b1a0 100644 --- a/reflex/components/react_player/__init__.py +++ b/reflex/components/react_player/__init__.py @@ -1,5 +1,6 @@ """React Player component for audio and video.""" +from . import react_player from .audio import Audio from .video import Video diff --git a/reflex/components/react_player/audio.pyi b/reflex/components/react_player/audio.pyi index 2556c8e83..1841829af 100644 --- a/reflex/components/react_player/audio.pyi +++ b/reflex/components/react_player/audio.pyi @@ -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, diff --git a/reflex/components/react_player/react_player.py b/reflex/components/react_player/react_player.py index 7ad45b093..b2c58b754 100644 --- a/reflex/components/react_player/react_player.py +++ b/reflex/components/react_player/react_player.py @@ -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)] diff --git a/reflex/components/react_player/react_player.pyi b/reflex/components/react_player/react_player.pyi index 9a445c294..e4027cf40 100644 --- a/reflex/components/react_player/react_player.pyi +++ b/reflex/components/react_player/react_player.pyi @@ -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, diff --git a/reflex/components/react_player/video.pyi b/reflex/components/react_player/video.pyi index d46e2617d..a05e3747b 100644 --- a/reflex/components/react_player/video.pyi +++ b/reflex/components/react_player/video.pyi @@ -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, diff --git a/reflex/utils/pyi_generator.py b/reflex/utils/pyi_generator.py index 026a53bca..1fc17341b 100644 --- a/reflex/utils/pyi_generator.py +++ b/reflex/utils/pyi_generator.py @@ -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)