diff --git a/docker-example/production-app-platform/Dockerfile b/docker-example/production-app-platform/Dockerfile index b0f6c69fc..3dd9f1fed 100644 --- a/docker-example/production-app-platform/Dockerfile +++ b/docker-example/production-app-platform/Dockerfile @@ -25,7 +25,7 @@ # Stage 1: init FROM python:3.11 as init -ARG uv=/root/.cargo/bin/uv +ARG uv=/root/.local/bin/uv # Install `uv` for faster package boostrapping ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh diff --git a/docker-example/production-compose/Dockerfile b/docker-example/production-compose/Dockerfile index f73473df7..42345af40 100644 --- a/docker-example/production-compose/Dockerfile +++ b/docker-example/production-compose/Dockerfile @@ -4,7 +4,7 @@ # Stage 1: init FROM python:3.11 as init -ARG uv=/root/.cargo/bin/uv +ARG uv=/root/.local/bin/uv # Install `uv` for faster package boostrapping ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh diff --git a/poetry.lock b/poetry.lock index 9d80c8693..8199c975a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1350,8 +1350,8 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, {version = ">=1.22.4", markers = "python_version < \"3.11\""}, ] python-dateutil = ">=2.8.2" @@ -1669,8 +1669,8 @@ files = [ annotated-types = ">=0.6.0" pydantic-core = "2.23.4" typing-extensions = [ - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, {version = ">=4.6.1", markers = "python_version < \"3.13\""}, + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, ] [package.extras] @@ -3050,4 +3050,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "593a52e9f54e95b50074f1bc4b7cdbabe4fab325051c72b23219268c0c9aa3ba" +content-hash = "937f0cadb1a4566117dad8d0be6018ad1a8fe9aeb19c499d2a010d36ef391ee1" diff --git a/pyproject.toml b/pyproject.toml index 1e48daf1c..20bf81d92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "reflex" -version = "0.6.5dev1" +version = "0.6.6dev1" description = "Web apps in pure Python." license = "Apache-2.0" authors = [ @@ -49,7 +49,7 @@ wrapt = [ {version = ">=1.11.0,<2.0", python = "<3.11"}, ] packaging = ">=23.1,<25.0" -reflex-hosting-cli = ">=0.1.5,<2.0" +reflex-hosting-cli = ">=0.1.15,<2.0" charset-normalizer = ">=3.3.2,<4.0" wheel = ">=0.42.0,<1.0" build = ">=1.0.3,<2.0" diff --git a/reflex/__init__.py b/reflex/__init__.py index cfb971a99..3941542f2 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -298,6 +298,7 @@ _MAPPING: dict = { "components.moment": ["MomentDelta", "moment"], "config": ["Config", "DBConfig"], "constants": ["Env"], + "constants.colors": ["Color"], "event": [ "EventChain", "EventHandler", @@ -338,7 +339,7 @@ _MAPPING: dict = { ], "istate.wrappers": ["get_state"], "style": ["Style", "toggle_color_mode"], - "utils.imports": ["ImportVar"], + "utils.imports": ["ImportDict", "ImportVar"], "utils.serializers": ["serializer"], "vars": ["Var", "field", "Field"], } diff --git a/reflex/__init__.pyi b/reflex/__init__.pyi index 2d22fe497..30a498db4 100644 --- a/reflex/__init__.pyi +++ b/reflex/__init__.pyi @@ -152,6 +152,7 @@ from .components.suneditor import editor as editor from .config import Config as Config from .config import DBConfig as DBConfig from .constants import Env as Env +from .constants.colors import Color as Color from .event import EventChain as EventChain from .event import EventHandler as EventHandler from .event import background as background @@ -192,6 +193,7 @@ from .state import dynamic as dynamic from .state import var as var from .style import Style as Style from .style import toggle_color_mode as toggle_color_mode +from .utils.imports import ImportDict as ImportDict from .utils.imports import ImportVar as ImportVar from .utils.serializers import serializer as serializer from .vars import Field as Field diff --git a/reflex/base.py b/reflex/base.py index e661d3ade..692f123a8 100644 --- a/reflex/base.py +++ b/reflex/base.py @@ -130,8 +130,8 @@ class Base(BaseModel): # pyright: ignore [reportUnboundVariable] Returns: The value of the field. """ - if isinstance(key, str) and key in self.__fields__: + if isinstance(key, str): # Seems like this function signature was wrong all along? # If the user wants a field that we know of, get it and pass it off to _get_value - key = getattr(self, key) + return getattr(self, key, key) return key diff --git a/reflex/components/base/error_boundary.py b/reflex/components/base/error_boundary.py index 83becc034..f328773c2 100644 --- a/reflex/components/base/error_boundary.py +++ b/reflex/components/base/error_boundary.py @@ -2,14 +2,15 @@ from __future__ import annotations -from typing import Dict, List, Tuple +from typing import Dict, Tuple -from reflex.compiler.compiler import _compile_component from reflex.components.component import Component -from reflex.components.el import div, p -from reflex.event import EventHandler +from reflex.components.datadisplay.logo import svg_logo +from reflex.components.el import a, button, details, div, h2, hr, p, pre, summary +from reflex.event import EventHandler, set_clipboard from reflex.state import FrontendEventExceptionState from reflex.vars.base import Var +from reflex.vars.function import ArgsFunctionOperation def on_error_spec( @@ -40,38 +41,7 @@ class ErrorBoundary(Component): on_error: EventHandler[on_error_spec] # Rendered instead of the children when an error is caught. - Fallback_component: Var[Component] = Var(_js_expr="Fallback")._replace( - _var_type=Component - ) - - def add_custom_code(self) -> List[str]: - """Add custom Javascript code into the page that contains this component. - - Custom code is inserted at module level, after any imports. - - Returns: - The custom code to add. - """ - fallback_container = div( - p("Ooops...Unknown Reflex error has occured:"), - p( - Var(_js_expr="error.message"), - color="red", - ), - p("Please contact the support."), - ) - - compiled_fallback = _compile_component(fallback_container) - - return [ - f""" - function Fallback({{ error, resetErrorBoundary }}) {{ - return ( - {compiled_fallback} - ); - }} - """ - ] + fallback_render: Var[Component] @classmethod def create(cls, *children, **props): @@ -86,6 +56,99 @@ class ErrorBoundary(Component): """ if "on_error" not in props: props["on_error"] = FrontendEventExceptionState.handle_frontend_exception + if "fallback_render" not in props: + props["fallback_render"] = ArgsFunctionOperation.create( + ("event_args",), + Var.create( + div( + div( + div( + h2( + "An error occurred while rendering this page.", + font_size="1.25rem", + font_weight="bold", + ), + p( + "This is an error with the application itself.", + opacity="0.75", + ), + details( + summary("Error message", padding="0.5rem"), + div( + div( + pre( + Var( + _js_expr="event_args.error.stack", + ), + ), + padding="0.5rem", + width="fit-content", + ), + width="100%", + max_height="50vh", + overflow="auto", + background="#000", + color="#fff", + border_radius="0.25rem", + ), + button( + "Copy", + on_click=set_clipboard( + Var(_js_expr="event_args.error.stack"), + ), + padding="0.35rem 0.75rem", + margin="0.5rem", + background="#fff", + color="#000", + border="1px solid #000", + border_radius="0.25rem", + font_weight="bold", + ), + ), + display="flex", + flex_direction="column", + gap="1rem", + max_width="50ch", + border="1px solid #888888", + border_radius="0.25rem", + padding="1rem", + ), + hr( + border_color="currentColor", + opacity="0.25", + ), + a( + div( + "Built with ", + svg_logo("currentColor"), + display="flex", + align_items="baseline", + justify_content="center", + font_family="monospace", + gap="0.5rem", + ), + href="https://reflex.dev", + ), + display="flex", + flex_direction="column", + gap="1rem", + ), + height="100%", + width="100%", + position="absolute", + display="flex", + align_items="center", + justify_content="center", + ) + ), + _var_type=Component, + ) + else: + props["fallback_render"] = ArgsFunctionOperation.create( + ("event_args",), + props["fallback_render"], + _var_type=Component, + ) return super().create(*children, **props) diff --git a/reflex/components/base/error_boundary.pyi b/reflex/components/base/error_boundary.pyi index dfe34fc9e..2e01c7da0 100644 --- a/reflex/components/base/error_boundary.pyi +++ b/reflex/components/base/error_boundary.pyi @@ -3,7 +3,7 @@ # ------------------- DO NOT EDIT ---------------------- # This file was generated by `reflex/utils/pyi_generator.py`! # ------------------------------------------------------ -from typing import Any, Dict, List, Optional, Tuple, Union, overload +from typing import Any, Dict, Optional, Tuple, Union, overload from reflex.components.component import Component from reflex.event import BASE_STATE, EventType @@ -15,13 +15,12 @@ def on_error_spec( ) -> Tuple[Var[str], Var[str]]: ... class ErrorBoundary(Component): - def add_custom_code(self) -> List[str]: ... @overload @classmethod def create( # type: ignore cls, *children, - Fallback_component: Optional[Union[Component, Var[Component]]] = None, + fallback_render: Optional[Union[Component, Var[Component]]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -57,7 +56,7 @@ class ErrorBoundary(Component): Args: *children: The children of the component. on_error: Fired when the boundary catches an error. - Fallback_component: Rendered instead of the children when an error is caught. + fallback_render: Rendered instead of the children when an error is caught. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/core/cond.py b/reflex/components/core/cond.py index e0c47f0fe..5b6ee2a7f 100644 --- a/reflex/components/core/cond.py +++ b/reflex/components/core/cond.py @@ -171,6 +171,14 @@ def cond(condition: Any, c1: Any, c2: Any = None) -> Component | Var: ) +@overload +def color_mode_cond(light: Component, dark: Component | None = None) -> Component: ... # type: ignore + + +@overload +def color_mode_cond(light: Any, dark: Any = None) -> Var: ... + + def color_mode_cond(light: Any, dark: Any = None) -> Var | Component: """Create a component or Prop based on color_mode. diff --git a/reflex/components/datadisplay/logo.py b/reflex/components/datadisplay/logo.py index beb9b9d10..d960b8cee 100644 --- a/reflex/components/datadisplay/logo.py +++ b/reflex/components/datadisplay/logo.py @@ -1,22 +1,23 @@ """A Reflex logo component.""" +from typing import Union + import reflex as rx -def logo(**props): - """A Reflex logo. +def svg_logo(color: Union[str, rx.Var[str]] = rx.color_mode_cond("#110F1F", "white")): + """A Reflex logo SVG. Args: - **props: The props to pass to the component. + color: The color of the logo. Returns: - The logo component. + The Reflex logo SVG. """ def logo_path(d): return rx.el.svg.path( d=d, - fill=rx.color_mode_cond("#110F1F", "white"), ) paths = [ @@ -28,18 +29,30 @@ def logo(**props): "M47.04 4.8799V0.399902H49.28V4.8799H47.04ZM53.76 4.8799V0.399902H56V4.8799H53.76ZM49.28 7.1199V4.8799H53.76V7.1199H49.28ZM47.04 11.5999V7.1199H49.28V11.5999H47.04ZM53.76 11.5999V7.1199H56V11.5999H53.76Z", ] + return rx.el.svg( + *[logo_path(d) for d in paths], + width="56", + height="12", + viewBox="0 0 56 12", + fill=color, + xmlns="http://www.w3.org/2000/svg", + ) + + +def logo(**props): + """A Reflex logo. + + Args: + **props: The props to pass to the component. + + Returns: + The logo component. + """ return rx.center( rx.link( rx.hstack( "Built with ", - rx.el.svg( - *[logo_path(d) for d in paths], - width="56", - height="12", - viewBox="0 0 56 12", - fill="none", - xmlns="http://www.w3.org/2000/svg", - ), + svg_logo(), text_align="center", align="center", padding="1em", diff --git a/reflex/components/radix/themes/base.py b/reflex/components/radix/themes/base.py index 7a0fc809f..65a9ae835 100644 --- a/reflex/components/radix/themes/base.py +++ b/reflex/components/radix/themes/base.py @@ -112,6 +112,9 @@ class RadixThemesComponent(Component): library = "@radix-ui/themes@^3.0.0" + # Temporary pin < 3.1.5 until radix-ui/themes#627 is resolved. + library = library + " && <3.1.5" + # "Fake" prop color_scheme is used to avoid shadowing CSS prop "color". _rename_props: Dict[str, str] = {"colorScheme": "color"} diff --git a/reflex/event.py b/reflex/event.py index a64d4d6c1..85a2541a5 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -899,7 +899,7 @@ def remove_session_storage(key: str) -> EventSpec: ) -def set_clipboard(content: str) -> EventSpec: +def set_clipboard(content: Union[str, Var[str]]) -> EventSpec: """Set the text in content in the clipboard. Args: diff --git a/reflex/state.py b/reflex/state.py index 95f2d7ac8..a55b4a82c 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -46,6 +46,7 @@ from reflex import event from reflex.config import get_config from reflex.istate.data import RouterData from reflex.istate.storage import ClientStorageBase +from reflex.model import Model from reflex.vars.base import ( ComputedVar, DynamicRouteVar, @@ -1791,15 +1792,20 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): if value is None: continue hinted_args = value_inside_optional(hinted_args) - if ( - isinstance(value, dict) - and inspect.isclass(hinted_args) - and ( - dataclasses.is_dataclass(hinted_args) - or issubclass(hinted_args, Base) - ) - ): - payload[arg] = hinted_args(**value) + if isinstance(value, dict) and inspect.isclass(hinted_args): + if issubclass(hinted_args, Model): + # Remove non-fields from the payload + payload[arg] = hinted_args( + **{ + key: value + for key, value in value.items() + if key in hinted_args.__fields__ + } + ) + elif dataclasses.is_dataclass(hinted_args) or issubclass( + hinted_args, Base + ): + payload[arg] = hinted_args(**value) if isinstance(value, list) and (hinted_args is set or hinted_args is Set): payload[arg] = set(value) if isinstance(value, list) and ( @@ -1942,7 +1948,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): ) subdelta: Dict[str, Any] = { - prop: self.get_value(getattr(self, prop)) + prop: self.get_value(prop) for prop in delta_vars if not types.is_backend_base_variable(prop, type(self)) } @@ -2034,9 +2040,10 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): Returns: The value of the field. """ - if isinstance(key, MutableProxy): - return super().get_value(key.__wrapped__) - return super().get_value(key) + value = super().get_value(key) + if isinstance(value, MutableProxy): + return value.__wrapped__ + return value def dict( self, include_computed: bool = True, initial: bool = False, **kwargs @@ -2058,8 +2065,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): self._mark_dirty() base_vars = { - prop_name: self.get_value(getattr(self, prop_name)) - for prop_name in self.base_vars + prop_name: self.get_value(prop_name) for prop_name in self.base_vars } if initial and include_computed: computed_vars = { @@ -2068,7 +2074,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): cv._initial_value if is_computed_var(cv) and not isinstance(cv._initial_value, types.Unset) - else self.get_value(getattr(self, prop_name)) + else self.get_value(prop_name) ) for prop_name, cv in self.computed_vars.items() if not cv._backend @@ -2076,7 +2082,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): elif include_computed: computed_vars = { # Include the computed vars. - prop_name: self.get_value(getattr(self, prop_name)) + prop_name: self.get_value(prop_name) for prop_name, cv in self.computed_vars.items() if not cv._backend } diff --git a/tests/units/components/core/test_banner.py b/tests/units/components/core/test_banner.py index 7add913ea..fe6de5eae 100644 --- a/tests/units/components/core/test_banner.py +++ b/tests/units/components/core/test_banner.py @@ -4,6 +4,7 @@ from reflex.components.core.banner import ( ConnectionPulser, WebsocketTargetURL, ) +from reflex.components.radix.themes.base import RadixThemesComponent from reflex.components.radix.themes.typography.text import Text @@ -24,7 +25,7 @@ def test_connection_banner(): "react", "$/utils/context", "$/utils/state", - "@radix-ui/themes@^3.0.0", + RadixThemesComponent().library or "", "$/env.json", ) ) @@ -42,7 +43,7 @@ def test_connection_modal(): "react", "$/utils/context", "$/utils/state", - "@radix-ui/themes@^3.0.0", + RadixThemesComponent().library or "", "$/env.json", ) ) diff --git a/tests/units/test_state.py b/tests/units/test_state.py index ce339f413..b27bf42c8 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -3412,3 +3412,33 @@ def test_typed_state() -> None: field: rx.Field[str] = rx.field("") _ = TypedState(field="str") + + +def test_get_value(): + class GetValueState(rx.State): + foo: str = "FOO" + bar: str = "BAR" + + state = GetValueState() + + assert state.dict() == { + state.get_full_name(): { + "foo": "FOO", + "bar": "BAR", + } + } + assert state.get_delta() == {} + + state.bar = "foo" + + assert state.dict() == { + state.get_full_name(): { + "foo": "FOO", + "bar": "foo", + } + } + assert state.get_delta() == { + state.get_full_name(): { + "bar": "foo", + } + }