|
@@ -251,36 +251,36 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
# Attributes to add to the html root tag of every page.
|
|
|
html_custom_attrs: Optional[Dict[str, str]] = None
|
|
|
|
|
|
- # A map from a route to an unevaluated page. PRIVATE.
|
|
|
- unevaluated_pages: Dict[str, UnevaluatedPage] = dataclasses.field(
|
|
|
+ # A map from a route to an unevaluated page.
|
|
|
+ _unevaluated_pages: Dict[str, UnevaluatedPage] = dataclasses.field(
|
|
|
default_factory=dict
|
|
|
)
|
|
|
|
|
|
- # A map from a page route to the component to render. Users should use `add_page`. PRIVATE.
|
|
|
- pages: Dict[str, Component] = dataclasses.field(default_factory=dict)
|
|
|
+ # A map from a page route to the component to render. Users should use `add_page`.
|
|
|
+ _pages: Dict[str, Component] = dataclasses.field(default_factory=dict)
|
|
|
|
|
|
- # The backend API object. PRIVATE.
|
|
|
- api: FastAPI = None # type: ignore
|
|
|
+ # The backend API object.
|
|
|
+ _api: FastAPI | None = None
|
|
|
|
|
|
- # The state class to use for the app. PRIVATE.
|
|
|
- state: Optional[Type[BaseState]] = None
|
|
|
+ # The state class to use for the app.
|
|
|
+ _state: Optional[Type[BaseState]] = None
|
|
|
|
|
|
# Class to manage many client states.
|
|
|
_state_manager: Optional[StateManager] = None
|
|
|
|
|
|
- # Mapping from a route to event handlers to trigger when the page loads. PRIVATE.
|
|
|
- load_events: Dict[str, List[IndividualEventType[[], Any]]] = dataclasses.field(
|
|
|
+ # Mapping from a route to event handlers to trigger when the page loads.
|
|
|
+ _load_events: Dict[str, List[IndividualEventType[[], Any]]] = dataclasses.field(
|
|
|
default_factory=dict
|
|
|
)
|
|
|
|
|
|
- # Admin dashboard to view and manage the database. PRIVATE.
|
|
|
+ # Admin dashboard to view and manage the database.
|
|
|
admin_dash: Optional[AdminDash] = None
|
|
|
|
|
|
- # The async server name space. PRIVATE.
|
|
|
- event_namespace: Optional[EventNamespace] = None
|
|
|
+ # The async server name space.
|
|
|
+ _event_namespace: Optional[EventNamespace] = None
|
|
|
|
|
|
- # Background tasks that are currently running. PRIVATE.
|
|
|
- background_tasks: Set[asyncio.Task] = dataclasses.field(default_factory=set)
|
|
|
+ # Background tasks that are currently running.
|
|
|
+ _background_tasks: Set[asyncio.Task] = dataclasses.field(default_factory=set)
|
|
|
|
|
|
# Frontend Error Handler Function
|
|
|
frontend_exception_handler: Callable[[Exception], None] = (
|
|
@@ -292,6 +292,24 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
[Exception], Union[EventSpec, List[EventSpec], None]
|
|
|
] = default_backend_exception_handler
|
|
|
|
|
|
+ @property
|
|
|
+ def api(self) -> FastAPI | None:
|
|
|
+ """Get the backend api.
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ The backend api.
|
|
|
+ """
|
|
|
+ return self._api
|
|
|
+
|
|
|
+ @property
|
|
|
+ def event_namespace(self) -> EventNamespace | None:
|
|
|
+ """Get the event namespace.
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ The event namespace.
|
|
|
+ """
|
|
|
+ return self._event_namespace
|
|
|
+
|
|
|
def __post_init__(self):
|
|
|
"""Initialize the app.
|
|
|
|
|
@@ -311,7 +329,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
set_breakpoints(self.style.pop("breakpoints"))
|
|
|
|
|
|
# Set up the API.
|
|
|
- self.api = FastAPI(lifespan=self._run_lifespan_tasks)
|
|
|
+ self._api = FastAPI(lifespan=self._run_lifespan_tasks)
|
|
|
self._add_cors()
|
|
|
self._add_default_endpoints()
|
|
|
|
|
@@ -334,8 +352,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
|
|
|
def _enable_state(self) -> None:
|
|
|
"""Enable state for the app."""
|
|
|
- if not self.state:
|
|
|
- self.state = State
|
|
|
+ if not self._state:
|
|
|
+ self._state = State
|
|
|
self._setup_state()
|
|
|
|
|
|
def _setup_state(self) -> None:
|
|
@@ -344,13 +362,13 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
Raises:
|
|
|
RuntimeError: If the socket server is invalid.
|
|
|
"""
|
|
|
- if not self.state:
|
|
|
+ if not self._state:
|
|
|
return
|
|
|
|
|
|
config = get_config()
|
|
|
|
|
|
# Set up the state manager.
|
|
|
- self._state_manager = StateManager.create(state=self.state)
|
|
|
+ self._state_manager = StateManager.create(state=self._state)
|
|
|
|
|
|
# Set up the Socket.IO AsyncServer.
|
|
|
if not self.sio:
|
|
@@ -381,12 +399,13 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
namespace = config.get_event_namespace()
|
|
|
|
|
|
# Create the event namespace and attach the main app. Not related to any paths.
|
|
|
- self.event_namespace = EventNamespace(namespace, self)
|
|
|
+ self._event_namespace = EventNamespace(namespace, self)
|
|
|
|
|
|
# Register the event namespace with the socket.
|
|
|
self.sio.register_namespace(self.event_namespace)
|
|
|
# Mount the socket app with the API.
|
|
|
- self.api.mount(str(constants.Endpoint.EVENT), socket_app)
|
|
|
+ if self.api:
|
|
|
+ self.api.mount(str(constants.Endpoint.EVENT), socket_app)
|
|
|
|
|
|
# Check the exception handlers
|
|
|
self._validate_exception_handlers()
|
|
@@ -397,24 +416,35 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
Returns:
|
|
|
The string representation of the app.
|
|
|
"""
|
|
|
- return f"<App state={self.state.__name__ if self.state else None}>"
|
|
|
+ return f"<App state={self._state.__name__ if self._state else None}>"
|
|
|
|
|
|
def __call__(self) -> FastAPI:
|
|
|
"""Run the backend api instance.
|
|
|
|
|
|
+ Raises:
|
|
|
+ ValueError: If the app has not been initialized.
|
|
|
+
|
|
|
Returns:
|
|
|
The backend api.
|
|
|
"""
|
|
|
+ if not self.api:
|
|
|
+ raise ValueError("The app has not been initialized.")
|
|
|
return self.api
|
|
|
|
|
|
def _add_default_endpoints(self):
|
|
|
"""Add default api endpoints (ping)."""
|
|
|
# To test the server.
|
|
|
+ if not self.api:
|
|
|
+ return
|
|
|
+
|
|
|
self.api.get(str(constants.Endpoint.PING))(ping)
|
|
|
self.api.get(str(constants.Endpoint.HEALTH))(health)
|
|
|
|
|
|
def _add_optional_endpoints(self):
|
|
|
"""Add optional api endpoints (_upload)."""
|
|
|
+ if not self.api:
|
|
|
+ return
|
|
|
+
|
|
|
if Upload.is_used:
|
|
|
# To upload files.
|
|
|
self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
|
|
@@ -432,6 +462,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
|
|
|
def _add_cors(self):
|
|
|
"""Add CORS middleware to the app."""
|
|
|
+ if not self.api:
|
|
|
+ return
|
|
|
self.api.add_middleware(
|
|
|
cors.CORSMiddleware,
|
|
|
allow_credentials=True,
|
|
@@ -521,13 +553,13 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
# 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 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.
|
|
|
- self.unevaluated_pages.pop(route)
|
|
|
+ self._unevaluated_pages.pop(route)
|
|
|
|
|
|
- if route in self.unevaluated_pages:
|
|
|
+ if route in self._unevaluated_pages:
|
|
|
route_name = (
|
|
|
f"`{route}` or `/`"
|
|
|
if route == constants.PageNames.INDEX_ROUTE
|
|
@@ -540,15 +572,15 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
|
|
|
# Setup dynamic args for the route.
|
|
|
# this state assignment is only required for tests using the deprecated state kwarg for App
|
|
|
- state = self.state if self.state else State
|
|
|
+ state = self._state if self._state else State
|
|
|
state.setup_dynamic_args(get_route_args(route))
|
|
|
|
|
|
if on_load:
|
|
|
- self.load_events[route] = (
|
|
|
+ self._load_events[route] = (
|
|
|
on_load if isinstance(on_load, list) else [on_load]
|
|
|
)
|
|
|
|
|
|
- self.unevaluated_pages[route] = UnevaluatedPage(
|
|
|
+ self._unevaluated_pages[route] = UnevaluatedPage(
|
|
|
component=component,
|
|
|
route=route,
|
|
|
title=title,
|
|
@@ -563,10 +595,10 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
|
|
|
Args:
|
|
|
route: The route of the page to compile.
|
|
|
- save_page: If True, the compiled page is saved to self.pages.
|
|
|
+ save_page: If True, the compiled page is saved to self._pages.
|
|
|
"""
|
|
|
component, enable_state = compiler.compile_unevaluated_page(
|
|
|
- route, self.unevaluated_pages[route], self.state, self.style, self.theme
|
|
|
+ route, self._unevaluated_pages[route], self._state, self.style, self.theme
|
|
|
)
|
|
|
|
|
|
if enable_state:
|
|
@@ -575,7 +607,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
# Add the page.
|
|
|
self._check_routes_conflict(route)
|
|
|
if save_page:
|
|
|
- self.pages[route] = component
|
|
|
+ self._pages[route] = component
|
|
|
|
|
|
def get_load_events(self, route: str) -> list[IndividualEventType[[], Any]]:
|
|
|
"""Get the load events for a route.
|
|
@@ -589,7 +621,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
route = route.lstrip("/")
|
|
|
if route == "":
|
|
|
route = constants.PageNames.INDEX_ROUTE
|
|
|
- return self.load_events.get(route, [])
|
|
|
+ return self._load_events.get(route, [])
|
|
|
|
|
|
def _check_routes_conflict(self, new_route: str):
|
|
|
"""Verify if there is any conflict between the new route and any existing route.
|
|
@@ -613,7 +645,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
constants.RouteRegex.SINGLE_CATCHALL_SEGMENT,
|
|
|
constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT,
|
|
|
)
|
|
|
- for route in self.pages:
|
|
|
+ for route in self._pages:
|
|
|
replaced_route = replace_brackets_with_keywords(route)
|
|
|
for rw, r, nr in zip(
|
|
|
replaced_route.split("/"), route.split("/"), new_route.split("/")
|
|
@@ -671,6 +703,9 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
def _setup_admin_dash(self):
|
|
|
"""Setup the admin dash."""
|
|
|
# Get the admin dash.
|
|
|
+ if not self.api:
|
|
|
+ return
|
|
|
+
|
|
|
admin_dash = self.admin_dash
|
|
|
|
|
|
if admin_dash and admin_dash.models:
|
|
@@ -775,10 +810,10 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
|
|
|
def _setup_overlay_component(self):
|
|
|
"""If a State is not used and no overlay_component is specified, do not render the connection modal."""
|
|
|
- if self.state is None and self.overlay_component is default_overlay_component:
|
|
|
+ if self._state is None and self.overlay_component is default_overlay_component:
|
|
|
self.overlay_component = None
|
|
|
- for k, component in self.pages.items():
|
|
|
- self.pages[k] = self._add_overlay_to_component(component)
|
|
|
+ for k, component in self._pages.items():
|
|
|
+ self._pages[k] = self._add_overlay_to_component(component)
|
|
|
|
|
|
def _add_error_boundary_to_component(self, component: Component) -> Component:
|
|
|
if self.error_boundary is None:
|
|
@@ -790,14 +825,14 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
|
|
|
def _setup_error_boundary(self):
|
|
|
"""If a State is not used and no error_boundary is specified, do not render the error boundary."""
|
|
|
- if self.state is None and self.error_boundary is default_error_boundary:
|
|
|
+ if self._state is None and self.error_boundary is default_error_boundary:
|
|
|
self.error_boundary = None
|
|
|
|
|
|
- for k, component in self.pages.items():
|
|
|
+ for k, component in self._pages.items():
|
|
|
# Skip the 404 page
|
|
|
if k == constants.Page404.SLUG:
|
|
|
continue
|
|
|
- self.pages[k] = self._add_error_boundary_to_component(component)
|
|
|
+ self._pages[k] = self._add_error_boundary_to_component(component)
|
|
|
|
|
|
def _apply_decorated_pages(self):
|
|
|
"""Add @rx.page decorated pages to the app.
|
|
@@ -823,11 +858,11 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
Raises:
|
|
|
VarDependencyError: When a computed var has an invalid dependency.
|
|
|
"""
|
|
|
- if not self.state:
|
|
|
+ if not self._state:
|
|
|
return
|
|
|
|
|
|
if not state:
|
|
|
- state = self.state
|
|
|
+ state = self._state
|
|
|
|
|
|
for var in state.computed_vars.values():
|
|
|
if not var._cache:
|
|
@@ -853,13 +888,13 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
"""
|
|
|
from reflex.utils.exceptions import ReflexRuntimeError
|
|
|
|
|
|
- self.pages = {}
|
|
|
+ self._pages = {}
|
|
|
|
|
|
def get_compilation_time() -> str:
|
|
|
return str(datetime.now().time()).split(".")[0]
|
|
|
|
|
|
# Render a default 404 page if the user didn't supply one
|
|
|
- if constants.Page404.SLUG not in self.unevaluated_pages:
|
|
|
+ if constants.Page404.SLUG not in self._unevaluated_pages:
|
|
|
self.add_page(route=constants.Page404.SLUG)
|
|
|
|
|
|
# Fix up the style.
|
|
@@ -877,7 +912,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
|
|
|
should_compile = self._should_compile()
|
|
|
|
|
|
- for route in self.unevaluated_pages:
|
|
|
+ for route in self._unevaluated_pages:
|
|
|
console.debug(f"Evaluating page: {route}")
|
|
|
self._compile_page(route, save_page=should_compile)
|
|
|
|
|
@@ -904,7 +939,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
progress.start()
|
|
|
task = progress.add_task(
|
|
|
f"[{get_compilation_time()}] Compiling:",
|
|
|
- total=len(self.pages)
|
|
|
+ total=len(self._pages)
|
|
|
+ fixed_pages_within_executor
|
|
|
+ adhoc_steps_without_executor,
|
|
|
)
|
|
@@ -923,7 +958,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
|
|
|
# This has to happen before compiling stateful components as that
|
|
|
# prevents recursive functions from reaching all components.
|
|
|
- for component in self.pages.values():
|
|
|
+ for component in self._pages.values():
|
|
|
# Add component._get_all_imports() to all_imports.
|
|
|
all_imports.update(component._get_all_imports())
|
|
|
|
|
@@ -938,12 +973,12 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
stateful_components_path,
|
|
|
stateful_components_code,
|
|
|
page_components,
|
|
|
- ) = compiler.compile_stateful_components(self.pages.values())
|
|
|
+ ) = compiler.compile_stateful_components(self._pages.values())
|
|
|
|
|
|
progress.advance(task)
|
|
|
|
|
|
# Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
|
|
|
- if code_uses_state_contexts(stateful_components_code) and self.state is None:
|
|
|
+ if code_uses_state_contexts(stateful_components_code) and self._state is None:
|
|
|
raise ReflexRuntimeError(
|
|
|
"To access rx.State in frontend components, at least one "
|
|
|
"subclass of rx.State must be defined in the app."
|
|
@@ -980,10 +1015,10 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
max_workers=environment.REFLEX_COMPILE_THREADS.get() or None
|
|
|
)
|
|
|
|
|
|
- for route, component in zip(self.pages, page_components):
|
|
|
+ for route, component in zip(self._pages, page_components):
|
|
|
ExecutorSafeFunctions.COMPONENTS[route] = component
|
|
|
|
|
|
- ExecutorSafeFunctions.STATE = self.state
|
|
|
+ ExecutorSafeFunctions.STATE = self._state
|
|
|
|
|
|
with executor:
|
|
|
result_futures = []
|
|
@@ -993,7 +1028,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
result_futures.append(f)
|
|
|
|
|
|
# Compile the pre-compiled pages.
|
|
|
- for route in self.pages:
|
|
|
+ for route in self._pages:
|
|
|
_submit_work(
|
|
|
ExecutorSafeFunctions.compile_page,
|
|
|
route,
|
|
@@ -1028,7 +1063,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
|
|
|
# Compile the contexts.
|
|
|
compile_results.append(
|
|
|
- compiler.compile_contexts(self.state, self.theme),
|
|
|
+ compiler.compile_contexts(self._state, self.theme),
|
|
|
)
|
|
|
if self.theme is not None:
|
|
|
# Fix #2992 by removing the top-level appearance prop
|
|
@@ -1150,9 +1185,9 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
)
|
|
|
|
|
|
task = asyncio.create_task(_coro())
|
|
|
- self.background_tasks.add(task)
|
|
|
+ self._background_tasks.add(task)
|
|
|
# Clean up task from background_tasks set when complete.
|
|
|
- task.add_done_callback(self.background_tasks.discard)
|
|
|
+ task.add_done_callback(self._background_tasks.discard)
|
|
|
return task
|
|
|
|
|
|
def _validate_exception_handlers(self):
|
|
@@ -1162,11 +1197,11 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
ValueError: If the custom exception handlers are invalid.
|
|
|
|
|
|
"""
|
|
|
- FRONTEND_ARG_SPEC = {
|
|
|
+ frontend_arg_spec = {
|
|
|
"exception": Exception,
|
|
|
}
|
|
|
|
|
|
- BACKEND_ARG_SPEC = {
|
|
|
+ backend_arg_spec = {
|
|
|
"exception": Exception,
|
|
|
}
|
|
|
|
|
@@ -1174,8 +1209,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
["frontend", "backend"],
|
|
|
[self.frontend_exception_handler, self.backend_exception_handler],
|
|
|
[
|
|
|
- FRONTEND_ARG_SPEC,
|
|
|
- BACKEND_ARG_SPEC,
|
|
|
+ frontend_arg_spec,
|
|
|
+ backend_arg_spec,
|
|
|
],
|
|
|
):
|
|
|
if hasattr(handler_fn, "__name__"):
|