[REF-2172] Add DECORATED_PAGES before compiling in thread (#2841)
This commit is contained in:
parent
7b43b923b8
commit
eb18ce90d5
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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 (
|
||||
|
@ -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):
|
||||
|
@ -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,4 +1,5 @@
|
||||
"""Test state inheritance."""
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
from typing import Generator
|
||||
|
@ -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")
|
||||
|
@ -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"),
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user