Skip to content

Commit

Permalink
[REF-2172] Add DECORATED_PAGES before compiling in thread (#2841)
Browse files Browse the repository at this point in the history
  • Loading branch information
masenf committed Mar 12, 2024
1 parent 7b43b92 commit eb18ce9
Show file tree
Hide file tree
Showing 16 changed files with 84 additions and 35 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/integration_app_harness.yml
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion 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
12 changes: 12 additions & 0 deletions integration/init-test/in_docker_test_script.sh
Expand Up @@ -2,6 +2,7 @@

set -euxo pipefail

SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
export TELEMETRY_ENABLED=false

function do_export () {
Expand All @@ -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"
Expand All @@ -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
4 changes: 3 additions & 1 deletion integration/test_call_script.py
Expand Up @@ -12,6 +12,8 @@

def CallScript():
"""A test app for browser javascript integration."""
from typing import Dict, List, Optional, Union

import reflex as rx

inline_scripts = """
Expand All @@ -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

Expand Down
6 changes: 5 additions & 1 deletion 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

Expand All @@ -12,10 +14,12 @@

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):
Expand Down
7 changes: 5 additions & 2 deletions 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
Expand All @@ -11,10 +12,12 @@

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}")
Expand All @@ -27,7 +30,7 @@ class EventFiringComponent(rx.Component):

tag = "EventFiringComponent"

def _get_custom_code(self) -> str | None:
def _get_custom_code(self) -> Optional[str]:
return """
function EventFiringComponent(props) {
return (
Expand Down
4 changes: 3 additions & 1 deletion 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

Expand All @@ -14,14 +15,15 @@ def EventChain():
"""App with chained event handlers."""
import asyncio
import time
from typing import List

import reflex as rx

# repeated here since the outer global isn't exported into the App module
MANY_EVENTS = 50

class State(rx.State):
event_order: list[str] = []
event_order: List[str] = []
interim_value: str = ""

def event_no_args(self):
Expand Down
16 changes: 10 additions & 6 deletions integration/test_form_submit.py
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions integration/test_state_inheritance.py
@@ -1,4 +1,5 @@
"""Test state inheritance."""
from __future__ import annotations

from contextlib import suppress
from typing import Generator
Expand Down
12 changes: 7 additions & 5 deletions integration/test_upload.py
Expand Up @@ -13,19 +13,21 @@

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")
Expand Down
14 changes: 8 additions & 6 deletions integration/test_var_operations.py
Expand Up @@ -11,6 +11,8 @@

def VarOperations():
"""App with var operations."""
from typing import Dict, List

import reflex as rx

class VarOperationState(rx.State):
Expand All @@ -19,15 +21,15 @@ class VarOperationState(rx.State):
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)
Expand Down Expand Up @@ -556,7 +558,7 @@ def index():
),
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"),
Expand Down
17 changes: 13 additions & 4 deletions reflex/app.py
Expand Up @@ -706,16 +706,25 @@ def compile(self):
)
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()
Expand Down
6 changes: 4 additions & 2 deletions reflex/app_module_for_backend.py
Expand Up @@ -5,13 +5,16 @@

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
Expand All @@ -25,7 +28,6 @@
del app_module
del compile_future
del get_app
del get_compiled_app
del is_prod_mode
del constants
del ThreadPoolExecutor
3 changes: 2 additions & 1 deletion 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

Expand Down Expand Up @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion reflex/utils/prerequisites.py
Expand Up @@ -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


Expand Down
6 changes: 3 additions & 3 deletions reflex/vars.py
Expand Up @@ -633,7 +633,7 @@ def __getitem__(self, i: Any) -> 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

Expand Down Expand Up @@ -1449,7 +1449,7 @@ def split(self, other: str | Var[str] = " ") -> 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,
)

Expand Down Expand Up @@ -1555,7 +1555,7 @@ def range(

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,
Expand Down

0 comments on commit eb18ce9

Please sign in to comment.