[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:
|
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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
@ -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
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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 (
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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"),
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user