get this working with relative imports and hot reload

This commit is contained in:
Elijah 2025-01-13 10:50:25 +00:00
parent c9d967e1f5
commit d32033e210
5 changed files with 64 additions and 32 deletions

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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()