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 import constants
from reflex.utils import telemetry from reflex.utils import telemetry
from reflex.utils.exec import is_prod_mode 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": if constants.CompileVars.APP != "app":
raise AssertionError("unexpected variable name for 'app'") raise AssertionError("unexpected variable name for 'app'")
telemetry.send("compile") telemetry.send("compile")
app_module = get_app(reload=False) app, app_module = get_and_validate_app(reload=False)
app = getattr(app_module, constants.CompileVars.APP)
# For py3.9 compatibility when redis is used, we MUST add any decorator pages # 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). # before compiling the app in a thread to avoid event loop error (REF-2172).
app._apply_decorated_pages() app._apply_decorated_pages()
@ -30,7 +29,7 @@ if is_prod_mode():
# ensure only "app" is exposed. # ensure only "app" is exposed.
del app_module del app_module
del compile_future del compile_future
del get_app del get_and_validate_app
del is_prod_mode del is_prod_mode
del telemetry del telemetry
del constants del constants

View File

@ -730,24 +730,36 @@ class Config(Base):
"REDIS_URL is required when using the redis state manager." "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 @property
def app_module(self) -> ModuleType | None: def app_module(self) -> ModuleType | None:
"""Get the app module. """Return the app module if `app_module_path` is set.
Returns: Returns:
The app module. The app module.
""" """
return (
def load_via_spec(path): self._load_via_spec(self.app_module_path) if self.app_module_path else None
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
@property @property
def module(self) -> str: def module(self) -> str:

View File

@ -1759,7 +1759,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
except Exception as ex: except Exception as ex:
state._clean() 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) event_specs = app_instance.backend_exception_handler(ex)
@ -1871,7 +1871,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
except Exception as ex: except Exception as ex:
telemetry.send_error(ex, context="backend") 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) 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. 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)) app_instance.frontend_exception_handler(Exception(stack))
@ -2422,7 +2422,7 @@ class OnLoadInternalState(State):
The list of events to queue for on load handling. The list of events to queue for on load handling.
""" """
# Do not app._compile()! It should be already compiled by now. # 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) load_events = app.get_load_events(self.router.page.path)
if not load_events: if not load_events:
self.is_hydrated = True self.is_hydrated = True
@ -2589,7 +2589,7 @@ class StateProxy(wrapt.ObjectProxy):
""" """
super().__init__(state_instance) super().__init__(state_instance)
# compile is not relevant to backend logic # 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_substate_path = tuple(state_instance.get_full_name().split("."))
self._self_actx = None self._self_actx = None
self._self_mutable = False self._self_mutable = False
@ -3682,7 +3682,7 @@ def get_state_manager() -> StateManager:
Returns: Returns:
The state manager. The state manager.
""" """
app = getattr(prerequisites.get_app(), constants.CompileVars.APP) app = prerequisites.get_and_validate_app().app
return app.state_manager return app.state_manager

View File

@ -240,6 +240,19 @@ def run_backend(
run_uvicorn_backend(host, port, loglevel) 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): def run_uvicorn_backend(host, port, loglevel: LogLevel):
"""Run the backend in development mode using Uvicorn. """Run the backend in development mode using Uvicorn.
@ -256,7 +269,7 @@ def run_uvicorn_backend(host, port, loglevel: LogLevel):
port=port, port=port,
log_level=loglevel.value, log_level=loglevel.value,
reload=True, 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, interface=Interfaces.ASGI,
log_level=LogLevels(loglevel.value), log_level=LogLevels(loglevel.value),
reload=True, reload=True,
reload_paths=[Path(get_config().app_name)], reload_paths=get_reload_dirs(),
reload_ignore_dirs=[".web"], reload_ignore_dirs=[".web"],
).serve() ).serve()
except ImportError: except ImportError:

View File

@ -17,11 +17,13 @@ import stat
import sys import sys
import tempfile import tempfile
import time import time
import typing
import zipfile import zipfile
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from types import ModuleType from types import ModuleType
from typing import Callable, List, Optional from typing import Callable, List, Optional
from collections import namedtuple
import httpx import httpx
import typer import typer
@ -42,9 +44,12 @@ from reflex.utils.exceptions import (
from reflex.utils.format import format_library_name from reflex.utils.format import format_library_name
from reflex.utils.registry import _get_npm_registry from reflex.utils.registry import _get_npm_registry
if typing.TYPE_CHECKING:
from reflex.app import App
CURRENTLY_INSTALLING_NODE = False CURRENTLY_INSTALLING_NODE = False
AppInfo = namedtuple("AppInfo", ["app", "module"])
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class Template: class Template:
"""A template for a Reflex app.""" """A template for a Reflex app."""
@ -296,7 +301,6 @@ def get_app(reload: bool = False) -> ModuleType:
if not config.app_module if not config.app_module
else config.app_module else config.app_module
) )
if reload: if reload:
from reflex.state import reload_state_module from reflex.state import reload_state_module
@ -312,19 +316,24 @@ def get_app(reload: bool = False) -> ModuleType:
raise raise
def get_and_validate_app(reload: bool = False): def get_and_validate_app(reload: bool = False) -> AppInfo:
"""Get the app module based on the default config and validate it. """Get the app instance based on the default config and validate it.
Args: Args:
reload: Re-import the app module from disk reload: Re-import the app module from disk
Returns:
The app instance and the app module.
""" """
from reflex.app import App from reflex.app import App
app_module = get_app(reload=reload) app_module = get_app(reload=reload)
app = getattr(app_module, constants.CompileVars.APP) app = getattr(app_module, constants.CompileVars.APP)
if not isinstance(app, App): if not isinstance(app, App):
raise RuntimeError("The app object in rxconfig must be an instance of rx.App.") raise RuntimeError(
return app "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: 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: Returns:
The compiled app based on the default config. The compiled app based on the default config.
""" """
app_module = get_app(reload=reload) app, app_module = get_and_validate_app(reload=reload)
app = getattr(app_module, constants.CompileVars.APP)
# For py3.9 compatibility when redis is used, we MUST add any decorator pages # 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). # before compiling the app in a thread to avoid event loop error (REF-2172).
app._apply_decorated_pages() app._apply_decorated_pages()