From d32033e2106171e99d6031700a194a234c526361 Mon Sep 17 00:00:00 2001 From: Elijah Date: Mon, 13 Jan 2025 10:50:25 +0000 Subject: [PATCH] get this working with relative imports and hot reload --- reflex/app_module_for_backend.py | 7 +++---- reflex/config.py | 36 +++++++++++++++++++++----------- reflex/state.py | 12 +++++------ reflex/utils/exec.py | 17 +++++++++++++-- reflex/utils/prerequisites.py | 24 ++++++++++++++------- 5 files changed, 64 insertions(+), 32 deletions(-) diff --git a/reflex/app_module_for_backend.py b/reflex/app_module_for_backend.py index 8109fc3d6..b0ae0a29f 100644 --- a/reflex/app_module_for_backend.py +++ b/reflex/app_module_for_backend.py @@ -7,14 +7,13 @@ from concurrent.futures import ThreadPoolExecutor from reflex import constants from reflex.utils import telemetry from reflex.utils.exec import is_prod_mode -from reflex.utils.prerequisites import get_app +from reflex.utils.prerequisites import get_and_validate_app if constants.CompileVars.APP != "app": raise AssertionError("unexpected variable name for 'app'") telemetry.send("compile") -app_module = get_app(reload=False) -app = getattr(app_module, constants.CompileVars.APP) +app, app_module = get_and_validate_app(reload=False) # For py3.9 compatibility when redis is used, we MUST add any decorator pages # before compiling the app in a thread to avoid event loop error (REF-2172). app._apply_decorated_pages() @@ -30,7 +29,7 @@ if is_prod_mode(): # ensure only "app" is exposed. del app_module del compile_future -del get_app +del get_and_validate_app del is_prod_mode del telemetry del constants diff --git a/reflex/config.py b/reflex/config.py index 29ff07e3c..4f2c80add 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -730,24 +730,36 @@ class Config(Base): "REDIS_URL is required when using the redis state manager." ) + @staticmethod + def _load_via_spec(path: str) -> ModuleType: + """Load a module dynamically using its file path. + + Args: + path: The path to the module. + + Returns: + The loaded module. + """ + module_name = Path(path).stem + module_path = Path(path).resolve() + sys.path.insert(0, str(module_path.parent.parent)) + spec = importlib.util.spec_from_file_location(module_name, path) + module = importlib.util.module_from_spec(spec) + # Set the package name to the parent directory of the module (for relative imports) + module.__package__ = module_path.parent.name + spec.loader.exec_module(module) + return module + @property def app_module(self) -> ModuleType | None: - """Get the app module. + """Return the app module if `app_module_path` is set. Returns: The app module. """ - - def load_via_spec(path): - module_name = Path(path).stem - spec = importlib.util.spec_from_file_location(module_name, path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - if self.app_module_path: - return load_via_spec(self.app_module_path) - return None + return ( + self._load_via_spec(self.app_module_path) if self.app_module_path else None + ) @property def module(self) -> str: diff --git a/reflex/state.py b/reflex/state.py index a31aae032..9b0f258b8 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1759,7 +1759,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): except Exception as ex: state._clean() - app_instance = getattr(prerequisites.get_app(), constants.CompileVars.APP) + app_instance = prerequisites.get_and_validate_app().app event_specs = app_instance.backend_exception_handler(ex) @@ -1871,7 +1871,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): except Exception as ex: telemetry.send_error(ex, context="backend") - app_instance = getattr(prerequisites.get_app(), constants.CompileVars.APP) + app_instance = prerequisites.get_and_validate_app().app event_specs = app_instance.backend_exception_handler(ex) @@ -2383,7 +2383,7 @@ class FrontendEventExceptionState(State): component_stack: The stack trace of the component where the exception occurred. """ - app_instance = getattr(prerequisites.get_app(), constants.CompileVars.APP) + app_instance, _ = prerequisites.get_and_validate_app() app_instance.frontend_exception_handler(Exception(stack)) @@ -2422,7 +2422,7 @@ class OnLoadInternalState(State): The list of events to queue for on load handling. """ # Do not app._compile()! It should be already compiled by now. - app = getattr(prerequisites.get_app(), constants.CompileVars.APP) + app = prerequisites.get_and_validate_app().app load_events = app.get_load_events(self.router.page.path) if not load_events: self.is_hydrated = True @@ -2589,7 +2589,7 @@ class StateProxy(wrapt.ObjectProxy): """ super().__init__(state_instance) # compile is not relevant to backend logic - self._self_app = getattr(prerequisites.get_app(), constants.CompileVars.APP) + self._self_app = prerequisites.get_and_validate_app().app self._self_substate_path = tuple(state_instance.get_full_name().split(".")) self._self_actx = None self._self_mutable = False @@ -3682,7 +3682,7 @@ def get_state_manager() -> StateManager: Returns: The state manager. """ - app = getattr(prerequisites.get_app(), constants.CompileVars.APP) + app = prerequisites.get_and_validate_app().app return app.state_manager diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index 621c4a608..22de8b948 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -240,6 +240,19 @@ def run_backend( run_uvicorn_backend(host, port, loglevel) +def get_reload_dirs() -> list[str]: + """Get the reload directories for the backend. + + Returns: + The reload directories for the backend. + """ + config = get_config() + reload_dirs = [config.app_name] + if app_module_path := config.app_module_path: + reload_dirs.append(str(Path(app_module_path).resolve().parent.parent)) + return reload_dirs + + def run_uvicorn_backend(host, port, loglevel: LogLevel): """Run the backend in development mode using Uvicorn. @@ -256,7 +269,7 @@ def run_uvicorn_backend(host, port, loglevel: LogLevel): port=port, log_level=loglevel.value, reload=True, - reload_dirs=[get_config().app_name], + reload_dirs=get_reload_dirs(), ) @@ -281,7 +294,7 @@ def run_granian_backend(host, port, loglevel: LogLevel): interface=Interfaces.ASGI, log_level=LogLevels(loglevel.value), reload=True, - reload_paths=[Path(get_config().app_name)], + reload_paths=get_reload_dirs(), reload_ignore_dirs=[".web"], ).serve() except ImportError: diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index f181f3b1c..154a8b033 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -17,11 +17,13 @@ import stat import sys import tempfile import time +import typing import zipfile from datetime import datetime from pathlib import Path from types import ModuleType from typing import Callable, List, Optional +from collections import namedtuple import httpx import typer @@ -42,9 +44,12 @@ from reflex.utils.exceptions import ( from reflex.utils.format import format_library_name from reflex.utils.registry import _get_npm_registry +if typing.TYPE_CHECKING: + from reflex.app import App + CURRENTLY_INSTALLING_NODE = False - +AppInfo = namedtuple("AppInfo", ["app", "module"]) @dataclasses.dataclass(frozen=True) class Template: """A template for a Reflex app.""" @@ -296,7 +301,6 @@ def get_app(reload: bool = False) -> ModuleType: if not config.app_module else config.app_module ) - if reload: from reflex.state import reload_state_module @@ -312,19 +316,24 @@ def get_app(reload: bool = False) -> ModuleType: raise -def get_and_validate_app(reload: bool = False): - """Get the app module based on the default config and validate it. +def get_and_validate_app(reload: bool = False) -> AppInfo: + """Get the app instance based on the default config and validate it. Args: reload: Re-import the app module from disk + + Returns: + The app instance and the app module. """ from reflex.app import App app_module = get_app(reload=reload) app = getattr(app_module, constants.CompileVars.APP) if not isinstance(app, App): - raise RuntimeError("The app object in rxconfig must be an instance of rx.App.") - return app + raise RuntimeError( + "The app instance in the specified app_module_path in rxconfig must be an instance of rx.App." + ) + return AppInfo(app=app, module=app_module) def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType: @@ -337,8 +346,7 @@ def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType: Returns: The compiled app based on the default config. """ - app_module = get_app(reload=reload) - app = getattr(app_module, constants.CompileVars.APP) + app, app_module = get_and_validate_app(reload=reload) # For py3.9 compatibility when redis is used, we MUST add any decorator pages # before compiling the app in a thread to avoid event loop error (REF-2172). app._apply_decorated_pages()