diff --git a/integration/conftest.py b/integration/conftest.py index 212ac9981d1..7696a11b0df 100644 --- a/integration/conftest.py +++ b/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.testing import AppHarness, AppHarnessProd DISPLAY = None @@ -63,15 +65,32 @@ 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): + os.environ[reflex.constants.base.ENV_MODE_ENV_VAR] = ( + reflex.constants.base.Env.PROD.value + ) + yield harness + if issubclass(harness, AppHarnessProd): + _ = os.environ.pop(reflex.constants.base.ENV_MODE_ENV_VAR, None) diff --git a/integration/test_computed_vars.py b/integration/test_computed_vars.py index 28f774de52c..a9da4f72598 100644 --- a/integration/test_computed_vars.py +++ b/integration/test_computed_vars.py @@ -104,7 +104,6 @@ def index() -> rx.Component: ), ) - # raise Exception(State.count3._deps(objclass=State)) app = rx.App() app.add_page(index) diff --git a/integration/test_minified_states.py b/integration/test_minified_states.py index b78f15710dd..eb3efc8ec38 100644 --- a/integration/test_minified_states.py +++ b/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/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 81ac401009a..5d14abaed0f 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("___")); } /** @@ -768,7 +768,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 4345e244ff4..2567624e8ab 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(), ) @@ -49,7 +49,7 @@ def _compile_app(app_root: Component) -> str: Returns: The compiled app. """ - 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()}, @@ -66,7 +66,7 @@ def _compile_theme(theme: dict) -> 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: @@ -85,7 +85,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), @@ -94,7 +94,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, @@ -121,7 +121,7 @@ def _compile_page( # Compile the code to render the component. kwargs = {"state_name": state.get_name()} if state 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(), @@ -177,7 +177,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: @@ -189,7 +189,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( @@ -217,7 +217,7 @@ def _compile_components( # Compile the components page. return ( - templates.COMPONENTS.render( + templates.components().render( imports=utils.compile_imports(imports), components=component_renders, ), @@ -295,7 +295,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), ) @@ -312,7 +312,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 c868a0cbb74..debb20a3d27 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/base/error_boundary.py b/reflex/components/base/error_boundary.py index e90f0ed63be..5ea9a1b4ac7 100644 --- a/reflex/components/base/error_boundary.py +++ b/reflex/components/base/error_boundary.py @@ -43,7 +43,20 @@ def add_hooks(self) -> List[str | Var]: Returns: The hooks to add. """ - return [Hooks.EVENTS, Hooks.FRONTEND_ERRORS] + from reflex.state import FrontendEventExceptionState + + return [ + Hooks.EVENTS, + f""" + const logFrontendError = (error, info) => {{ + if (process.env.NODE_ENV === "production") {{ + addEvents([Event("{FrontendEventExceptionState.get_full_name()}.handle_frontend_exception", {{ + stack: error.stack, + }})]) + }} + }} + """, + ] def add_custom_code(self) -> List[str]: """Add custom Javascript code into the page that contains this component. diff --git a/reflex/components/component.py b/reflex/components/component.py index bb3e9053f27..6c0b03fccd1 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -23,7 +23,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.tags import Tag from reflex.constants import ( @@ -2116,7 +2116,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/constants/compiler.py b/reflex/constants/compiler.py index 836e4848b89..ebbaf6a645a 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -80,34 +80,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): @@ -167,16 +147,6 @@ class Hooks(SimpleNamespace): } })""" - FRONTEND_ERRORS = f""" - const logFrontendError = (error, info) => {{ - if (process.env.NODE_ENV === "production") {{ - addEvents([Event("{CompileVars.FRONTEND_EXCEPTION_STATE_FULL}.handle_frontend_exception", {{ - stack: error.stack, - }})]) - }} - }} - """ - class MemoizationDisposition(enum.Enum): """The conditions under which a component should be memoized.""" diff --git a/reflex/custom_components/custom_components.py b/reflex/custom_components/custom_components.py index a47de6febfb..fe2bd4697b0 100644 --- a/reflex/custom_components/custom_components.py +++ b/reflex/custom_components/custom_components.py @@ -64,7 +64,7 @@ def _create_package_config(module_name: str, package_name: str): with open(CustomComponents.PYPROJECT_TOML, "w") as f: f.write( - templates.CUSTOM_COMPONENTS_PYPROJECT_TOML.render( + templates.custom_components_pyproject_toml().render( module_name=module_name, package_name=package_name ) ) @@ -103,7 +103,7 @@ def _create_readme(module_name: str, package_name: str): with open(CustomComponents.PACKAGE_README, "w") as f: f.write( - templates.CUSTOM_COMPONENTS_README.render( + templates.custom_components_readme().render( module_name=module_name, package_name=package_name, ) @@ -132,7 +132,7 @@ def _write_source_and_init_py( "w", ) as f: f.write( - templates.CUSTOM_COMPONENTS_SOURCE.render( + templates.custom_components_source().render( component_class_name=component_class_name, module_name=module_name ) ) @@ -144,7 +144,7 @@ def _write_source_and_init_py( ), "w", ) as f: - f.write(templates.CUSTOM_COMPONENTS_INIT_FILE.render(module_name=module_name)) + f.write(templates.custom_components_init.render(module_name=module_name)) def _populate_demo_app(name_variants: NameVariants): @@ -171,7 +171,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 8f865e0a1dd..c2e8d121eca 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -294,7 +294,7 @@ def __call__(self, *args: Any) -> EventSpec: # 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: @@ -302,12 +302,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 @@ -324,25 +320,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") @@ -416,9 +415,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, *args, @@ -520,10 +516,6 @@ def __init_subclass__(cls, mixin: bool = False, **kwargs): if mixin: return - # 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() @@ -843,18 +835,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() @@ -1924,10 +1910,6 @@ class State(BaseState): class FrontendEventExceptionState(State): """Substate for handling frontend exceptions.""" - _state_name: ClassVar[Optional[str]] = ( - constants.CompileVars.FRONTEND_EXCEPTION_STATE - ) - def handle_frontend_exception(self, stack: str) -> None: """Handle frontend exceptions. @@ -1945,10 +1927,6 @@ def handle_frontend_exception(self, 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. @@ -1974,8 +1952,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 bde218c5ec9..1fd90f629ef 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -45,10 +45,12 @@ import reflex.utils.processes from reflex.state import ( BaseState, + State, StateManagerMemory, StateManagerRedis, reload_state_module, ) +from reflex.utils.types import override try: from selenium import webdriver # pyright: ignore [reportMissingImports] @@ -136,7 +138,7 @@ def create( root: pathlib.Path, app_source: Optional[types.FunctionType | types.ModuleType | str] = None, app_name: Optional[str] = None, - ) -> "AppHarness": + ) -> AppHarness: """Create an AppHarness instance at root. Args: @@ -186,7 +188,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 ) @@ -202,7 +211,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]: @@ -392,7 +401,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: @@ -422,7 +431,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: @@ -901,6 +910,7 @@ def _run_frontend(self): ) self.frontend_server.serve_forever() + @override def _start_frontend(self): # Set up the frontend. with chdir(self.app_path): @@ -912,21 +922,23 @@ 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.") - os.environ[reflex.constants.SKIP_COMPILE_ENV_VAR] = "yes" + os.environ[reflex.constants.base.SKIP_COMPILE_ENV_VAR] = "yes" self.backend = uvicorn.Server( uvicorn.Config( app=self.app_instance, @@ -939,12 +951,27 @@ 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: - os.environ.pop(reflex.constants.SKIP_COMPILE_ENV_VAR, None) + _ = os.environ.pop(reflex.constants.base.SKIP_COMPILE_ENV_VAR, None) + + @override + def start(self) -> AppHarnessProd: + """Start AppHarnessProd instance. + + Returns: + self + """ + os.environ[reflex.constants.base.ENV_MODE_ENV_VAR] = ( + reflex.constants.base.Env.PROD.value + ) + _ = super().start() + return self + @override def stop(self): """Stop the frontend python webserver.""" super().stop() @@ -952,3 +979,4 @@ def stop(self): self.frontend_server.shutdown() if self.frontend_thread is not None: self.frontend_thread.join() + _ = os.environ.pop(reflex.constants.base.ENV_MODE_ENV_VAR) diff --git a/reflex/utils/build.py b/reflex/utils/build.py index 7a67ec32e94..fb120d30340 100644 --- a/reflex/utils/build.py +++ b/reflex/utils/build.py @@ -56,7 +56,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 76556012999..1978217163c 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -391,7 +391,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( @@ -559,7 +559,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/test_app.py b/tests/test_app.py index 489ace51105..0eb09946eb4 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -980,7 +980,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/test_minify_state.py b/tests/test_minify_state.py index e4dea43ef13..1e49a227e69 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/test_state.py b/tests/test_state.py index aa5705b09a9..31f12969718 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -49,6 +49,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": ""}, @@ -2748,7 +2749,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", @@ -2792,7 +2793,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/utils/test_utils.py b/tests/utils/test_utils.py index e905ff58710..af3dc148e64 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -233,7 +233,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. @@ -241,9 +241,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 )