diff --git a/.github/workflows/integration_app_harness.yml b/.github/workflows/integration_app_harness.yml index ed74ac39d..18bc61f6f 100644 --- a/.github/workflows/integration_app_harness.yml +++ b/.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 diff --git a/integration/init-test/Dockerfile b/integration/init-test/Dockerfile index 8bc6e4b13..aa11344b1 100644 --- a/integration/init-test/Dockerfile +++ b/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 diff --git a/integration/init-test/in_docker_test_script.sh b/integration/init-test/in_docker_test_script.sh index beecfc91c..733cbf4ad 100755 --- a/integration/init-test/in_docker_test_script.sh +++ b/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 \ No newline at end of file diff --git a/integration/test_call_script.py b/integration/test_call_script.py index 3aea81e10..607d467c7 100644 --- a/integration/test_call_script.py +++ b/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 diff --git a/integration/test_dynamic_routes.py b/integration/test_dynamic_routes.py index 97a2a7070..d70d72d07 100644 --- a/integration/test_dynamic_routes.py +++ b/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): diff --git a/integration/test_event_actions.py b/integration/test_event_actions.py index 67605639a..59ee5f537 100644 --- a/integration/test_event_actions.py +++ b/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 ( diff --git a/integration/test_event_chain.py b/integration/test_event_chain.py index da9a197b3..194d04b11 100644 --- a/integration/test_event_chain.py +++ b/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): diff --git a/integration/test_form_submit.py b/integration/test_form_submit.py index eab0253ca..a6b2c2eb3 100644 --- a/integration/test_form_submit.py +++ b/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) diff --git a/integration/test_state_inheritance.py b/integration/test_state_inheritance.py index cdbe30321..6b3c99ca1 100644 --- a/integration/test_state_inheritance.py +++ b/integration/test_state_inheritance.py @@ -1,4 +1,5 @@ """Test state inheritance.""" +from __future__ import annotations from contextlib import suppress from typing import Generator diff --git a/integration/test_upload.py b/integration/test_upload.py index c703a6747..b2dae4bde 100644 --- a/integration/test_upload.py +++ b/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") diff --git a/integration/test_var_operations.py b/integration/test_var_operations.py index f0f53ab83..768aa3d34 100644 --- a/integration/test_var_operations.py +++ b/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 = "
hello
" 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"), diff --git a/reflex/app.py b/reflex/app.py index 783125415..2c71129c9 100644 --- a/reflex/app.py +++ b/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() diff --git a/reflex/app_module_for_backend.py b/reflex/app_module_for_backend.py index da5f51671..b9febba5d 100644 --- a/reflex/app_module_for_backend.py +++ b/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 diff --git a/reflex/components/radix/themes/components/radio_group.py b/reflex/components/radix/themes/components/radio_group.py index 7ff901f37..15c378aaf 100644 --- a/reflex/components/radix/themes/components/radio_group.py +++ b/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. diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index daa7821ca..c82d98791 100644 --- a/reflex/utils/prerequisites.py +++ b/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 diff --git a/reflex/vars.py b/reflex/vars.py index 9b1725cc4..e3bb14e03 100644 --- a/reflex/vars.py +++ b/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,