Clean up config and app API (#3197)

This commit is contained in:
Nikhil Rao 2024-05-02 18:15:28 -07:00 committed by GitHub
parent db47d39979
commit 7903a1020d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 177 additions and 327 deletions

View File

@ -237,7 +237,7 @@ def test_app_10_compile_time_cold(benchmark, app_with_10_components):
def benchmark_fn():
with chdir(app_with_10_components.app_path):
app_with_10_components.app_instance.compile_()
app_with_10_components.app_instance._compile()
benchmark.pedantic(benchmark_fn, setup=setup, rounds=10)
@ -262,7 +262,7 @@ def test_app_10_compile_time_warm(benchmark, app_with_10_components):
def benchmark_fn():
with chdir(app_with_10_components.app_path):
app_with_10_components.app_instance.compile_()
app_with_10_components.app_instance._compile()
benchmark(benchmark_fn)
@ -290,7 +290,7 @@ def test_app_100_compile_time_cold(benchmark, app_with_100_components):
def benchmark_fn():
with chdir(app_with_100_components.app_path):
app_with_100_components.app_instance.compile_()
app_with_100_components.app_instance._compile()
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
@ -315,7 +315,7 @@ def test_app_100_compile_time_warm(benchmark, app_with_100_components):
def benchmark_fn():
with chdir(app_with_100_components.app_path):
app_with_100_components.app_instance.compile_()
app_with_100_components.app_instance._compile()
benchmark(benchmark_fn)
@ -343,7 +343,7 @@ def test_app_1000_compile_time_cold(benchmark, app_with_1000_components):
def benchmark_fn():
with chdir(app_with_1000_components.app_path):
app_with_1000_components.app_instance.compile_()
app_with_1000_components.app_instance._compile()
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
@ -368,6 +368,6 @@ def test_app_1000_compile_time_warm(benchmark, app_with_1000_components):
def benchmark_fn():
with chdir(app_with_1000_components.app_path):
app_with_1000_components.app_instance.compile_()
app_with_1000_components.app_instance._compile()
benchmark(benchmark_fn)

View File

@ -326,7 +326,7 @@ def test_app_1_compile_time_cold(benchmark, app_with_one_page):
def benchmark_fn():
with chdir(app_with_one_page.app_path):
app_with_one_page.app_instance.compile_()
app_with_one_page.app_instance._compile()
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
app_with_one_page._reload_state_module()
@ -352,7 +352,7 @@ def test_app_1_compile_time_warm(benchmark, app_with_one_page):
def benchmark_fn():
with chdir(app_with_one_page.app_path):
app_with_one_page.app_instance.compile_()
app_with_one_page.app_instance._compile()
benchmark(benchmark_fn)
app_with_one_page._reload_state_module()
@ -381,7 +381,7 @@ def test_app_10_compile_time_cold(benchmark, app_with_ten_pages):
def benchmark_fn():
with chdir(app_with_ten_pages.app_path):
app_with_ten_pages.app_instance.compile_()
app_with_ten_pages.app_instance._compile()
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
app_with_ten_pages._reload_state_module()
@ -407,7 +407,7 @@ def test_app_10_compile_time_warm(benchmark, app_with_ten_pages):
def benchmark_fn():
with chdir(app_with_ten_pages.app_path):
app_with_ten_pages.app_instance.compile_()
app_with_ten_pages.app_instance._compile()
benchmark(benchmark_fn)
app_with_ten_pages._reload_state_module()
@ -436,7 +436,7 @@ def test_app_100_compile_time_cold(benchmark, app_with_hundred_pages):
def benchmark_fn():
with chdir(app_with_hundred_pages.app_path):
app_with_hundred_pages.app_instance.compile_()
app_with_hundred_pages.app_instance._compile()
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
app_with_hundred_pages._reload_state_module()
@ -462,7 +462,7 @@ def test_app_100_compile_time_warm(benchmark, app_with_hundred_pages):
def benchmark_fn():
with chdir(app_with_hundred_pages.app_path):
app_with_hundred_pages.app_instance.compile_()
app_with_hundred_pages.app_instance._compile()
benchmark(benchmark_fn)
app_with_hundred_pages._reload_state_module()
@ -491,7 +491,7 @@ def test_app_1000_compile_time_cold(benchmark, app_with_thousand_pages):
def benchmark_fn():
with chdir(app_with_thousand_pages.app_path):
app_with_thousand_pages.app_instance.compile_()
app_with_thousand_pages.app_instance._compile()
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
app_with_thousand_pages._reload_state_module()
@ -517,7 +517,7 @@ def test_app_1000_compile_time_warm(benchmark, app_with_thousand_pages):
def benchmark_fn():
with chdir(app_with_thousand_pages.app_path):
app_with_thousand_pages.app_instance.compile_()
app_with_thousand_pages.app_instance._compile()
benchmark(benchmark_fn)
app_with_thousand_pages._reload_state_module()
@ -546,7 +546,7 @@ def test_app_10000_compile_time_cold(benchmark, app_with_ten_thousand_pages):
def benchmark_fn():
with chdir(app_with_ten_thousand_pages.app_path):
app_with_ten_thousand_pages.app_instance.compile_()
app_with_ten_thousand_pages.app_instance._compile()
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
app_with_ten_thousand_pages._reload_state_module()
@ -570,7 +570,7 @@ def test_app_10000_compile_time_warm(benchmark, app_with_ten_thousand_pages):
def benchmark_fn():
with chdir(app_with_ten_thousand_pages.app_path):
app_with_ten_thousand_pages.app_instance.compile_()
app_with_ten_thousand_pages.app_instance._compile()
benchmark(benchmark_fn)
app_with_ten_thousand_pages._reload_state_module()

View File

@ -102,61 +102,79 @@ class OverlayFragment(Fragment):
class App(Base):
"""A Reflex application."""
"""The main Reflex app that encapsulates the backend and frontend.
# A map from a page route to the component to render.
pages: Dict[str, Component] = {}
Every Reflex app needs an app defined in its main module.
# A list of URLs to stylesheets to include in the app.
stylesheets: List[str] = []
```python
# app.py
import reflex as rx
# The backend API object.
api: FastAPI = None # type: ignore
# Define state and pages
...
# The Socket.IO AsyncServer.
sio: Optional[AsyncServer] = None
app = rx.App(
# Set global level style.
style={...},
# Set the top level theme.
theme=rx.theme(accent_color="blue"),
)
```
"""
# The state class to use for the app.
state: Optional[Type[BaseState]] = None
# The global [theme](https://reflex.dev/docs/styling/theming/#theme) for the entire app.
theme: Optional[Component] = themes.theme(accent_color="blue")
# Class to manage many client states.
_state_manager: Optional[StateManager] = None
# The styling to apply to each component.
# The [global style](https://reflex.dev/docs/styling/overview/#global-styles}) for the app.
style: ComponentStyle = {}
# Middleware to add to the app.
middleware: List[Middleware] = []
# A list of URLs to [stylesheets](https://reflex.dev/docs/styling/custom-stylesheets/) to include in the app.
stylesheets: List[str] = []
# List of event handlers to trigger when a page loads.
load_events: Dict[str, List[Union[EventHandler, EventSpec]]] = {}
# Admin dashboard
admin_dash: Optional[AdminDash] = None
# The async server name space
event_namespace: Optional[EventNamespace] = None
# A component that is present on every page (defaults to the Connection Error banner).
overlay_component: Optional[
Union[Component, ComponentCallable]
] = default_overlay_component
# Components to add to the head of every page.
head_components: List[Component] = []
# The Socket.IO AsyncServer instance.
sio: Optional[AsyncServer] = None
# The language to add to the html root tag of every page.
html_lang: Optional[str] = None
# Attributes to add to the html root tag of every page.
html_custom_attrs: Optional[Dict[str, str]] = None
# A component that is present on every page.
overlay_component: Optional[
Union[Component, ComponentCallable]
] = default_overlay_component
# A map from a page route to the component to render. Users should use `add_page`. PRIVATE.
pages: Dict[str, Component] = {}
# Background tasks that are currently running
# The backend API object. PRIVATE.
api: FastAPI = None # type: ignore
# The state class to use for the app. PRIVATE.
state: Optional[Type[BaseState]] = None
# Class to manage many client states.
_state_manager: Optional[StateManager] = None
# Middleware to add to the app. Users should use `add_middleware`. PRIVATE.
middleware: List[Middleware] = []
# Mapping from a route to event handlers to trigger when the page loads. PRIVATE.
load_events: Dict[str, List[Union[EventHandler, EventSpec]]] = {}
# Admin dashboard to view and manage the database. PRIVATE.
admin_dash: Optional[AdminDash] = None
# The async server name space. PRIVATE.
event_namespace: Optional[EventNamespace] = None
# Background tasks that are currently running. PRIVATE.
background_tasks: Set[asyncio.Task] = set()
# The radix theme for the entire app
theme: Optional[Component] = themes.theme(accent_color="blue")
def __init__(self, **kwargs):
"""Initialize the app.
@ -195,25 +213,25 @@ class App(Base):
# Set up the API.
self.api = FastAPI()
self.add_cors()
self.add_default_endpoints()
self._add_cors()
self._add_default_endpoints()
self.setup_state()
self._setup_state()
# Set up the admin dash.
self.setup_admin_dash()
self._setup_admin_dash()
def enable_state(self) -> None:
def _enable_state(self) -> None:
"""Enable state for the app."""
if not self.state:
self.state = State
self.setup_state()
self._setup_state()
def setup_state(self) -> None:
def _setup_state(self) -> None:
"""Set up the state for the app.
Raises:
RuntimeError: If custom `sio` does not use `async_mode='asgi'`.
RuntimeError: If the socket server is invalid.
"""
if not self.state:
return
@ -244,7 +262,6 @@ class App(Base):
# Create the socket app. Note event endpoint constant replaces the default 'socket.io' path.
socket_app = ASGIApp(self.sio, socketio_path="")
namespace = config.get_event_namespace()
# Create the event namespace and attach the main app. Not related to any paths.
@ -271,12 +288,12 @@ class App(Base):
"""
return self.api
def add_default_endpoints(self):
def _add_default_endpoints(self):
"""Add default api endpoints (ping)."""
# To test the server.
self.api.get(str(constants.Endpoint.PING))(ping)
def add_optional_endpoints(self):
def _add_optional_endpoints(self):
"""Add optional api endpoints (_upload)."""
# To upload files.
if Upload.is_used:
@ -289,7 +306,7 @@ class App(Base):
name="uploaded_files",
)
def add_cors(self):
def _add_cors(self):
"""Add CORS middleware to the app."""
self.api.add_middleware(
cors.CORSMiddleware,
@ -313,7 +330,7 @@ class App(Base):
raise ValueError("The state manager has not been initialized.")
return self._state_manager
async def preprocess(self, state: BaseState, event: Event) -> StateUpdate | None:
async def _preprocess(self, state: BaseState, event: Event) -> StateUpdate | None:
"""Preprocess the event.
This is where middleware can modify the event before it is processed.
@ -337,7 +354,7 @@ class App(Base):
if out is not None:
return out # type: ignore
async def postprocess(
async def _postprocess(
self, state: BaseState, event: Event, update: StateUpdate
) -> StateUpdate:
"""Postprocess the event.
@ -468,14 +485,14 @@ class App(Base):
# Ensure state is enabled if this page uses state.
if self.state is None:
if on_load or component._has_event_triggers():
self.enable_state()
self._enable_state()
else:
for var in component._get_vars(include_children=True):
if not var._var_data:
continue
if not var._var_data.state:
continue
self.enable_state()
self._enable_state()
break
component = OverlayFragment.create(component)
@ -580,7 +597,7 @@ class App(Base):
"""Define a custom 404 page for any url having no match.
If there is no page defined on 'index' route, add the 404 page to it.
If there is no global catchall defined, add the 404 page with a catchall
If there is no global catchall defined, add the 404 page with a catchall.
Args:
component: The component to display at the page.
@ -602,7 +619,7 @@ class App(Base):
meta=meta,
)
def setup_admin_dash(self):
def _setup_admin_dash(self):
"""Setup the admin dash."""
# Get the admin dash.
admin_dash = self.admin_dash
@ -625,14 +642,14 @@ class App(Base):
admin.mount_to(self.api)
def get_frontend_packages(self, imports: Dict[str, set[ImportVar]]):
def _get_frontend_packages(self, imports: Dict[str, set[ImportVar]]):
"""Gets the frontend packages to be installed and filters out the unnecessary ones.
Args:
imports: A dictionary containing the imports used in the current page.
Example:
>>> get_frontend_packages({"react": "16.14.0", "react-dom": "16.14.0"})
>>> _get_frontend_packages({"react": "16.14.0", "react-dom": "16.14.0"})
"""
page_imports = {
i
@ -715,19 +732,6 @@ class App(Base):
for k, component in self.pages.items():
self.pages[k] = self._add_overlay_to_component(component)
def compile(self):
"""compile_() is the new function for performing compilation.
Reflex framework will call it automatically as needed.
"""
console.deprecate(
feature_name="app.compile()",
reason="Explicit calls to app.compile() are not needed."
" Method will be removed in 0.4.0",
deprecation_version="0.3.8",
removal_version="0.5.0",
)
return
def _apply_decorated_pages(self):
"""Add @rx.page decorated pages to the app.
@ -741,7 +745,7 @@ class App(Base):
for render, kwargs in DECORATED_PAGES[get_config().app_name]:
self.add_page(render, **kwargs)
def compile_(self, export: bool = False):
def _compile(self, export: bool = False):
"""Compile the app and output it to the pages folder.
Args:
@ -755,7 +759,7 @@ class App(Base):
self.add_custom_404_page()
# Add the optional endpoints (_upload)
self.add_optional_endpoints()
self._add_optional_endpoints()
if not self._should_compile():
return
@ -953,7 +957,7 @@ class App(Base):
progress.stop()
# Install frontend packages.
self.get_frontend_packages(all_imports)
self._get_frontend_packages(all_imports)
# Setup the next.config.js
transpile_packages = [
@ -1028,7 +1032,7 @@ class App(Base):
handler=handler, state=substate, payload=event.payload
):
# Postprocess the event.
update = await self.postprocess(state, event, update)
update = await self._postprocess(state, event, update)
# Send the update to the client.
await self.event_namespace.emit_update(
@ -1079,7 +1083,7 @@ async def process(
state.router = RouterData(router_data)
# Preprocess the event.
update = await app.preprocess(state, event)
update = await app._preprocess(state, event)
# If there was an update, yield it.
if update is not None:
@ -1095,7 +1099,7 @@ async def process(
# Process the event synchronously.
async for update in state._process(event):
# Postprocess the event.
update = await app.postprocess(state, event, update)
update = await app._postprocess(state, event, update)
# Yield the update.
yield update
@ -1216,7 +1220,7 @@ def upload(app: App):
async with app.state_manager.modify_state(event.substate_token) as state:
async for update in state._process(event):
# Postprocess the event.
update = await app.postprocess(state, event, update)
update = await app._postprocess(state, event, update)
yield update.json() + "\n"
# Stream updates to client

View File

@ -1,148 +0,0 @@
""" Generated with stubgen from mypy, then manually edited, do not regen."""
import asyncio
from fastapi import FastAPI
from fastapi import UploadFile as UploadFile
from reflex import constants as constants
from reflex.admin import AdminDash as AdminDash
from reflex.base import Base as Base
from reflex.compiler import compiler as compiler
from reflex.components import connection_modal as connection_modal
from reflex.components.component import (
Component as Component,
ComponentStyle as ComponentStyle,
)
from reflex.components.base.fragment import Fragment as Fragment
from reflex.config import get_config as get_config
from reflex.event import (
Event as Event,
EventHandler as EventHandler,
EventSpec as EventSpec,
)
from reflex.middleware import (
HydrateMiddleware as HydrateMiddleware,
Middleware as Middleware,
)
from reflex.model import Model as Model
from reflex.page import DECORATED_PAGES as DECORATED_PAGES
from reflex.route import (
catchall_in_route as catchall_in_route,
catchall_prefix as catchall_prefix,
get_route_args as get_route_args,
verify_route_validity as verify_route_validity,
)
from reflex.state import (
State as State,
BaseState as BaseState,
StateManager as StateManager,
StateUpdate as StateUpdate,
)
from reflex.utils import (
console as console,
format as format,
prerequisites as prerequisites,
types as types,
)
from socketio import ASGIApp, AsyncNamespace, AsyncServer
from typing import (
Any,
AsyncContextManager,
AsyncIterator,
Callable,
Coroutine,
Dict,
List,
Optional,
Set,
Type,
Union,
overload,
)
ComponentCallable = Callable[[], Component]
Reducer = Callable[[Event], Coroutine[Any, Any, StateUpdate]]
def default_overlay_component() -> Component: ...
class OverlayFragment(Fragment):
@overload
@classmethod
def create(cls, *children, **props) -> "OverlayFragment": ... # type: ignore
class App(Base):
pages: Dict[str, Component]
stylesheets: List[str]
api: FastAPI
sio: Optional[AsyncServer]
socket_app: Optional[ASGIApp]
state: Type[BaseState]
state_manager: StateManager
style: ComponentStyle
middleware: List[Middleware]
load_events: Dict[str, List[Union[EventHandler, EventSpec]]]
admin_dash: Optional[AdminDash]
event_namespace: Optional[AsyncNamespace]
overlay_component: Optional[Union[Component, ComponentCallable]]
background_tasks: Set[asyncio.Task] = set()
def __init__(
self,
stylesheets: Optional[List[str]] = None,
style: Optional[ComponentStyle] = None,
admin_dash: Optional[AdminDash] = None,
overlay_component: Optional[Union[Component, ComponentCallable]] = None,
**kwargs
) -> None: ...
def __call__(self) -> FastAPI: ...
def enable_state(self) -> None: ...
def add_default_endpoints(self) -> None: ...
def add_optional_endpoints(self): ...
def add_cors(self) -> None: ...
async def preprocess(self, state: State, event: Event) -> StateUpdate | None: ...
async def postprocess(
self, state: State, event: Event, update: StateUpdate
) -> StateUpdate: ...
def add_middleware(self, middleware: Middleware, index: int | None = ...): ...
def add_page(
self,
component: Component | ComponentCallable,
route: str | None = ...,
title: str = ...,
description: str = ...,
image=...,
on_load: EventHandler | EventSpec | list[EventHandler | EventSpec] | None = ...,
meta: list[dict[str, str]] = ...,
script_tags: list[Component] | None = ...,
): ...
def get_load_events(self, route: str) -> list[EventHandler | EventSpec]: ...
def add_custom_404_page(
self,
component: Component | ComponentCallable | None = ...,
title: str = ...,
image: str = ...,
description: str = ...,
on_load: EventHandler | EventSpec | list[EventHandler | EventSpec] | None = ...,
meta: list[dict[str, str]] = ...,
): ...
def setup_admin_dash(self) -> None: ...
def get_frontend_packages(self, imports: Dict[str, str]): ...
def compile(self) -> None: ...
def compile_(self) -> None: ...
def modify_state(self, token: str) -> AsyncContextManager[State]: ...
def _setup_overlay_component(self) -> None: ...
def _process_background(
self, state: State, event: Event
) -> asyncio.Task | None: ...
def process(
app: App, event: Event, sid: str, headers: Dict, client_ip: str
) -> AsyncIterator[StateUpdate]: ...
async def ping() -> str: ...
def upload(app: App): ...
class EventNamespace(AsyncNamespace):
app: App
def __init__(self, namespace: str, app: App) -> None: ...
def on_connect(self, sid, environ) -> None: ...
def on_disconnect(self, sid) -> None: ...
async def on_event(self, sid, data) -> None: ...
async def on_ping(self, sid) -> None: ...

View File

@ -15,7 +15,7 @@ app = getattr(app_module, constants.CompileVars.APP)
# For py3.8 and 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()
compile_future = ThreadPoolExecutor(max_workers=1).submit(app.compile_)
compile_future = ThreadPoolExecutor(max_workers=1).submit(app._compile)
compile_future.add_done_callback(
# Force background compile errors to print eagerly
lambda f: f.result()

View File

@ -135,29 +135,47 @@ class DBConfig(Base):
class Config(Base):
"""A Reflex config."""
"""The config defines runtime settings for the app.
By default, the config is defined in an `rxconfig.py` file in the root of the app.
```python
# rxconfig.py
import reflex as rx
config = rx.Config(
app_name="myapp",
api_url="http://localhost:8000",
)
```
Every config value can be overridden by an environment variable with the same name in uppercase.
For example, `db_url` can be overridden by setting the `DB_URL` environment variable.
See the [configuration](https://reflex.dev/docs/getting-started/configuration/) docs for more info.
"""
class Config:
"""Pydantic config for the config."""
validate_assignment = True
# The name of the app.
# The name of the app (should match the name of the app directory).
app_name: str
# The log level to use.
loglevel: constants.LogLevel = constants.LogLevel.INFO
# The port to run the frontend on.
# The port to run the frontend on. NOTE: When running in dev mode, the next available port will be used if this is taken.
frontend_port: int = 3000
# The path to run the frontend on.
# The path to run the frontend on. For example, "/app" will run the frontend on http://localhost:3000/app
frontend_path: str = ""
# The port to run the backend on.
# The port to run the backend on. NOTE: When running in dev mode, the next available port will be used if this is taken.
backend_port: int = 8000
# The backend url the frontend will connect to.
# The backend url the frontend will connect to. This must be updated if the backend is hosted elsewhere, or in production.
api_url: str = f"http://localhost:{backend_port}"
# The url the frontend will be hosted on.
@ -166,10 +184,10 @@ class Config(Base):
# The url the backend will be hosted on.
backend_host: str = "0.0.0.0"
# The database url.
# The database url used by rx.Model.
db_url: Optional[str] = "sqlite:///reflex.db"
# The redis url.
# The redis url
redis_url: Optional[str] = None
# Telemetry opt-in.
@ -190,9 +208,6 @@ class Config(Base):
# Whether to enable or disable nextJS gzip compression.
next_compression: bool = True
# The event namespace for ws connection
event_namespace: Optional[str] = None
# Additional frontend packages to install.
frontend_packages: List[str] = []
@ -216,9 +231,6 @@ class Config(Base):
"""
super().__init__(*args, **kwargs)
# Check for deprecated values.
self.check_deprecated_values(**kwargs)
# Update the config from environment variables.
env_kwargs = self.update_from_env()
for key, env_value in env_kwargs.items():
@ -238,29 +250,8 @@ class Config(Base):
"""
return ".".join([self.app_name, self.app_name])
@staticmethod
def check_deprecated_values(**kwargs):
"""Check for deprecated config values.
Args:
**kwargs: The kwargs passed to the config.
Raises:
ValueError: If a deprecated config value is found.
"""
if "db_config" in kwargs:
raise ValueError("db_config is deprecated - use db_url instead")
if "admin_dash" in kwargs:
raise ValueError(
"admin_dash is deprecated in the config - pass it as a param to rx.App instead"
)
if "env_path" in kwargs:
raise ValueError(
"env_path is deprecated - use environment variables instead"
)
def update_from_env(self) -> dict[str, Any]:
"""Update the config from environment variables.
"""Update the config values based on set environment variables.
Returns:
The updated config values.
@ -300,20 +291,11 @@ class Config(Base):
return updated_values
def get_event_namespace(self) -> str:
"""Get the websocket event namespace.
"""Get the path that the backend Websocket server lists on.
Returns:
The namespace for websocket.
"""
if self.event_namespace:
console.deprecate(
feature_name="Passing event_namespace in the config",
reason="",
deprecation_version="0.3.5",
removal_version="0.5.0",
)
return f'/{self.event_namespace.strip("/")}'
event_url = constants.Endpoint.EVENT.get_url()
return urllib.parse.urlsplit(event_url).path

View File

@ -157,7 +157,7 @@ def _run(
if prerequisites.needs_reinit(frontend=frontend):
_init(name=config.app_name, loglevel=loglevel)
# If something is running on the ports, ask the user if they want to kill or change it.
# Find the next available open port.
if frontend and processes.is_process_on_port(frontend_port):
frontend_port = processes.change_port(frontend_port, "frontend")

View File

@ -1909,7 +1909,7 @@ class OnLoadInternalState(State):
Returns:
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)
load_events = app.get_load_events(self.router.page.path)
if not load_events:
@ -1927,8 +1927,45 @@ class OnLoadInternalState(State):
class ComponentState(Base):
"""The base class for a State that is copied for each Component associated with it."""
"""Base class to allow for the creation of a state instance per component.
This allows for the bundling of UI and state logic into a single class,
where each instance has a separate instance of the state.
Subclass this class and define vars and event handlers in the traditional way.
Then define a `get_component` method that returns the UI for the component instance.
See the full [docs](https://reflex.dev/docs/substates/component-state/) for more.
Basic example:
```python
# Subclass ComponentState and define vars and event handlers.
class Counter(rx.ComponentState):
# Define vars that change.
count: int = 0
# Define event handlers.
def increment(self):
self.count += 1
def decrement(self):
self.count -= 1
@classmethod
def get_component(cls, **props):
# Access the state vars and event handlers using `cls`.
return rx.hstack(
rx.button("Decrement", on_click=cls.decrement),
rx.text(cls.count),
rx.button("Increment", on_click=cls.increment),
**props,
)
counter = Counter.create()
```
"""
# The number of components created from this class.
_per_component_state_instance_count: ClassVar[int] = 0
@classmethod

View File

@ -42,7 +42,6 @@ import reflex.utils.prerequisites
import reflex.utils.processes
from reflex.state import (
BaseState,
State,
StateManagerMemory,
StateManagerRedis,
reload_state_module,
@ -598,7 +597,7 @@ class AppHarness:
await self.state_manager.close()
@contextlib.asynccontextmanager
async def modify_state(self, token: str) -> AsyncIterator[State]:
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
"""Modify the state associated with the given token and send update to frontend.
Args:

View File

@ -240,7 +240,7 @@ def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType:
# For py3.8 and 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()
app.compile_(export=export)
app._compile(export=export)
return app_module

View File

@ -34,7 +34,7 @@ PWD = Path(".").resolve()
EXCLUDED_FILES = [
"__init__.py",
"app.py",
# "app.py",
"component.py",
"bare.py",
"foreach.py",

View File

@ -260,6 +260,7 @@ def test_add_page_set_route_dynamic(index_page, windows_platform: bool):
windows_platform: Whether the system is windows.
"""
app = App(state=EmptyState)
assert app.state is not None
route = "/test/[dynamic]"
if windows_platform:
route.lstrip("/").replace("/", "\\")
@ -953,6 +954,7 @@ async def test_dynamic_route_var_route_change_completed_on_load(
if windows_platform:
route.lstrip("/").replace("/", "\\")
app = app_module_mock.app = App(state=DynamicState)
assert app.state is not None
assert arg_name not in app.state.vars
app.add_page(index_page, route=route, on_load=DynamicState.on_load) # type: ignore
assert arg_name in app.state.vars
@ -1147,7 +1149,7 @@ async def test_process_events(mocker, token: str):
"ip": "127.0.0.1",
}
app = App(state=GenState)
mocker.patch.object(app, "postprocess", AsyncMock())
mocker.patch.object(app, "_postprocess", AsyncMock())
event = Event(
token=token, name="gen_state.go", payload={"c": 5}, router_data=router_data
)
@ -1156,7 +1158,7 @@ async def test_process_events(mocker, token: str):
pass
assert (await app.state_manager.get_state(event.substate_token)).value == 5
assert app.postprocess.call_count == 6
assert app._postprocess.call_count == 6
if isinstance(app.state_manager, StateManagerRedis):
await app.state_manager.close()
@ -1236,7 +1238,7 @@ def compilable_app(tmp_path) -> Generator[tuple[App, Path], None, None]:
web_dir.mkdir(parents=True)
(web_dir / "package.json").touch()
app = App(theme=None)
app.get_frontend_packages = unittest.mock.Mock()
app._get_frontend_packages = unittest.mock.Mock()
with chdir(app_path):
yield app, web_dir
@ -1249,7 +1251,7 @@ def test_app_wrap_compile_theme(compilable_app):
"""
app, web_dir = compilable_app
app.theme = rx.theme(accent_color="plum")
app.compile_()
app._compile()
app_js_contents = (web_dir / "pages" / "_app.js").read_text()
app_js_lines = [
line.strip() for line in app_js_contents.splitlines() if line.strip()
@ -1299,7 +1301,7 @@ def test_app_wrap_priority(compilable_app):
return Fragment1.create(Fragment3.create())
app.add_page(page)
app.compile_()
app._compile()
app_js_contents = (web_dir / "pages" / "_app.js").read_text()
app_js_lines = [
line.strip() for line in app_js_contents.splitlines() if line.strip()
@ -1371,7 +1373,7 @@ def test_raise_on_state():
"""Test that the state is set."""
# state kwargs is deprecated, we just make sure the app is created anyway.
_app = App(state=State)
print(_app.state)
assert _app.state is not None
assert issubclass(_app.state, State)
@ -1387,7 +1389,7 @@ def test_app_with_optional_endpoints():
app = App()
Upload.is_used = True
app.add_optional_endpoints()
app._add_optional_endpoints()
# TODO: verify the availability of the endpoints in app.api
@ -1395,7 +1397,7 @@ def test_app_state_manager():
app = App()
with pytest.raises(ValueError):
app.state_manager
app.enable_state()
app._enable_state()
assert app.state_manager is not None
assert isinstance(app.state_manager, (StateManagerMemory, StateManagerRedis))
@ -1479,7 +1481,7 @@ def test_app_with_transpile_packages(compilable_app, export):
C1.create(), C2.create(), C3.create(), C4.create(), C5.create()
)
app.add_page(page, route="/")
app.compile_(export=export)
app._compile(export=export)
next_config = (web_dir / "next.config.js").read_text()
transpile_packages_match = re.search(r"transpilePackages: (\[.*?\])", next_config)

View File

@ -1,6 +1,5 @@
import multiprocessing
import os
from typing import Any, Dict
import pytest
@ -25,25 +24,6 @@ def test_set_app_name(base_config_values):
assert config.app_name == base_config_values["app_name"]
@pytest.mark.parametrize(
"param",
[
"db_config",
"admin_dash",
"env_path",
],
)
def test_deprecated_params(base_config_values: Dict[str, Any], param):
"""Test that deprecated params are removed from the config.
Args:
base_config_values: Config values.
param: The deprecated param.
"""
with pytest.raises(ValueError):
rx.Config(**base_config_values, **{param: "test"}) # type: ignore
@pytest.mark.parametrize(
"env_var, value",
[
@ -87,12 +67,6 @@ def test_update_from_env(base_config_values, monkeypatch, env_var, value):
{"app_name": "test_app", "api_url": "http://example.com/api"},
f"/api{Endpoint.EVENT}",
),
({"app_name": "test_app", "event_namespace": "/event"}, f"/event"),
({"app_name": "test_app", "event_namespace": "event"}, f"/event"),
({"app_name": "test_app", "event_namespace": "event/"}, f"/event"),
({"app_name": "test_app", "event_namespace": "/_event"}, f"{Endpoint.EVENT}"),
({"app_name": "test_app", "event_namespace": "_event"}, f"{Endpoint.EVENT}"),
({"app_name": "test_app", "event_namespace": "_event/"}, f"{Endpoint.EVENT}"),
],
)
def test_event_namespace(mocker, kwargs, expected):

View File

@ -24,7 +24,7 @@ def test_app_harness(tmp_path):
app = rx.App(state=State)
app.add_page(lambda: rx.text("Basic App"), route="/", title="index")
app.compile_()
app._compile()
with AppHarness.create(
root=tmp_path,