From 673c488dc0613a5de93c82f6aa99d0b2b31449c5 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 4 May 2024 12:53:16 +0200 Subject: [PATCH 01/31] wip minified state names --- reflex/constants/compiler.py | 24 +++++++++-- reflex/state.py | 77 ++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py index b7ffef1613..da6da3c4ce 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -1,6 +1,7 @@ """Compiler variables.""" import enum +import os from enum import Enum from types import SimpleNamespace @@ -61,18 +62,35 @@ class CompileVars(SimpleNamespace): CONNECT_ERROR = "connectErrors" # The name of the function for converting a dict to an event. TO_EVENT = "Event" + # The env var to toggle minification of states. + ENV_MINIFY_STATES = "REFLEX_MINIFY_STATES" + # Whether to minify states. + MINIFY_STATES = os.environ.get(ENV_MINIFY_STATES, False) # The name of the internal on_load event. - ON_LOAD_INTERNAL = "reflex___state____on_load_internal_state.on_load_internal" + ON_LOAD_INTERNAL = ( + "l" + if MINIFY_STATES + else "reflex___state____on_load_internal_state.on_load_internal" + ) # The name of the internal event to update generic state vars. UPDATE_VARS_INTERNAL = ( - "reflex___state____update_vars_internal_state.update_vars_internal" + "u" + if MINIFY_STATES + else ("reflex___state____update_vars_internal_state.update_vars_internal") ) # The name of the frontend event exception state - FRONTEND_EXCEPTION_STATE = "reflex___state____frontend_event_exception_state" + FRONTEND_EXCEPTION_STATE = ( + "e" if MINIFY_STATES else "reflex___state____frontend_event_exception_state" + ) # The full name of the frontend exception state FRONTEND_EXCEPTION_STATE_FULL = ( f"reflex___state____state.{FRONTEND_EXCEPTION_STATE}" ) + INTERNAL_STATE_NAMES = { + ON_LOAD_INTERNAL, + UPDATE_VARS_INTERNAL, + FRONTEND_EXCEPTION_STATE_FULL, + } class PageNames(SimpleNamespace): diff --git a/reflex/state.py b/reflex/state.py index a53df7b6f9..783d007f5d 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -285,6 +285,61 @@ def get_var_for_field(cls: Type[BaseState], f: ModelField): ) +# Keep track of all state instances to calculate minified state names +state_count = 0 + +all_state_names: Set[str] = set() + + +def next_minified_state_name() -> str: + """Get the next minified state name. + + Returns: + The next minified state name. + + Raises: + RuntimeError: If the minified state name already exists. + """ + global state_count + global all_state_names + num = state_count + + # All possible chars for minified state name + chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_" + base = len(chars) + state_name = "" + + if num == 0: + state_name = chars[0] + + while num > 0: + state_name = chars[num % base] + state_name + num = num // base + + state_count += 1 + + if state_name in all_state_names: + raise RuntimeError(f"Minified state name {state_name} already exists") + all_state_names.add(state_name) + + return state_name + + +def generate_state_name() -> str: + """Generate a minified state name. + + Returns: + The minified state name. + + Raises: + ValueError: If no more minified state names are available + """ + while name := next_minified_state_name(): + if name not in constants.CompileVars.INTERNAL_STATE_NAMES: + return name + raise ValueError("No more minified state names available") + + class BaseState(Base, ABC, extra=pydantic.Extra.allow): """The state of the app.""" @@ -354,6 +409,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): # A special event handler for setting base vars. setvar: ClassVar[EventHandler] + # Minified state name + _state_name: ClassVar[Optional[str]] = None + def __init__( self, parent_state: BaseState | None = None, @@ -459,6 +517,10 @@ def __init_subclass__(cls, mixin: bool = False, **kwargs): if "" in cls.__qualname__: cls._handle_local_def() + # Generate a minified state name by converting state count to string + if not cls._state_name: + cls._state_name = generate_state_name() + # Validate the module name. cls._validate_module_name() @@ -874,7 +936,16 @@ def get_name(cls) -> str: Returns: The name of the state. + + Raises: + RuntimeError: If the state name is not set. """ + if constants.CompileVars.MINIFY_STATES: + if not cls._state_name: + raise RuntimeError( + "State name minification is enabled, but state name is not set." + ) + return cls._state_name module = cls.__module__.replace(".", "___") return format.to_snake_case(f"{module}___{cls.__name__}") @@ -2218,6 +2289,8 @@ def wrapper() -> Component: class FrontendEventExceptionState(State): """Substate for handling frontend exceptions.""" + _state_name: Optional[str] = constants.CompileVars.FRONTEND_EXCEPTION_STATE + @event def handle_frontend_exception(self, stack: str, component_stack: str) -> None: """Handle frontend exceptions. @@ -2237,6 +2310,8 @@ def handle_frontend_exception(self, stack: str, component_stack: str) -> None: class UpdateVarsInternalState(State): """Substate for handling internal state var updates.""" + _state_name: Optional[str] = constants.CompileVars.UPDATE_VARS_INTERNAL + async def update_vars_internal(self, vars: dict[str, Any]) -> None: """Apply updates to fully qualified state vars. @@ -2262,6 +2337,8 @@ class OnLoadInternalState(State): This is a separate substate to avoid deserializing the entire state tree for every page navigation. """ + _state_name: Optional[str] = constants.CompileVars.ON_LOAD_INTERNAL + def on_load_internal(self) -> list[Event | EventSpec] | None: """Queue on_load handlers for the current page. From 0c406f4bacb57321b759da6b100e5c08a69e46b0 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 24 Jul 2024 02:51:06 +0200 Subject: [PATCH 02/31] cleanup --- reflex/state.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 783d007f5d..4b70913e93 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -335,8 +335,9 @@ def generate_state_name() -> str: ValueError: If no more minified state names are available """ while name := next_minified_state_name(): - if name not in constants.CompileVars.INTERNAL_STATE_NAMES: - return name + if name in constants.CompileVars.INTERNAL_STATE_NAMES: + continue + return name raise ValueError("No more minified state names available") From 1b0577a7e1f56a3e504523c41ef55c5d114627c8 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 24 Jul 2024 03:00:13 +0200 Subject: [PATCH 03/31] all _state_names should be classvars --- reflex/state.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 4b70913e93..1368743aad 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -2290,7 +2290,9 @@ def wrapper() -> Component: class FrontendEventExceptionState(State): """Substate for handling frontend exceptions.""" - _state_name: Optional[str] = constants.CompileVars.FRONTEND_EXCEPTION_STATE + _state_name: ClassVar[Optional[str]] = ( + constants.CompileVars.FRONTEND_EXCEPTION_STATE + ) @event def handle_frontend_exception(self, stack: str, component_stack: str) -> None: @@ -2311,7 +2313,7 @@ def handle_frontend_exception(self, stack: str, component_stack: str) -> None: class UpdateVarsInternalState(State): """Substate for handling internal state var updates.""" - _state_name: Optional[str] = constants.CompileVars.UPDATE_VARS_INTERNAL + _state_name: ClassVar[Optional[str]] = constants.CompileVars.UPDATE_VARS_INTERNAL async def update_vars_internal(self, vars: dict[str, Any]) -> None: """Apply updates to fully qualified state vars. @@ -2338,7 +2340,7 @@ class OnLoadInternalState(State): This is a separate substate to avoid deserializing the entire state tree for every page navigation. """ - _state_name: Optional[str] = constants.CompileVars.ON_LOAD_INTERNAL + _state_name: ClassVar[Optional[str]] = constants.CompileVars.ON_LOAD_INTERNAL def on_load_internal(self) -> list[Event | EventSpec] | None: """Queue on_load handlers for the current page. From bae98e80ed3e6ff5e2bbcccc715c2c373ee9a5ac Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Thu, 25 Jul 2024 22:22:44 +0200 Subject: [PATCH 04/31] fix hardcoded event handlers and states --- reflex/constants/compiler.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py index da6da3c4ce..5a6240b948 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -66,18 +66,18 @@ class CompileVars(SimpleNamespace): ENV_MINIFY_STATES = "REFLEX_MINIFY_STATES" # Whether to minify states. MINIFY_STATES = os.environ.get(ENV_MINIFY_STATES, False) + # The name of the OnLoadInternal state. + ON_LOAD_INTERNAL_STATE = ( + "l" if MINIFY_STATES else "reflex___state____on_load_internal_state" + ) # The name of the internal on_load event. - ON_LOAD_INTERNAL = ( - "l" - if MINIFY_STATES - else "reflex___state____on_load_internal_state.on_load_internal" + ON_LOAD_INTERNAL = f"{ON_LOAD_INTERNAL_STATE}.on_load_internal" + # The name of the UpdateVarsInternal state. + UPDATE_VARS_INTERNAL_STATE = ( + "u" if MINIFY_STATES else "reflex___state____update_vars_internal_state" ) # The name of the internal event to update generic state vars. - UPDATE_VARS_INTERNAL = ( - "u" - if MINIFY_STATES - else ("reflex___state____update_vars_internal_state.update_vars_internal") - ) + UPDATE_VARS_INTERNAL = f"{UPDATE_VARS_INTERNAL_STATE}.update_vars_internal" # The name of the frontend event exception state FRONTEND_EXCEPTION_STATE = ( "e" if MINIFY_STATES else "reflex___state____frontend_event_exception_state" @@ -87,9 +87,9 @@ class CompileVars(SimpleNamespace): f"reflex___state____state.{FRONTEND_EXCEPTION_STATE}" ) INTERNAL_STATE_NAMES = { - ON_LOAD_INTERNAL, - UPDATE_VARS_INTERNAL, - FRONTEND_EXCEPTION_STATE_FULL, + ON_LOAD_INTERNAL_STATE, + UPDATE_VARS_INTERNAL_STATE, + FRONTEND_EXCEPTION_STATE, } From 8fc8fd9ec72cb8ba2dc1a954799ab9c435d96786 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Thu, 25 Jul 2024 22:39:34 +0200 Subject: [PATCH 05/31] fix state name init for substates, thanks @masenf --- reflex/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/state.py b/reflex/state.py index 1368743aad..92ce351b5e 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -519,7 +519,7 @@ def __init_subclass__(cls, mixin: bool = False, **kwargs): cls._handle_local_def() # Generate a minified state name by converting state count to string - if not cls._state_name: + if not cls._state_name or cls._state_name in all_state_names: cls._state_name = generate_state_name() # Validate the module name. From 51aef9fd2246d9888f17d366fed314a944bfcc6a Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Thu, 25 Jul 2024 23:02:29 +0200 Subject: [PATCH 06/31] enable minified state names by default in prod --- reflex/constants/compiler.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py index 5a6240b948..ed175fda61 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -6,7 +6,7 @@ from types import SimpleNamespace from reflex.base import Base -from reflex.constants import Dirs +from reflex.constants import ENV_MODE_ENV_VAR, Dirs, Env from reflex.utils.imports import ImportVar # The prefix used to create setters for state vars. @@ -15,6 +15,9 @@ # The file used to specify no compilation. NOCOMPILE_FILE = "nocompile" +# The env var to toggle minification of states. +ENV_MINIFY_STATES = "REFLEX_MINIFY_STATES" + class Ext(SimpleNamespace): """Extension used in Reflex.""" @@ -31,6 +34,20 @@ class Ext(SimpleNamespace): EXE = ".exe" +def minify_states() -> bool: + """Whether to minify states. + + Returns: + True if states should be minified. + """ + env = os.environ.get(ENV_MINIFY_STATES, None) + if env is not None: + return env.lower() == "true" + + # minify states in prod by default + return os.environ.get(ENV_MODE_ENV_VAR, "") == Env.PROD.value + + class CompileVars(SimpleNamespace): """The variables used during compilation.""" @@ -62,10 +79,10 @@ class CompileVars(SimpleNamespace): CONNECT_ERROR = "connectErrors" # The name of the function for converting a dict to an event. TO_EVENT = "Event" - # The env var to toggle minification of states. - ENV_MINIFY_STATES = "REFLEX_MINIFY_STATES" + # Whether to minify states. - MINIFY_STATES = os.environ.get(ENV_MINIFY_STATES, False) + MINIFY_STATES = minify_states() + # The name of the OnLoadInternal state. ON_LOAD_INTERNAL_STATE = ( "l" if MINIFY_STATES else "reflex___state____on_load_internal_state" From 7bf15b4f44eb3f6c83f42b825393610d6d1c4184 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Thu, 25 Jul 2024 23:14:07 +0200 Subject: [PATCH 07/31] add simple test that minified state names are unique --- reflex/state.py | 2 +- tests/test_minify_state.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 tests/test_minify_state.py diff --git a/reflex/state.py b/reflex/state.py index 92ce351b5e..d567ed40cf 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -286,7 +286,7 @@ def get_var_for_field(cls: Type[BaseState], f: ModelField): # Keep track of all state instances to calculate minified state names -state_count = 0 +state_count: int = 0 all_state_names: Set[str] = set() diff --git a/tests/test_minify_state.py b/tests/test_minify_state.py new file mode 100644 index 0000000000..e4dea43ef1 --- /dev/null +++ b/tests/test_minify_state.py @@ -0,0 +1,17 @@ +from typing import Set + +from reflex.state import all_state_names, next_minified_state_name + + +def test_next_minified_state_name(): + """Test that the next_minified_state_name function returns unique state names.""" + current_state_count = len(all_state_names) + state_names: Set[str] = set() + gen: int = 10000 + for _ in range(gen): + state_name = next_minified_state_name() + assert state_name not in state_names + state_names.add(state_name) + assert len(state_names) == gen + + assert len(all_state_names) == current_state_count + gen From e9cedd2a92ea63ffa5b3ff9ce5d4e26664ab6112 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 27 Jul 2024 21:29:20 +0200 Subject: [PATCH 08/31] fix default state names --- reflex/state.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index d567ed40cf..f97697a4a9 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -2313,7 +2313,9 @@ def handle_frontend_exception(self, stack: str, component_stack: str) -> None: class UpdateVarsInternalState(State): """Substate for handling internal state var updates.""" - _state_name: ClassVar[Optional[str]] = constants.CompileVars.UPDATE_VARS_INTERNAL + _state_name: ClassVar[Optional[str]] = ( + constants.CompileVars.UPDATE_VARS_INTERNAl_STATE + ) async def update_vars_internal(self, vars: dict[str, Any]) -> None: """Apply updates to fully qualified state vars. @@ -2340,7 +2342,7 @@ class OnLoadInternalState(State): This is a separate substate to avoid deserializing the entire state tree for every page navigation. """ - _state_name: ClassVar[Optional[str]] = constants.CompileVars.ON_LOAD_INTERNAL + _state_name: ClassVar[Optional[str]] = constants.CompileVars.ON_LOAD_INTERNAL_STATE def on_load_internal(self) -> list[Event | EventSpec] | None: """Queue on_load handlers for the current page. From 7287c3a167b5180ad1d663de967635e814ddf632 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 27 Jul 2024 21:30:42 +0200 Subject: [PATCH 09/31] fix typo --- reflex/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/state.py b/reflex/state.py index f97697a4a9..d5a4debff6 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -2314,7 +2314,7 @@ class UpdateVarsInternalState(State): """Substate for handling internal state var updates.""" _state_name: ClassVar[Optional[str]] = ( - constants.CompileVars.UPDATE_VARS_INTERNAl_STATE + constants.CompileVars.UPDATE_VARS_INTERNAL_STATE ) async def update_vars_internal(self, vars: dict[str, Any]) -> None: From 215a8343f48f91fc695c62c5b5c5d2f15bcb102e Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 27 Jul 2024 21:57:18 +0200 Subject: [PATCH 10/31] wip minified state integration test --- tests/integration/test_minified_states.py | 99 +++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 tests/integration/test_minified_states.py diff --git a/tests/integration/test_minified_states.py b/tests/integration/test_minified_states.py new file mode 100644 index 0000000000..b78f15710d --- /dev/null +++ b/tests/integration/test_minified_states.py @@ -0,0 +1,99 @@ +"""Integration tests for minified state names.""" + +from __future__ import annotations + +import time +from typing import Generator, Type + +import pytest +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + +from reflex.testing import AppHarness + + +def TestApp(): + """A test app for minified state names.""" + import reflex as rx + + class TestAppState(rx.State): + """State for the TestApp app.""" + + pass + + app = rx.App() + + @app.add_page + def index(): + return rx.vstack( + rx.input( + value=TestAppState.router.session.client_token, + is_read_only=True, + id="token", + ), + ) + + +@pytest.fixture(scope="module") +def test_app( + app_harness_env: Type[AppHarness], tmp_path_factory: pytest.TempPathFactory +) -> Generator[AppHarness, None, None]: + """Start TestApp app at tmp_path via AppHarness. + + Args: + app_harness_env: either AppHarness (dev) or AppHarnessProd (prod) + tmp_path_factory: pytest tmp_path_factory fixture + + Yields: + running AppHarness instance + + """ + with app_harness_env.create( + root=tmp_path_factory.mktemp("test_app"), + app_name=f"testapp_{app_harness_env.__name__.lower()}", + app_source=TestApp, # type: ignore + ) as harness: + yield harness + + +@pytest.fixture +def driver(test_app: AppHarness) -> Generator[WebDriver, None, None]: + """Get an instance of the browser open to the test_app app. + + Args: + test_app: harness for TestApp app + + Yields: + WebDriver instance. + + """ + assert test_app.app_instance is not None, "app is not running" + driver = test_app.frontend() + try: + yield driver + finally: + driver.quit() + + +def test_minified_states( + test_app: AppHarness, + driver: WebDriver, +) -> None: + """Test minified state names. + + Args: + test_app: harness for TestApp + driver: WebDriver instance. + + """ + assert test_app.app_instance is not None, "app is not running" + + # get a reference to the connected client + token_input = driver.find_element(By.ID, "token") + assert token_input + + # wait for the backend connection to send the token + token = test_app.poll_for_value(token_input) + assert token From dadfb5663adfd41946457845d2a40ae26eda54cc Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Tue, 30 Jul 2024 21:17:25 +0200 Subject: [PATCH 11/31] wip: more dynamic jinja contexts, tests for minification --- reflex/.templates/web/utils/state.js | 4 +- reflex/compiler/compiler.py | 22 +- reflex/compiler/templates.py | 203 ++++++++++++++---- reflex/components/component.py | 4 +- reflex/components/dynamic.py | 14 +- reflex/config.py | 3 + reflex/constants/compiler.py | 46 ++-- reflex/custom_components/custom_components.py | 10 +- reflex/state.py | 52 ++--- reflex/testing.py | 38 +++- reflex/utils/build.py | 2 +- reflex/utils/prerequisites.py | 4 +- tests/integration/conftest.py | 25 ++- tests/integration/test_computed_vars.py | 1 - tests/integration/test_minified_states.py | 100 +++++++-- tests/test_minify_state.py | 7 +- tests/units/test_app.py | 2 +- tests/units/test_state.py | 5 +- tests/units/utils/test_utils.py | 6 +- 19 files changed, 372 insertions(+), 176 deletions(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 66df09ee11..b696f39ac8 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -117,7 +117,7 @@ export const isStateful = () => { if (event_queue.length === 0) { return false; } - return event_queue.some((event) => event.name.startsWith("reflex___state")); + return event_queue.some(event => event.name.includes("___")); }; /** @@ -810,7 +810,7 @@ export const useEventLoop = ( const vars = {}; vars[storage_to_state_map[e.key]] = e.newValue; const event = Event( - `${state_name}.reflex___state____update_vars_internal_state.update_vars_internal`, + `${state_name}.{{ update_vars_internal }}`, { vars: vars } ); addEvents([event], e); diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 9f81f319d4..4122a0938a 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -34,7 +34,7 @@ def _compile_document_root(root: Component) -> str: Returns: The compiled document root. """ - return templates.DOCUMENT_ROOT.render( + return templates.document_root().render( imports=utils.compile_imports(root._get_all_imports()), document=root.render(), ) @@ -72,7 +72,7 @@ def _compile_app(app_root: Component) -> str: ("utils_state", f"$/{constants.Dirs.UTILS}/state"), ] - return templates.APP_ROOT.render( + return templates.app_root().render( imports=utils.compile_imports(app_root._get_all_imports()), custom_codes=app_root._get_all_custom_code(), hooks={**app_root._get_all_hooks_internal(), **app_root._get_all_hooks()}, @@ -90,7 +90,7 @@ def _compile_theme(theme: str) -> str: Returns: The compiled theme. """ - return templates.THEME.render(theme=theme) + return templates.theme().render(theme=theme) def _compile_contexts(state: Optional[Type[BaseState]], theme: Component | None) -> str: @@ -109,7 +109,7 @@ def _compile_contexts(state: Optional[Type[BaseState]], theme: Component | None) last_compiled_time = str(datetime.now()) return ( - templates.CONTEXT.render( + templates.context().render( initial_state=utils.compile_state(state), state_name=state.get_name(), client_storage=utils.compile_client_storage(state), @@ -118,7 +118,7 @@ def _compile_contexts(state: Optional[Type[BaseState]], theme: Component | None) default_color_mode=appearance, ) if state - else templates.CONTEXT.render( + else templates.context().render( is_dev_mode=not is_prod_mode(), default_color_mode=appearance, last_compiled_time=last_compiled_time, @@ -145,7 +145,7 @@ def _compile_page( # Compile the code to render the component. kwargs = {"state_name": state.get_name()} if state is not None else {} - return templates.PAGE.render( + return templates.page().render( imports=imports, dynamic_imports=component._get_all_dynamic_imports(), custom_codes=component._get_all_custom_code(), @@ -201,7 +201,7 @@ def _compile_root_stylesheet(stylesheets: list[str]) -> str: ) stylesheet = f"../{constants.Dirs.PUBLIC}/{stylesheet.strip('/')}" sheets.append(stylesheet) if stylesheet not in sheets else None - return templates.STYLE.render(stylesheets=sheets) + return templates.style().render(stylesheets=sheets) def _compile_component(component: Component | StatefulComponent) -> str: @@ -213,7 +213,7 @@ def _compile_component(component: Component | StatefulComponent) -> str: Returns: The compiled component. """ - return templates.COMPONENT.render(component=component) + return templates.component().render(component=component) def _compile_components( @@ -241,7 +241,7 @@ def _compile_components( # Compile the components page. return ( - templates.COMPONENTS.render( + templates.components().render( imports=utils.compile_imports(imports), components=component_renders, ), @@ -319,7 +319,7 @@ def get_shared_components_recursive(component: BaseComponent): f"$/{constants.Dirs.UTILS}/{constants.PageNames.STATEFUL_COMPONENTS}", None ) - return templates.STATEFUL_COMPONENTS.render( + return templates.stateful_components().render( imports=utils.compile_imports(all_imports), memoized_code="\n".join(rendered_components), ) @@ -336,7 +336,7 @@ def _compile_tailwind( Returns: The compiled Tailwind config. """ - return templates.TAILWIND_CONFIG.render( + return templates.tailwind_config().render( **config, ) diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index c868a0cbb7..debb20a3d2 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -11,6 +11,12 @@ class ReflexJinjaEnvironment(Environment): def __init__(self) -> None: """Set default environment.""" + from reflex.state import ( + FrontendEventExceptionState, + OnLoadInternalState, + UpdateVarsInternalState, + ) + extensions = ["jinja2.ext.debug"] super().__init__( extensions=extensions, @@ -42,9 +48,9 @@ def __init__(self) -> None: "set_color_mode": constants.ColorMode.SET, "use_color_mode": constants.ColorMode.USE, "hydrate": constants.CompileVars.HYDRATE, - "on_load_internal": constants.CompileVars.ON_LOAD_INTERNAL, - "update_vars_internal": constants.CompileVars.UPDATE_VARS_INTERNAL, - "frontend_exception_state": constants.CompileVars.FRONTEND_EXCEPTION_STATE_FULL, + "on_load_internal": f"{OnLoadInternalState.get_name()}.on_load_internal", + "update_vars_internal": f"{UpdateVarsInternalState.get_name()}.update_vars_internal", + "frontend_exception_state": FrontendEventExceptionState.get_full_name(), } @@ -60,61 +66,172 @@ def get_template(name: str) -> Template: return ReflexJinjaEnvironment().get_template(name=name) -# Template for the Reflex config file. -RXCONFIG = get_template("app/rxconfig.py.jinja2") +def rxconfig(): + """Template for the Reflex config file. + + Returns: + Template: The template for the Reflex config file. + """ + return get_template("app/rxconfig.py.jinja2") + + +def document_root(): + """Code to render a NextJS Document root. + + Returns: + Template: The template for the NextJS Document root. + """ + return get_template("web/pages/_document.js.jinja2") -# Code to render a NextJS Document root. -DOCUMENT_ROOT = get_template("web/pages/_document.js.jinja2") -# Code to render NextJS App root. -APP_ROOT = get_template("web/pages/_app.js.jinja2") +def app_root(): + """Code to render NextJS App root. -# Template for the theme file. -THEME = get_template("web/utils/theme.js.jinja2") + Returns: + Template: The template for the NextJS App root. + """ + return get_template("web/pages/_app.js.jinja2") -# Template for the context file. -CONTEXT = get_template("web/utils/context.js.jinja2") -# Template for Tailwind config. -TAILWIND_CONFIG = get_template("web/tailwind.config.js.jinja2") +def theme(): + """Template for the theme file. -# Template to render a component tag. -COMPONENT = get_template("web/pages/component.js.jinja2") + Returns: + Template: The template for the theme file. + """ + return get_template("web/utils/theme.js.jinja2") -# Code to render a single NextJS page. -PAGE = get_template("web/pages/index.js.jinja2") -# Code to render the custom components page. -COMPONENTS = get_template("web/pages/custom_component.js.jinja2") +def context(): + """Template for the context file. -# Code to render Component instances as part of StatefulComponent -STATEFUL_COMPONENT = get_template("web/pages/stateful_component.js.jinja2") + Returns: + Template: The template for the context file. + """ + return get_template("web/utils/context.js.jinja2") -# Code to render StatefulComponent to an external file to be shared -STATEFUL_COMPONENTS = get_template("web/pages/stateful_components.js.jinja2") -# Sitemap config file. -SITEMAP_CONFIG = "module.exports = {config}".format +def tailwind_config(): + """Template for Tailwind config. -# Code to render the root stylesheet. -STYLE = get_template("web/styles/styles.css.jinja2") + Returns: + Template: The template for the Tailwind config + """ + return get_template("web/tailwind.config.js.jinja2") -# Code that generate the package json file -PACKAGE_JSON = get_template("web/package.json.jinja2") -# Code that generate the pyproject.toml file for custom components. -CUSTOM_COMPONENTS_PYPROJECT_TOML = get_template( - "custom_components/pyproject.toml.jinja2" -) +def component(): + """Template to render a component tag. -# Code that generates the README file for custom components. -CUSTOM_COMPONENTS_README = get_template("custom_components/README.md.jinja2") + Returns: + Template: The template for the component tag. + """ + return get_template("web/pages/component.js.jinja2") -# Code that generates the source file for custom components. -CUSTOM_COMPONENTS_SOURCE = get_template("custom_components/src.py.jinja2") -# Code that generates the init file for custom components. -CUSTOM_COMPONENTS_INIT_FILE = get_template("custom_components/__init__.py.jinja2") +def page(): + """Code to render a single NextJS page. + + Returns: + Template: The template for the NextJS page. + """ + return get_template("web/pages/index.js.jinja2") + + +def components(): + """Code to render the custom components page. + + Returns: + Template: The template for the custom components page. + """ + return get_template("web/pages/custom_component.js.jinja2") + + +def stateful_component(): + """Code to render Component instances as part of StatefulComponent. + + Returns: + Template: The template for the StatefulComponent. + """ + return get_template("web/pages/stateful_component.js.jinja2") + + +def stateful_components(): + """Code to render StatefulComponent to an external file to be shared. + + Returns: + Template: The template for the StatefulComponent. + """ + return get_template("web/pages/stateful_components.js.jinja2") + -# Code that generates the demo app main py file for testing custom components. -CUSTOM_COMPONENTS_DEMO_APP = get_template("custom_components/demo_app.py.jinja2") +def sitemap_config(): + """Sitemap config file. + + Returns: + Template: The template for the sitemap config file. + """ + return "module.exports = {config}".format + + +def style(): + """Code to render the root stylesheet. + + Returns: + Template: The template for the root stylesheet + """ + return get_template("web/styles/styles.css.jinja2") + + +def package_json(): + """Code that generate the package json file. + + Returns: + Template: The template for the package json file + """ + return get_template("web/package.json.jinja2") + + +def custom_components_pyproject_toml(): + """Code that generate the pyproject.toml file for custom components. + + Returns: + Template: The template for the pyproject.toml file + """ + return get_template("custom_components/pyproject.toml.jinja2") + + +def custom_components_readme(): + """Code that generates the README file for custom components. + + Returns: + Template: The template for the README file + """ + return get_template("custom_components/README.md.jinja2") + + +def custom_components_source(): + """Code that generates the source file for custom components. + + Returns: + Template: The template for the source file + """ + return get_template("custom_components/src.py.jinja2") + + +def custom_components_init(): + """Code that generates the init file for custom components. + + Returns: + Template: The template for the init file + """ + return get_template("custom_components/__init__.py.jinja2") + + +def custom_components_demo_app(): + """Code that generates the demo app main py file for testing custom components. + + Returns: + Template: The template for the demo app main py file + """ + return get_template("custom_components/demo_app.py.jinja2") diff --git a/reflex/components/component.py b/reflex/components/component.py index 470ba1145a..f1bb8c8f1f 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -24,7 +24,7 @@ import reflex.state from reflex.base import Base -from reflex.compiler.templates import STATEFUL_COMPONENT +from reflex.compiler.templates import stateful_component from reflex.components.core.breakpoints import Breakpoints from reflex.components.dynamic import load_dynamic_serializer from reflex.components.tags import Tag @@ -2134,7 +2134,7 @@ def _render_stateful_code( component.event_triggers[event_trigger] = memo_trigger # Render the code for this component and hooks. - return STATEFUL_COMPONENT.render( + return stateful_component().render( tag_name=tag_name, memo_trigger_hooks=memo_trigger_hooks, component=component, diff --git a/reflex/components/dynamic.py b/reflex/components/dynamic.py index c0e172224c..2fa0adfaf9 100644 --- a/reflex/components/dynamic.py +++ b/reflex/components/dynamic.py @@ -80,7 +80,7 @@ def make_component(component: Component) -> str: ) rendered_components[ - templates.STATEFUL_COMPONENT.render( + templates.stateful_component().render( tag_name="MySSRComponent", memo_trigger_hooks=[], component=component, @@ -101,10 +101,14 @@ def make_component(component: Component) -> str: else: imports[lib] = names - module_code_lines = templates.STATEFUL_COMPONENTS.render( - imports=utils.compile_imports(imports), - memoized_code="\n".join(rendered_components), - ).splitlines()[1:] + module_code_lines = ( + templates.stateful_components() + .render( + imports=utils.compile_imports(imports), + memoized_code="\n".join(rendered_components), + ) + .splitlines()[1:] + ) # Rewrite imports from `/` to destructure from window for ix, line in enumerate(module_code_lines[:]): diff --git a/reflex/config.py b/reflex/config.py index 049cc2e834..9fb02965ed 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -545,6 +545,9 @@ class EnvironmentVariables: # Where to save screenshots when tests fail. SCREENSHOT_DIR: EnvVar[Optional[Path]] = env_var(None) + # Whether to minify state names. + REFLEX_MINIFY_STATES: EnvVar[Optional[bool]] = env_var(False) + environment = EnvironmentVariables() diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py index ed175fda61..0754af6246 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -6,7 +6,7 @@ from types import SimpleNamespace from reflex.base import Base -from reflex.constants import ENV_MODE_ENV_VAR, Dirs, Env +from reflex.constants import Dirs, Env from reflex.utils.imports import ImportVar # The prefix used to create setters for state vars. @@ -40,12 +40,14 @@ def minify_states() -> bool: Returns: True if states should be minified. """ - env = os.environ.get(ENV_MINIFY_STATES, None) + from reflex.config import environment + + env = environment.REFLEX_MINIFY_STATES.get() if env is not None: - return env.lower() == "true" + return env # minify states in prod by default - return os.environ.get(ENV_MODE_ENV_VAR, "") == Env.PROD.value + return environment.REFLEX_ENV_MODE.get() == Env.PROD class CompileVars(SimpleNamespace): @@ -80,34 +82,14 @@ class CompileVars(SimpleNamespace): # The name of the function for converting a dict to an event. TO_EVENT = "Event" - # Whether to minify states. - MINIFY_STATES = minify_states() - - # The name of the OnLoadInternal state. - ON_LOAD_INTERNAL_STATE = ( - "l" if MINIFY_STATES else "reflex___state____on_load_internal_state" - ) - # The name of the internal on_load event. - ON_LOAD_INTERNAL = f"{ON_LOAD_INTERNAL_STATE}.on_load_internal" - # The name of the UpdateVarsInternal state. - UPDATE_VARS_INTERNAL_STATE = ( - "u" if MINIFY_STATES else "reflex___state____update_vars_internal_state" - ) - # The name of the internal event to update generic state vars. - UPDATE_VARS_INTERNAL = f"{UPDATE_VARS_INTERNAL_STATE}.update_vars_internal" - # The name of the frontend event exception state - FRONTEND_EXCEPTION_STATE = ( - "e" if MINIFY_STATES else "reflex___state____frontend_event_exception_state" - ) - # The full name of the frontend exception state - FRONTEND_EXCEPTION_STATE_FULL = ( - f"reflex___state____state.{FRONTEND_EXCEPTION_STATE}" - ) - INTERNAL_STATE_NAMES = { - ON_LOAD_INTERNAL_STATE, - UPDATE_VARS_INTERNAL_STATE, - FRONTEND_EXCEPTION_STATE, - } + @classmethod + def MINIFY_STATES(cls) -> bool: + """Whether to minify states. + + Returns: + True if states should be minified. + """ + return minify_states() class PageNames(SimpleNamespace): diff --git a/reflex/custom_components/custom_components.py b/reflex/custom_components/custom_components.py index 7681caebcb..f9ea41d6d7 100644 --- a/reflex/custom_components/custom_components.py +++ b/reflex/custom_components/custom_components.py @@ -65,7 +65,7 @@ def _create_package_config(module_name: str, package_name: str): pyproject = Path(CustomComponents.PYPROJECT_TOML) pyproject.write_text( - templates.CUSTOM_COMPONENTS_PYPROJECT_TOML.render( + templates.custom_components_pyproject_toml().render( module_name=module_name, package_name=package_name, reflex_version=constants.Reflex.VERSION, @@ -106,7 +106,7 @@ def _create_readme(module_name: str, package_name: str): readme = Path(CustomComponents.PACKAGE_README) readme.write_text( - templates.CUSTOM_COMPONENTS_README.render( + templates.custom_components_readme().render( module_name=module_name, package_name=package_name, ) @@ -129,14 +129,14 @@ def _write_source_and_init_py( module_path = custom_component_src_dir / f"{module_name}.py" module_path.write_text( - templates.CUSTOM_COMPONENTS_SOURCE.render( + templates.custom_components_source().render( component_class_name=component_class_name, module_name=module_name ) ) init_path = custom_component_src_dir / CustomComponents.INIT_FILE init_path.write_text( - templates.CUSTOM_COMPONENTS_INIT_FILE.render(module_name=module_name) + templates.custom_components_init.render(module_name=module_name) ) @@ -164,7 +164,7 @@ def _populate_demo_app(name_variants: NameVariants): # This source file is rendered using jinja template file. with open(f"{demo_app_name}/{demo_app_name}.py", "w") as f: f.write( - templates.CUSTOM_COMPONENTS_DEMO_APP.render( + templates.custom_components_demo_app().render( custom_component_module_dir=name_variants.custom_component_module_dir, module_name=name_variants.module_name, ) diff --git a/reflex/state.py b/reflex/state.py index d5a4debff6..fce6a1464a 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -288,7 +288,7 @@ def get_var_for_field(cls: Type[BaseState], f: ModelField): # Keep track of all state instances to calculate minified state names state_count: int = 0 -all_state_names: Set[str] = set() +minified_state_names: Dict[str, str] = {} def next_minified_state_name() -> str: @@ -296,12 +296,8 @@ def next_minified_state_name() -> str: Returns: The next minified state name. - - Raises: - RuntimeError: If the minified state name already exists. """ global state_count - global all_state_names num = state_count # All possible chars for minified state name @@ -318,25 +314,28 @@ def next_minified_state_name() -> str: state_count += 1 - if state_name in all_state_names: - raise RuntimeError(f"Minified state name {state_name} already exists") - all_state_names.add(state_name) - return state_name -def generate_state_name() -> str: +def get_minified_state_name(state_name: str) -> str: """Generate a minified state name. + Args: + state_name: The state name to minify. + Returns: The minified state name. Raises: ValueError: If no more minified state names are available """ + if state_name in minified_state_names: + return minified_state_names[state_name] + while name := next_minified_state_name(): - if name in constants.CompileVars.INTERNAL_STATE_NAMES: + if name in minified_state_names.values(): continue + minified_state_names[state_name] = name return name raise ValueError("No more minified state names available") @@ -410,9 +409,6 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): # A special event handler for setting base vars. setvar: ClassVar[EventHandler] - # Minified state name - _state_name: ClassVar[Optional[str]] = None - def __init__( self, parent_state: BaseState | None = None, @@ -518,10 +514,6 @@ def __init_subclass__(cls, mixin: bool = False, **kwargs): if "" in cls.__qualname__: cls._handle_local_def() - # Generate a minified state name by converting state count to string - if not cls._state_name or cls._state_name in all_state_names: - cls._state_name = generate_state_name() - # Validate the module name. cls._validate_module_name() @@ -937,18 +929,12 @@ def get_name(cls) -> str: Returns: The name of the state. - - Raises: - RuntimeError: If the state name is not set. """ - if constants.CompileVars.MINIFY_STATES: - if not cls._state_name: - raise RuntimeError( - "State name minification is enabled, but state name is not set." - ) - return cls._state_name module = cls.__module__.replace(".", "___") - return format.to_snake_case(f"{module}___{cls.__name__}") + state_name = format.to_snake_case(f"{module}___{cls.__name__}") + if constants.compiler.CompileVars.MINIFY_STATES(): + return get_minified_state_name(state_name) + return state_name @classmethod @functools.lru_cache() @@ -2290,10 +2276,6 @@ def wrapper() -> Component: class FrontendEventExceptionState(State): """Substate for handling frontend exceptions.""" - _state_name: ClassVar[Optional[str]] = ( - constants.CompileVars.FRONTEND_EXCEPTION_STATE - ) - @event def handle_frontend_exception(self, stack: str, component_stack: str) -> None: """Handle frontend exceptions. @@ -2313,10 +2295,6 @@ def handle_frontend_exception(self, stack: str, component_stack: str) -> None: class UpdateVarsInternalState(State): """Substate for handling internal state var updates.""" - _state_name: ClassVar[Optional[str]] = ( - constants.CompileVars.UPDATE_VARS_INTERNAL_STATE - ) - async def update_vars_internal(self, vars: dict[str, Any]) -> None: """Apply updates to fully qualified state vars. @@ -2342,8 +2320,6 @@ class OnLoadInternalState(State): This is a separate substate to avoid deserializing the entire state tree for every page navigation. """ - _state_name: ClassVar[Optional[str]] = constants.CompileVars.ON_LOAD_INTERNAL_STATE - def on_load_internal(self) -> list[Event | EventSpec] | None: """Queue on_load handlers for the current page. diff --git a/reflex/testing.py b/reflex/testing.py index bb7ead2d98..505f010421 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -46,12 +46,14 @@ from reflex.config import environment from reflex.state import ( BaseState, + State, StateManager, StateManagerDisk, StateManagerMemory, StateManagerRedis, reload_state_module, ) +from reflex.utils.types import override try: from selenium import webdriver # pyright: ignore [reportMissingImports] @@ -141,7 +143,7 @@ def create( types.FunctionType | types.ModuleType | str | functools.partial[Any] ] = None, app_name: Optional[str] = None, - ) -> "AppHarness": + ) -> AppHarness: """Create an AppHarness instance at root. Args: @@ -191,7 +193,14 @@ def get_state_name(self, state_cls_name: str) -> str: Returns: The state name + + Raises: + NotImplementedError: when minified state names are enabled """ + if reflex.constants.CompileVars.MINIFY_STATES(): + raise NotImplementedError( + "This API is not available with minified state names." + ) return reflex.utils.format.to_snake_case( f"{self.app_name}___{self.app_name}___" + state_cls_name ) @@ -207,7 +216,7 @@ def get_full_state_name(self, path: List[str]) -> str: """ # NOTE: using State.get_name() somehow causes trouble here # path = [State.get_name()] + [self.get_state_name(p) for p in path] - path = ["reflex___state____state"] + [self.get_state_name(p) for p in path] + path = [State.get_name()] + [self.get_state_name(p) for p in path] return ".".join(path) def _get_globals_from_signature(self, func: Any) -> dict[str, Any]: @@ -412,7 +421,7 @@ def consume_frontend_output(): self.frontend_output_thread = threading.Thread(target=consume_frontend_output) self.frontend_output_thread.start() - def start(self) -> "AppHarness": + def start(self) -> AppHarness: """Start the backend in a new thread and dev frontend as a separate process. Returns: @@ -442,7 +451,7 @@ def get_app_global_source(key, value): return f"{key} = {value!r}" return inspect.getsource(value) - def __enter__(self) -> "AppHarness": + def __enter__(self) -> AppHarness: """Contextmanager protocol for `start()`. Returns: @@ -921,6 +930,7 @@ def _run_frontend(self): ) self.frontend_server.serve_forever() + @override def _start_frontend(self): # Set up the frontend. with chdir(self.app_path): @@ -932,17 +942,19 @@ def _start_frontend(self): zipping=False, frontend=True, backend=False, - loglevel=reflex.constants.LogLevel.INFO, + loglevel=reflex.constants.base.LogLevel.INFO, ) self.frontend_thread = threading.Thread(target=self._run_frontend) self.frontend_thread.start() + @override def _wait_frontend(self): - self._poll_for(lambda: self.frontend_server is not None) + _ = self._poll_for(lambda: self.frontend_server is not None) if self.frontend_server is None or not self.frontend_server.socket.fileno(): raise RuntimeError("Frontend did not start") + @override def _start_backend(self): if self.app_instance is None: raise RuntimeError("App was not initialized.") @@ -959,12 +971,25 @@ def _start_backend(self): self.backend_thread = threading.Thread(target=self.backend.run) self.backend_thread.start() + @override def _poll_for_servers(self, timeout: TimeoutType = None) -> socket.socket: try: return super()._poll_for_servers(timeout) finally: environment.REFLEX_SKIP_COMPILE.set(None) + @override + def start(self) -> AppHarnessProd: + """Start AppHarnessProd instance. + + Returns: + self + """ + environment.REFLEX_ENV_MODE.set(reflex.constants.base.Env.PROD) + _ = super().start() + return self + + @override def stop(self): """Stop the frontend python webserver.""" super().stop() @@ -972,3 +997,4 @@ def stop(self): self.frontend_server.shutdown() if self.frontend_thread is not None: self.frontend_thread.join() + environment.REFLEX_ENV_MODE.set(None) diff --git a/reflex/utils/build.py b/reflex/utils/build.py index 14709d99ce..8fae83c45a 100644 --- a/reflex/utils/build.py +++ b/reflex/utils/build.py @@ -44,7 +44,7 @@ def generate_sitemap_config(deploy_url: str, export=False): config = json.dumps(config) sitemap = prerequisites.get_web_dir() / constants.Next.SITEMAP_CONFIG_FILE - sitemap.write_text(templates.SITEMAP_CONFIG(config=config)) + sitemap.write_text(templates.sitemap_config()(config=config)) def _zip( diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 7124d46c4c..6ceea4b051 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -436,7 +436,7 @@ def create_config(app_name: str): config_name = f"{re.sub(r'[^a-zA-Z]', '', app_name).capitalize()}Config" with open(constants.Config.FILE, "w") as f: console.debug(f"Creating {constants.Config.FILE}") - f.write(templates.RXCONFIG.render(app_name=app_name, config_name=config_name)) + f.write(templates.rxconfig().render(app_name=app_name, config_name=config_name)) def initialize_gitignore( @@ -604,7 +604,7 @@ def initialize_web_directory(): def _compile_package_json(): - return templates.PACKAGE_JSON.render( + return templates.package_json().render( scripts={ "dev": constants.PackageJson.Commands.DEV, "export": constants.PackageJson.Commands.EXPORT, diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index f7b825f162..5c456ec75e 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -3,9 +3,11 @@ import os import re from pathlib import Path +from typing import Generator, Type import pytest +import reflex.constants from reflex.config import environment from reflex.testing import AppHarness, AppHarnessProd @@ -64,15 +66,30 @@ def pytest_exception_interact(node, call, report): @pytest.fixture( - scope="session", params=[AppHarness, AppHarnessProd], ids=["dev", "prod"] + scope="session", + params=[ + AppHarness, + AppHarnessProd, + ], + ids=[ + reflex.constants.Env.DEV.value, + reflex.constants.Env.PROD.value, + ], ) -def app_harness_env(request): +def app_harness_env( + request: pytest.FixtureRequest, +) -> Generator[Type[AppHarness], None, None]: """Parametrize the AppHarness class to use for the test, either dev or prod. Args: request: The pytest fixture request object. - Returns: + Yields: The AppHarness class to use for the test. """ - return request.param + harness: Type[AppHarness] = request.param + if issubclass(harness, AppHarnessProd): + environment.REFLEX_ENV_MODE.set(reflex.constants.Env.PROD) + yield harness + if issubclass(harness, AppHarnessProd): + environment.REFLEX_ENV_MODE.set(None) diff --git a/tests/integration/test_computed_vars.py b/tests/integration/test_computed_vars.py index 1f585cd8b2..3331ee74f0 100644 --- a/tests/integration/test_computed_vars.py +++ b/tests/integration/test_computed_vars.py @@ -106,7 +106,6 @@ def index() -> rx.Component: ), ) - # raise Exception(State.count3._deps(objclass=State)) app = rx.App() app.add_page(index) diff --git a/tests/integration/test_minified_states.py b/tests/integration/test_minified_states.py index b78f15710d..eb3efc8ec3 100644 --- a/tests/integration/test_minified_states.py +++ b/tests/integration/test_minified_states.py @@ -2,20 +2,24 @@ from __future__ import annotations -import time -from typing import Generator, Type +import os +from functools import partial +from typing import Generator, Optional, Type import pytest from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.support.ui import WebDriverWait -from reflex.testing import AppHarness +from reflex.constants.compiler import ENV_MINIFY_STATES +from reflex.testing import AppHarness, AppHarnessProd -def TestApp(): - """A test app for minified state names.""" +def TestApp(minify: bool | None) -> None: + """A test app for minified state names. + + Args: + minify: whether to minify state names + """ import reflex as rx class TestAppState(rx.State): @@ -25,7 +29,6 @@ class TestAppState(rx.State): app = rx.App() - @app.add_page def index(): return rx.vstack( rx.input( @@ -33,27 +36,64 @@ def index(): is_read_only=True, id="token", ), + rx.text(f"minify: {minify}", id="minify"), + rx.text(TestAppState.get_name(), id="state_name"), + rx.text(TestAppState.get_full_name(), id="state_full_name"), ) + app.add_page(index) + + +@pytest.fixture( + params=[ + pytest.param(False), + pytest.param(True), + pytest.param(None), + ], +) +def minify_state_env( + request: pytest.FixtureRequest, +) -> Generator[Optional[bool], None, None]: + """Set the environment variable to minify state names. + + Args: + request: pytest fixture request + + Yields: + minify_states: whether to minify state names + """ + minify_states: Optional[bool] = request.param + if minify_states is None: + _ = os.environ.pop(ENV_MINIFY_STATES, None) + else: + os.environ[ENV_MINIFY_STATES] = str(minify_states).lower() + yield minify_states + if minify_states is not None: + os.environ.pop(ENV_MINIFY_STATES, None) + -@pytest.fixture(scope="module") +@pytest.fixture def test_app( - app_harness_env: Type[AppHarness], tmp_path_factory: pytest.TempPathFactory + app_harness_env: Type[AppHarness], + tmp_path_factory: pytest.TempPathFactory, + minify_state_env: Optional[bool], ) -> Generator[AppHarness, None, None]: """Start TestApp app at tmp_path via AppHarness. Args: app_harness_env: either AppHarness (dev) or AppHarnessProd (prod) tmp_path_factory: pytest tmp_path_factory fixture + minify_state_env: need to request this fixture to set env before the app starts Yields: running AppHarness instance """ + name = f"testapp_{app_harness_env.__name__.lower()}" with app_harness_env.create( - root=tmp_path_factory.mktemp("test_app"), - app_name=f"testapp_{app_harness_env.__name__.lower()}", - app_source=TestApp, # type: ignore + root=tmp_path_factory.mktemp(name), + app_name=name, + app_source=partial(TestApp, minify=minify_state_env), # pyright: ignore[reportArgumentType] ) as harness: yield harness @@ -80,16 +120,33 @@ def driver(test_app: AppHarness) -> Generator[WebDriver, None, None]: def test_minified_states( test_app: AppHarness, driver: WebDriver, + minify_state_env: Optional[bool], ) -> None: """Test minified state names. Args: test_app: harness for TestApp driver: WebDriver instance. + minify_state_env: whether state minification is enabled by env var. """ assert test_app.app_instance is not None, "app is not running" + is_prod = isinstance(test_app, AppHarnessProd) + + # default to minifying in production + should_minify: bool = is_prod + + # env overrides default + if minify_state_env is not None: + should_minify = minify_state_env + + # TODO: reload internal states, or refactor VarData to reference state object instead of name + if should_minify: + pytest.skip( + "minify tests are currently not working, because _var_set_states writes the state names during import time" + ) + # get a reference to the connected client token_input = driver.find_element(By.ID, "token") assert token_input @@ -97,3 +154,20 @@ def test_minified_states( # wait for the backend connection to send the token token = test_app.poll_for_value(token_input) assert token + + state_name_text = driver.find_element(By.ID, "state_name") + assert state_name_text + state_name = state_name_text.text + + state_full_name_text = driver.find_element(By.ID, "state_full_name") + assert state_full_name_text + _ = state_full_name_text.text + + assert test_app.app_module + module_state_prefix = test_app.app_module.__name__.replace(".", "___") + # prod_module_suffix = "prod" if is_prod else "" + + if should_minify: + assert len(state_name) == 1 + else: + assert state_name == f"{module_state_prefix}____test_app_state" diff --git a/tests/test_minify_state.py b/tests/test_minify_state.py index e4dea43ef1..1e49a227e6 100644 --- a/tests/test_minify_state.py +++ b/tests/test_minify_state.py @@ -1,17 +1,14 @@ from typing import Set -from reflex.state import all_state_names, next_minified_state_name +from reflex.state import next_minified_state_name def test_next_minified_state_name(): """Test that the next_minified_state_name function returns unique state names.""" - current_state_count = len(all_state_names) state_names: Set[str] = set() - gen: int = 10000 + gen = 10000 for _ in range(gen): state_name = next_minified_state_name() assert state_name not in state_names state_names.add(state_name) assert len(state_names) == gen - - assert len(all_state_names) == current_state_count + gen diff --git a/tests/units/test_app.py b/tests/units/test_app.py index 1e34a67c39..e2a2c20c86 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -1032,7 +1032,7 @@ def _dynamic_state_event(name, val, **kwargs): prev_exp_val = "" for exp_index, exp_val in enumerate(exp_vals): on_load_internal = _event( - name=f"{state.get_full_name()}.{constants.CompileVars.ON_LOAD_INTERNAL.rpartition('.')[2]}", + name=f"{state.get_full_name()}.on_load_internal", val=exp_val, ) exp_router_data = { diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 83e348cd2d..0d1895d70d 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -54,6 +54,7 @@ LOCK_EXPIRATION = 2000 if CI else 300 LOCK_EXPIRE_SLEEP = 2.5 if CI else 0.4 +ON_LOAD_INTERNAL = f"{OnLoadInternalState.get_name()}.on_load_internal" formatted_router = { "session": {"client_token": "", "client_ip": "", "session_id": ""}, @@ -2793,7 +2794,7 @@ async def test_preprocess(app_module_mock, token, test_state, expected, mocker): app=app, event=Event( token=token, - name=f"{state.get_name()}.{CompileVars.ON_LOAD_INTERNAL}", + name=f"{state.get_name()}.{ON_LOAD_INTERNAL}", router_data={RouteVar.PATH: "/", RouteVar.ORIGIN: "/", RouteVar.QUERY: {}}, ), sid="sid", @@ -2840,7 +2841,7 @@ async def test_preprocess_multiple_load_events(app_module_mock, token, mocker): app=app, event=Event( token=token, - name=f"{state.get_full_name()}.{CompileVars.ON_LOAD_INTERNAL}", + name=f"{state.get_full_name()}.{ON_LOAD_INTERNAL}", router_data={RouteVar.PATH: "/", RouteVar.ORIGIN: "/", RouteVar.QUERY: {}}, ), sid="sid", diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index cc98c3acef..abe3dd5fc7 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -275,7 +275,7 @@ def test_unsupported_literals(cls: type): ], ) def test_create_config(app_name, expected_config_name, mocker): - """Test templates.RXCONFIG is formatted with correct app name and config class name. + """Test templates.rxconfig is formatted with correct app name and config class name. Args: app_name: App name. @@ -283,9 +283,9 @@ def test_create_config(app_name, expected_config_name, mocker): mocker: Mocker object. """ mocker.patch("builtins.open") - tmpl_mock = mocker.patch("reflex.compiler.templates.RXCONFIG") + tmpl_mock = mocker.patch("reflex.compiler.templates.rxconfig") prerequisites.create_config(app_name) - tmpl_mock.render.assert_called_with( + tmpl_mock().render.assert_called_with( app_name=app_name, config_name=expected_config_name ) From 4e76e4d6acf64ed12847d9869da6ffaaea77caae Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 4 Aug 2024 21:43:07 +0200 Subject: [PATCH 12/31] fix type ignore comment --- tests/integration/test_minified_states.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_minified_states.py b/tests/integration/test_minified_states.py index eb3efc8ec3..64c8469bcf 100644 --- a/tests/integration/test_minified_states.py +++ b/tests/integration/test_minified_states.py @@ -93,7 +93,7 @@ def test_app( with app_harness_env.create( root=tmp_path_factory.mktemp(name), app_name=name, - app_source=partial(TestApp, minify=minify_state_env), # pyright: ignore[reportArgumentType] + app_source=partial(TestApp, minify=minify_state_env), # type: ignore ) as harness: yield harness From 79fc10957d62b30a85db838e82793cb54645d207 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 4 Aug 2024 22:36:56 +0200 Subject: [PATCH 13/31] move state.js to jinja, related to #3738 --- .../web/utils/state.js.jinja2} | 4 ++-- reflex/compiler/compiler.py | 21 +++++++++++++++++++ reflex/compiler/templates.py | 11 ++++++++++ reflex/compiler/utils.py | 9 ++++++++ reflex/constants/base.py | 2 ++ reflex/constants/compiler.py | 1 - 6 files changed, 45 insertions(+), 3 deletions(-) rename reflex/.templates/{web/utils/state.js => jinja/web/utils/state.js.jinja2} (99%) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/jinja/web/utils/state.js.jinja2 similarity index 99% rename from reflex/.templates/web/utils/state.js rename to reflex/.templates/jinja/web/utils/state.js.jinja2 index b696f39ac8..ef92ba35af 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/jinja/web/utils/state.js.jinja2 @@ -810,7 +810,7 @@ export const useEventLoop = ( const vars = {}; vars[storage_to_state_map[e.key]] = e.newValue; const event = Event( - `${state_name}.{{ update_vars_internal }}`, + `${state_name}.{{ const.update_vars_internal }}`, { vars: vars } ); addEvents([event], e); @@ -824,7 +824,7 @@ export const useEventLoop = ( // Route after the initial page hydration. useEffect(() => { const change_start = () => { - const main_state_dispatch = dispatch["reflex___state____state"]; + const main_state_dispatch = dispatch["{{ const.state_name }}"]; if (main_state_dispatch !== undefined) { main_state_dispatch({ is_hydrated: false }); } diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 4122a0938a..998b6fc8bd 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -126,6 +126,15 @@ def _compile_contexts(state: Optional[Type[BaseState]], theme: Component | None) ) +def _compile_state() -> str: + """Compile the state. + + Returns: + The compiled state. + """ + return templates.state().render() + + def _compile_page( component: BaseComponent, state: Type[BaseState] | None, @@ -424,6 +433,18 @@ def compile_contexts( return output_path, _compile_contexts(state, theme) +def compile_state() -> tuple[str, str]: + """Compile the state. + + Returns: + The path and code of the compiled state. + """ + output_path = utils.get_state_path() + + code = _compile_state() + return output_path, code + + def compile_page( path: str, component: BaseComponent, state: Type[BaseState] | None ) -> tuple[str, str]: diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index debb20a3d2..aa7628bb87 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -14,6 +14,7 @@ def __init__(self) -> None: from reflex.state import ( FrontendEventExceptionState, OnLoadInternalState, + State, UpdateVarsInternalState, ) @@ -48,6 +49,7 @@ def __init__(self) -> None: "set_color_mode": constants.ColorMode.SET, "use_color_mode": constants.ColorMode.USE, "hydrate": constants.CompileVars.HYDRATE, + "state_name": State.get_name(), "on_load_internal": f"{OnLoadInternalState.get_name()}.on_load_internal", "update_vars_internal": f"{UpdateVarsInternalState.get_name()}.update_vars_internal", "frontend_exception_state": FrontendEventExceptionState.get_full_name(), @@ -111,6 +113,15 @@ def context(): return get_template("web/utils/context.js.jinja2") +def state(): + """Template for the state file. + + Returns: + Template: The template for the state file. + """ + return get_template("web/utils/state.js.jinja2") + + def tailwind_config(): """Template for Tailwind config. diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index 29398da87d..ad6933e632 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -399,6 +399,15 @@ def get_context_path() -> str: return str(get_web_dir() / (constants.Dirs.CONTEXTS_PATH + constants.Ext.JS)) +def get_state_path() -> str: + """Get the path of the state file. + + Returns: + The path of the state module. + """ + return str(get_web_dir() / (constants.Dirs.STATES_PATH + constants.Ext.JS)) + + def get_components_path() -> str: """Get the path of the compiled components. diff --git a/reflex/constants/base.py b/reflex/constants/base.py index 6ec73cdf0e..2a1db1ac28 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -35,6 +35,8 @@ class Dirs(SimpleNamespace): COMPONENTS_PATH = "/".join([UTILS, "components"]) # The name of the contexts file. CONTEXTS_PATH = "/".join([UTILS, "context"]) + # The name of the states file. + STATES_PATH = "/".join([UTILS, "state"]) # The name of the output static directory. STATIC = "_static" # The name of the public html directory served at "/" diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py index 0754af6246..3fdbf2c485 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -1,7 +1,6 @@ """Compiler variables.""" import enum -import os from enum import Enum from types import SimpleNamespace From c26d626bf22d573ffb8ac52cd3072db897b64def Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Mon, 5 Aug 2024 21:57:30 +0200 Subject: [PATCH 14/31] Revert "move state.js to jinja, related to #3738" This reverts commit 4d2a72c4e86be3c8466cc701daf173ea70cba90d. --- .../state.js.jinja2 => web/utils/state.js} | 4 ++-- reflex/compiler/compiler.py | 21 ------------------- reflex/compiler/templates.py | 11 ---------- reflex/compiler/utils.py | 9 -------- reflex/constants/base.py | 2 -- 5 files changed, 2 insertions(+), 45 deletions(-) rename reflex/.templates/{jinja/web/utils/state.js.jinja2 => web/utils/state.js} (99%) diff --git a/reflex/.templates/jinja/web/utils/state.js.jinja2 b/reflex/.templates/web/utils/state.js similarity index 99% rename from reflex/.templates/jinja/web/utils/state.js.jinja2 rename to reflex/.templates/web/utils/state.js index ef92ba35af..da0934da56 100644 --- a/reflex/.templates/jinja/web/utils/state.js.jinja2 +++ b/reflex/.templates/web/utils/state.js @@ -810,7 +810,7 @@ export const useEventLoop = ( const vars = {}; vars[storage_to_state_map[e.key]] = e.newValue; const event = Event( - `${state_name}.{{ const.update_vars_internal }}`, + `${state_name}.{{ update_vars_internal }}`, { vars: vars } ); addEvents([event], e); @@ -824,7 +824,7 @@ export const useEventLoop = ( // Route after the initial page hydration. useEffect(() => { const change_start = () => { - const main_state_dispatch = dispatch["{{ const.state_name }}"]; + const main_state_dispatch = dispatch["state"]; if (main_state_dispatch !== undefined) { main_state_dispatch({ is_hydrated: false }); } diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 998b6fc8bd..4122a0938a 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -126,15 +126,6 @@ def _compile_contexts(state: Optional[Type[BaseState]], theme: Component | None) ) -def _compile_state() -> str: - """Compile the state. - - Returns: - The compiled state. - """ - return templates.state().render() - - def _compile_page( component: BaseComponent, state: Type[BaseState] | None, @@ -433,18 +424,6 @@ def compile_contexts( return output_path, _compile_contexts(state, theme) -def compile_state() -> tuple[str, str]: - """Compile the state. - - Returns: - The path and code of the compiled state. - """ - output_path = utils.get_state_path() - - code = _compile_state() - return output_path, code - - def compile_page( path: str, component: BaseComponent, state: Type[BaseState] | None ) -> tuple[str, str]: diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index aa7628bb87..debb20a3d2 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -14,7 +14,6 @@ def __init__(self) -> None: from reflex.state import ( FrontendEventExceptionState, OnLoadInternalState, - State, UpdateVarsInternalState, ) @@ -49,7 +48,6 @@ def __init__(self) -> None: "set_color_mode": constants.ColorMode.SET, "use_color_mode": constants.ColorMode.USE, "hydrate": constants.CompileVars.HYDRATE, - "state_name": State.get_name(), "on_load_internal": f"{OnLoadInternalState.get_name()}.on_load_internal", "update_vars_internal": f"{UpdateVarsInternalState.get_name()}.update_vars_internal", "frontend_exception_state": FrontendEventExceptionState.get_full_name(), @@ -113,15 +111,6 @@ def context(): return get_template("web/utils/context.js.jinja2") -def state(): - """Template for the state file. - - Returns: - Template: The template for the state file. - """ - return get_template("web/utils/state.js.jinja2") - - def tailwind_config(): """Template for Tailwind config. diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index ad6933e632..29398da87d 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -399,15 +399,6 @@ def get_context_path() -> str: return str(get_web_dir() / (constants.Dirs.CONTEXTS_PATH + constants.Ext.JS)) -def get_state_path() -> str: - """Get the path of the state file. - - Returns: - The path of the state module. - """ - return str(get_web_dir() / (constants.Dirs.STATES_PATH + constants.Ext.JS)) - - def get_components_path() -> str: """Get the path of the compiled components. diff --git a/reflex/constants/base.py b/reflex/constants/base.py index 2a1db1ac28..6ec73cdf0e 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -35,8 +35,6 @@ class Dirs(SimpleNamespace): COMPONENTS_PATH = "/".join([UTILS, "components"]) # The name of the contexts file. CONTEXTS_PATH = "/".join([UTILS, "context"]) - # The name of the states file. - STATES_PATH = "/".join([UTILS, "state"]) # The name of the output static directory. STATIC = "_static" # The name of the public html directory served at "/" From f163d4156395e10cc5ca3175c4d75fb986d3a1e2 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Mon, 5 Aug 2024 21:58:00 +0200 Subject: [PATCH 15/31] pass constants via js consts to state.js --- reflex/.templates/jinja/web/utils/context.js.jinja2 | 2 ++ reflex/.templates/web/utils/state.js | 6 ++++-- reflex/compiler/templates.py | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/reflex/.templates/jinja/web/utils/context.js.jinja2 b/reflex/.templates/jinja/web/utils/context.js.jinja2 index 2428cfa9d2..b2c64cfd08 100644 --- a/reflex/.templates/jinja/web/utils/context.js.jinja2 +++ b/reflex/.templates/jinja/web/utils/context.js.jinja2 @@ -23,6 +23,8 @@ export const clientStorage = {{ client_storage|json_dumps }} export const clientStorage = {} {% endif %} +export const main_state_name = "{{const.main_state_name}}" +export const update_vars_internal = "{{const.update_vars_internal}}" {% if state_name %} export const state_name = "{{state_name}}" diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index da0934da56..d73f020d5e 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -12,6 +12,8 @@ import { onLoadInternalEvent, state_name, exception_state_name, + main_state_name, + update_vars_internal, } from "$/utils/context.js"; import debounce from "$/utils/helpers/debounce"; import throttle from "$/utils/helpers/throttle"; @@ -810,7 +812,7 @@ export const useEventLoop = ( const vars = {}; vars[storage_to_state_map[e.key]] = e.newValue; const event = Event( - `${state_name}.{{ update_vars_internal }}`, + `${state_name}.${update_vars_internal}`, { vars: vars } ); addEvents([event], e); @@ -824,7 +826,7 @@ export const useEventLoop = ( // Route after the initial page hydration. useEffect(() => { const change_start = () => { - const main_state_dispatch = dispatch["state"]; + const main_state_dispatch = dispatch[main_state_name]; if (main_state_dispatch !== undefined) { main_state_dispatch({ is_hydrated: false }); } diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index debb20a3d2..886e4c1c95 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -14,6 +14,7 @@ def __init__(self) -> None: from reflex.state import ( FrontendEventExceptionState, OnLoadInternalState, + State, UpdateVarsInternalState, ) @@ -48,6 +49,7 @@ def __init__(self) -> None: "set_color_mode": constants.ColorMode.SET, "use_color_mode": constants.ColorMode.USE, "hydrate": constants.CompileVars.HYDRATE, + "main_state_name": State.get_name(), "on_load_internal": f"{OnLoadInternalState.get_name()}.on_load_internal", "update_vars_internal": f"{UpdateVarsInternalState.get_name()}.update_vars_internal", "frontend_exception_state": FrontendEventExceptionState.get_full_name(), From 4bbd9884118bebf9f37038c474de405ab517b8f4 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Tue, 8 Oct 2024 16:40:41 -0700 Subject: [PATCH 16/31] test_minified_states: remove skip -- things seem to be working as-is --- tests/integration/test_minified_states.py | 31 +++++++++-------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/tests/integration/test_minified_states.py b/tests/integration/test_minified_states.py index 64c8469bcf..dc6c275f1b 100644 --- a/tests/integration/test_minified_states.py +++ b/tests/integration/test_minified_states.py @@ -14,7 +14,7 @@ from reflex.testing import AppHarness, AppHarnessProd -def TestApp(minify: bool | None) -> None: +def MinifiedStatesApp(minify: bool | None) -> None: """A test app for minified state names. Args: @@ -22,8 +22,8 @@ def TestApp(minify: bool | None) -> None: """ import reflex as rx - class TestAppState(rx.State): - """State for the TestApp app.""" + class MinifiedState(rx.State): + """State for the MinifiedStatesApp app.""" pass @@ -32,13 +32,13 @@ class TestAppState(rx.State): def index(): return rx.vstack( rx.input( - value=TestAppState.router.session.client_token, + value=MinifiedState.router.session.client_token, is_read_only=True, id="token", ), rx.text(f"minify: {minify}", id="minify"), - rx.text(TestAppState.get_name(), id="state_name"), - rx.text(TestAppState.get_full_name(), id="state_full_name"), + rx.text(MinifiedState.get_name(), id="state_name"), + rx.text(MinifiedState.get_full_name(), id="state_full_name"), ) app.add_page(index) @@ -78,7 +78,7 @@ def test_app( tmp_path_factory: pytest.TempPathFactory, minify_state_env: Optional[bool], ) -> Generator[AppHarness, None, None]: - """Start TestApp app at tmp_path via AppHarness. + """Start MinifiedStatesApp app at tmp_path via AppHarness. Args: app_harness_env: either AppHarness (dev) or AppHarnessProd (prod) @@ -89,11 +89,11 @@ def test_app( running AppHarness instance """ - name = f"testapp_{app_harness_env.__name__.lower()}" + name = f"testminifiedstates_{app_harness_env.__name__.lower()}" with app_harness_env.create( root=tmp_path_factory.mktemp(name), app_name=name, - app_source=partial(TestApp, minify=minify_state_env), # type: ignore + app_source=partial(MinifiedStatesApp, minify=minify_state_env), # type: ignore ) as harness: yield harness @@ -103,7 +103,7 @@ def driver(test_app: AppHarness) -> Generator[WebDriver, None, None]: """Get an instance of the browser open to the test_app app. Args: - test_app: harness for TestApp app + test_app: harness for MinifiedStatesApp app Yields: WebDriver instance. @@ -125,7 +125,7 @@ def test_minified_states( """Test minified state names. Args: - test_app: harness for TestApp + test_app: harness for MinifiedStatesApp driver: WebDriver instance. minify_state_env: whether state minification is enabled by env var. @@ -141,12 +141,6 @@ def test_minified_states( if minify_state_env is not None: should_minify = minify_state_env - # TODO: reload internal states, or refactor VarData to reference state object instead of name - if should_minify: - pytest.skip( - "minify tests are currently not working, because _var_set_states writes the state names during import time" - ) - # get a reference to the connected client token_input = driver.find_element(By.ID, "token") assert token_input @@ -165,9 +159,8 @@ def test_minified_states( assert test_app.app_module module_state_prefix = test_app.app_module.__name__.replace(".", "___") - # prod_module_suffix = "prod" if is_prod else "" if should_minify: assert len(state_name) == 1 else: - assert state_name == f"{module_state_prefix}____test_app_state" + assert state_name == f"{module_state_prefix}____minified_state" From 6eb808ca2ca3bcfef338519e184f3deef55da6c6 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Tue, 8 Oct 2024 16:41:19 -0700 Subject: [PATCH 17/31] state.js: more reliable isStateful detection --- reflex/.templates/web/utils/state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index d73f020d5e..bc64c6c24d 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -119,7 +119,7 @@ export const isStateful = () => { if (event_queue.length === 0) { return false; } - return event_queue.some(event => event.name.includes("___")); + return event_queue.some(event => event.name.startsWith(main_state_name)); }; /** From db2b5b0320a154186ef221d7e42f439ca0e5b2a8 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Tue, 8 Oct 2024 16:42:31 -0700 Subject: [PATCH 18/31] AppHarness: handle `get_state_name` for minified names Also clear the StatefulComponent cache between AppInstance runs --- reflex/testing.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/reflex/testing.py b/reflex/testing.py index 505f010421..117280f2ef 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -43,6 +43,7 @@ import reflex.utils.format import reflex.utils.prerequisites import reflex.utils.processes +from reflex.components.component import StatefulComponent from reflex.config import environment from reflex.state import ( BaseState, @@ -51,6 +52,7 @@ StateManagerDisk, StateManagerMemory, StateManagerRedis, + minified_state_names, reload_state_module, ) from reflex.utils.types import override @@ -193,17 +195,13 @@ def get_state_name(self, state_cls_name: str) -> str: Returns: The state name - - Raises: - NotImplementedError: when minified state names are enabled """ - if reflex.constants.CompileVars.MINIFY_STATES(): - raise NotImplementedError( - "This API is not available with minified state names." - ) - return reflex.utils.format.to_snake_case( + state_name = reflex.utils.format.to_snake_case( f"{self.app_name}___{self.app_name}___" + state_cls_name ) + if reflex.constants.CompileVars.MINIFY_STATES(): + return minified_state_names.get(state_name, state_name) + return state_name def get_full_state_name(self, path: List[str]) -> str: """Get the full state name for the given state class name. @@ -284,6 +282,8 @@ def _initialize_app(self): ) self.app_module_path.write_text(source_code) with chdir(self.app_path): + # Reset stateful component cache for new app + StatefulComponent.tag_to_stateful_component.clear() # ensure config and app are reloaded when testing different app reflex.config.get_config(reload=True) # Save decorated pages before importing the test app module From d32604713c8944bb2111bb5badf26c0c3100012c Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Fri, 1 Nov 2024 22:48:36 +0100 Subject: [PATCH 19/31] implement default_factory for EnvVar, improve env_var typing also migrate environment to be a class singleton to prevent unintended chaos with default factories --- reflex/app.py | 16 +++- reflex/compiler/compiler.py | 4 +- reflex/components/core/upload.py | 4 +- reflex/config.py | 92 ++++++++++++++----- reflex/constants/base.py | 15 +-- reflex/constants/compiler.py | 30 +----- reflex/constants/installer.py | 8 +- reflex/custom_components/custom_components.py | 6 +- reflex/model.py | 16 ++-- reflex/reflex.py | 16 ++-- reflex/state.py | 6 +- reflex/testing.py | 18 ++-- reflex/utils/exec.py | 16 ++-- reflex/utils/net.py | 4 +- reflex/utils/path_ops.py | 6 +- reflex/utils/prerequisites.py | 21 +++-- reflex/utils/registry.py | 4 +- reflex/utils/telemetry.py | 4 +- tests/integration/conftest.py | 13 ++- tests/integration/test_minified_states.py | 11 +-- tests/units/test_config.py | 21 ++++- tests/units/utils/test_utils.py | 6 +- 22 files changed, 193 insertions(+), 144 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index ae3f904c03..dea50d981c 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -67,7 +67,7 @@ ) from reflex.components.core.upload import Upload, get_upload_dir from reflex.components.radix import themes -from reflex.config import environment, get_config +from reflex.config import EnvironmentVariables, get_config from reflex.event import ( Event, EventHandler, @@ -506,7 +506,10 @@ def add_page( # Check if the route given is valid verify_route_validity(route) - if route in self.unevaluated_pages and environment.RELOAD_CONFIG.is_set(): + if ( + route in self.unevaluated_pages + and EnvironmentVariables.RELOAD_CONFIG.is_set() + ): # when the app is reloaded(typically for app harness tests), we should maintain # the latest render function of a route.This applies typically to decorated pages # since they are only added when app._compile is called. @@ -723,7 +726,7 @@ def _should_compile(self) -> bool: Whether the app should be compiled. """ # Check the environment variable. - if environment.REFLEX_SKIP_COMPILE.get(): + if EnvironmentVariables.REFLEX_SKIP_COMPILE.get(): return False nocompile = prerequisites.get_web_dir() / constants.NOCOMPILE_FILE @@ -946,7 +949,10 @@ def get_compilation_time() -> str: executor = None if ( platform.system() in ("Linux", "Darwin") - and (number_of_processes := environment.REFLEX_COMPILE_PROCESSES.get()) + and ( + number_of_processes + := EnvironmentVariables.REFLEX_COMPILE_PROCESSES.get() + ) is not None ): executor = concurrent.futures.ProcessPoolExecutor( @@ -955,7 +961,7 @@ def get_compilation_time() -> str: ) else: executor = concurrent.futures.ThreadPoolExecutor( - max_workers=environment.REFLEX_COMPILE_THREADS.get() + max_workers=EnvironmentVariables.REFLEX_COMPILE_THREADS.get() ) for route, component in zip(self.pages, page_components): diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 4122a0938a..d6bc533cae 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -16,7 +16,7 @@ CustomComponent, StatefulComponent, ) -from reflex.config import environment, get_config +from reflex.config import EnvironmentVariables, get_config from reflex.state import BaseState from reflex.style import SYSTEM_COLOR_MODE from reflex.utils.exec import is_prod_mode @@ -527,7 +527,7 @@ def remove_tailwind_from_postcss() -> tuple[str, str]: def purge_web_pages_dir(): """Empty out .web/pages directory.""" - if not is_prod_mode() and environment.REFLEX_PERSIST_WEB_DIR.get(): + if not is_prod_mode() and EnvironmentVariables.REFLEX_PERSIST_WEB_DIR.get(): # Skip purging the web directory in dev mode if REFLEX_PERSIST_WEB_DIR is set. return diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index fe8845e8f1..377fe9afad 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -13,7 +13,7 @@ ) from reflex.components.el.elements.forms import Input from reflex.components.radix.themes.layout.box import Box -from reflex.config import environment +from reflex.config import EnvironmentVariables from reflex.constants import Dirs from reflex.constants.compiler import Hooks, Imports from reflex.event import ( @@ -132,7 +132,7 @@ def get_upload_dir() -> Path: """ Upload.is_used = True - uploaded_files_dir = environment.REFLEX_UPLOADED_FILES_DIR.get() + uploaded_files_dir = EnvironmentVariables.REFLEX_UPLOADED_FILES_DIR.get() uploaded_files_dir.mkdir(parents=True, exist_ok=True) return uploaded_files_dir diff --git a/reflex/config.py b/reflex/config.py index 9fb02965ed..81e7d3b36d 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -13,6 +13,7 @@ from typing import ( TYPE_CHECKING, Any, + Callable, Dict, Generic, List, @@ -312,26 +313,47 @@ def interpret_env_var_value( T = TypeVar("T") +ENV_VAR_DEFAULT_FACTORY = Callable[[], T] + class EnvVar(Generic[T]): """Environment variable.""" name: str default: Any + default_factory: Optional[ENV_VAR_DEFAULT_FACTORY] type_: T - def __init__(self, name: str, default: Any, type_: T) -> None: + def __init__( + self, + name: str, + default: Any, + default_factory: Optional[ENV_VAR_DEFAULT_FACTORY], + type_: T, + ) -> None: """Initialize the environment variable. Args: name: The environment variable name. default: The default value. + default_factory: The default factory. type_: The type of the value. """ self.name = name self.default = default + self.default_factory = default_factory self.type_ = type_ + def get_default(self) -> T: + """Get the default value. + + Returns: + The default value. + """ + if self.default_factory is not None: + return self.default_factory() + return self.default + def interpret(self, value: str) -> T: """Interpret the environment variable value. @@ -371,7 +393,7 @@ def get(self) -> T: env_value = self.getenv() if env_value is not None: return env_value - return self.default + return self.get_default() def set(self, value: T | None) -> None: """Set the environment variable. None unsets the variable. @@ -392,16 +414,24 @@ class env_var: # type: ignore name: str default: Any + default_factory: Optional[ENV_VAR_DEFAULT_FACTORY] internal: bool = False - def __init__(self, default: Any, internal: bool = False) -> None: + def __init__( + self, + default: Any = None, + default_factory: Optional[ENV_VAR_DEFAULT_FACTORY] = None, + internal: bool = False, + ) -> None: """Initialize the descriptor. Args: default: The default value. + default_factory: The default factory. internal: Whether the environment variable is reflex internal. """ self.default = default + self.default_factory = default_factory self.internal = internal def __set_name__(self, owner, name): @@ -427,22 +457,30 @@ def __get__(self, instance, owner): env_name = self.name if self.internal: env_name = f"__{env_name}" - return EnvVar(name=env_name, default=self.default, type_=type_) + return EnvVar( + name=env_name, + default=self.default, + type_=type_, + default_factory=self.default_factory, + ) + if TYPE_CHECKING: -if TYPE_CHECKING: + def __new__( + cls, + default: Optional[T] = None, + default_factory: Optional[ENV_VAR_DEFAULT_FACTORY[T]] = None, + internal: bool = False, + ) -> EnvVar[T]: + """Create a new EnvVar instance. - def env_var(default, internal=False) -> EnvVar: - """Typing helper for the env_var descriptor. - - Args: - default: The default value. - internal: Whether the environment variable is reflex internal. - - Returns: - The EnvVar instance. - """ - return default + Args: + cls: The class. + default: The default value. + default_factory: The default factory. + internal: Whether the environment variable is reflex internal. + """ + ... class PathExistsFlag: @@ -455,6 +493,16 @@ class PathExistsFlag: class EnvironmentVariables: """Environment variables class to instantiate environment variables.""" + def __init__(self): + """Initialize the environment variables. + + Raises: + NotImplementedError: Always. + """ + raise NotImplementedError( + f"{type(self).__name__} is a class singleton and not meant to be instantiated." + ) + # Whether to use npm over bun to install frontend packages. REFLEX_USE_NPM: EnvVar[bool] = env_var(False) @@ -545,11 +593,13 @@ class EnvironmentVariables: # Where to save screenshots when tests fail. SCREENSHOT_DIR: EnvVar[Optional[Path]] = env_var(None) - # Whether to minify state names. - REFLEX_MINIFY_STATES: EnvVar[Optional[bool]] = env_var(False) - - -environment = EnvironmentVariables() + # Whether to minify state names. Default to true in prod mode and false otherwise. + REFLEX_MINIFY_STATES: EnvVar[Optional[bool]] = env_var( + default_factory=lambda: ( + EnvironmentVariables.REFLEX_ENV_MODE.get() == constants.Env.PROD + ) + or False + ) class Config(Base): diff --git a/reflex/constants/base.py b/reflex/constants/base.py index 6ec73cdf0e..59505884c4 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -109,10 +109,10 @@ def REFLEX_BUILD_URL(cls): Returns: The URL to redirect to reflex.build. """ - from reflex.config import environment + from reflex.config import EnvironmentVariables return ( - environment.REFLEX_BUILD_FRONTEND.get() + EnvironmentVariables.REFLEX_BUILD_FRONTEND.get() + "/gen?reflex_init_token={reflex_init_token}" ) @@ -124,9 +124,12 @@ def REFLEX_BUILD_POLL_URL(cls): Returns: The URL to poll waiting for the user to select a generation. """ - from reflex.config import environment + from reflex.config import EnvironmentVariables - return environment.REFLEX_BUILD_BACKEND.get() + "/api/init/{reflex_init_token}" + return ( + EnvironmentVariables.REFLEX_BUILD_BACKEND.get() + + "/api/init/{reflex_init_token}" + ) @classproperty @classmethod @@ -136,10 +139,10 @@ def REFLEX_BUILD_CODE_URL(cls): Returns: The URL to fetch the generation's reflex code. """ - from reflex.config import environment + from reflex.config import EnvironmentVariables return ( - environment.REFLEX_BUILD_BACKEND.get() + EnvironmentVariables.REFLEX_BUILD_BACKEND.get() + "/api/gen/{generation_hash}/refactored" ) diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py index 3fdbf2c485..d2966cd21b 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -5,7 +5,7 @@ from types import SimpleNamespace from reflex.base import Base -from reflex.constants import Dirs, Env +from reflex.constants import Dirs from reflex.utils.imports import ImportVar # The prefix used to create setters for state vars. @@ -14,9 +14,6 @@ # The file used to specify no compilation. NOCOMPILE_FILE = "nocompile" -# The env var to toggle minification of states. -ENV_MINIFY_STATES = "REFLEX_MINIFY_STATES" - class Ext(SimpleNamespace): """Extension used in Reflex.""" @@ -33,22 +30,6 @@ class Ext(SimpleNamespace): EXE = ".exe" -def minify_states() -> bool: - """Whether to minify states. - - Returns: - True if states should be minified. - """ - from reflex.config import environment - - env = environment.REFLEX_MINIFY_STATES.get() - if env is not None: - return env - - # minify states in prod by default - return environment.REFLEX_ENV_MODE.get() == Env.PROD - - class CompileVars(SimpleNamespace): """The variables used during compilation.""" @@ -81,15 +62,6 @@ class CompileVars(SimpleNamespace): # The name of the function for converting a dict to an event. TO_EVENT = "Event" - @classmethod - def MINIFY_STATES(cls) -> bool: - """Whether to minify states. - - Returns: - True if states should be minified. - """ - return minify_states() - class PageNames(SimpleNamespace): """The name of basic pages deployed in NextJS.""" diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index 26a53f2d8a..378027395f 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -61,9 +61,9 @@ def ROOT_PATH(cls): Returns: The directory to store the bun. """ - from reflex.config import environment + from reflex.config import EnvironmentVariables - return environment.REFLEX_DIR.get() / "bun" + return EnvironmentVariables.REFLEX_DIR.get() / "bun" @classproperty @classmethod @@ -98,9 +98,9 @@ def DIR(cls) -> Path: Returns: The directory to store fnm. """ - from reflex.config import environment + from reflex.config import EnvironmentVariables - return environment.REFLEX_DIR.get() / "fnm" + return EnvironmentVariables.REFLEX_DIR.get() / "fnm" @classproperty @classmethod diff --git a/reflex/custom_components/custom_components.py b/reflex/custom_components/custom_components.py index f9ea41d6d7..73e11542cb 100644 --- a/reflex/custom_components/custom_components.py +++ b/reflex/custom_components/custom_components.py @@ -17,7 +17,7 @@ from tomlkit.exceptions import TOMLKitError from reflex import constants -from reflex.config import environment, get_config +from reflex.config import EnvironmentVariables, get_config from reflex.constants import CustomComponents from reflex.utils import console @@ -609,14 +609,14 @@ def publish( help="The API token to use for authentication on python package repository. If token is provided, no username/password should be provided at the same time", ), username: Optional[str] = typer.Option( - environment.TWINE_USERNAME.get(), + EnvironmentVariables.TWINE_USERNAME.get(), "-u", "--username", show_default="TWINE_USERNAME environment variable value if set", help="The username to use for authentication on python package repository. Username and password must both be provided.", ), password: Optional[str] = typer.Option( - environment.TWINE_PASSWORD.get(), + EnvironmentVariables.TWINE_PASSWORD.get(), "-p", "--password", show_default="TWINE_PASSWORD environment variable value if set", diff --git a/reflex/model.py b/reflex/model.py index 4b070ec678..9252b3a5c0 100644 --- a/reflex/model.py +++ b/reflex/model.py @@ -17,7 +17,7 @@ import sqlalchemy.orm from reflex.base import Base -from reflex.config import environment, get_config +from reflex.config import EnvironmentVariables, get_config from reflex.utils import console from reflex.utils.compat import sqlmodel, sqlmodel_field_has_primary_key @@ -38,12 +38,12 @@ def get_engine(url: str | None = None) -> sqlalchemy.engine.Engine: url = url or conf.db_url if url is None: raise ValueError("No database url configured") - if not environment.ALEMBIC_CONFIG.get().exists(): + if not EnvironmentVariables.ALEMBIC_CONFIG.get().exists(): console.warn( "Database is not initialized, run [bold]reflex db init[/bold] first." ) # Print the SQL queries if the log level is INFO or lower. - echo_db_query = environment.SQLALCHEMY_ECHO.get() + echo_db_query = EnvironmentVariables.SQLALCHEMY_ECHO.get() # Needed for the admin dash on sqlite. connect_args = {"check_same_thread": False} if url.startswith("sqlite") else {} return sqlmodel.create_engine(url, echo=echo_db_query, connect_args=connect_args) @@ -231,7 +231,7 @@ def _alembic_config(): Returns: tuple of (config, script_directory) """ - config = alembic.config.Config(environment.ALEMBIC_CONFIG.get()) + config = alembic.config.Config(EnvironmentVariables.ALEMBIC_CONFIG.get()) return config, alembic.script.ScriptDirectory( config.get_main_option("script_location", default="version"), ) @@ -266,8 +266,8 @@ def _alembic_render_item( def alembic_init(cls): """Initialize alembic for the project.""" alembic.command.init( - config=alembic.config.Config(environment.ALEMBIC_CONFIG.get()), - directory=str(environment.ALEMBIC_CONFIG.get().parent / "alembic"), + config=alembic.config.Config(EnvironmentVariables.ALEMBIC_CONFIG.get()), + directory=str(EnvironmentVariables.ALEMBIC_CONFIG.get().parent / "alembic"), ) @classmethod @@ -287,7 +287,7 @@ def alembic_autogenerate( Returns: True when changes have been detected. """ - if not environment.ALEMBIC_CONFIG.get().exists(): + if not EnvironmentVariables.ALEMBIC_CONFIG.get().exists(): return False config, script_directory = cls._alembic_config() @@ -388,7 +388,7 @@ def migrate(cls, autogenerate: bool = False) -> bool | None: True - indicating the process was successful. None - indicating the process was skipped. """ - if not environment.ALEMBIC_CONFIG.get().exists(): + if not EnvironmentVariables.ALEMBIC_CONFIG.get().exists(): return with cls.get_db_engine().connect() as connection: diff --git a/reflex/reflex.py b/reflex/reflex.py index 6ccba01d32..45ec28c7ee 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -13,7 +13,7 @@ from reflex_cli.utils import dependency from reflex import constants -from reflex.config import environment, get_config +from reflex.config import EnvironmentVariables, get_config from reflex.custom_components.custom_components import custom_components_cli from reflex.state import reset_disk_state_manager from reflex.utils import console, redir, telemetry @@ -160,7 +160,7 @@ def _run( console.set_log_level(loglevel) # Set env mode in the environment - environment.REFLEX_ENV_MODE.set(env) + EnvironmentVariables.REFLEX_ENV_MODE.set(env) # Show system info exec.output_system_info() @@ -277,13 +277,13 @@ def run( False, "--frontend-only", help="Execute only frontend.", - envvar=environment.REFLEX_FRONTEND_ONLY.name, + envvar=EnvironmentVariables.REFLEX_FRONTEND_ONLY.name, ), backend: bool = typer.Option( False, "--backend-only", help="Execute only backend.", - envvar=environment.REFLEX_BACKEND_ONLY.name, + envvar=EnvironmentVariables.REFLEX_BACKEND_ONLY.name, ), frontend_port: str = typer.Option( config.frontend_port, help="Specify a different frontend port." @@ -302,8 +302,8 @@ def run( if frontend and backend: console.error("Cannot use both --frontend-only and --backend-only options.") raise typer.Exit(1) - environment.REFLEX_BACKEND_ONLY.set(backend) - environment.REFLEX_FRONTEND_ONLY.set(frontend) + EnvironmentVariables.REFLEX_BACKEND_ONLY.set(backend) + EnvironmentVariables.REFLEX_FRONTEND_ONLY.set(frontend) _run(env, frontend, backend, frontend_port, backend_port, backend_host, loglevel) @@ -405,7 +405,7 @@ def logout( def _skip_compile(): """Skip the compile step.""" - environment.REFLEX_SKIP_COMPILE.set(True) + EnvironmentVariables.REFLEX_SKIP_COMPILE.set(True) @db_cli.command(name="init") @@ -420,7 +420,7 @@ def db_init(): return # Check the alembic config. - if environment.ALEMBIC_CONFIG.get().exists(): + if EnvironmentVariables.ALEMBIC_CONFIG.get().exists(): console.error( "Database is already initialized. Use " "[bold]reflex db makemigrations[/bold] to create schema change " diff --git a/reflex/state.py b/reflex/state.py index fce6a1464a..d47aa52dad 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -69,7 +69,7 @@ import reflex.istate.dynamic from reflex import constants from reflex.base import Base -from reflex.config import environment +from reflex.config import EnvironmentVariables from reflex.event import ( BACKGROUND_TASK_MARKER, Event, @@ -932,7 +932,7 @@ def get_name(cls) -> str: """ module = cls.__module__.replace(".", "___") state_name = format.to_snake_case(f"{module}___{cls.__name__}") - if constants.compiler.CompileVars.MINIFY_STATES(): + if EnvironmentVariables.REFLEX_MINIFY_STATES.get(): return get_minified_state_name(state_name) return state_name @@ -3435,7 +3435,7 @@ async def _wait_lock(self, lock_key: bytes, lock_id: bytes) -> None: ) except ResponseError: # Some redis servers only allow out-of-band configuration, so ignore errors here. - if not environment.REFLEX_IGNORE_REDIS_CONFIG_ERROR.get(): + if not EnvironmentVariables.REFLEX_IGNORE_REDIS_CONFIG_ERROR.get(): raise async with self.redis.pubsub() as pubsub: await pubsub.psubscribe(lock_key_channel) diff --git a/reflex/testing.py b/reflex/testing.py index 117280f2ef..810a4ed578 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -44,7 +44,7 @@ import reflex.utils.prerequisites import reflex.utils.processes from reflex.components.component import StatefulComponent -from reflex.config import environment +from reflex.config import EnvironmentVariables from reflex.state import ( BaseState, State, @@ -199,7 +199,7 @@ def get_state_name(self, state_cls_name: str) -> str: state_name = reflex.utils.format.to_snake_case( f"{self.app_name}___{self.app_name}___" + state_cls_name ) - if reflex.constants.CompileVars.MINIFY_STATES(): + if EnvironmentVariables.REFLEX_MINIFY_STATES.get(): return minified_state_names.get(state_name, state_name) return state_name @@ -626,10 +626,10 @@ def frontend( if self.frontend_url is None: raise RuntimeError("Frontend is not running.") want_headless = False - if environment.APP_HARNESS_HEADLESS.get(): + if EnvironmentVariables.APP_HARNESS_HEADLESS.get(): want_headless = True if driver_clz is None: - requested_driver = environment.APP_HARNESS_DRIVER.get() + requested_driver = EnvironmentVariables.APP_HARNESS_DRIVER.get() driver_clz = getattr(webdriver, requested_driver) if driver_options is None: driver_options = getattr(webdriver, f"{requested_driver}Options")() @@ -651,7 +651,7 @@ def frontend( driver_options.add_argument("headless") if driver_options is None: raise RuntimeError(f"Could not determine options for {driver_clz}") - if args := environment.APP_HARNESS_DRIVER_ARGS.get(): + if args := EnvironmentVariables.APP_HARNESS_DRIVER_ARGS.get(): for arg in args.split(","): driver_options.add_argument(arg) if driver_option_args is not None: @@ -958,7 +958,7 @@ def _wait_frontend(self): def _start_backend(self): if self.app_instance is None: raise RuntimeError("App was not initialized.") - environment.REFLEX_SKIP_COMPILE.set(True) + EnvironmentVariables.REFLEX_SKIP_COMPILE.set(True) self.backend = uvicorn.Server( uvicorn.Config( app=self.app_instance, @@ -976,7 +976,7 @@ def _poll_for_servers(self, timeout: TimeoutType = None) -> socket.socket: try: return super()._poll_for_servers(timeout) finally: - environment.REFLEX_SKIP_COMPILE.set(None) + EnvironmentVariables.REFLEX_SKIP_COMPILE.set(None) @override def start(self) -> AppHarnessProd: @@ -985,7 +985,7 @@ def start(self) -> AppHarnessProd: Returns: self """ - environment.REFLEX_ENV_MODE.set(reflex.constants.base.Env.PROD) + EnvironmentVariables.REFLEX_ENV_MODE.set(reflex.constants.base.Env.PROD) _ = super().start() return self @@ -997,4 +997,4 @@ def stop(self): self.frontend_server.shutdown() if self.frontend_thread is not None: self.frontend_thread.join() - environment.REFLEX_ENV_MODE.set(None) + EnvironmentVariables.REFLEX_ENV_MODE.set(None) diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index fb613810a7..17cfc23b91 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -15,7 +15,7 @@ import psutil from reflex import constants -from reflex.config import environment, get_config +from reflex.config import EnvironmentVariables, get_config from reflex.constants.base import LogLevel from reflex.utils import console, path_ops from reflex.utils.prerequisites import get_web_dir @@ -184,7 +184,7 @@ def should_use_granian(): Returns: True if Granian should be used. """ - return environment.REFLEX_USE_GRANIAN.get() + return EnvironmentVariables.REFLEX_USE_GRANIAN.get() def get_app_module(): @@ -370,7 +370,7 @@ def run_uvicorn_backend_prod(host, port, loglevel): run=True, show_logs=True, env={ - environment.REFLEX_SKIP_COMPILE.name: "true" + EnvironmentVariables.REFLEX_SKIP_COMPILE.name: "true" }, # skip compile for prod backend ) @@ -407,7 +407,7 @@ def run_granian_backend_prod(host, port, loglevel): run=True, show_logs=True, env={ - environment.REFLEX_SKIP_COMPILE.name: "true" + EnvironmentVariables.REFLEX_SKIP_COMPILE.name: "true" }, # skip compile for prod backend ) except ImportError: @@ -493,7 +493,7 @@ def is_prod_mode() -> bool: Returns: True if the app is running in production mode or False if running in dev mode. """ - current_mode = environment.REFLEX_ENV_MODE.get() + current_mode = EnvironmentVariables.REFLEX_ENV_MODE.get() return current_mode == constants.Env.PROD @@ -509,7 +509,7 @@ def is_frontend_only() -> bool: deprecation_version="0.6.5", removal_version="0.7.0", ) - return environment.REFLEX_FRONTEND_ONLY.get() + return EnvironmentVariables.REFLEX_FRONTEND_ONLY.get() def is_backend_only() -> bool: @@ -524,7 +524,7 @@ def is_backend_only() -> bool: deprecation_version="0.6.5", removal_version="0.7.0", ) - return environment.REFLEX_BACKEND_ONLY.get() + return EnvironmentVariables.REFLEX_BACKEND_ONLY.get() def should_skip_compile() -> bool: @@ -539,4 +539,4 @@ def should_skip_compile() -> bool: deprecation_version="0.6.5", removal_version="0.7.0", ) - return environment.REFLEX_SKIP_COMPILE.get() + return EnvironmentVariables.REFLEX_SKIP_COMPILE.get() diff --git a/reflex/utils/net.py b/reflex/utils/net.py index acc2029124..f664bf9b60 100644 --- a/reflex/utils/net.py +++ b/reflex/utils/net.py @@ -2,7 +2,7 @@ import httpx -from ..config import environment +from ..config import EnvironmentVariables from . import console @@ -12,7 +12,7 @@ def _httpx_verify_kwarg() -> bool: Returns: True if SSL verification is enabled, False otherwise """ - return not environment.SSL_NO_VERIFY.get() + return not EnvironmentVariables.SSL_NO_VERIFY.get() def get(url: str, **kwargs) -> httpx.Response: diff --git a/reflex/utils/path_ops.py b/reflex/utils/path_ops.py index a2ba2b1512..de3dbe32c1 100644 --- a/reflex/utils/path_ops.py +++ b/reflex/utils/path_ops.py @@ -9,7 +9,7 @@ from pathlib import Path from reflex import constants -from reflex.config import environment +from reflex.config import EnvironmentVariables # Shorthand for join. join = os.linesep.join @@ -136,7 +136,7 @@ def use_system_node() -> bool: Returns: Whether the system node should be used. """ - return environment.REFLEX_USE_SYSTEM_NODE.get() + return EnvironmentVariables.REFLEX_USE_SYSTEM_NODE.get() def use_system_bun() -> bool: @@ -145,7 +145,7 @@ def use_system_bun() -> bool: Returns: Whether the system bun should be used. """ - return environment.REFLEX_USE_SYSTEM_BUN.get() + return EnvironmentVariables.REFLEX_USE_SYSTEM_BUN.get() def get_node_bin_path() -> Path | None: diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 6ceea4b051..a00aa6e754 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -33,7 +33,7 @@ from reflex import constants, model from reflex.compiler import templates -from reflex.config import Config, environment, get_config +from reflex.config import Config, EnvironmentVariables, get_config from reflex.utils import console, net, path_ops, processes from reflex.utils.exceptions import GeneratedCodeHasNoFunctionDefs from reflex.utils.format import format_library_name @@ -69,7 +69,7 @@ def get_web_dir() -> Path: Returns: The working directory. """ - return environment.REFLEX_WEB_WORKDIR.get() + return EnvironmentVariables.REFLEX_WEB_WORKDIR.get() def _python_version_check(): @@ -260,7 +260,7 @@ def windows_npm_escape_hatch() -> bool: Returns: If the user has set REFLEX_USE_NPM. """ - return environment.REFLEX_USE_NPM.get() + return EnvironmentVariables.REFLEX_USE_NPM.get() def get_app(reload: bool = False) -> ModuleType: @@ -278,7 +278,7 @@ def get_app(reload: bool = False) -> ModuleType: from reflex.utils import telemetry try: - environment.RELOAD_CONFIG.set(reload) + EnvironmentVariables.RELOAD_CONFIG.set(reload) config = get_config() if not config.app_name: raise RuntimeError( @@ -1019,7 +1019,7 @@ def needs_reinit(frontend: bool = True) -> bool: return False # Make sure the .reflex directory exists. - if not environment.REFLEX_DIR.get().exists(): + if not EnvironmentVariables.REFLEX_DIR.get().exists(): return True # Make sure the .web directory exists in frontend mode. @@ -1124,7 +1124,7 @@ def ensure_reflex_installation_id() -> Optional[int]: """ try: initialize_reflex_user_directory() - installation_id_file = environment.REFLEX_DIR.get() / "installation_id" + installation_id_file = EnvironmentVariables.REFLEX_DIR.get() / "installation_id" installation_id = None if installation_id_file.exists(): @@ -1149,7 +1149,7 @@ def ensure_reflex_installation_id() -> Optional[int]: def initialize_reflex_user_directory(): """Initialize the reflex user directory.""" # Create the reflex directory. - path_ops.mkdir(environment.REFLEX_DIR.get()) + path_ops.mkdir(EnvironmentVariables.REFLEX_DIR.get()) def initialize_frontend_dependencies(): @@ -1174,7 +1174,7 @@ def check_db_initialized() -> bool: """ if ( get_config().db_url is not None - and not environment.ALEMBIC_CONFIG.get().exists() + and not EnvironmentVariables.ALEMBIC_CONFIG.get().exists() ): console.error( "Database is not initialized. Run [bold]reflex db init[/bold] first." @@ -1185,7 +1185,10 @@ def check_db_initialized() -> bool: def check_schema_up_to_date(): """Check if the sqlmodel metadata matches the current database schema.""" - if get_config().db_url is None or not environment.ALEMBIC_CONFIG.get().exists(): + if ( + get_config().db_url is None + or not EnvironmentVariables.ALEMBIC_CONFIG.get().exists() + ): return with model.Model.get_db_engine().connect() as connection: try: diff --git a/reflex/utils/registry.py b/reflex/utils/registry.py index d98178c618..988186aa17 100644 --- a/reflex/utils/registry.py +++ b/reflex/utils/registry.py @@ -2,7 +2,7 @@ import httpx -from reflex.config import environment +from reflex.config import EnvironmentVariables from reflex.utils import console, net @@ -55,4 +55,4 @@ def _get_npm_registry() -> str: Returns: str: """ - return environment.NPM_CONFIG_REGISTRY.get() or get_best_registry() + return EnvironmentVariables.NPM_CONFIG_REGISTRY.get() or get_best_registry() diff --git a/reflex/utils/telemetry.py b/reflex/utils/telemetry.py index 806b916fcb..1d265e9ab0 100644 --- a/reflex/utils/telemetry.py +++ b/reflex/utils/telemetry.py @@ -8,7 +8,7 @@ import platform import warnings -from reflex.config import environment +from reflex.config import EnvironmentVariables try: from datetime import UTC, datetime @@ -95,7 +95,7 @@ def _raise_on_missing_project_hash() -> bool: False when compilation should be skipped (i.e. no .web directory is required). Otherwise return True. """ - return not environment.REFLEX_SKIP_COMPILE.get() + return not EnvironmentVariables.REFLEX_SKIP_COMPILE.get() def _prepare_event(event: str, **kwargs) -> dict: diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 5c456ec75e..712c30a4ea 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -8,7 +8,7 @@ import pytest import reflex.constants -from reflex.config import environment +from reflex.config import EnvironmentVariables from reflex.testing import AppHarness, AppHarnessProd DISPLAY = None @@ -24,7 +24,10 @@ def xvfb(): Yields: the pyvirtualdisplay object that the browser will be open on """ - if os.environ.get("GITHUB_ACTIONS") and not environment.APP_HARNESS_HEADLESS.get(): + if ( + os.environ.get("GITHUB_ACTIONS") + and not EnvironmentVariables.APP_HARNESS_HEADLESS.get() + ): from pyvirtualdisplay.smartdisplay import ( # pyright: ignore [reportMissingImports] SmartDisplay, ) @@ -45,7 +48,7 @@ def pytest_exception_interact(node, call, report): call: The pytest call describing when/where the test was invoked. report: The pytest log report object. """ - screenshot_dir = environment.SCREENSHOT_DIR.get() + screenshot_dir = EnvironmentVariables.SCREENSHOT_DIR.get() if DISPLAY is None or screenshot_dir is None: return @@ -89,7 +92,7 @@ def app_harness_env( """ harness: Type[AppHarness] = request.param if issubclass(harness, AppHarnessProd): - environment.REFLEX_ENV_MODE.set(reflex.constants.Env.PROD) + EnvironmentVariables.REFLEX_ENV_MODE.set(reflex.constants.Env.PROD) yield harness if issubclass(harness, AppHarnessProd): - environment.REFLEX_ENV_MODE.set(None) + EnvironmentVariables.REFLEX_ENV_MODE.set(None) diff --git a/tests/integration/test_minified_states.py b/tests/integration/test_minified_states.py index dc6c275f1b..37c5c8a0fd 100644 --- a/tests/integration/test_minified_states.py +++ b/tests/integration/test_minified_states.py @@ -2,7 +2,6 @@ from __future__ import annotations -import os from functools import partial from typing import Generator, Optional, Type @@ -10,7 +9,7 @@ from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver -from reflex.constants.compiler import ENV_MINIFY_STATES +from reflex.config import EnvironmentVariables from reflex.testing import AppHarness, AppHarnessProd @@ -63,13 +62,9 @@ def minify_state_env( minify_states: whether to minify state names """ minify_states: Optional[bool] = request.param - if minify_states is None: - _ = os.environ.pop(ENV_MINIFY_STATES, None) - else: - os.environ[ENV_MINIFY_STATES] = str(minify_states).lower() + EnvironmentVariables.REFLEX_MINIFY_STATES.set(minify_states) yield minify_states - if minify_states is not None: - os.environ.pop(ENV_MINIFY_STATES, None) + EnvironmentVariables.REFLEX_MINIFY_STATES.set(None) @pytest.fixture diff --git a/tests/units/test_config.py b/tests/units/test_config.py index e5d4622bd6..b0603aec65 100644 --- a/tests/units/test_config.py +++ b/tests/units/test_config.py @@ -8,9 +8,9 @@ import reflex as rx import reflex.config from reflex.config import ( + EnvironmentVariables, EnvVar, env_var, - environment, interpret_boolean_env, interpret_enum_env, interpret_int_env, @@ -216,7 +216,7 @@ def test_replace_defaults( def reflex_dir_constant() -> Path: - return environment.REFLEX_DIR.get() + return EnvironmentVariables.REFLEX_DIR.get() def test_reflex_dir_env_var(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: @@ -253,6 +253,11 @@ class TestEnv: INTERNAL: EnvVar[str] = env_var("default", internal=True) BOOLEAN: EnvVar[bool] = env_var(False) + # default_factory with other env_var as fallback + BLUBB_OR_BLA: EnvVar[str] = env_var( + default_factory=lambda: TestEnv.BLUBB.getenv() or "bla" + ) + assert TestEnv.BLUBB.get() == "default" assert TestEnv.BLUBB.name == "BLUBB" TestEnv.BLUBB.set("new") @@ -280,3 +285,15 @@ class TestEnv: assert TestEnv.BOOLEAN.get() is False TestEnv.BOOLEAN.set(None) assert "BOOLEAN" not in os.environ + + assert TestEnv.BLUBB_OR_BLA.get() == "bla" + TestEnv.BLUBB.set("new") + assert TestEnv.BLUBB_OR_BLA.get() == "new" + TestEnv.BLUBB.set(None) + assert TestEnv.BLUBB_OR_BLA.get() == "bla" + TestEnv.BLUBB_OR_BLA.set("test") + assert TestEnv.BLUBB_OR_BLA.get() == "test" + TestEnv.BLUBB.set("other") + assert TestEnv.BLUBB_OR_BLA.get() == "test" + TestEnv.BLUBB_OR_BLA.set(None) + TestEnv.BLUBB.set(None) diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index abe3dd5fc7..f7a64b4cfb 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -10,7 +10,7 @@ from reflex import constants from reflex.base import Base -from reflex.config import environment +from reflex.config import EnvironmentVariables from reflex.event import EventHandler from reflex.state import BaseState from reflex.utils import ( @@ -598,7 +598,7 @@ def test_style_prop_with_event_handler_value(callable): def test_is_prod_mode() -> None: """Test that the prod mode is correctly determined.""" - environment.REFLEX_ENV_MODE.set(constants.Env.PROD) + EnvironmentVariables.REFLEX_ENV_MODE.set(constants.Env.PROD) assert utils_exec.is_prod_mode() - environment.REFLEX_ENV_MODE.set(None) + EnvironmentVariables.REFLEX_ENV_MODE.set(None) assert not utils_exec.is_prod_mode() From 4116ab9f66f08ea2dd3914a670e874bc8438d4c7 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Tue, 5 Nov 2024 22:34:59 +0100 Subject: [PATCH 20/31] do not set appharness env twice, already done in AppHarnessProd --- tests/integration/conftest.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 712c30a4ea..337f4e1749 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -3,7 +3,7 @@ import os import re from pathlib import Path -from typing import Generator, Type +from typing import Type import pytest @@ -81,18 +81,14 @@ def pytest_exception_interact(node, call, report): ) def app_harness_env( request: pytest.FixtureRequest, -) -> Generator[Type[AppHarness], None, None]: +) -> Type[AppHarness]: """Parametrize the AppHarness class to use for the test, either dev or prod. Args: request: The pytest fixture request object. - Yields: + Returns: The AppHarness class to use for the test. """ harness: Type[AppHarness] = request.param - if issubclass(harness, AppHarnessProd): - EnvironmentVariables.REFLEX_ENV_MODE.set(reflex.constants.Env.PROD) - yield harness - if issubclass(harness, AppHarnessProd): - EnvironmentVariables.REFLEX_ENV_MODE.set(None) + return harness From 84c14a189fccb39815971c2b0ad4f00a9e0effb2 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Tue, 5 Nov 2024 22:35:47 +0100 Subject: [PATCH 21/31] cleanup conflicts --- reflex/testing.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/reflex/testing.py b/reflex/testing.py index 810a4ed578..fa01743643 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -43,7 +43,6 @@ import reflex.utils.format import reflex.utils.prerequisites import reflex.utils.processes -from reflex.components.component import StatefulComponent from reflex.config import EnvironmentVariables from reflex.state import ( BaseState, @@ -282,8 +281,6 @@ def _initialize_app(self): ) self.app_module_path.write_text(source_code) with chdir(self.app_path): - # Reset stateful component cache for new app - StatefulComponent.tag_to_stateful_component.clear() # ensure config and app are reloaded when testing different app reflex.config.get_config(reload=True) # Save decorated pages before importing the test app module From 24caf5fef14661818a53bdb4735cff6bef81a59c Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 6 Nov 2024 01:32:12 +0100 Subject: [PATCH 22/31] cleanup unnecessary or False --- reflex/config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/reflex/config.py b/reflex/config.py index 81e7d3b36d..ac7ae34b84 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -598,7 +598,6 @@ def __init__(self): default_factory=lambda: ( EnvironmentVariables.REFLEX_ENV_MODE.get() == constants.Env.PROD ) - or False ) From f94328f74f8254d7c24ed36a596f3099cbb7c11d Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 6 Nov 2024 23:58:01 +0100 Subject: [PATCH 23/31] delete unused function, add cleanup fixture for test_is_prod_mode --- reflex/config.py | 27 ++------------------------- reflex/reflex.py | 1 + tests/units/utils/test_utils.py | 18 ++++++++++++++++-- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/reflex/config.py b/reflex/config.py index ac7ae34b84..b12487f4bd 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -148,28 +148,6 @@ def get_url(self) -> str: return f"{self.engine}://{path}/{self.database}" -def get_default_value_for_field(field: dataclasses.Field) -> Any: - """Get the default value for a field. - - Args: - field: The field. - - Returns: - The default value. - - Raises: - ValueError: If no default value is found. - """ - if field.default != dataclasses.MISSING: - return field.default - elif field.default_factory != dataclasses.MISSING: - return field.default_factory() - else: - raise ValueError( - f"Missing value for environment variable {field.name} and no default value found" - ) - - # TODO: Change all interpret_.* signatures to value: str, field: dataclasses.Field once we migrate rx.Config to dataclasses def interpret_boolean_env(value: str, field_name: str) -> bool: """Interpret a boolean environment variable value. @@ -595,9 +573,8 @@ def __init__(self): # Whether to minify state names. Default to true in prod mode and false otherwise. REFLEX_MINIFY_STATES: EnvVar[Optional[bool]] = env_var( - default_factory=lambda: ( - EnvironmentVariables.REFLEX_ENV_MODE.get() == constants.Env.PROD - ) + default_factory=lambda: EnvironmentVariables.REFLEX_ENV_MODE.get() + == constants.Env.PROD ) diff --git a/reflex/reflex.py b/reflex/reflex.py index ab5467759e..d2340f5747 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -304,6 +304,7 @@ def run( raise typer.Exit(1) EnvironmentVariables.REFLEX_BACKEND_ONLY.set(backend) EnvironmentVariables.REFLEX_FRONTEND_ONLY.set(frontend) + EnvironmentVariables.REFLEX_ENV_MODE.set(env) _run(env, frontend, backend, frontend_port, backend_port, backend_host, loglevel) diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index 83482fb255..7c9449cda0 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -596,8 +596,22 @@ def test_style_prop_with_event_handler_value(callable): ) -def test_is_prod_mode() -> None: - """Test that the prod mode is correctly determined.""" +@pytest.fixture +def cleanup_reflex_env_mode(): + """Cleanup the reflex env mode. + + Yields: None + """ + yield + EnvironmentVariables.REFLEX_ENV_MODE.set(None) + + +def test_is_prod_mode(cleanup_reflex_env_mode: None) -> None: + """Test that the prod mode is correctly determined. + + Args: + cleanup_reflex_env_mode: Fixture to cleanup the reflex env mode. + """ EnvironmentVariables.REFLEX_ENV_MODE.set(constants.Env.PROD) assert utils_exec.is_prod_mode() EnvironmentVariables.REFLEX_ENV_MODE.set(None) From 6ce9471e6feb05b280623d7ad19e4d7dca0ccb6e Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Thu, 7 Nov 2024 00:03:04 +0100 Subject: [PATCH 24/31] set env mode before importing modules that contain rx.State subclasses --- reflex/reflex.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/reflex/reflex.py b/reflex/reflex.py index d2340f5747..a7443e56c1 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -2,6 +2,7 @@ from __future__ import annotations +# WARNING: do not import any modules that contain rx.State subclasses here import atexit import os from pathlib import Path @@ -15,7 +16,6 @@ from reflex import constants from reflex.config import EnvironmentVariables, get_config from reflex.custom_components.custom_components import custom_components_cli -from reflex.state import reset_disk_state_manager from reflex.utils import console, redir, telemetry # Disable typer+rich integration for help panels @@ -154,14 +154,17 @@ def _run( loglevel: constants.LogLevel = config.loglevel, ): """Run the app in the given directory.""" + + # Set env mode in the environment + # This must be set before importing modules that contain rx.State subclasses + EnvironmentVariables.REFLEX_ENV_MODE.set(env) + + from reflex.state import reset_disk_state_manager from reflex.utils import build, exec, prerequisites, processes # Set the log level. console.set_log_level(loglevel) - # Set env mode in the environment - EnvironmentVariables.REFLEX_ENV_MODE.set(env) - # Show system info exec.output_system_info() @@ -304,7 +307,6 @@ def run( raise typer.Exit(1) EnvironmentVariables.REFLEX_BACKEND_ONLY.set(backend) EnvironmentVariables.REFLEX_FRONTEND_ONLY.set(frontend) - EnvironmentVariables.REFLEX_ENV_MODE.set(env) _run(env, frontend, backend, frontend_port, backend_port, backend_host, loglevel) From 74c336f2941bf9378b77c1b004873d54449865f0 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Thu, 7 Nov 2024 01:06:10 +0100 Subject: [PATCH 25/31] forgot ruffing --- reflex/config.py | 1 - reflex/reflex.py | 1 - 2 files changed, 2 deletions(-) diff --git a/reflex/config.py b/reflex/config.py index b12487f4bd..05177f011b 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -2,7 +2,6 @@ from __future__ import annotations -import dataclasses import enum import importlib import inspect diff --git a/reflex/reflex.py b/reflex/reflex.py index a7443e56c1..1ff84e4351 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -154,7 +154,6 @@ def _run( loglevel: constants.LogLevel = config.loglevel, ): """Run the app in the given directory.""" - # Set env mode in the environment # This must be set before importing modules that contain rx.State subclasses EnvironmentVariables.REFLEX_ENV_MODE.set(env) From 2e12feb1208a65aadc86391d9435bb35c935a6cd Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Fri, 8 Nov 2024 22:34:47 +0100 Subject: [PATCH 26/31] darglinting it --- tests/units/utils/test_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index 7c9449cda0..435089e227 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -600,7 +600,8 @@ def test_style_prop_with_event_handler_value(callable): def cleanup_reflex_env_mode(): """Cleanup the reflex env mode. - Yields: None + Yields: + None """ yield EnvironmentVariables.REFLEX_ENV_MODE.set(None) From a639f526dade02252f9b64ced9bccdecc7f3a214 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 12 Nov 2024 20:00:02 -0800 Subject: [PATCH 27/31] add typing to function vars (#4372) * add typing to function vars * import ParamSpec from typing_extensions * remove ellipsis as they are not supported in 3.9 * try importing everything from extensions * special case 3.9 * don't use Any from extensions * get typevar from extensions --- reflex/event.py | 9 +- reflex/utils/telemetry.py | 3 +- reflex/vars/base.py | 22 ++- reflex/vars/function.py | 320 +++++++++++++++++++++++++++++++++----- tests/units/test_var.py | 2 +- 5 files changed, 309 insertions(+), 47 deletions(-) diff --git a/reflex/event.py b/reflex/event.py index 85a2541a59..312c9887f6 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -45,6 +45,8 @@ from reflex.vars.base import LiteralVar, Var from reflex.vars.function import ( ArgsFunctionOperation, + ArgsFunctionOperationBuilder, + BuilderFunctionVar, FunctionArgs, FunctionStringVar, FunctionVar, @@ -797,8 +799,7 @@ def scroll_to(elem_id: str, align_to_top: bool | Var[bool] = True) -> EventSpec: get_element_by_id = FunctionStringVar.create("document.getElementById") return run_script( - get_element_by_id(elem_id) - .call(elem_id) + get_element_by_id.call(elem_id) .to(ObjectVar) .scrollIntoView.to(FunctionVar) .call(align_to_top), @@ -1580,7 +1581,7 @@ def create( ) -class EventChainVar(FunctionVar, python_types=EventChain): +class EventChainVar(BuilderFunctionVar, python_types=EventChain): """Base class for event chain vars.""" @@ -1592,7 +1593,7 @@ class EventChainVar(FunctionVar, python_types=EventChain): # Note: LiteralVar is second in the inheritance list allowing it act like a # CachedVarOperation (ArgsFunctionOperation) and get the _js_expr from the # _cached_var_name property. -class LiteralEventChainVar(ArgsFunctionOperation, LiteralVar, EventChainVar): +class LiteralEventChainVar(ArgsFunctionOperationBuilder, LiteralVar, EventChainVar): """A literal event chain var.""" _var_value: EventChain = dataclasses.field(default=None) # type: ignore diff --git a/reflex/utils/telemetry.py b/reflex/utils/telemetry.py index 1d265e9ab0..2a596ea5cc 100644 --- a/reflex/utils/telemetry.py +++ b/reflex/utils/telemetry.py @@ -51,7 +51,8 @@ def get_python_version() -> str: Returns: The Python version. """ - return platform.python_version() + # Remove the "+" from the version string in case user is using a pre-release version. + return platform.python_version().rstrip("+") def get_reflex_version() -> str: diff --git a/reflex/vars/base.py b/reflex/vars/base.py index b9aa55eb37..200f693def 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -361,21 +361,29 @@ def _var_is_string(self) -> bool: return False def __init_subclass__( - cls, python_types: Tuple[GenericType, ...] | GenericType = types.Unset, **kwargs + cls, + python_types: Tuple[GenericType, ...] | GenericType = types.Unset(), + default_type: GenericType = types.Unset(), + **kwargs, ): """Initialize the subclass. Args: python_types: The python types that the var represents. + default_type: The default type of the var. Defaults to the first python type. **kwargs: Additional keyword arguments. """ super().__init_subclass__(**kwargs) - if python_types is not types.Unset: + if python_types or default_type: python_types = ( - python_types if isinstance(python_types, tuple) else (python_types,) + (python_types if isinstance(python_types, tuple) else (python_types,)) + if python_types + else () ) + default_type = default_type or (python_types[0] if python_types else Any) + @dataclasses.dataclass( eq=False, frozen=True, @@ -388,7 +396,7 @@ class ToVarOperation(ToOperation, cls): default=Var(_js_expr="null", _var_type=None), ) - _default_var_type: ClassVar[GenericType] = python_types[0] + _default_var_type: ClassVar[GenericType] = default_type ToVarOperation.__name__ = f'To{cls.__name__.removesuffix("Var")}Operation' @@ -588,6 +596,12 @@ def to( output: type[list] | type[tuple] | type[set], ) -> ArrayVar: ... + @overload + def to( + self, + output: type[dict], + ) -> ObjectVar[dict]: ... + @overload def to( self, output: Type[ObjectVar], var_type: Type[VAR_INSIDE] diff --git a/reflex/vars/function.py b/reflex/vars/function.py index 98f3b23358..c65b38f707 100644 --- a/reflex/vars/function.py +++ b/reflex/vars/function.py @@ -4,32 +4,177 @@ import dataclasses import sys -from typing import Any, Callable, Optional, Sequence, Tuple, Type, Union +from typing import Any, Callable, Optional, Sequence, Tuple, Type, Union, overload + +from typing_extensions import Concatenate, Generic, ParamSpec, Protocol, TypeVar from reflex.utils import format from reflex.utils.types import GenericType from .base import CachedVarOperation, LiteralVar, Var, VarData, cached_property_no_lock +P = ParamSpec("P") +V1 = TypeVar("V1") +V2 = TypeVar("V2") +V3 = TypeVar("V3") +V4 = TypeVar("V4") +V5 = TypeVar("V5") +V6 = TypeVar("V6") +R = TypeVar("R") + + +class ReflexCallable(Protocol[P, R]): + """Protocol for a callable.""" + + __call__: Callable[P, R] + -class FunctionVar(Var[Callable], python_types=Callable): +CALLABLE_TYPE = TypeVar("CALLABLE_TYPE", bound=ReflexCallable, infer_variance=True) +OTHER_CALLABLE_TYPE = TypeVar( + "OTHER_CALLABLE_TYPE", bound=ReflexCallable, infer_variance=True +) + + +class FunctionVar(Var[CALLABLE_TYPE], default_type=ReflexCallable[Any, Any]): """Base class for immutable function vars.""" - def __call__(self, *args: Var | Any) -> ArgsFunctionOperation: - """Call the function with the given arguments. + @overload + def partial(self) -> FunctionVar[CALLABLE_TYPE]: ... + + @overload + def partial( + self: FunctionVar[ReflexCallable[Concatenate[V1, P], R]], + arg1: Union[V1, Var[V1]], + ) -> FunctionVar[ReflexCallable[P, R]]: ... + + @overload + def partial( + self: FunctionVar[ReflexCallable[Concatenate[V1, V2, P], R]], + arg1: Union[V1, Var[V1]], + arg2: Union[V2, Var[V2]], + ) -> FunctionVar[ReflexCallable[P, R]]: ... + + @overload + def partial( + self: FunctionVar[ReflexCallable[Concatenate[V1, V2, V3, P], R]], + arg1: Union[V1, Var[V1]], + arg2: Union[V2, Var[V2]], + arg3: Union[V3, Var[V3]], + ) -> FunctionVar[ReflexCallable[P, R]]: ... + + @overload + def partial( + self: FunctionVar[ReflexCallable[Concatenate[V1, V2, V3, V4, P], R]], + arg1: Union[V1, Var[V1]], + arg2: Union[V2, Var[V2]], + arg3: Union[V3, Var[V3]], + arg4: Union[V4, Var[V4]], + ) -> FunctionVar[ReflexCallable[P, R]]: ... + + @overload + def partial( + self: FunctionVar[ReflexCallable[Concatenate[V1, V2, V3, V4, V5, P], R]], + arg1: Union[V1, Var[V1]], + arg2: Union[V2, Var[V2]], + arg3: Union[V3, Var[V3]], + arg4: Union[V4, Var[V4]], + arg5: Union[V5, Var[V5]], + ) -> FunctionVar[ReflexCallable[P, R]]: ... + + @overload + def partial( + self: FunctionVar[ReflexCallable[Concatenate[V1, V2, V3, V4, V5, V6, P], R]], + arg1: Union[V1, Var[V1]], + arg2: Union[V2, Var[V2]], + arg3: Union[V3, Var[V3]], + arg4: Union[V4, Var[V4]], + arg5: Union[V5, Var[V5]], + arg6: Union[V6, Var[V6]], + ) -> FunctionVar[ReflexCallable[P, R]]: ... + + @overload + def partial( + self: FunctionVar[ReflexCallable[P, R]], *args: Var | Any + ) -> FunctionVar[ReflexCallable[P, R]]: ... + + @overload + def partial(self, *args: Var | Any) -> FunctionVar: ... + + def partial(self, *args: Var | Any) -> FunctionVar: # type: ignore + """Partially apply the function with the given arguments. Args: - *args: The arguments to call the function with. + *args: The arguments to partially apply the function with. Returns: - The function call operation. + The partially applied function. """ + if not args: + return ArgsFunctionOperation.create((), self) return ArgsFunctionOperation.create( ("...args",), VarOperationCall.create(self, *args, Var(_js_expr="...args")), ) - def call(self, *args: Var | Any) -> VarOperationCall: + @overload + def call( + self: FunctionVar[ReflexCallable[[V1], R]], arg1: Union[V1, Var[V1]] + ) -> VarOperationCall[[V1], R]: ... + + @overload + def call( + self: FunctionVar[ReflexCallable[[V1, V2], R]], + arg1: Union[V1, Var[V1]], + arg2: Union[V2, Var[V2]], + ) -> VarOperationCall[[V1, V2], R]: ... + + @overload + def call( + self: FunctionVar[ReflexCallable[[V1, V2, V3], R]], + arg1: Union[V1, Var[V1]], + arg2: Union[V2, Var[V2]], + arg3: Union[V3, Var[V3]], + ) -> VarOperationCall[[V1, V2, V3], R]: ... + + @overload + def call( + self: FunctionVar[ReflexCallable[[V1, V2, V3, V4], R]], + arg1: Union[V1, Var[V1]], + arg2: Union[V2, Var[V2]], + arg3: Union[V3, Var[V3]], + arg4: Union[V4, Var[V4]], + ) -> VarOperationCall[[V1, V2, V3, V4], R]: ... + + @overload + def call( + self: FunctionVar[ReflexCallable[[V1, V2, V3, V4, V5], R]], + arg1: Union[V1, Var[V1]], + arg2: Union[V2, Var[V2]], + arg3: Union[V3, Var[V3]], + arg4: Union[V4, Var[V4]], + arg5: Union[V5, Var[V5]], + ) -> VarOperationCall[[V1, V2, V3, V4, V5], R]: ... + + @overload + def call( + self: FunctionVar[ReflexCallable[[V1, V2, V3, V4, V5, V6], R]], + arg1: Union[V1, Var[V1]], + arg2: Union[V2, Var[V2]], + arg3: Union[V3, Var[V3]], + arg4: Union[V4, Var[V4]], + arg5: Union[V5, Var[V5]], + arg6: Union[V6, Var[V6]], + ) -> VarOperationCall[[V1, V2, V3, V4, V5, V6], R]: ... + + @overload + def call( + self: FunctionVar[ReflexCallable[P, R]], *args: Var | Any + ) -> VarOperationCall[P, R]: ... + + @overload + def call(self, *args: Var | Any) -> Var: ... + + def call(self, *args: Var | Any) -> Var: # type: ignore """Call the function with the given arguments. Args: @@ -38,19 +183,29 @@ def call(self, *args: Var | Any) -> VarOperationCall: Returns: The function call operation. """ - return VarOperationCall.create(self, *args) + return VarOperationCall.create(self, *args).guess_type() + + __call__ = call + + +class BuilderFunctionVar( + FunctionVar[CALLABLE_TYPE], default_type=ReflexCallable[Any, Any] +): + """Base class for immutable function vars with the builder pattern.""" + + __call__ = FunctionVar.partial -class FunctionStringVar(FunctionVar): +class FunctionStringVar(FunctionVar[CALLABLE_TYPE]): """Base class for immutable function vars from a string.""" @classmethod def create( cls, func: str, - _var_type: Type[Callable] = Callable, + _var_type: Type[OTHER_CALLABLE_TYPE] = ReflexCallable[Any, Any], _var_data: VarData | None = None, - ) -> FunctionStringVar: + ) -> FunctionStringVar[OTHER_CALLABLE_TYPE]: """Create a new function var from a string. Args: @@ -60,7 +215,7 @@ def create( Returns: The function var. """ - return cls( + return FunctionStringVar( _js_expr=func, _var_type=_var_type, _var_data=_var_data, @@ -72,10 +227,10 @@ def create( frozen=True, **{"slots": True} if sys.version_info >= (3, 10) else {}, ) -class VarOperationCall(CachedVarOperation, Var): +class VarOperationCall(Generic[P, R], CachedVarOperation, Var[R]): """Base class for immutable vars that are the result of a function call.""" - _func: Optional[FunctionVar] = dataclasses.field(default=None) + _func: Optional[FunctionVar[ReflexCallable[P, R]]] = dataclasses.field(default=None) _args: Tuple[Union[Var, Any], ...] = dataclasses.field(default_factory=tuple) @cached_property_no_lock @@ -103,7 +258,7 @@ def _cached_get_all_var_data(self) -> VarData | None: @classmethod def create( cls, - func: FunctionVar, + func: FunctionVar[ReflexCallable[P, R]], *args: Var | Any, _var_type: GenericType = Any, _var_data: VarData | None = None, @@ -118,9 +273,15 @@ def create( Returns: The function call var. """ + function_return_type = ( + func._var_type.__args__[1] + if getattr(func._var_type, "__args__", None) + else Any + ) + var_type = _var_type if _var_type is not Any else function_return_type return cls( _js_expr="", - _var_type=_var_type, + _var_type=var_type, _var_data=_var_data, _func=func, _args=args, @@ -157,6 +318,33 @@ class FunctionArgs: rest: Optional[str] = None +def format_args_function_operation( + args: FunctionArgs, return_expr: Var | Any, explicit_return: bool +) -> str: + """Format an args function operation. + + Args: + args: The function arguments. + return_expr: The return expression. + explicit_return: Whether to use explicit return syntax. + + Returns: + The formatted args function operation. + """ + arg_names_str = ", ".join( + [arg if isinstance(arg, str) else arg.to_javascript() for arg in args.args] + ) + (f", ...{args.rest}" if args.rest else "") + + return_expr_str = str(LiteralVar.create(return_expr)) + + # Wrap return expression in curly braces if explicit return syntax is used. + return_expr_str_wrapped = ( + format.wrap(return_expr_str, "{", "}") if explicit_return else return_expr_str + ) + + return f"(({arg_names_str}) => {return_expr_str_wrapped})" + + @dataclasses.dataclass( eq=False, frozen=True, @@ -176,24 +364,10 @@ def _cached_var_name(self) -> str: Returns: The name of the var. """ - arg_names_str = ", ".join( - [ - arg if isinstance(arg, str) else arg.to_javascript() - for arg in self._args.args - ] - ) + (f", ...{self._args.rest}" if self._args.rest else "") - - return_expr_str = str(LiteralVar.create(self._return_expr)) - - # Wrap return expression in curly braces if explicit return syntax is used. - return_expr_str_wrapped = ( - format.wrap(return_expr_str, "{", "}") - if self._explicit_return - else return_expr_str + return format_args_function_operation( + self._args, self._return_expr, self._explicit_return ) - return f"(({arg_names_str}) => {return_expr_str_wrapped})" - @classmethod def create( cls, @@ -203,7 +377,7 @@ def create( explicit_return: bool = False, _var_type: GenericType = Callable, _var_data: VarData | None = None, - ) -> ArgsFunctionOperation: + ): """Create a new function var. Args: @@ -226,8 +400,80 @@ def create( ) -JSON_STRINGIFY = FunctionStringVar.create("JSON.stringify") -ARRAY_ISARRAY = FunctionStringVar.create("Array.isArray") -PROTOTYPE_TO_STRING = FunctionStringVar.create( - "((__to_string) => __to_string.toString())" +@dataclasses.dataclass( + eq=False, + frozen=True, + **{"slots": True} if sys.version_info >= (3, 10) else {}, ) +class ArgsFunctionOperationBuilder(CachedVarOperation, BuilderFunctionVar): + """Base class for immutable function defined via arguments and return expression with the builder pattern.""" + + _args: FunctionArgs = dataclasses.field(default_factory=FunctionArgs) + _return_expr: Union[Var, Any] = dataclasses.field(default=None) + _explicit_return: bool = dataclasses.field(default=False) + + @cached_property_no_lock + def _cached_var_name(self) -> str: + """The name of the var. + + Returns: + The name of the var. + """ + return format_args_function_operation( + self._args, self._return_expr, self._explicit_return + ) + + @classmethod + def create( + cls, + args_names: Sequence[Union[str, DestructuredArg]], + return_expr: Var | Any, + rest: str | None = None, + explicit_return: bool = False, + _var_type: GenericType = Callable, + _var_data: VarData | None = None, + ): + """Create a new function var. + + Args: + args_names: The names of the arguments. + return_expr: The return expression of the function. + rest: The name of the rest argument. + explicit_return: Whether to use explicit return syntax. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The function var. + """ + return cls( + _js_expr="", + _var_type=_var_type, + _var_data=_var_data, + _args=FunctionArgs(args=tuple(args_names), rest=rest), + _return_expr=return_expr, + _explicit_return=explicit_return, + ) + + +if python_version := sys.version_info[:2] >= (3, 10): + JSON_STRINGIFY = FunctionStringVar.create( + "JSON.stringify", _var_type=ReflexCallable[[Any], str] + ) + ARRAY_ISARRAY = FunctionStringVar.create( + "Array.isArray", _var_type=ReflexCallable[[Any], bool] + ) + PROTOTYPE_TO_STRING = FunctionStringVar.create( + "((__to_string) => __to_string.toString())", + _var_type=ReflexCallable[[Any], str], + ) +else: + JSON_STRINGIFY = FunctionStringVar.create( + "JSON.stringify", _var_type=ReflexCallable[Any, str] + ) + ARRAY_ISARRAY = FunctionStringVar.create( + "Array.isArray", _var_type=ReflexCallable[Any, bool] + ) + PROTOTYPE_TO_STRING = FunctionStringVar.create( + "((__to_string) => __to_string.toString())", + _var_type=ReflexCallable[Any, str], + ) diff --git a/tests/units/test_var.py b/tests/units/test_var.py index 5944739213..4940246e79 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -928,7 +928,7 @@ def test_function_var(): == '(((a, b) => ({ ["args"] : [a, b], ["result"] : a + b }))(1, 2))' ) - increment_func = addition_func(1) + increment_func = addition_func.partial(1) assert ( str(increment_func.call(2)) == "(((...args) => (((a, b) => a + b)(1, ...args)))(2))" From a154a7d92063939c78e16ddca2504243c38deae3 Mon Sep 17 00:00:00 2001 From: Elijah Ahianyo Date: Wed, 13 Nov 2024 10:24:36 +0000 Subject: [PATCH 28/31] Add template name to reflex init success msg (#4349) * Add template name to reflex init success msg * fix pyright message --- reflex/reflex.py | 5 +++-- reflex/utils/prerequisites.py | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/reflex/reflex.py b/reflex/reflex.py index 6e6482e8a7..a5fce27cf9 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -106,7 +106,7 @@ def _init( template = constants.Templates.DEFAULT # Initialize the app. - prerequisites.initialize_app(app_name, template) + template = prerequisites.initialize_app(app_name, template) # If a reflex.build generation hash is available, download the code and apply it to the main module. if generation_hash: @@ -120,8 +120,9 @@ def _init( # Initialize the requirements.txt. prerequisites.initialize_requirements_txt() + template_msg = "" if template else f" using the {template} template" # Finish initializing the app. - console.success(f"Initialized {app_name}") + console.success(f"Initialized {app_name}{template_msg}") @cli.command() diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 28456d9bc4..7652a3c294 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -1381,7 +1381,7 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str shutil.rmtree(unzip_dir) -def initialize_app(app_name: str, template: str | None = None): +def initialize_app(app_name: str, template: str | None = None) -> str | None: """Initialize the app either from a remote template or a blank app. If the config file exists, it is considered as reinit. Args: @@ -1390,6 +1390,9 @@ def initialize_app(app_name: str, template: str | None = None): Raises: Exit: If template is directly provided in the command flag and is invalid. + + Returns: + The name of the template. """ # Local imports to avoid circular imports. from reflex.utils import telemetry @@ -1444,6 +1447,7 @@ def initialize_app(app_name: str, template: str | None = None): ) telemetry.send("init", template=template) + return template def initialize_main_module_index_from_generation(app_name: str, generation_hash: str): From 1e45a8e14048e4fcfd8b67ecc379b9bc433b036c Mon Sep 17 00:00:00 2001 From: Alek Petuskey Date: Wed, 13 Nov 2024 20:42:29 -0800 Subject: [PATCH 29/31] Update bug_report.md (#4382) --- .github/ISSUE_TEMPLATE/bug_report.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 4ba472338f..b3966b5d01 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,6 @@ name: Bug report about: Create a report to help us improve title: '' -labels: bug assignees: '' --- From 0be2c3e810125d00f631b848f10ba81f3cf6a900 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Thu, 21 Nov 2024 02:13:41 +0100 Subject: [PATCH 30/31] fix: properly cleanup env vars with pytest fixtures --- reflex/testing.py | 12 ------------ tests/integration/conftest.py | 13 +++++++++---- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/reflex/testing.py b/reflex/testing.py index 5223beda6c..036cf219ab 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -975,17 +975,6 @@ def _poll_for_servers(self, timeout: TimeoutType = None) -> socket.socket: finally: EnvironmentVariables.REFLEX_SKIP_COMPILE.set(None) - @override - def start(self) -> AppHarnessProd: - """Start AppHarnessProd instance. - - Returns: - self - """ - EnvironmentVariables.REFLEX_ENV_MODE.set(reflex.constants.base.Env.PROD) - _ = super().start() - return self - @override def stop(self): """Stop the frontend python webserver.""" @@ -994,4 +983,3 @@ def stop(self): self.frontend_server.shutdown() if self.frontend_thread is not None: self.frontend_thread.join() - EnvironmentVariables.REFLEX_ENV_MODE.set(None) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 337f4e1749..aaae9faeb9 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -3,12 +3,13 @@ import os import re from pathlib import Path -from typing import Type +from typing import Generator, Type import pytest import reflex.constants from reflex.config import EnvironmentVariables +from reflex.constants.base import Env from reflex.testing import AppHarness, AppHarnessProd DISPLAY = None @@ -81,14 +82,18 @@ def pytest_exception_interact(node, call, report): ) def app_harness_env( request: pytest.FixtureRequest, -) -> Type[AppHarness]: +) -> Generator[Type[AppHarness], None, None]: """Parametrize the AppHarness class to use for the test, either dev or prod. Args: request: The pytest fixture request object. - Returns: + Yields: The AppHarness class to use for the test. """ harness: Type[AppHarness] = request.param - return harness + if issubclass(harness, AppHarnessProd): + EnvironmentVariables.REFLEX_ENV_MODE.set(Env.PROD) + yield harness + if isinstance(harness, AppHarnessProd): + EnvironmentVariables.REFLEX_ENV_MODE.set(None) From 0262143a4d5624c90b09071c5367e2f533c4c96b Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Fri, 22 Nov 2024 21:21:57 +0100 Subject: [PATCH 31/31] prevent env api breaking change --- reflex/app.py | 16 ++++--------- reflex/compiler/compiler.py | 4 ++-- reflex/components/core/upload.py | 4 ++-- reflex/config.py | 6 +++-- reflex/constants/base.py | 15 +++++------- reflex/constants/installer.py | 8 +++---- reflex/custom_components/custom_components.py | 6 ++--- reflex/model.py | 16 ++++++------- reflex/reflex.py | 16 ++++++------- reflex/state.py | 18 +++++++-------- reflex/testing.py | 14 +++++------ reflex/utils/exec.py | 16 ++++++------- reflex/utils/net.py | 4 ++-- reflex/utils/path_ops.py | 6 ++--- reflex/utils/prerequisites.py | 23 ++++++++----------- reflex/utils/registry.py | 4 ++-- reflex/utils/telemetry.py | 4 ++-- tests/integration/conftest.py | 13 ++++------- tests/integration/test_minified_states.py | 6 ++--- tests/units/test_config.py | 4 ++-- tests/units/utils/test_utils.py | 8 +++---- 21 files changed, 98 insertions(+), 113 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 678868fbbb..afc40e3b88 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -65,7 +65,7 @@ ) from reflex.components.core.upload import Upload, get_upload_dir from reflex.components.radix import themes -from reflex.config import EnvironmentVariables, get_config +from reflex.config import environment, get_config from reflex.event import ( BASE_STATE, Event, @@ -503,10 +503,7 @@ def add_page( # Check if the route given is valid verify_route_validity(route) - if ( - route in self.unevaluated_pages - and EnvironmentVariables.RELOAD_CONFIG.is_set() - ): + if route in self.unevaluated_pages and environment.RELOAD_CONFIG.is_set(): # when the app is reloaded(typically for app harness tests), we should maintain # the latest render function of a route.This applies typically to decorated pages # since they are only added when app._compile is called. @@ -723,7 +720,7 @@ def _should_compile(self) -> bool: Whether the app should be compiled. """ # Check the environment variable. - if EnvironmentVariables.REFLEX_SKIP_COMPILE.get(): + if environment.REFLEX_SKIP_COMPILE.get(): return False nocompile = prerequisites.get_web_dir() / constants.NOCOMPILE_FILE @@ -945,10 +942,7 @@ def get_compilation_time() -> str: executor = None if ( platform.system() in ("Linux", "Darwin") - and ( - number_of_processes - := EnvironmentVariables.REFLEX_COMPILE_PROCESSES.get() - ) + and (number_of_processes := environment.REFLEX_COMPILE_PROCESSES.get()) is not None ): executor = concurrent.futures.ProcessPoolExecutor( @@ -957,7 +951,7 @@ def get_compilation_time() -> str: ) else: executor = concurrent.futures.ThreadPoolExecutor( - max_workers=EnvironmentVariables.REFLEX_COMPILE_THREADS.get() + max_workers=environment.REFLEX_COMPILE_THREADS.get() ) for route, component in zip(self.pages, page_components): diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index d6bc533cae..4122a0938a 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -16,7 +16,7 @@ CustomComponent, StatefulComponent, ) -from reflex.config import EnvironmentVariables, get_config +from reflex.config import environment, get_config from reflex.state import BaseState from reflex.style import SYSTEM_COLOR_MODE from reflex.utils.exec import is_prod_mode @@ -527,7 +527,7 @@ def remove_tailwind_from_postcss() -> tuple[str, str]: def purge_web_pages_dir(): """Empty out .web/pages directory.""" - if not is_prod_mode() and EnvironmentVariables.REFLEX_PERSIST_WEB_DIR.get(): + if not is_prod_mode() and environment.REFLEX_PERSIST_WEB_DIR.get(): # Skip purging the web directory in dev mode if REFLEX_PERSIST_WEB_DIR is set. return diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index c4f2ec0e22..33dfae40fe 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -14,7 +14,7 @@ ) from reflex.components.el.elements.forms import Input from reflex.components.radix.themes.layout.box import Box -from reflex.config import EnvironmentVariables +from reflex.config import environment from reflex.constants import Dirs from reflex.constants.compiler import Hooks, Imports from reflex.event import ( @@ -133,7 +133,7 @@ def get_upload_dir() -> Path: """ Upload.is_used = True - uploaded_files_dir = EnvironmentVariables.REFLEX_UPLOADED_FILES_DIR.get() + uploaded_files_dir = environment.REFLEX_UPLOADED_FILES_DIR.get() uploaded_files_dir.mkdir(parents=True, exist_ok=True) return uploaded_files_dir diff --git a/reflex/config.py b/reflex/config.py index 7ce6d2d7d7..a882ce9d21 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -591,11 +591,13 @@ def __init__(self): # Whether to minify state names. Default to true in prod mode and false otherwise. REFLEX_MINIFY_STATES: EnvVar[Optional[bool]] = env_var( - default_factory=lambda: EnvironmentVariables.REFLEX_ENV_MODE.get() - == constants.Env.PROD + default_factory=lambda: environment.REFLEX_ENV_MODE.get() == constants.Env.PROD ) +environment = EnvironmentVariables + + class Config(Base): """The config defines runtime settings for the app. diff --git a/reflex/constants/base.py b/reflex/constants/base.py index 48d2593dcd..3266043c5a 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -123,10 +123,10 @@ def REFLEX_BUILD_URL(cls): Returns: The URL to redirect to reflex.build. """ - from reflex.config import EnvironmentVariables + from reflex.config import environment return ( - EnvironmentVariables.REFLEX_BUILD_FRONTEND.get() + environment.REFLEX_BUILD_FRONTEND.get() + "/gen?reflex_init_token={reflex_init_token}" ) @@ -138,12 +138,9 @@ def REFLEX_BUILD_POLL_URL(cls): Returns: The URL to poll waiting for the user to select a generation. """ - from reflex.config import EnvironmentVariables + from reflex.config import environment - return ( - EnvironmentVariables.REFLEX_BUILD_BACKEND.get() - + "/api/init/{reflex_init_token}" - ) + return environment.REFLEX_BUILD_BACKEND.get() + "/api/init/{reflex_init_token}" @classproperty @classmethod @@ -153,10 +150,10 @@ def REFLEX_BUILD_CODE_URL(cls): Returns: The URL to fetch the generation's reflex code. """ - from reflex.config import EnvironmentVariables + from reflex.config import environment return ( - EnvironmentVariables.REFLEX_BUILD_BACKEND.get() + environment.REFLEX_BUILD_BACKEND.get() + "/api/gen/{generation_hash}/refactored" ) diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index 0b91aa7acf..0b45586ddc 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -61,9 +61,9 @@ def ROOT_PATH(cls): Returns: The directory to store the bun. """ - from reflex.config import EnvironmentVariables + from reflex.config import environment - return EnvironmentVariables.REFLEX_DIR.get() / "bun" + return environment.REFLEX_DIR.get() / "bun" @classproperty @classmethod @@ -103,9 +103,9 @@ def DIR(cls) -> Path: Returns: The directory to store fnm. """ - from reflex.config import EnvironmentVariables + from reflex.config import environment - return EnvironmentVariables.REFLEX_DIR.get() / "fnm" + return environment.REFLEX_DIR.get() / "fnm" @classproperty @classmethod diff --git a/reflex/custom_components/custom_components.py b/reflex/custom_components/custom_components.py index acd550af40..1406e37bc1 100644 --- a/reflex/custom_components/custom_components.py +++ b/reflex/custom_components/custom_components.py @@ -17,7 +17,7 @@ from tomlkit.exceptions import TOMLKitError from reflex import constants -from reflex.config import EnvironmentVariables, get_config +from reflex.config import environment, get_config from reflex.constants import CustomComponents from reflex.utils import console @@ -609,14 +609,14 @@ def publish( help="The API token to use for authentication on python package repository. If token is provided, no username/password should be provided at the same time", ), username: Optional[str] = typer.Option( - EnvironmentVariables.TWINE_USERNAME.get(), + environment.TWINE_USERNAME.get(), "-u", "--username", show_default="TWINE_USERNAME environment variable value if set", help="The username to use for authentication on python package repository. Username and password must both be provided.", ), password: Optional[str] = typer.Option( - EnvironmentVariables.TWINE_PASSWORD.get(), + environment.TWINE_PASSWORD.get(), "-p", "--password", show_default="TWINE_PASSWORD environment variable value if set", diff --git a/reflex/model.py b/reflex/model.py index 9252b3a5c0..4b070ec678 100644 --- a/reflex/model.py +++ b/reflex/model.py @@ -17,7 +17,7 @@ import sqlalchemy.orm from reflex.base import Base -from reflex.config import EnvironmentVariables, get_config +from reflex.config import environment, get_config from reflex.utils import console from reflex.utils.compat import sqlmodel, sqlmodel_field_has_primary_key @@ -38,12 +38,12 @@ def get_engine(url: str | None = None) -> sqlalchemy.engine.Engine: url = url or conf.db_url if url is None: raise ValueError("No database url configured") - if not EnvironmentVariables.ALEMBIC_CONFIG.get().exists(): + if not environment.ALEMBIC_CONFIG.get().exists(): console.warn( "Database is not initialized, run [bold]reflex db init[/bold] first." ) # Print the SQL queries if the log level is INFO or lower. - echo_db_query = EnvironmentVariables.SQLALCHEMY_ECHO.get() + echo_db_query = environment.SQLALCHEMY_ECHO.get() # Needed for the admin dash on sqlite. connect_args = {"check_same_thread": False} if url.startswith("sqlite") else {} return sqlmodel.create_engine(url, echo=echo_db_query, connect_args=connect_args) @@ -231,7 +231,7 @@ def _alembic_config(): Returns: tuple of (config, script_directory) """ - config = alembic.config.Config(EnvironmentVariables.ALEMBIC_CONFIG.get()) + config = alembic.config.Config(environment.ALEMBIC_CONFIG.get()) return config, alembic.script.ScriptDirectory( config.get_main_option("script_location", default="version"), ) @@ -266,8 +266,8 @@ def _alembic_render_item( def alembic_init(cls): """Initialize alembic for the project.""" alembic.command.init( - config=alembic.config.Config(EnvironmentVariables.ALEMBIC_CONFIG.get()), - directory=str(EnvironmentVariables.ALEMBIC_CONFIG.get().parent / "alembic"), + config=alembic.config.Config(environment.ALEMBIC_CONFIG.get()), + directory=str(environment.ALEMBIC_CONFIG.get().parent / "alembic"), ) @classmethod @@ -287,7 +287,7 @@ def alembic_autogenerate( Returns: True when changes have been detected. """ - if not EnvironmentVariables.ALEMBIC_CONFIG.get().exists(): + if not environment.ALEMBIC_CONFIG.get().exists(): return False config, script_directory = cls._alembic_config() @@ -388,7 +388,7 @@ def migrate(cls, autogenerate: bool = False) -> bool | None: True - indicating the process was successful. None - indicating the process was skipped. """ - if not EnvironmentVariables.ALEMBIC_CONFIG.get().exists(): + if not environment.ALEMBIC_CONFIG.get().exists(): return with cls.get_db_engine().connect() as connection: diff --git a/reflex/reflex.py b/reflex/reflex.py index 1b5db81bf4..9c3a96c89d 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -15,7 +15,7 @@ from reflex_cli.v2.deployments import check_version, hosting_cli from reflex import constants -from reflex.config import EnvironmentVariables, get_config +from reflex.config import environment, get_config from reflex.custom_components.custom_components import custom_components_cli from reflex.utils import console, telemetry @@ -136,7 +136,7 @@ def _run( """Run the app in the given directory.""" # Set env mode in the environment # This must be set before importing modules that contain rx.State subclasses - EnvironmentVariables.REFLEX_ENV_MODE.set(env) + environment.REFLEX_ENV_MODE.set(env) from reflex.state import reset_disk_state_manager from reflex.utils import build, exec, prerequisites, processes @@ -259,13 +259,13 @@ def run( False, "--frontend-only", help="Execute only frontend.", - envvar=EnvironmentVariables.REFLEX_FRONTEND_ONLY.name, + envvar=environment.REFLEX_FRONTEND_ONLY.name, ), backend: bool = typer.Option( False, "--backend-only", help="Execute only backend.", - envvar=EnvironmentVariables.REFLEX_BACKEND_ONLY.name, + envvar=environment.REFLEX_BACKEND_ONLY.name, ), frontend_port: str = typer.Option( config.frontend_port, help="Specify a different frontend port." @@ -284,8 +284,8 @@ def run( if frontend and backend: console.error("Cannot use both --frontend-only and --backend-only options.") raise typer.Exit(1) - EnvironmentVariables.REFLEX_BACKEND_ONLY.set(backend) - EnvironmentVariables.REFLEX_FRONTEND_ONLY.set(frontend) + environment.REFLEX_BACKEND_ONLY.set(backend) + environment.REFLEX_FRONTEND_ONLY.set(frontend) _run(env, frontend, backend, frontend_port, backend_port, backend_host, loglevel) @@ -415,7 +415,7 @@ def logoutv2( def _skip_compile(): """Skip the compile step.""" - EnvironmentVariables.REFLEX_SKIP_COMPILE.set(True) + environment.REFLEX_SKIP_COMPILE.set(True) @db_cli.command(name="init") @@ -430,7 +430,7 @@ def db_init(): return # Check the alembic config. - if EnvironmentVariables.ALEMBIC_CONFIG.get().exists(): + if environment.ALEMBIC_CONFIG.get().exists(): console.error( "Database is already initialized. Use " "[bold]reflex db makemigrations[/bold] to create schema change " diff --git a/reflex/state.py b/reflex/state.py index 9fe877089d..4c2fb8f59d 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -76,7 +76,7 @@ import reflex.istate.dynamic from reflex import constants from reflex.base import Base -from reflex.config import EnvironmentVariables +from reflex.config import environment from reflex.event import ( BACKGROUND_TASK_MARKER, Event, @@ -118,11 +118,9 @@ var = computed_var -if EnvironmentVariables.REFLEX_PERF_MODE.get() != PerformanceMode.OFF: +if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF: # If the state is this large, it's considered a performance issue. - TOO_LARGE_SERIALIZED_STATE = ( - EnvironmentVariables.REFLEX_STATE_SIZE_LIMIT.get() * 1024 - ) + TOO_LARGE_SERIALIZED_STATE = environment.REFLEX_STATE_SIZE_LIMIT.get() * 1024 # Only warn about each state class size once. _WARNED_ABOUT_STATE_SIZE: Set[str] = set() @@ -954,7 +952,7 @@ def get_name(cls) -> str: """ module = cls.__module__.replace(".", "___") state_name = format.to_snake_case(f"{module}___{cls.__name__}") - if EnvironmentVariables.REFLEX_MINIFY_STATES.get(): + if environment.REFLEX_MINIFY_STATES.get(): return get_minified_state_name(state_name) return state_name @@ -2188,9 +2186,9 @@ def _check_state_size( f"State {state_full_name} serializes to {pickle_state_size} bytes " + "which may present performance issues. Consider reducing the size of this state." ) - if EnvironmentVariables.REFLEX_PERF_MODE.get() == PerformanceMode.WARN: + if environment.REFLEX_PERF_MODE.get() == PerformanceMode.WARN: console.warn(msg) - elif EnvironmentVariables.REFLEX_PERF_MODE.get() == PerformanceMode.RAISE: + elif environment.REFLEX_PERF_MODE.get() == PerformanceMode.RAISE: raise StateTooLargeError(msg) _WARNED_ABOUT_STATE_SIZE.add(state_full_name) @@ -2233,7 +2231,7 @@ def _serialize(self) -> bytes: """ try: pickle_state = pickle.dumps((self._to_schema(), self)) - if EnvironmentVariables.REFLEX_PERF_MODE.get() != PerformanceMode.OFF: + if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF: self._check_state_size(len(pickle_state)) return pickle_state except HANDLED_PICKLE_ERRORS as og_pickle_error: @@ -3516,7 +3514,7 @@ async def _wait_lock(self, lock_key: bytes, lock_id: bytes) -> None: ) except ResponseError: # Some redis servers only allow out-of-band configuration, so ignore errors here. - if not EnvironmentVariables.REFLEX_IGNORE_REDIS_CONFIG_ERROR.get(): + if not environment.REFLEX_IGNORE_REDIS_CONFIG_ERROR.get(): raise async with self.redis.pubsub() as pubsub: await pubsub.psubscribe(lock_key_channel) diff --git a/reflex/testing.py b/reflex/testing.py index 036cf219ab..ac0b563fde 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -43,7 +43,7 @@ import reflex.utils.format import reflex.utils.prerequisites import reflex.utils.processes -from reflex.config import EnvironmentVariables +from reflex.config import environment from reflex.state import ( BaseState, State, @@ -198,7 +198,7 @@ def get_state_name(self, state_cls_name: str) -> str: state_name = reflex.utils.format.to_snake_case( f"{self.app_name}___{self.app_name}___" + state_cls_name ) - if EnvironmentVariables.REFLEX_MINIFY_STATES.get(): + if environment.REFLEX_MINIFY_STATES.get(): return minified_state_names.get(state_name, state_name) return state_name @@ -623,10 +623,10 @@ def frontend( if self.frontend_url is None: raise RuntimeError("Frontend is not running.") want_headless = False - if EnvironmentVariables.APP_HARNESS_HEADLESS.get(): + if environment.APP_HARNESS_HEADLESS.get(): want_headless = True if driver_clz is None: - requested_driver = EnvironmentVariables.APP_HARNESS_DRIVER.get() + requested_driver = environment.APP_HARNESS_DRIVER.get() driver_clz = getattr(webdriver, requested_driver) if driver_options is None: driver_options = getattr(webdriver, f"{requested_driver}Options")() @@ -648,7 +648,7 @@ def frontend( driver_options.add_argument("headless") if driver_options is None: raise RuntimeError(f"Could not determine options for {driver_clz}") - if args := EnvironmentVariables.APP_HARNESS_DRIVER_ARGS.get(): + if args := environment.APP_HARNESS_DRIVER_ARGS.get(): for arg in args.split(","): driver_options.add_argument(arg) if driver_option_args is not None: @@ -955,7 +955,7 @@ def _wait_frontend(self): def _start_backend(self): if self.app_instance is None: raise RuntimeError("App was not initialized.") - EnvironmentVariables.REFLEX_SKIP_COMPILE.set(True) + environment.REFLEX_SKIP_COMPILE.set(True) self.backend = uvicorn.Server( uvicorn.Config( app=self.app_instance, @@ -973,7 +973,7 @@ def _poll_for_servers(self, timeout: TimeoutType = None) -> socket.socket: try: return super()._poll_for_servers(timeout) finally: - EnvironmentVariables.REFLEX_SKIP_COMPILE.set(None) + environment.REFLEX_SKIP_COMPILE.set(None) @override def stop(self): diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index aae8773686..5291de095e 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -15,7 +15,7 @@ import psutil from reflex import constants -from reflex.config import EnvironmentVariables, get_config +from reflex.config import environment, get_config from reflex.constants.base import LogLevel from reflex.utils import console, path_ops from reflex.utils.prerequisites import get_web_dir @@ -184,7 +184,7 @@ def should_use_granian(): Returns: True if Granian should be used. """ - return EnvironmentVariables.REFLEX_USE_GRANIAN.get() + return environment.REFLEX_USE_GRANIAN.get() def get_app_module(): @@ -370,7 +370,7 @@ def run_uvicorn_backend_prod(host, port, loglevel): run=True, show_logs=True, env={ - EnvironmentVariables.REFLEX_SKIP_COMPILE.name: "true" + environment.REFLEX_SKIP_COMPILE.name: "true" }, # skip compile for prod backend ) @@ -407,7 +407,7 @@ def run_granian_backend_prod(host, port, loglevel): run=True, show_logs=True, env={ - EnvironmentVariables.REFLEX_SKIP_COMPILE.name: "true" + environment.REFLEX_SKIP_COMPILE.name: "true" }, # skip compile for prod backend ) except ImportError: @@ -493,7 +493,7 @@ def is_prod_mode() -> bool: Returns: True if the app is running in production mode or False if running in dev mode. """ - current_mode = EnvironmentVariables.REFLEX_ENV_MODE.get() + current_mode = environment.REFLEX_ENV_MODE.get() return current_mode == constants.Env.PROD @@ -509,7 +509,7 @@ def is_frontend_only() -> bool: deprecation_version="0.6.5", removal_version="0.7.0", ) - return EnvironmentVariables.REFLEX_FRONTEND_ONLY.get() + return environment.REFLEX_FRONTEND_ONLY.get() def is_backend_only() -> bool: @@ -524,7 +524,7 @@ def is_backend_only() -> bool: deprecation_version="0.6.5", removal_version="0.7.0", ) - return EnvironmentVariables.REFLEX_BACKEND_ONLY.get() + return environment.REFLEX_BACKEND_ONLY.get() def should_skip_compile() -> bool: @@ -539,4 +539,4 @@ def should_skip_compile() -> bool: deprecation_version="0.6.5", removal_version="0.7.0", ) - return EnvironmentVariables.REFLEX_SKIP_COMPILE.get() + return environment.REFLEX_SKIP_COMPILE.get() diff --git a/reflex/utils/net.py b/reflex/utils/net.py index f664bf9b60..acc2029124 100644 --- a/reflex/utils/net.py +++ b/reflex/utils/net.py @@ -2,7 +2,7 @@ import httpx -from ..config import EnvironmentVariables +from ..config import environment from . import console @@ -12,7 +12,7 @@ def _httpx_verify_kwarg() -> bool: Returns: True if SSL verification is enabled, False otherwise """ - return not EnvironmentVariables.SSL_NO_VERIFY.get() + return not environment.SSL_NO_VERIFY.get() def get(url: str, **kwargs) -> httpx.Response: diff --git a/reflex/utils/path_ops.py b/reflex/utils/path_ops.py index de3dbe32c1..a2ba2b1512 100644 --- a/reflex/utils/path_ops.py +++ b/reflex/utils/path_ops.py @@ -9,7 +9,7 @@ from pathlib import Path from reflex import constants -from reflex.config import EnvironmentVariables +from reflex.config import environment # Shorthand for join. join = os.linesep.join @@ -136,7 +136,7 @@ def use_system_node() -> bool: Returns: Whether the system node should be used. """ - return EnvironmentVariables.REFLEX_USE_SYSTEM_NODE.get() + return environment.REFLEX_USE_SYSTEM_NODE.get() def use_system_bun() -> bool: @@ -145,7 +145,7 @@ def use_system_bun() -> bool: Returns: Whether the system bun should be used. """ - return EnvironmentVariables.REFLEX_USE_SYSTEM_BUN.get() + return environment.REFLEX_USE_SYSTEM_BUN.get() def get_node_bin_path() -> Path | None: diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index d06d7215f0..35eb3234fe 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -33,7 +33,7 @@ from reflex import constants, model from reflex.compiler import templates -from reflex.config import Config, EnvironmentVariables, get_config +from reflex.config import Config, environment, get_config from reflex.utils import console, net, path_ops, processes, redir from reflex.utils.exceptions import ( GeneratedCodeHasNoFunctionDefs, @@ -72,7 +72,7 @@ def get_web_dir() -> Path: Returns: The working directory. """ - return EnvironmentVariables.REFLEX_WEB_WORKDIR.get() + return environment.REFLEX_WEB_WORKDIR.get() def _python_version_check(): @@ -93,7 +93,7 @@ def check_latest_package_version(package_name: str): Args: package_name: The name of the package. """ - if EnvironmentVariables.REFLEX_CHECK_LATEST_VERSION.get() is False: + if environment.REFLEX_CHECK_LATEST_VERSION.get() is False: return try: # Get the latest version from PyPI @@ -265,7 +265,7 @@ def windows_npm_escape_hatch() -> bool: Returns: If the user has set REFLEX_USE_NPM. """ - return EnvironmentVariables.REFLEX_USE_NPM.get() + return environment.REFLEX_USE_NPM.get() def get_app(reload: bool = False) -> ModuleType: @@ -283,7 +283,7 @@ def get_app(reload: bool = False) -> ModuleType: from reflex.utils import telemetry try: - EnvironmentVariables.RELOAD_CONFIG.set(reload) + environment.RELOAD_CONFIG.set(reload) config = get_config() if not config.app_name: raise RuntimeError( @@ -1026,7 +1026,7 @@ def needs_reinit(frontend: bool = True) -> bool: return False # Make sure the .reflex directory exists. - if not EnvironmentVariables.REFLEX_DIR.get().exists(): + if not environment.REFLEX_DIR.get().exists(): return True # Make sure the .web directory exists in frontend mode. @@ -1131,7 +1131,7 @@ def ensure_reflex_installation_id() -> Optional[int]: """ try: initialize_reflex_user_directory() - installation_id_file = EnvironmentVariables.REFLEX_DIR.get() / "installation_id" + installation_id_file = environment.REFLEX_DIR.get() / "installation_id" installation_id = None if installation_id_file.exists(): @@ -1156,7 +1156,7 @@ def ensure_reflex_installation_id() -> Optional[int]: def initialize_reflex_user_directory(): """Initialize the reflex user directory.""" # Create the reflex directory. - path_ops.mkdir(EnvironmentVariables.REFLEX_DIR.get()) + path_ops.mkdir(environment.REFLEX_DIR.get()) def initialize_frontend_dependencies(): @@ -1181,7 +1181,7 @@ def check_db_initialized() -> bool: """ if ( get_config().db_url is not None - and not EnvironmentVariables.ALEMBIC_CONFIG.get().exists() + and not environment.ALEMBIC_CONFIG.get().exists() ): console.error( "Database is not initialized. Run [bold]reflex db init[/bold] first." @@ -1192,10 +1192,7 @@ def check_db_initialized() -> bool: def check_schema_up_to_date(): """Check if the sqlmodel metadata matches the current database schema.""" - if ( - get_config().db_url is None - or not EnvironmentVariables.ALEMBIC_CONFIG.get().exists() - ): + if get_config().db_url is None or not environment.ALEMBIC_CONFIG.get().exists(): return with model.Model.get_db_engine().connect() as connection: try: diff --git a/reflex/utils/registry.py b/reflex/utils/registry.py index 988186aa17..d98178c618 100644 --- a/reflex/utils/registry.py +++ b/reflex/utils/registry.py @@ -2,7 +2,7 @@ import httpx -from reflex.config import EnvironmentVariables +from reflex.config import environment from reflex.utils import console, net @@ -55,4 +55,4 @@ def _get_npm_registry() -> str: Returns: str: """ - return EnvironmentVariables.NPM_CONFIG_REGISTRY.get() or get_best_registry() + return environment.NPM_CONFIG_REGISTRY.get() or get_best_registry() diff --git a/reflex/utils/telemetry.py b/reflex/utils/telemetry.py index 2a596ea5cc..815d37a1be 100644 --- a/reflex/utils/telemetry.py +++ b/reflex/utils/telemetry.py @@ -8,7 +8,7 @@ import platform import warnings -from reflex.config import EnvironmentVariables +from reflex.config import environment try: from datetime import UTC, datetime @@ -96,7 +96,7 @@ def _raise_on_missing_project_hash() -> bool: False when compilation should be skipped (i.e. no .web directory is required). Otherwise return True. """ - return not EnvironmentVariables.REFLEX_SKIP_COMPILE.get() + return not environment.REFLEX_SKIP_COMPILE.get() def _prepare_event(event: str, **kwargs) -> dict: diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index aaae9faeb9..75dbd7f77c 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -8,7 +8,7 @@ import pytest import reflex.constants -from reflex.config import EnvironmentVariables +from reflex.config import environment from reflex.constants.base import Env from reflex.testing import AppHarness, AppHarnessProd @@ -25,10 +25,7 @@ def xvfb(): Yields: the pyvirtualdisplay object that the browser will be open on """ - if ( - os.environ.get("GITHUB_ACTIONS") - and not EnvironmentVariables.APP_HARNESS_HEADLESS.get() - ): + if os.environ.get("GITHUB_ACTIONS") and not environment.APP_HARNESS_HEADLESS.get(): from pyvirtualdisplay.smartdisplay import ( # pyright: ignore [reportMissingImports] SmartDisplay, ) @@ -49,7 +46,7 @@ def pytest_exception_interact(node, call, report): call: The pytest call describing when/where the test was invoked. report: The pytest log report object. """ - screenshot_dir = EnvironmentVariables.SCREENSHOT_DIR.get() + screenshot_dir = environment.SCREENSHOT_DIR.get() if DISPLAY is None or screenshot_dir is None: return @@ -93,7 +90,7 @@ def app_harness_env( """ harness: Type[AppHarness] = request.param if issubclass(harness, AppHarnessProd): - EnvironmentVariables.REFLEX_ENV_MODE.set(Env.PROD) + environment.REFLEX_ENV_MODE.set(Env.PROD) yield harness if isinstance(harness, AppHarnessProd): - EnvironmentVariables.REFLEX_ENV_MODE.set(None) + environment.REFLEX_ENV_MODE.set(None) diff --git a/tests/integration/test_minified_states.py b/tests/integration/test_minified_states.py index 37c5c8a0fd..da63b6f7c4 100644 --- a/tests/integration/test_minified_states.py +++ b/tests/integration/test_minified_states.py @@ -9,7 +9,7 @@ from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver -from reflex.config import EnvironmentVariables +from reflex.config import environment from reflex.testing import AppHarness, AppHarnessProd @@ -62,9 +62,9 @@ def minify_state_env( minify_states: whether to minify state names """ minify_states: Optional[bool] = request.param - EnvironmentVariables.REFLEX_MINIFY_STATES.set(minify_states) + environment.REFLEX_MINIFY_STATES.set(minify_states) yield minify_states - EnvironmentVariables.REFLEX_MINIFY_STATES.set(None) + environment.REFLEX_MINIFY_STATES.set(None) @pytest.fixture diff --git a/tests/units/test_config.py b/tests/units/test_config.py index b0603aec65..308c839461 100644 --- a/tests/units/test_config.py +++ b/tests/units/test_config.py @@ -8,9 +8,9 @@ import reflex as rx import reflex.config from reflex.config import ( - EnvironmentVariables, EnvVar, env_var, + environment, interpret_boolean_env, interpret_enum_env, interpret_int_env, @@ -216,7 +216,7 @@ def test_replace_defaults( def reflex_dir_constant() -> Path: - return EnvironmentVariables.REFLEX_DIR.get() + return environment.REFLEX_DIR.get() def test_reflex_dir_env_var(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index 29c6f8fb7b..e3dae7a2be 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -10,7 +10,7 @@ from reflex import constants from reflex.base import Base -from reflex.config import EnvironmentVariables +from reflex.config import environment from reflex.event import EventHandler from reflex.state import BaseState from reflex.utils import build, prerequisites, types @@ -600,7 +600,7 @@ def cleanup_reflex_env_mode(): None """ yield - EnvironmentVariables.REFLEX_ENV_MODE.set(None) + environment.REFLEX_ENV_MODE.set(None) def test_is_prod_mode(cleanup_reflex_env_mode: None) -> None: @@ -609,7 +609,7 @@ def test_is_prod_mode(cleanup_reflex_env_mode: None) -> None: Args: cleanup_reflex_env_mode: Fixture to cleanup the reflex env mode. """ - EnvironmentVariables.REFLEX_ENV_MODE.set(constants.Env.PROD) + environment.REFLEX_ENV_MODE.set(constants.Env.PROD) assert utils_exec.is_prod_mode() - EnvironmentVariables.REFLEX_ENV_MODE.set(None) + environment.REFLEX_ENV_MODE.set(None) assert not utils_exec.is_prod_mode()