[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:
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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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 (

View File

@ -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):

View File

@ -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)

View File

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

View File

@ -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")

View File

@ -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"),

View File

@ -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()

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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,