From 7388617b7212f15023d63dc7871d4e46471d328a Mon Sep 17 00:00:00 2001
From: jackie-pc <jackie@pynecone.io>
Date: Mon, 18 Dec 2023 16:06:21 -0800
Subject: [PATCH] apps should no longer call "app.compile()" (#2291)

---
 README.md                             |  1 -
 docs/es/README.md                     |  2 --
 docs/in/README.md                     |  2 --
 docs/it/README.md                     |  2 --
 docs/kr/README.md                     |  2 --
 docs/pt/pt_br/README.md               |  2 --
 docs/tr/README.md                     |  2 --
 docs/zh/zh_cn/README.md               |  2 --
 docs/zh/zh_tw/README.md               |  2 --
 integration/test_background_task.py   |  1 -
 integration/test_call_script.py       |  2 --
 integration/test_client_storage.py    |  1 -
 integration/test_connection_banner.py |  1 -
 integration/test_dynamic_routes.py    |  1 -
 integration/test_event_actions.py     |  1 -
 integration/test_event_chain.py       |  2 --
 integration/test_form_submit.py       |  4 ----
 integration/test_input.py             |  2 --
 integration/test_login_flow.py        |  1 -
 integration/test_server_side_event.py |  2 --
 integration/test_table.py             |  2 --
 integration/test_upload.py            |  1 -
 integration/test_var_operations.py    |  2 --
 reflex/app.py                         | 13 +++++++++++++
 reflex/app.pyi                        |  1 +
 reflex/app_module_for_backend.py      | 13 +++++++++++++
 reflex/reflex.py                      | 10 ++++++----
 reflex/state.py                       |  2 ++
 reflex/testing.py                     |  2 +-
 reflex/utils/exec.py                  |  4 ++--
 reflex/utils/export.py                |  2 +-
 reflex/utils/prerequisites.py         | 15 +++++++++++++++
 tests/test_app.py                     |  4 ++--
 tests/test_state.py                   |  6 +++++-
 tests/test_testing.py                 |  2 +-
 35 files changed, 62 insertions(+), 52 deletions(-)
 create mode 100644 reflex/app_module_for_backend.py

diff --git a/README.md b/README.md
index 0b301ea0d..874c2de92 100644
--- a/README.md
+++ b/README.md
@@ -123,7 +123,6 @@ def index():
 # Add state and page to the app.
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## Let's break this down.
diff --git a/docs/es/README.md b/docs/es/README.md
index dd82b7461..74c3e24a7 100644
--- a/docs/es/README.md
+++ b/docs/es/README.md
@@ -121,7 +121,6 @@ def index():
 # Add state and page to the app.
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## Vamos a desglosarlo.
@@ -191,7 +190,6 @@ Añadimos una página desde la raíz (root) de la aplicación al componente de 
 
 ```python
 app.add_page(index, title="DALL-E")
-app.compile()
 ```
 
 Puedes crear una aplicación multipágina añadiendo más páginas.
diff --git a/docs/in/README.md b/docs/in/README.md
index 220504ef4..12e662f11 100644
--- a/docs/in/README.md
+++ b/docs/in/README.md
@@ -120,7 +120,6 @@ def index():
 # Add state and page to the app.
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## चलो इसे विस्तार से देखते हैं।
@@ -190,7 +189,6 @@ app = rx.App()
 
 ```python
 app.add_page(index, title="DALL-E")
-app.compile()
 ```
 
 आप और पेज जोड़कर एक मल्टी-पेज एप्लिकेशन बना सकते हैं।
diff --git a/docs/it/README.md b/docs/it/README.md
index 04e3f1e9b..77274ae03 100644
--- a/docs/it/README.md
+++ b/docs/it/README.md
@@ -121,7 +121,6 @@ def index():
 # Aggiungi stato e pagina all'app.
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## Analizziamolo
@@ -191,7 +190,6 @@ Possiamo aggiungere una pagina dalla radice dell'app al componente dell'indice.A
 
 ```python
 app.add_page(index, title="DALL-E")
-app.compile()
 ```
 
 Puoi creare un'app multi-pagina aggiungendo altre pagine.
diff --git a/docs/kr/README.md b/docs/kr/README.md
index b671458d6..2e443c855 100644
--- a/docs/kr/README.md
+++ b/docs/kr/README.md
@@ -121,7 +121,6 @@ def index():
 # Add state and page to the app.
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## 하나씩 살펴보겠습니다.
@@ -194,7 +193,6 @@ app = rx.App()
 
 ```python
 app.add_page(index, title="DALL-E")
-app.compile()
 ```
 
 여러 페이지를 추가하여 멀티 페이지 앱을 만들 수 있습니다.
diff --git a/docs/pt/pt_br/README.md b/docs/pt/pt_br/README.md
index ac69f009f..96708a75b 100644
--- a/docs/pt/pt_br/README.md
+++ b/docs/pt/pt_br/README.md
@@ -121,7 +121,6 @@ def index():
 # Adição do estado e da página no app.
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## Vamos por partes.
@@ -192,7 +191,6 @@ Adicionamos uma página na raíz do app, apontando para o componente index. Tamb
 
 ```python
 app.add_page(index, title="DALL-E")
-app.compile()
 ```
 
 Você pode criar mais páginas e adicioná-las ao seu app.
diff --git a/docs/tr/README.md b/docs/tr/README.md
index 970161adf..9b95a6fc4 100644
--- a/docs/tr/README.md
+++ b/docs/tr/README.md
@@ -124,7 +124,6 @@ def index():
 # Sayfa ve durumu uygulamaya ekleyin.
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## Daha Detaylı İceleyelim
@@ -194,7 +193,6 @@ Uygulamamızın kök dizinine index bileşeninden bir sayfa ekliyoruz. Ayrıca s
 
 ```python
 app.add_page(index, title="DALL-E")
-app.compile()
 ```
 
 Daha fazla sayfa ekleyerek çok sayfalı bir uygulama oluşturabilirsiniz.
diff --git a/docs/zh/zh_cn/README.md b/docs/zh/zh_cn/README.md
index bf6dbe179..6e6da1282 100644
--- a/docs/zh/zh_cn/README.md
+++ b/docs/zh/zh_cn/README.md
@@ -122,7 +122,6 @@ def index():
 # Add state and page to the app.
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## 让我们分解以上步骤.
@@ -194,7 +193,6 @@ app = rx.App()
 
 ```python
 app.add_page(index, title="DALL-E")
-app.compile()
 ```
 
 您可以通过增加更多页面来创建一个多页面的应用.
diff --git a/docs/zh/zh_tw/README.md b/docs/zh/zh_tw/README.md
index 098f661a6..ced699f47 100644
--- a/docs/zh/zh_tw/README.md
+++ b/docs/zh/zh_tw/README.md
@@ -121,7 +121,6 @@ def index():
 # 把狀態跟頁面添加到應用程式。
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## 讓我們來拆解一下。
@@ -192,7 +191,6 @@ app = rx.App()
 
 ```python
 app.add_page(index, title="DALL-E")
-app.compile()
 ```
 
 你可以添加更多頁面至路由藉此來建立多頁面應用程式(multi-page app)
diff --git a/integration/test_background_task.py b/integration/test_background_task.py
index bc70ff01f..799d68c2f 100644
--- a/integration/test_background_task.py
+++ b/integration/test_background_task.py
@@ -95,7 +95,6 @@ def BackgroundTask():
 
     app = rx.App(state=rx.State)
     app.add_page(index)
-    app.compile()
 
 
 @pytest.fixture(scope="session")
diff --git a/integration/test_call_script.py b/integration/test_call_script.py
index 95bdf37c9..c3a364306 100644
--- a/integration/test_call_script.py
+++ b/integration/test_call_script.py
@@ -227,8 +227,6 @@ def CallScript():
             rx.button("Reset", id="reset", on_click=CallScriptState.reset_),
         )
 
-    app.compile()
-
 
 @pytest.fixture(scope="session")
 def call_script(tmp_path_factory) -> Generator[AppHarness, None, None]:
diff --git a/integration/test_client_storage.py b/integration/test_client_storage.py
index a9a311d8c..2678f3d52 100644
--- a/integration/test_client_storage.py
+++ b/integration/test_client_storage.py
@@ -100,7 +100,6 @@ def ClientSide():
     app = rx.App(state=rx.State)
     app.add_page(index)
     app.add_page(index, route="/foo")
-    app.compile()
 
 
 @pytest.fixture(scope="session")
diff --git a/integration/test_connection_banner.py b/integration/test_connection_banner.py
index 0468e6b53..293d2e412 100644
--- a/integration/test_connection_banner.py
+++ b/integration/test_connection_banner.py
@@ -21,7 +21,6 @@ def ConnectionBanner():
 
     app = rx.App(state=rx.State)
     app.add_page(index)
-    app.compile()
 
 
 @pytest.fixture()
diff --git a/integration/test_dynamic_routes.py b/integration/test_dynamic_routes.py
index cafb9c61b..7cb64754f 100644
--- a/integration/test_dynamic_routes.py
+++ b/integration/test_dynamic_routes.py
@@ -66,7 +66,6 @@ def DynamicRoute():
     app.add_page(index, route="/page/[page_id]", on_load=DynamicState.on_load)  # type: ignore
     app.add_page(index, route="/static/x", on_load=DynamicState.on_load)  # type: ignore
     app.add_custom_404_page(on_load=DynamicState.on_load)  # type: ignore
-    app.compile()
 
 
 @pytest.fixture(scope="session")
diff --git a/integration/test_event_actions.py b/integration/test_event_actions.py
index da444a5cf..89c8ece22 100644
--- a/integration/test_event_actions.py
+++ b/integration/test_event_actions.py
@@ -132,7 +132,6 @@ def TestEventAction():
 
     app = rx.App(state=rx.State)
     app.add_page(index)
-    app.compile()
 
 
 @pytest.fixture(scope="session")
diff --git a/integration/test_event_chain.py b/integration/test_event_chain.py
index 7d003635e..ec3f125b4 100644
--- a/integration/test_event_chain.py
+++ b/integration/test_event_chain.py
@@ -242,8 +242,6 @@ def EventChain():
     app.add_page(on_mount_return_chain)
     app.add_page(on_mount_yield_chain)
 
-    app.compile()
-
 
 @pytest.fixture(scope="session")
 def event_chain(tmp_path_factory) -> Generator[AppHarness, None, None]:
diff --git a/integration/test_form_submit.py b/integration/test_form_submit.py
index 8d36cc01d..b64846c1b 100644
--- a/integration/test_form_submit.py
+++ b/integration/test_form_submit.py
@@ -62,8 +62,6 @@ def FormSubmit():
             height="100vh",
         )
 
-    app.compile()
-
 
 def FormSubmitName():
     """App with a form using on_submit."""
@@ -124,8 +122,6 @@ def FormSubmitName():
             height="100vh",
         )
 
-    app.compile()
-
 
 @pytest.fixture(
     scope="session", params=[FormSubmit, FormSubmitName], ids=["id", "name"]
diff --git a/integration/test_input.py b/integration/test_input.py
index 2ca15424e..111124349 100644
--- a/integration/test_input.py
+++ b/integration/test_input.py
@@ -40,8 +40,6 @@ def FullyControlledInput():
             rx.button("CLEAR", on_click=rx.set_value("on_change_input", "")),
         )
 
-    app.compile()
-
 
 @pytest.fixture()
 def fully_controlled_input(tmp_path) -> Generator[AppHarness, None, None]:
diff --git a/integration/test_login_flow.py b/integration/test_login_flow.py
index f53635743..a4272fe39 100644
--- a/integration/test_login_flow.py
+++ b/integration/test_login_flow.py
@@ -45,7 +45,6 @@ def LoginSample():
     app = rx.App(state=rx.State)
     app.add_page(index)
     app.add_page(login)
-    app.compile()
 
 
 @pytest.fixture(scope="session")
diff --git a/integration/test_server_side_event.py b/integration/test_server_side_event.py
index 31a38ba36..b8e8d21c0 100644
--- a/integration/test_server_side_event.py
+++ b/integration/test_server_side_event.py
@@ -75,8 +75,6 @@ def ServerSideEvent():
             ),
         )
 
-    app.compile()
-
 
 @pytest.fixture(scope="session")
 def server_side_event(tmp_path_factory) -> Generator[AppHarness, None, None]:
diff --git a/integration/test_table.py b/integration/test_table.py
index 00e6a8b22..560d70b94 100644
--- a/integration/test_table.py
+++ b/integration/test_table.py
@@ -88,8 +88,6 @@ def Table():
             )
         )
 
-    app.compile()
-
 
 @pytest.fixture()
 def table(tmp_path_factory) -> Generator[AppHarness, None, None]:
diff --git a/integration/test_upload.py b/integration/test_upload.py
index 13fb28d36..c832f5acf 100644
--- a/integration/test_upload.py
+++ b/integration/test_upload.py
@@ -115,7 +115,6 @@ def UploadFile():
 
     app = rx.App(state=rx.State)
     app.add_page(index)
-    app.compile()
 
 
 @pytest.fixture(scope="session")
diff --git a/integration/test_var_operations.py b/integration/test_var_operations.py
index a0f245fe4..4c56c01f5 100644
--- a/integration/test_var_operations.py
+++ b/integration/test_var_operations.py
@@ -565,8 +565,6 @@ def VarOperations():
             ),
         )
 
-    app.compile()
-
 
 @pytest.fixture(scope="session")
 def var_operations(tmp_path_factory) -> Generator[AppHarness, None, None]:
diff --git a/reflex/app.py b/reflex/app.py
index 63930acea..948d3622a 100644
--- a/reflex/app.py
+++ b/reflex/app.py
@@ -619,6 +619,19 @@ class App(Base):
         return True
 
     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.4.0",
+        )
+        return
+
+    def compile_(self):
         """Compile the app and output it to the pages folder."""
         # add the pages before the compile check so App know onload methods
         for render, kwargs in DECORATED_PAGES:
diff --git a/reflex/app.pyi b/reflex/app.pyi
index b3b77bd20..38175ca28 100644
--- a/reflex/app.pyi
+++ b/reflex/app.pyi
@@ -120,6 +120,7 @@ class App(Base):
     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 _process_background(
         self, state: State, event: Event
diff --git a/reflex/app_module_for_backend.py b/reflex/app_module_for_backend.py
new file mode 100644
index 000000000..4a5b10719
--- /dev/null
+++ b/reflex/app_module_for_backend.py
@@ -0,0 +1,13 @@
+"""Shims the real reflex app module for running backend server (uvicorn or gunicorn).
+Only the app attribute is explicitly exposed.
+"""
+from reflex import constants
+from reflex.utils.prerequisites import get_compiled_app
+
+if "app" != constants.CompileVars.APP:
+    raise AssertionError("unexpected variable name for 'app'")
+app = getattr(get_compiled_app(), constants.CompileVars.APP)
+
+# ensure only "app" is exposed.
+del get_compiled_app
+del constants
diff --git a/reflex/reflex.py b/reflex/reflex.py
index d558b2e17..dc0fb58ea 100644
--- a/reflex/reflex.py
+++ b/reflex/reflex.py
@@ -178,7 +178,7 @@ def _run(
     if frontend:
         prerequisites.update_next_config()
         # Get the app module.
-        prerequisites.get_app()
+        prerequisites.get_compiled_app()
 
     # Warn if schema is not up to date.
     prerequisites.check_schema_up_to_date()
@@ -362,7 +362,7 @@ def db_init():
 
     # Initialize the database.
     _skip_compile()
-    prerequisites.get_app()
+    prerequisites.get_compiled_app()
     model.Model.alembic_init()
     model.Model.migrate(autogenerate=True)
 
@@ -373,8 +373,9 @@ def migrate():
     from reflex import model
     from reflex.utils import prerequisites
 
+    # TODO see if we can use `get_app()` instead (no compile).  Would _skip_compile still be needed then?
     _skip_compile()
-    prerequisites.get_app()
+    prerequisites.get_compiled_app()
     if not prerequisites.check_db_initialized():
         return
     model.Model.migrate()
@@ -393,8 +394,9 @@ def makemigrations(
     from reflex import model
     from reflex.utils import prerequisites
 
+    # TODO see if we can use `get_app()` instead (no compile).  Would _skip_compile still be needed then?
     _skip_compile()
-    prerequisites.get_app()
+    prerequisites.get_compiled_app()
     if not prerequisites.check_db_initialized():
         return
     with model.Model.get_db_engine().connect() as connection:
diff --git a/reflex/state.py b/reflex/state.py
index db756d5c2..04e2554a5 100644
--- a/reflex/state.py
+++ b/reflex/state.py
@@ -1316,6 +1316,7 @@ class State(BaseState):
         Returns:
             The list of events to queue for on load handling.
         """
+        # 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 and self.is_hydrated:
@@ -1364,6 +1365,7 @@ class StateProxy(wrapt.ObjectProxy):
             state_instance: The state instance to proxy.
         """
         super().__init__(state_instance)
+        # compile is not relevant to backend logic
         self._self_app = getattr(prerequisites.get_app(), constants.CompileVars.APP)
         self._self_substate_path = state_instance.get_full_name().split(".")
         self._self_actx = None
diff --git a/reflex/testing.py b/reflex/testing.py
index 4e32f52b3..63a56ff06 100644
--- a/reflex/testing.py
+++ b/reflex/testing.py
@@ -165,7 +165,7 @@ class AppHarness:
             # reset rx.State subclasses
             State.class_subclasses.clear()
             # self.app_module.app.
-            self.app_module = reflex.utils.prerequisites.get_app(reload=True)
+            self.app_module = reflex.utils.prerequisites.get_compiled_app(reload=True)
         self.app_instance = self.app_module.app
         if isinstance(self.app_instance.state_manager, StateManagerRedis):
             # Create our own redis connection for testing.
diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py
index 04b225a18..a992534b4 100644
--- a/reflex/utils/exec.py
+++ b/reflex/utils/exec.py
@@ -160,7 +160,7 @@ def run_backend(
     import uvicorn
 
     config = get_config()
-    app_module = f"{config.app_name}.{config.app_name}:{constants.CompileVars.APP}"
+    app_module = f"reflex.app_module_for_backend:{constants.CompileVars.APP}"
 
     # Create a .nocompile file to skip compile for backend.
     if os.path.exists(constants.Dirs.WEB):
@@ -196,7 +196,7 @@ def run_backend_prod(
     config = get_config()
     RUN_BACKEND_PROD = f"gunicorn --worker-class {config.gunicorn_worker_class} --preload --timeout {config.timeout} --log-level critical".split()
     RUN_BACKEND_PROD_WINDOWS = f"uvicorn --timeout-keep-alive {config.timeout}".split()
-    app_module = f"{config.app_name}.{config.app_name}:{constants.CompileVars.APP}"
+    app_module = f"reflex.app_module_for_backend:{constants.CompileVars.APP}"
     command = (
         [
             *RUN_BACKEND_PROD_WINDOWS,
diff --git a/reflex/utils/export.py b/reflex/utils/export.py
index f2b8e69a8..12a6933fb 100644
--- a/reflex/utils/export.py
+++ b/reflex/utils/export.py
@@ -56,7 +56,7 @@ def export(
         # Update some parameters for export
         prerequisites.update_next_config(export=True)
         # Ensure module can be imported and app.compile() is called.
-        prerequisites.get_app()
+        prerequisites.get_compiled_app()
         # Set up .web directory and install frontend dependencies.
         build.setup_frontend(Path.cwd())
 
diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py
index 2b9c6d8c3..58bb6bdd2 100644
--- a/reflex/utils/prerequisites.py
+++ b/reflex/utils/prerequisites.py
@@ -160,9 +160,24 @@ def get_app(reload: bool = False) -> ModuleType:
     app = __import__(module, fromlist=(constants.CompileVars.APP,))
     if reload:
         importlib.reload(app)
+
     return app
 
 
+def get_compiled_app(reload: bool = False) -> ModuleType:
+    """Get the app module based on the default config after first compiling it.
+
+    Args:
+        reload: Re-import the app module from disk
+
+    Returns:
+        The compiled app based on the default config.
+    """
+    app_module = get_app(reload=reload)
+    getattr(app_module, constants.CompileVars.APP).compile_()
+    return app_module
+
+
 def get_redis() -> Redis | None:
     """Get the redis client.
 
diff --git a/tests/test_app.py b/tests/test_app.py
index eb1b783ed..ac4028536 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -1213,7 +1213,7 @@ def test_app_wrap_compile_theme(compilable_app):
     """
     app, web_dir = compilable_app
     app.theme = rdxt.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()
@@ -1263,7 +1263,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()
diff --git a/tests/test_state.py b/tests/test_state.py
index de9187efc..423886655 100644
--- a/tests/test_state.py
+++ b/tests/test_state.py
@@ -1590,7 +1590,11 @@ def mock_app(monkeypatch, state_manager: StateManager) -> rx.App:
     app.state = TestState
     app._state_manager = state_manager
     app.event_namespace.emit = AsyncMock()  # type: ignore
-    monkeypatch.setattr(prerequisites, "get_app", lambda: app_module)
+
+    def _mock_get_app(*args, **kwargs):
+        return app_module
+
+    monkeypatch.setattr(prerequisites, "get_app", _mock_get_app)
     return app
 
 
diff --git a/tests/test_testing.py b/tests/test_testing.py
index ff87534ba..10b3d9663 100644
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -21,7 +21,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,