Переглянути джерело

Remove app_module_for_backend (#4749)

* Remove app_module_for_backend

* Replace app_module_for_backend with ASGI factory on `rx.App.__call__`
* Consolidate module spec for uvicorn, gunicorn, and granian

* update unit test

* granian is funny

* 🐶

* possibly

* don't compile the app on backend

* potentially less hacky solution

* use pool executor with functions not returning anything

* dang it darglint

---------

Co-authored-by: Lendemor <thomas.brandeho@gmail.com>
Co-authored-by: Khaleel Al-Adhami <khaleel.aladhami@gmail.com>
Masen Furer 2 місяців тому
батько
коміт
0e2d392bf8

+ 16 - 0
reflex/app.py

@@ -576,6 +576,22 @@ class App(MiddlewareMixin, LifespanMixin):
         """
         if not self.api:
             raise ValueError("The app has not been initialized.")
+
+        # 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).
+        self._apply_decorated_pages()
+
+        compile_future = concurrent.futures.ThreadPoolExecutor(max_workers=1).submit(
+            self._compile
+        )
+        compile_future.add_done_callback(
+            # Force background compile errors to print eagerly
+            lambda f: f.result()
+        )
+        # Wait for the compile to finish in prod mode to ensure all optional endpoints are mounted.
+        if is_prod_mode():
+            compile_future.result()
+
         return self.api
 
     def _add_default_endpoints(self):

+ 0 - 33
reflex/app_module_for_backend.py

@@ -1,33 +0,0 @@
-"""Shims the real reflex app module for running backend server (uvicorn or gunicorn).
-Only the app attribute is explicitly exposed.
-"""
-
-from concurrent.futures import ThreadPoolExecutor
-
-from reflex import constants
-from reflex.utils.exec import is_prod_mode
-from reflex.utils.prerequisites import get_and_validate_app
-
-if constants.CompileVars.APP != "app":
-    raise AssertionError("unexpected variable name for 'app'")
-
-app, app_module = get_and_validate_app(reload=False)
-# For py3.9 compatibility when redis is used, we MUST add any decorator pages
-# before compiling the app in a thread to avoid event loop error (REF-2172).
-app._apply_decorated_pages()
-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()
-)
-# Wait for the compile to finish in prod mode to ensure all optional endpoints are mounted.
-if is_prod_mode():
-    compile_future.result()
-
-# ensure only "app" is exposed.
-del app_module
-del compile_future
-del get_and_validate_app
-del is_prod_mode
-del constants
-del ThreadPoolExecutor

+ 13 - 3
reflex/reflex.py

@@ -3,6 +3,7 @@
 from __future__ import annotations
 
 import atexit
+import concurrent.futures
 from pathlib import Path
 
 import typer
@@ -14,6 +15,7 @@ from reflex.config import environment, get_config
 from reflex.custom_components.custom_components import custom_components_cli
 from reflex.state import reset_disk_state_manager
 from reflex.utils import console, redir, telemetry
+from reflex.utils.exec import should_use_granian
 
 # Disable typer+rich integration for help panels
 typer.core.rich = None  # pyright: ignore [reportPrivateImportUsage]
@@ -203,9 +205,17 @@ def _run(
 
     prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
 
-    if frontend:
-        # Get the app module.
-        prerequisites.get_compiled_app()
+    # Get the app module.
+    app_task = prerequisites.compile_app if frontend else prerequisites.validate_app
+
+    # Granian fails if the app is already imported.
+    if should_use_granian():
+        compile_future = concurrent.futures.ProcessPoolExecutor(max_workers=1).submit(
+            app_task
+        )
+        compile_future.result()
+    else:
+        app_task()
 
     # Warn if schema is not up to date.
     prerequisites.check_schema_up_to_date()

+ 9 - 18
reflex/utils/exec.py

@@ -200,22 +200,9 @@ def get_app_module():
     Returns:
         The app module for the backend.
     """
-    return f"reflex.app_module_for_backend:{constants.CompileVars.APP}"
-
-
-def get_granian_target():
-    """Get the Granian target for the backend.
-
-    Returns:
-        The Granian target for the backend.
-    """
-    import reflex
-
-    app_module_path = Path(reflex.__file__).parent / "app_module_for_backend.py"
+    config = get_config()
 
-    return (
-        f"{app_module_path!s}:{constants.CompileVars.APP}.{constants.CompileVars.API}"
-    )
+    return f"{config.module}:{constants.CompileVars.APP}"
 
 
 def run_backend(
@@ -317,7 +304,8 @@ def run_uvicorn_backend(host: str, port: int, loglevel: LogLevel):
     import uvicorn
 
     uvicorn.run(
-        app=f"{get_app_module()}.{constants.CompileVars.API}",
+        app=f"{get_app_module()}",
+        factory=True,
         host=host,
         port=port,
         log_level=loglevel.value,
@@ -341,7 +329,8 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel):
         from granian.server import Server as Granian
 
         Granian(
-            target=get_granian_target(),
+            target=get_app_module(),
+            factory=True,
             address=host,
             port=port,
             interface=Interfaces.ASGI,
@@ -419,6 +408,7 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel):
             *("--host", host),
             *("--port", str(port)),
             *("--workers", str(_get_backend_workers())),
+            "--factory",
             app_module,
         ]
         if constants.IS_WINDOWS
@@ -482,7 +472,8 @@ def run_granian_backend_prod(host: str, port: int, loglevel: LogLevel):
             str(port),
             "--interface",
             str(Interfaces.ASGI),
-            get_granian_target(),
+            "--factory",
+            get_app_module(),
         ]
         processes.new_process(
             command,

+ 19 - 0
reflex/utils/prerequisites.py

@@ -412,6 +412,15 @@ def get_and_validate_app(reload: bool = False) -> AppInfo:
     return AppInfo(app=app, module=app_module)
 
 
+def validate_app(reload: bool = False) -> None:
+    """Validate the app instance based on the default config.
+
+    Args:
+        reload: Re-import the app module from disk
+    """
+    get_and_validate_app(reload=reload)
+
+
 def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType:
     """Get the app module based on the default config after first compiling it.
 
@@ -430,6 +439,16 @@ def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType:
     return app_module
 
 
+def compile_app(reload: bool = False, export: bool = False) -> None:
+    """Compile the app module based on the default config.
+
+    Args:
+        reload: Re-import the app module from disk
+        export: Compile the app for export
+    """
+    get_compiled_app(reload=reload, export=export)
+
+
 def get_redis() -> Redis | None:
     """Get the asynchronous redis client.
 

+ 1 - 0
tests/units/test_page.py

@@ -7,6 +7,7 @@ def test_page_decorator():
     def foo_():
         return text("foo")
 
+    DECORATED_PAGES.clear()
     assert len(DECORATED_PAGES) == 0
     decorated_foo_ = page()(foo_)
     assert decorated_foo_ == foo_