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

This commit is contained in:
Masen Furer 2024-03-12 10:28:37 -07:00 committed by GitHub
parent 7b43b923b8
commit eb18ce90d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 84 additions and 35 deletions

View File

@ -19,7 +19,7 @@ jobs:
strategy: strategy:
matrix: matrix:
state_manager: [ "redis", "memory" ] 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 runs-on: ubuntu-latest
services: services:
# Label used to access the service container # Label used to access the service container

View File

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

View File

@ -2,6 +2,7 @@
set -euxo pipefail set -euxo pipefail
SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
export TELEMETRY_ENABLED=false export TELEMETRY_ENABLED=false
function do_export () { function do_export () {
@ -11,6 +12,15 @@ function do_export () {
rm -rf ~/.local/share/reflex ~/"$template"/.web rm -rf ~/.local/share/reflex ~/"$template"/.web
reflex init --template "$template" reflex init --template "$template"
reflex export 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" echo "Preparing test project dir"
@ -20,6 +30,8 @@ source ~/venv/bin/activate
echo "Installing reflex from local repo code" echo "Installing reflex from local repo code"
pip install /reflex-repo pip install /reflex-repo
redis-server &
echo "Running reflex init in test project dir" echo "Running reflex init in test project dir"
do_export blank do_export blank
do_export sidebar do_export sidebar

View File

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

View File

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

View File

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

View File

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

View File

@ -17,14 +17,16 @@ def FormSubmit(form_component):
Args: Args:
form_component: The str name of the form component to use. form_component: The str name of the form component to use.
""" """
from typing import Dict, List
import reflex as rx import reflex as rx
class FormState(rx.State): 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 self.form_data = form_data
app = rx.App(state=rx.State) app = rx.App(state=rx.State)
@ -74,14 +76,16 @@ def FormSubmitName(form_component):
Args: Args:
form_component: The str name of the form component to use. form_component: The str name of the form component to use.
""" """
from typing import Dict, List
import reflex as rx import reflex as rx
class FormState(rx.State): class FormState(rx.State):
form_data: dict = {} form_data: Dict = {}
val: str = "foo" 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 self.form_data = form_data
app = rx.App(state=rx.State) app = rx.App(state=rx.State)

View File

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

View File

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

View File

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

View File

@ -706,16 +706,25 @@ class App(Base):
) )
return 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): def compile_(self):
"""Compile the app and output it to the pages folder. """Compile the app and output it to the pages folder.
Raises: Raises:
RuntimeError: When any page uses state, but no rx.State subclass is defined. 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 # Render a default 404 page if the user didn't supply one
if constants.Page404.SLUG not in self.pages: if constants.Page404.SLUG not in self.pages:
self.add_custom_404_page() self.add_custom_404_page()

View File

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

View File

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

View File

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

View File

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