From 7903a1020dc98c4f68d216ca5cd6784da233f39e Mon Sep 17 00:00:00 2001 From: Nikhil Rao Date: Thu, 2 May 2024 18:15:28 -0700 Subject: [PATCH] Clean up config and app API (#3197) --- .../test_benchmark_compile_components.py | 12 +- benchmarks/test_benchmark_compile_pages.py | 20 +-- reflex/app.py | 150 +++++++++--------- reflex/app.pyi | 148 ----------------- reflex/app_module_for_backend.py | 2 +- reflex/config.py | 74 ++++----- reflex/reflex.py | 2 +- reflex/state.py | 41 ++++- reflex/testing.py | 3 +- reflex/utils/prerequisites.py | 2 +- reflex/utils/pyi_generator.py | 2 +- tests/test_app.py | 20 +-- tests/test_config.py | 26 --- tests/test_testing.py | 2 +- 14 files changed, 177 insertions(+), 327 deletions(-) delete mode 100644 reflex/app.pyi diff --git a/benchmarks/test_benchmark_compile_components.py b/benchmarks/test_benchmark_compile_components.py index 3c26149e8..14d5d4d89 100644 --- a/benchmarks/test_benchmark_compile_components.py +++ b/benchmarks/test_benchmark_compile_components.py @@ -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) diff --git a/benchmarks/test_benchmark_compile_pages.py b/benchmarks/test_benchmark_compile_pages.py index 795218164..97e1bb68e 100644 --- a/benchmarks/test_benchmark_compile_pages.py +++ b/benchmarks/test_benchmark_compile_pages.py @@ -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() diff --git a/reflex/app.py b/reflex/app.py index 95c9de2c6..677f84aa7 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -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 diff --git a/reflex/app.pyi b/reflex/app.pyi deleted file mode 100644 index cab598e87..000000000 --- a/reflex/app.pyi +++ /dev/null @@ -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: ... diff --git a/reflex/app_module_for_backend.py b/reflex/app_module_for_backend.py index b9febba5d..8d97725f9 100644 --- a/reflex/app_module_for_backend.py +++ b/reflex/app_module_for_backend.py @@ -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() diff --git a/reflex/config.py b/reflex/config.py index f10b38e96..e00a9213b 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -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 diff --git a/reflex/reflex.py b/reflex/reflex.py index e0038d8ea..010cceaeb 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -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") diff --git a/reflex/state.py b/reflex/state.py index 26dfed93e..e29aa1a98 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -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 diff --git a/reflex/testing.py b/reflex/testing.py index f7b4833be..52eadb7f4 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -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: diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 606f70d2b..b2fb3946e 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -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 diff --git a/reflex/utils/pyi_generator.py b/reflex/utils/pyi_generator.py index a7f8acb63..e4ba068cf 100644 --- a/reflex/utils/pyi_generator.py +++ b/reflex/utils/pyi_generator.py @@ -34,7 +34,7 @@ PWD = Path(".").resolve() EXCLUDED_FILES = [ "__init__.py", - "app.py", + # "app.py", "component.py", "bare.py", "foreach.py", diff --git a/tests/test_app.py b/tests/test_app.py index 1e1df01bf..6e8939059 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -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) diff --git a/tests/test_config.py b/tests/test_config.py index 1ba2f548d..31dd77649 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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): diff --git a/tests/test_testing.py b/tests/test_testing.py index b438b4fd8..c53e9e775 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -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,