diff --git a/reflex/app.py b/reflex/app.py index eb453bd0b..6a17a56b6 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -947,12 +947,12 @@ class App(MiddlewareMixin, LifespanMixin): is not None ): executor = concurrent.futures.ProcessPoolExecutor( - max_workers=number_of_processes, + max_workers=number_of_processes or None, mp_context=multiprocessing.get_context("fork"), ) else: executor = concurrent.futures.ThreadPoolExecutor( - max_workers=environment.REFLEX_COMPILE_THREADS.get() + max_workers=environment.REFLEX_COMPILE_THREADS.get() or None ) for route, component in zip(self.pages, page_components): diff --git a/reflex/assets.py b/reflex/assets.py index 8a50664b6..a9aa7a6a9 100644 --- a/reflex/assets.py +++ b/reflex/assets.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Optional from reflex import constants -from reflex.utils.exec import is_backend_only +from reflex.config import EnvironmentVariables def asset( @@ -52,7 +52,7 @@ def asset( The relative URL to the asset. """ assets = constants.Dirs.APP_ASSETS - backend_only = is_backend_only() + backend_only = EnvironmentVariables.REFLEX_BACKEND_ONLY.get() # Local asset handling if not shared: diff --git a/reflex/components/radix/themes/base.py b/reflex/components/radix/themes/base.py index 9a8a2d7c0..e90d41a5a 100644 --- a/reflex/components/radix/themes/base.py +++ b/reflex/components/radix/themes/base.py @@ -266,6 +266,7 @@ class Theme(RadixThemesComponent): _js_expr="{...theme.styles.global[':root'], ...theme.styles.global.body}" ), ) + tag.remove_props("appearance") return tag diff --git a/reflex/state.py b/reflex/state.py index 09e76159c..c73b1ac46 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -97,6 +97,7 @@ from reflex.utils.exceptions import ( ReflexRuntimeError, SetUndefinedStateVarError, StateSchemaMismatchError, + StateSerializationError, StateTooLargeError, ) from reflex.utils.exec import is_testing_env @@ -2193,8 +2194,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): Returns: The serialized state. + + Raises: + StateSerializationError: If the state cannot be serialized. """ payload = b"" + error = "" try: payload = pickle.dumps((self._to_schema(), self)) except HANDLED_PICKLE_ERRORS as og_pickle_error: @@ -2214,8 +2219,13 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): except HANDLED_PICKLE_ERRORS as ex: error += f"Dill was also unable to pickle the state: {ex}" console.warn(error) + if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF: self._check_state_size(len(payload)) + + if not payload: + raise StateSerializationError(error) + return payload @classmethod diff --git a/reflex/utils/exceptions.py b/reflex/utils/exceptions.py index 714dc912c..a89c4d4aa 100644 --- a/reflex/utils/exceptions.py +++ b/reflex/utils/exceptions.py @@ -155,6 +155,10 @@ class StateTooLargeError(ReflexError): """Raised when the state is too large to be serialized.""" +class StateSerializationError(ReflexError): + """Raised when the state cannot be serialized.""" + + class SystemPackageMissingError(ReflexError): """Raised when a system package is missing.""" diff --git a/tests/integration/tests_playwright/test_appearance.py b/tests/integration/tests_playwright/test_appearance.py new file mode 100644 index 000000000..60aeeaa6b --- /dev/null +++ b/tests/integration/tests_playwright/test_appearance.py @@ -0,0 +1,218 @@ +from typing import Generator + +import pytest +from playwright.sync_api import Page, expect + +from reflex.testing import AppHarness + + +def DefaultLightModeApp(): + import reflex as rx + from reflex.style import color_mode + + app = rx.App(theme=rx.theme(appearance="light")) + + @app.add_page + def index(): + return rx.text(color_mode) + + +def DefaultDarkModeApp(): + import reflex as rx + from reflex.style import color_mode + + app = rx.App(theme=rx.theme(appearance="dark")) + + @app.add_page + def index(): + return rx.text(color_mode) + + +def DefaultSystemModeApp(): + import reflex as rx + from reflex.style import color_mode + + app = rx.App() + + @app.add_page + def index(): + return rx.text(color_mode) + + +def ColorToggleApp(): + import reflex as rx + from reflex.style import color_mode, resolved_color_mode, set_color_mode + + app = rx.App(theme=rx.theme(appearance="light")) + + @app.add_page + def index(): + return rx.box( + rx.segmented_control.root( + rx.segmented_control.item( + rx.icon(tag="monitor", size=20), + value="system", + ), + rx.segmented_control.item( + rx.icon(tag="sun", size=20), + value="light", + ), + rx.segmented_control.item( + rx.icon(tag="moon", size=20), + value="dark", + ), + on_change=set_color_mode, + variant="classic", + radius="large", + value=color_mode, + ), + rx.text(color_mode, id="current_color_mode"), + rx.text(resolved_color_mode, id="resolved_color_mode"), + rx.text(rx.color_mode_cond("LightMode", "DarkMode"), id="color_mode_cond"), + ) + + +@pytest.fixture() +def light_mode_app(tmp_path_factory) -> Generator[AppHarness, None, None]: + """Start DefaultLightMode app at tmp_path via AppHarness. + + Args: + tmp_path_factory: pytest tmp_path_factory fixture + + Yields: + running AppHarness instance + + """ + with AppHarness.create( + root=tmp_path_factory.mktemp("appearance_app"), + app_source=DefaultLightModeApp, # type: ignore + ) as harness: + assert harness.app_instance is not None, "app is not running" + yield harness + + +@pytest.fixture() +def dark_mode_app(tmp_path_factory) -> Generator[AppHarness, None, None]: + """Start DefaultDarkMode app at tmp_path via AppHarness. + + Args: + tmp_path_factory: pytest tmp_path_factory fixture + + Yields: + running AppHarness instance + + """ + with AppHarness.create( + root=tmp_path_factory.mktemp("appearance_app"), + app_source=DefaultDarkModeApp, # type: ignore + ) as harness: + assert harness.app_instance is not None, "app is not running" + yield harness + + +@pytest.fixture() +def system_mode_app(tmp_path_factory) -> Generator[AppHarness, None, None]: + """Start DefaultSystemMode app at tmp_path via AppHarness. + + Args: + tmp_path_factory: pytest tmp_path_factory fixture + + Yields: + running AppHarness instance + + """ + with AppHarness.create( + root=tmp_path_factory.mktemp("appearance_app"), + app_source=DefaultSystemModeApp, # type: ignore + ) as harness: + assert harness.app_instance is not None, "app is not running" + yield harness + + +@pytest.fixture() +def color_toggle_app(tmp_path_factory) -> Generator[AppHarness, None, None]: + """Start ColorToggle app at tmp_path via AppHarness. + + Args: + tmp_path_factory: pytest tmp_path_factory fixture + + Yields: + running AppHarness instance + + """ + with AppHarness.create( + root=tmp_path_factory.mktemp("appearance_app"), + app_source=ColorToggleApp, # type: ignore + ) as harness: + assert harness.app_instance is not None, "app is not running" + yield harness + + +def test_appearance_light_mode(light_mode_app: AppHarness, page: Page): + assert light_mode_app.frontend_url is not None + page.goto(light_mode_app.frontend_url) + + expect(page.get_by_text("light")).to_be_visible() + + +def test_appearance_dark_mode(dark_mode_app: AppHarness, page: Page): + assert dark_mode_app.frontend_url is not None + page.goto(dark_mode_app.frontend_url) + + expect(page.get_by_text("dark")).to_be_visible() + + +def test_appearance_system_mode(system_mode_app: AppHarness, page: Page): + assert system_mode_app.frontend_url is not None + page.goto(system_mode_app.frontend_url) + + expect(page.get_by_text("system")).to_be_visible() + + +def test_appearance_color_toggle(color_toggle_app: AppHarness, page: Page): + assert color_toggle_app.frontend_url is not None + page.goto(color_toggle_app.frontend_url) + + # Radio buttons locators. + radio_system = page.get_by_role("radio").nth(0) + radio_light = page.get_by_role("radio").nth(1) + radio_dark = page.get_by_role("radio").nth(2) + + # Text locators to check. + current_color_mode = page.locator("id=current_color_mode") + resolved_color_mode = page.locator("id=resolved_color_mode") + color_mode_cond = page.locator("id=color_mode_cond") + root_body = page.locator('div[data-is-root-theme="true"]') + + # Background colors. + dark_background = "rgb(17, 17, 19)" # value based on dark native appearance, can change depending on the browser + light_background = "rgb(255, 255, 255)" + + # check initial state + expect(current_color_mode).to_have_text("light") + expect(resolved_color_mode).to_have_text("light") + expect(color_mode_cond).to_have_text("LightMode") + expect(root_body).to_have_css("background-color", light_background) + + # click dark mode + radio_dark.click() + expect(current_color_mode).to_have_text("dark") + expect(resolved_color_mode).to_have_text("dark") + expect(color_mode_cond).to_have_text("DarkMode") + expect(root_body).to_have_css("background-color", dark_background) + + # click light mode + radio_light.click() + expect(current_color_mode).to_have_text("light") + expect(resolved_color_mode).to_have_text("light") + expect(color_mode_cond).to_have_text("LightMode") + expect(root_body).to_have_css("background-color", light_background) + page.reload() + expect(root_body).to_have_css("background-color", light_background) + + # click system mode + radio_system.click() + expect(current_color_mode).to_have_text("system") + expect(resolved_color_mode).to_have_text("light") + expect(color_mode_cond).to_have_text("LightMode") + expect(root_body).to_have_css("background-color", light_background) diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 5c062ccb2..790df1b7d 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -55,7 +55,11 @@ from reflex.state import ( ) from reflex.testing import chdir from reflex.utils import format, prerequisites, types -from reflex.utils.exceptions import ReflexRuntimeError, SetUndefinedStateVarError +from reflex.utils.exceptions import ( + ReflexRuntimeError, + SetUndefinedStateVarError, + StateSerializationError, +) from reflex.utils.format import json_dumps from reflex.vars.base import Var, computed_var from tests.units.states.mutation import MutableSQLAModel, MutableTestState @@ -3433,8 +3437,9 @@ def test_fallback_pickle(): # Some object, like generator, are still unpicklable with dill. state3 = DillState(_reflex_internal_init=True) # type: ignore state3._g = (i for i in range(10)) - pk3 = state3._serialize() - assert len(pk3) == 0 + + with pytest.raises(StateSerializationError): + _ = state3._serialize() def test_typed_state() -> None: