Просмотр исходного кода

[REF-2172] Add DECORATED_PAGES before compiling in thread (#2841)

Masen Furer 1 год назад
Родитель
Сommit
eb18ce90d5

+ 1 - 1
.github/workflows/integration_app_harness.yml

@@ -19,7 +19,7 @@ jobs:
     strategy:
       matrix:
         state_manager: [ "redis", "memory" ]
-        python-version: ["3.11.5", "3.12.0"]
+        python-version: ["3.8.18", "3.11.5", "3.12.0"]
     runs-on: ubuntu-latest
     services:
       # Label used to access the service container

+ 2 - 1
integration/init-test/Dockerfile

@@ -1,7 +1,8 @@
-FROM python:3.11
+FROM python:3.8
 
 ARG USERNAME=kerrigan
 RUN useradd -m $USERNAME
+RUN apt-get update && apt-get install -y redis && rm -rf /var/lib/apt/lists/*
 USER $USERNAME
 
 WORKDIR /home/$USERNAME

+ 12 - 0
integration/init-test/in_docker_test_script.sh

@@ -2,6 +2,7 @@
 
 set -euxo pipefail
 
+SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
 export TELEMETRY_ENABLED=false
 
 function do_export () {
@@ -11,6 +12,15 @@ function do_export () {
     rm -rf ~/.local/share/reflex ~/"$template"/.web
     reflex init --template "$template"
     reflex export
+    (
+        cd "$SCRIPTPATH/../.."
+        scripts/integration.sh ~/"$template" dev
+        pkill -9 -f 'next-server|python3' || true
+        sleep 10
+        REDIS_URL=redis://localhost scripts/integration.sh ~/"$template" prod
+        pkill -9 -f 'next-server|python3' || true
+        sleep 10
+    )
 }
 
 echo "Preparing test project dir"
@@ -20,6 +30,8 @@ source ~/venv/bin/activate
 echo "Installing reflex from local repo code"
 pip install /reflex-repo
 
+redis-server &
+
 echo "Running reflex init in test project dir"
 do_export blank
 do_export sidebar

+ 3 - 1
integration/test_call_script.py

@@ -12,6 +12,8 @@ from reflex.testing import AppHarness
 
 def CallScript():
     """A test app for browser javascript integration."""
+    from typing import Dict, List, Optional, Union
+
     import reflex as rx
 
     inline_scripts = """
@@ -37,7 +39,7 @@ def CallScript():
     external_scripts = inline_scripts.replace("inline", "external")
 
     class CallScriptState(rx.State):
-        results: list[str | dict | list | None] = []
+        results: List[Optional[Union[str, Dict, List]]] = []
         inline_counter: int = 0
         external_counter: int = 0
 

+ 5 - 1
integration/test_dynamic_routes.py

@@ -1,4 +1,6 @@
 """Integration tests for dynamic route page behavior."""
+from __future__ import annotations
+
 from typing import Callable, Coroutine, Generator, Type
 from urllib.parse import urlsplit
 
@@ -12,10 +14,12 @@ from .utils import poll_for_navigation
 
 def DynamicRoute():
     """App for testing dynamic routes."""
+    from typing import List
+
     import reflex as rx
 
     class DynamicState(rx.State):
-        order: list[str] = []
+        order: List[str] = []
         page_id: str = ""
 
         def on_load(self):

+ 5 - 2
integration/test_event_actions.py

@@ -1,4 +1,5 @@
 """Ensure stopPropagation and preventDefault work as expected."""
+from __future__ import annotations
 
 import asyncio
 from typing import Callable, Coroutine, Generator
@@ -11,10 +12,12 @@ from reflex.testing import AppHarness, WebDriver
 
 def TestEventAction():
     """App for testing event_actions."""
+    from typing import List, Optional
+
     import reflex as rx
 
     class EventActionState(rx.State):
-        order: list[str]
+        order: List[str]
 
         def on_click(self, ev):
             self.order.append(f"on_click:{ev}")
@@ -27,7 +30,7 @@ def TestEventAction():
 
         tag = "EventFiringComponent"
 
-        def _get_custom_code(self) -> str | None:
+        def _get_custom_code(self) -> Optional[str]:
             return """
                 function EventFiringComponent(props) {
                     return (

+ 3 - 1
integration/test_event_chain.py

@@ -1,4 +1,5 @@
 """Ensure that Event Chains are properly queued and handled between frontend and backend."""
+from __future__ import annotations
 
 from typing import Generator
 
@@ -14,6 +15,7 @@ def EventChain():
     """App with chained event handlers."""
     import asyncio
     import time
+    from typing import List
 
     import reflex as rx
 
@@ -21,7 +23,7 @@ def EventChain():
     MANY_EVENTS = 50
 
     class State(rx.State):
-        event_order: list[str] = []
+        event_order: List[str] = []
         interim_value: str = ""
 
         def event_no_args(self):

+ 10 - 6
integration/test_form_submit.py

@@ -17,14 +17,16 @@ def FormSubmit(form_component):
     Args:
         form_component: The str name of the form component to use.
     """
+    from typing import Dict, List
+
     import reflex as rx
 
     class FormState(rx.State):
-        form_data: dict = {}
+        form_data: Dict = {}
 
-        var_options: list[str] = ["option3", "option4"]
+        var_options: List[str] = ["option3", "option4"]
 
-        def form_submit(self, form_data: dict):
+        def form_submit(self, form_data: Dict):
             self.form_data = form_data
 
     app = rx.App(state=rx.State)
@@ -74,14 +76,16 @@ def FormSubmitName(form_component):
     Args:
         form_component: The str name of the form component to use.
     """
+    from typing import Dict, List
+
     import reflex as rx
 
     class FormState(rx.State):
-        form_data: dict = {}
+        form_data: Dict = {}
         val: str = "foo"
-        options: list[str] = ["option1", "option2"]
+        options: List[str] = ["option1", "option2"]
 
-        def form_submit(self, form_data: dict):
+        def form_submit(self, form_data: Dict):
             self.form_data = form_data
 
     app = rx.App(state=rx.State)

+ 1 - 0
integration/test_state_inheritance.py

@@ -1,4 +1,5 @@
 """Test state inheritance."""
+from __future__ import annotations
 
 from contextlib import suppress
 from typing import Generator

+ 7 - 5
integration/test_upload.py

@@ -13,19 +13,21 @@ from reflex.testing import AppHarness, WebDriver
 
 def UploadFile():
     """App for testing dynamic routes."""
+    from typing import Dict, List
+
     import reflex as rx
 
     class UploadState(rx.State):
-        _file_data: dict[str, str] = {}
-        event_order: list[str] = []
-        progress_dicts: list[dict] = []
+        _file_data: Dict[str, str] = {}
+        event_order: List[str] = []
+        progress_dicts: List[dict] = []
 
-        async def handle_upload(self, files: list[rx.UploadFile]):
+        async def handle_upload(self, files: List[rx.UploadFile]):
             for file in files:
                 upload_data = await file.read()
                 self._file_data[file.filename or ""] = upload_data.decode("utf-8")
 
-        async def handle_upload_secondary(self, files: list[rx.UploadFile]):
+        async def handle_upload_secondary(self, files: List[rx.UploadFile]):
             for file in files:
                 upload_data = await file.read()
                 self._file_data[file.filename or ""] = upload_data.decode("utf-8")

+ 8 - 6
integration/test_var_operations.py

@@ -11,6 +11,8 @@ from reflex.testing import AppHarness
 
 def VarOperations():
     """App with var operations."""
+    from typing import Dict, List
+
     import reflex as rx
 
     class VarOperationState(rx.State):
@@ -19,15 +21,15 @@ def VarOperations():
         int_var3: int = 7
         float_var1: float = 10.5
         float_var2: float = 5.5
-        list1: list = [1, 2]
-        list2: list = [3, 4]
-        list3: list = ["first", "second", "third"]
+        list1: List = [1, 2]
+        list2: List = [3, 4]
+        list3: List = ["first", "second", "third"]
         str_var1: str = "first"
         str_var2: str = "second"
         str_var3: str = "ThIrD"
         str_var4: str = "a long string"
-        dict1: dict = {1: 2}
-        dict2: dict = {3: 4}
+        dict1: Dict = {1: 2}
+        dict2: Dict = {3: 4}
         html_str: str = "<div>hello</div>"
 
     app = rx.App(state=rx.State)
@@ -556,7 +558,7 @@ def VarOperations():
             ),
             rx.box(
                 rx.foreach(
-                    rx.Var.create_safe(list(range(0, 3))).to(list[int]),
+                    rx.Var.create_safe(list(range(0, 3))).to(List[int]),
                     lambda x: rx.foreach(
                         rx.Var.range(x),
                         lambda y: rx.text(VarOperationState.list1[y], as_="p"),

+ 13 - 4
reflex/app.py

@@ -706,16 +706,25 @@ class App(Base):
         )
         return
 
+    def _apply_decorated_pages(self):
+        """Add @rx.page decorated pages to the app.
+
+        This has to be done in the MainThread for py38 and py39 compatibility, so the
+        decorated pages are added to the app before the app is compiled (in a thread)
+        to workaround REF-2172.
+
+        This can move back into `compile_` when py39 support is dropped.
+        """
+        # Add the @rx.page decorated pages to collect on_load events.
+        for render, kwargs in DECORATED_PAGES:
+            self.add_page(render, **kwargs)
+
     def compile_(self):
         """Compile the app and output it to the pages folder.
 
         Raises:
             RuntimeError: When any page uses state, but no rx.State subclass is defined.
         """
-        # add the pages before the compile check so App know onload methods
-        for render, kwargs in DECORATED_PAGES:
-            self.add_page(render, **kwargs)
-
         # Render a default 404 page if the user didn't supply one
         if constants.Page404.SLUG not in self.pages:
             self.add_custom_404_page()

+ 4 - 2
reflex/app_module_for_backend.py

@@ -5,13 +5,16 @@ from concurrent.futures import ThreadPoolExecutor
 
 from reflex import constants
 from reflex.utils.exec import is_prod_mode
-from reflex.utils.prerequisites import get_app, get_compiled_app
+from reflex.utils.prerequisites import get_app
 
 if "app" != constants.CompileVars.APP:
     raise AssertionError("unexpected variable name for 'app'")
 
 app_module = get_app(reload=False)
 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.add_done_callback(
     # Force background compile errors to print eagerly
@@ -25,7 +28,6 @@ if is_prod_mode():
 del app_module
 del compile_future
 del get_app
-del get_compiled_app
 del is_prod_mode
 del constants
 del ThreadPoolExecutor

+ 2 - 1
reflex/components/radix/themes/components/radio_group.py

@@ -1,4 +1,5 @@
 """Interactive components provided by @radix-ui/themes."""
+from __future__ import annotations
 
 from typing import Any, Dict, List, Literal, Optional, Union
 
@@ -126,7 +127,7 @@ class HighLevelRadioGroup(RadixThemesComponent):
     def create(
         cls,
         items: Var[List[Optional[Union[str, int, float, list, dict, bool]]]],
-        **props
+        **props,
     ) -> Component:
         """Create a radio group component.
 

+ 5 - 1
reflex/utils/prerequisites.py

@@ -211,7 +211,11 @@ def get_compiled_app(reload: bool = False) -> ModuleType:
         The compiled app based on the default config.
     """
     app_module = get_app(reload=reload)
-    getattr(app_module, constants.CompileVars.APP).compile_()
+    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()
+    app.compile_()
     return app_module
 
 

+ 3 - 3
reflex/vars.py

@@ -633,7 +633,7 @@ class Var:
             if types.is_generic_alias(self._var_type):
                 index = i if not isinstance(i, Var) else 0
                 type_ = types.get_args(self._var_type)
-                type_ = type_[index % len(type_)]
+                type_ = type_[index % len(type_)] if type_ else Any
             elif types._issubclass(self._var_type, str):
                 type_ = str
 
@@ -1449,7 +1449,7 @@ class Var:
         return self._replace(
             _var_name=f"{self._var_name}.split({other._var_full_name})",
             _var_is_string=False,
-            _var_type=list[str],
+            _var_type=List[str],
             merge_var_data=other._var_data,
         )
 
@@ -1555,7 +1555,7 @@ class Var:
 
         return BaseVar(
             _var_name=f"Array.from(range({v1._var_full_name}, {v2._var_full_name}, {step._var_name}))",
-            _var_type=list[int],
+            _var_type=List[int],
             _var_is_local=False,
             _var_data=VarData.merge(
                 v1._var_data,