This commit is contained in:
Khaleel Al-Adhami 2024-10-28 12:14:19 -07:00
commit 4bfad2c4fa
13 changed files with 355 additions and 218 deletions

View File

@ -122,7 +122,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
# Show OS combos first in GUI # Show OS combos first in GUI
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest]
python-version: ['3.10.11', '3.11.4'] python-version: ['3.10.11', '3.11.4']
env: env:

View File

@ -3,7 +3,7 @@ fail_fast: true
repos: repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.7.0 rev: v0.7.1
hooks: hooks:
- id: ruff-format - id: ruff-format
args: [reflex, tests] args: [reflex, tests]

50
poetry.lock generated
View File

@ -1348,8 +1348,8 @@ files = [
[package.dependencies] [package.dependencies]
numpy = [ numpy = [
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
{version = ">=1.26.0", markers = "python_version >= \"3.12\""}, {version = ">=1.26.0", markers = "python_version >= \"3.12\""},
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
{version = ">=1.22.4", markers = "python_version < \"3.11\""}, {version = ">=1.22.4", markers = "python_version < \"3.11\""},
] ]
python-dateutil = ">=2.8.2" python-dateutil = ">=2.8.2"
@ -1667,8 +1667,8 @@ files = [
annotated-types = ">=0.6.0" annotated-types = ">=0.6.0"
pydantic-core = "2.23.4" pydantic-core = "2.23.4"
typing-extensions = [ typing-extensions = [
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
{version = ">=4.12.2", markers = "python_version >= \"3.13\""}, {version = ">=4.12.2", markers = "python_version >= \"3.13\""},
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
] ]
[package.extras] [package.extras]
@ -2164,13 +2164,13 @@ md = ["cmarkgfm (>=0.8.0)"]
[[package]] [[package]]
name = "redis" name = "redis"
version = "5.1.1" version = "5.2.0"
description = "Python client for Redis database and key-value store" description = "Python client for Redis database and key-value store"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "redis-5.1.1-py3-none-any.whl", hash = "sha256:f8ea06b7482a668c6475ae202ed8d9bcaa409f6e87fb77ed1043d912afd62e24"}, {file = "redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897"},
{file = "redis-5.1.1.tar.gz", hash = "sha256:f6c997521fedbae53387307c5d0bf784d9acc28d9f1d058abeac566ec4dbed72"}, {file = "redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0"},
] ]
[package.dependencies] [package.dependencies]
@ -2287,29 +2287,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.7.0" version = "0.7.1"
description = "An extremely fast Python linter and code formatter, written in Rust." description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "ruff-0.7.0-py3-none-linux_armv6l.whl", hash = "sha256:0cdf20c2b6ff98e37df47b2b0bd3a34aaa155f59a11182c1303cce79be715628"}, {file = "ruff-0.7.1-py3-none-linux_armv6l.whl", hash = "sha256:cb1bc5ed9403daa7da05475d615739cc0212e861b7306f314379d958592aaa89"},
{file = "ruff-0.7.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:496494d350c7fdeb36ca4ef1c9f21d80d182423718782222c29b3e72b3512737"}, {file = "ruff-0.7.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27c1c52a8d199a257ff1e5582d078eab7145129aa02721815ca8fa4f9612dc35"},
{file = "ruff-0.7.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:214b88498684e20b6b2b8852c01d50f0651f3cc6118dfa113b4def9f14faaf06"}, {file = "ruff-0.7.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:588a34e1ef2ea55b4ddfec26bbe76bc866e92523d8c6cdec5e8aceefeff02d99"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630fce3fefe9844e91ea5bbf7ceadab4f9981f42b704fae011bb8efcaf5d84be"}, {file = "ruff-0.7.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94fc32f9cdf72dc75c451e5f072758b118ab8100727168a3df58502b43a599ca"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:211d877674e9373d4bb0f1c80f97a0201c61bcd1e9d045b6e9726adc42c156aa"}, {file = "ruff-0.7.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:985818742b833bffa543a84d1cc11b5e6871de1b4e0ac3060a59a2bae3969250"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:194d6c46c98c73949a106425ed40a576f52291c12bc21399eb8f13a0f7073495"}, {file = "ruff-0.7.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32f1e8a192e261366c702c5fb2ece9f68d26625f198a25c408861c16dc2dea9c"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:82c2579b82b9973a110fab281860403b397c08c403de92de19568f32f7178598"}, {file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:699085bf05819588551b11751eff33e9ca58b1b86a6843e1b082a7de40da1565"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9af971fe85dcd5eaed8f585ddbc6bdbe8c217fb8fcf510ea6bca5bdfff56040e"}, {file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:344cc2b0814047dc8c3a8ff2cd1f3d808bb23c6658db830d25147339d9bf9ea7"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b641c7f16939b7d24b7bfc0be4102c56562a18281f84f635604e8a6989948914"}, {file = "ruff-0.7.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4316bbf69d5a859cc937890c7ac7a6551252b6a01b1d2c97e8fc96e45a7c8b4a"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d71672336e46b34e0c90a790afeac8a31954fd42872c1f6adaea1dff76fd44f9"}, {file = "ruff-0.7.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79d3af9dca4c56043e738a4d6dd1e9444b6d6c10598ac52d146e331eb155a8ad"},
{file = "ruff-0.7.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ab7d98c7eed355166f367597e513a6c82408df4181a937628dbec79abb2a1fe4"}, {file = "ruff-0.7.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5c121b46abde94a505175524e51891f829414e093cd8326d6e741ecfc0a9112"},
{file = "ruff-0.7.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1eb54986f770f49edb14f71d33312d79e00e629a57387382200b1ef12d6a4ef9"}, {file = "ruff-0.7.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8422104078324ea250886954e48f1373a8fe7de59283d747c3a7eca050b4e378"},
{file = "ruff-0.7.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dc452ba6f2bb9cf8726a84aa877061a2462afe9ae0ea1d411c53d226661c601d"}, {file = "ruff-0.7.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:56aad830af8a9db644e80098fe4984a948e2b6fc2e73891538f43bbe478461b8"},
{file = "ruff-0.7.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4b406c2dce5be9bad59f2de26139a86017a517e6bcd2688da515481c05a2cb11"}, {file = "ruff-0.7.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:658304f02f68d3a83c998ad8bf91f9b4f53e93e5412b8f2388359d55869727fd"},
{file = "ruff-0.7.0-py3-none-win32.whl", hash = "sha256:f6c968509f767776f524a8430426539587d5ec5c662f6addb6aa25bc2e8195ec"}, {file = "ruff-0.7.1-py3-none-win32.whl", hash = "sha256:b517a2011333eb7ce2d402652ecaa0ac1a30c114fbbd55c6b8ee466a7f600ee9"},
{file = "ruff-0.7.0-py3-none-win_amd64.whl", hash = "sha256:ff4aabfbaaba880e85d394603b9e75d32b0693152e16fa659a3064a85df7fce2"}, {file = "ruff-0.7.1-py3-none-win_amd64.whl", hash = "sha256:f38c41fcde1728736b4eb2b18850f6d1e3eedd9678c914dede554a70d5241307"},
{file = "ruff-0.7.0-py3-none-win_arm64.whl", hash = "sha256:10842f69c245e78d6adec7e1db0a7d9ddc2fff0621d730e61657b64fa36f207e"}, {file = "ruff-0.7.1-py3-none-win_arm64.whl", hash = "sha256:19aa200ec824c0f36d0c9114c8ec0087082021732979a359d6f3c390a6ff2a37"},
{file = "ruff-0.7.0.tar.gz", hash = "sha256:47a86360cf62d9cd53ebfb0b5eb0e882193fc191c6d717e8bef4462bc3b9ea2b"}, {file = "ruff-0.7.1.tar.gz", hash = "sha256:9d8a41d4aa2dad1575adb98a82870cf5db5f76b2938cf2206c22c940034a36f4"},
] ]
[[package]] [[package]]
@ -3048,4 +3048,4 @@ type = ["pytest-mypy"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "e03374b85bf10f0a7bb857969b2d6714f25affa63e14a48a88be9fa154b24326" content-hash = "547fdabf7a030c2a7c8d63eb5b2a3c5e821afa86390f08b895db038d30013904"

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "reflex" name = "reflex"
version = "0.6.4dev1" version = "0.6.5dev1"
description = "Web apps in pure Python." description = "Web apps in pure Python."
license = "Apache-2.0" license = "Apache-2.0"
authors = [ authors = [
@ -69,7 +69,7 @@ dill = ">=0.3.8"
toml = ">=0.10.2,<1.0" toml = ">=0.10.2,<1.0"
pytest-asyncio = ">=0.24.0" pytest-asyncio = ">=0.24.0"
pytest-cov = ">=4.0.0,<6.0" pytest-cov = ">=4.0.0,<6.0"
ruff = "^0.7.0" ruff = "0.7.1"
pandas = ">=2.1.1,<3.0" pandas = ">=2.1.1,<3.0"
pillow = ">=10.0.0,<12.0" pillow = ">=10.0.0,<12.0"
plotly = ">=5.13.0,<6.0" plotly = ">=5.13.0,<6.0"
@ -104,4 +104,4 @@ lint.pydocstyle.convention = "google"
[tool.pytest.ini_options] [tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function" asyncio_default_fixture_loop_scope = "function"
asyncio_mode = "auto" asyncio_mode = "auto"

View File

@ -6,6 +6,7 @@ import asyncio
import concurrent.futures import concurrent.futures
import contextlib import contextlib
import copy import copy
import dataclasses
import functools import functools
import inspect import inspect
import io import io
@ -18,6 +19,7 @@ import traceback
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import ( from typing import (
TYPE_CHECKING,
Any, Any,
AsyncIterator, AsyncIterator,
Callable, Callable,
@ -47,7 +49,10 @@ from reflex.app_mixins import AppMixin, LifespanMixin, MiddlewareMixin
from reflex.base import Base from reflex.base import Base
from reflex.compiler import compiler from reflex.compiler import compiler
from reflex.compiler import utils as compiler_utils from reflex.compiler import utils as compiler_utils
from reflex.compiler.compiler import ExecutorSafeFunctions from reflex.compiler.compiler import (
ExecutorSafeFunctions,
compile_theme,
)
from reflex.components.base.app_wrap import AppWrap from reflex.components.base.app_wrap import AppWrap
from reflex.components.base.error_boundary import ErrorBoundary from reflex.components.base.error_boundary import ErrorBoundary
from reflex.components.base.fragment import Fragment from reflex.components.base.fragment import Fragment
@ -88,6 +93,9 @@ from reflex.utils import codespaces, console, exceptions, format, prerequisites,
from reflex.utils.exec import is_prod_mode, is_testing_env, should_skip_compile from reflex.utils.exec import is_prod_mode, is_testing_env, should_skip_compile
from reflex.utils.imports import ImportVar from reflex.utils.imports import ImportVar
if TYPE_CHECKING:
from reflex.vars import Var
# Define custom types. # Define custom types.
ComponentCallable = Callable[[], Component] ComponentCallable = Callable[[], Component]
Reducer = Callable[[Event], Coroutine[Any, Any, StateUpdate]] Reducer = Callable[[Event], Coroutine[Any, Any, StateUpdate]]
@ -170,6 +178,21 @@ class OverlayFragment(Fragment):
pass pass
@dataclasses.dataclass(
frozen=True,
)
class UnevaluatedPage:
"""An uncompiled page."""
component: Union[Component, ComponentCallable]
route: str
title: Union[Var, str, None]
description: Union[Var, str, None]
image: str
on_load: Union[EventHandler, EventSpec, List[Union[EventHandler, EventSpec]], None]
meta: List[Dict[str, str]]
class App(MiddlewareMixin, LifespanMixin, Base): class App(MiddlewareMixin, LifespanMixin, Base):
"""The main Reflex app that encapsulates the backend and frontend. """The main Reflex app that encapsulates the backend and frontend.
@ -220,6 +243,9 @@ class App(MiddlewareMixin, LifespanMixin, Base):
# Attributes to add to the html root tag of every page. # Attributes to add to the html root tag of every page.
html_custom_attrs: Optional[Dict[str, str]] = None html_custom_attrs: Optional[Dict[str, str]] = None
# A map from a route to an unevaluated page. PRIVATE.
unevaluated_pages: Dict[str, UnevaluatedPage] = {}
# A map from a page route to the component to render. Users should use `add_page`. PRIVATE. # A map from a page route to the component to render. Users should use `add_page`. PRIVATE.
pages: Dict[str, Component] = {} pages: Dict[str, Component] = {}
@ -381,8 +407,8 @@ class App(MiddlewareMixin, LifespanMixin, Base):
def _add_optional_endpoints(self): def _add_optional_endpoints(self):
"""Add optional api endpoints (_upload).""" """Add optional api endpoints (_upload)."""
# To upload files.
if Upload.is_used: if Upload.is_used:
# To upload files.
self.api.post(str(constants.Endpoint.UPLOAD))(upload(self)) self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
# To access uploaded files. # To access uploaded files.
@ -442,8 +468,8 @@ class App(MiddlewareMixin, LifespanMixin, Base):
self, self,
component: Component | ComponentCallable, component: Component | ComponentCallable,
route: str | None = None, route: str | None = None,
title: str | None = None, title: str | Var | None = None,
description: str | None = None, description: str | Var | None = None,
image: str = constants.DefaultPage.IMAGE, image: str = constants.DefaultPage.IMAGE,
on_load: ( on_load: (
EventHandler | EventSpec | list[EventHandler | EventSpec] | None EventHandler | EventSpec | list[EventHandler | EventSpec] | None
@ -479,13 +505,13 @@ class App(MiddlewareMixin, LifespanMixin, Base):
# Check if the route given is valid # Check if the route given is valid
verify_route_validity(route) verify_route_validity(route)
if route in self.pages and os.getenv(constants.RELOAD_CONFIG): if route in self.unevaluated_pages and os.getenv(constants.RELOAD_CONFIG):
# when the app is reloaded(typically for app harness tests), we should maintain # when the app is reloaded(typically for app harness tests), we should maintain
# the latest render function of a route.This applies typically to decorated pages # the latest render function of a route.This applies typically to decorated pages
# since they are only added when app._compile is called. # since they are only added when app._compile is called.
self.pages.pop(route) self.unevaluated_pages.pop(route)
if route in self.pages: if route in self.unevaluated_pages:
route_name = ( route_name = (
f"`{route}` or `/`" f"`{route}` or `/`"
if route == constants.PageNames.INDEX_ROUTE if route == constants.PageNames.INDEX_ROUTE
@ -501,58 +527,38 @@ class App(MiddlewareMixin, LifespanMixin, Base):
state = self.state if self.state else State state = self.state if self.state else State
state.setup_dynamic_args(get_route_args(route)) state.setup_dynamic_args(get_route_args(route))
# Generate the component if it is a callable. if on_load:
component = self._generate_component(component) self.load_events[route] = (
on_load if isinstance(on_load, list) else [on_load]
)
# unpack components that return tuples in an rx.fragment. self.unevaluated_pages[route] = UnevaluatedPage(
if isinstance(component, tuple): component=component,
component = Fragment.create(*component) route=route,
title=title,
# Ensure state is enabled if this page uses state. description=description,
if self.state is None: image=image,
if on_load or component._has_stateful_event_triggers(): on_load=on_load,
self._enable_state() meta=meta,
else:
for var in component._get_vars(include_children=True):
var_data = var._get_all_var_data()
if not var_data:
continue
if not var_data.state:
continue
self._enable_state()
break
component = OverlayFragment.create(component)
meta_args = {
"title": (
title
if title is not None
else format.make_default_page_title(get_config().app_name, route)
),
"image": image,
"meta": meta,
}
if description is not None:
meta_args["description"] = description
# Add meta information to the component.
compiler_utils.add_meta(
component,
**meta_args,
) )
def _compile_page(self, route: str):
"""Compile a page.
Args:
route: The route of the page to compile.
"""
component, enable_state = compiler.compile_unevaluated_page(
route, self.unevaluated_pages[route], self.state, self.style, self.theme
)
if enable_state:
self._enable_state()
# Add the page. # Add the page.
self._check_routes_conflict(route) self._check_routes_conflict(route)
self.pages[route] = component self.pages[route] = component
# Add the load events.
if on_load:
if not isinstance(on_load, list):
on_load = [on_load]
self.load_events[route] = on_load
def get_load_events(self, route: str) -> list[EventHandler | EventSpec]: def get_load_events(self, route: str) -> list[EventHandler | EventSpec]:
"""Get the load events for a route. """Get the load events for a route.
@ -827,13 +833,33 @@ class App(MiddlewareMixin, LifespanMixin, Base):
""" """
from reflex.utils.exceptions import ReflexRuntimeError from reflex.utils.exceptions import ReflexRuntimeError
self.pages = {}
def get_compilation_time() -> str: def get_compilation_time() -> str:
return str(datetime.now().time()).split(".")[0] return str(datetime.now().time()).split(".")[0]
# 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.unevaluated_pages:
self.add_custom_404_page() self.add_custom_404_page()
# Fix up the style.
self.style = evaluate_style_namespaces(self.style)
# Add the app wrappers.
app_wrappers: Dict[tuple[int, str], Component] = {
# Default app wrap component renders {children}
(0, "AppWrap"): AppWrap.create()
}
if self.theme is not None:
# If a theme component was provided, wrap the app with it
app_wrappers[(20, "Theme")] = self.theme
# Fix #2992 by removing the top-level appearance prop
self.theme.appearance = None
for route in self.unevaluated_pages:
self._compile_page(route)
# Add the optional endpoints (_upload) # Add the optional endpoints (_upload)
self._add_optional_endpoints() self._add_optional_endpoints()
@ -868,28 +894,15 @@ class App(MiddlewareMixin, LifespanMixin, Base):
# Store the compile results. # Store the compile results.
compile_results = [] compile_results = []
# Add the app wrappers.
app_wrappers: Dict[tuple[int, str], Component] = {
# Default app wrap component renders {children}
(0, "AppWrap"): AppWrap.create()
}
if self.theme is not None:
# If a theme component was provided, wrap the app with it
app_wrappers[(20, "Theme")] = self.theme
progress.advance(task) progress.advance(task)
# Fix up the style.
self.style = evaluate_style_namespaces(self.style)
# Track imports and custom components found. # Track imports and custom components found.
all_imports = {} all_imports = {}
custom_components = set() custom_components = set()
for _route, component in self.pages.items(): # This has to happen before compiling stateful components as that
# Merge the component style with the app style. # prevents recursive functions from reaching all components.
component._add_style_recursive(self.style, self.theme) for component in self.pages.values():
# Add component._get_all_imports() to all_imports. # Add component._get_all_imports() to all_imports.
all_imports.update(component._get_all_imports()) all_imports.update(component._get_all_imports())
@ -899,8 +912,6 @@ class App(MiddlewareMixin, LifespanMixin, Base):
# Add the custom components from the page to the set. # Add the custom components from the page to the set.
custom_components |= component._get_all_custom_components() custom_components |= component._get_all_custom_components()
progress.advance(task)
# Perform auto-memoization of stateful components. # Perform auto-memoization of stateful components.
( (
stateful_components_path, stateful_components_path,
@ -927,31 +938,8 @@ class App(MiddlewareMixin, LifespanMixin, Base):
) )
) )
# Compile the contexts before fork.
compile_results.append(
compiler.compile_contexts(self.state, self.theme),
)
# Fix #2992 by removing the top-level appearance prop
if self.theme is not None:
self.theme.appearance = None
app_root = self._app_root(app_wrappers=app_wrappers)
progress.advance(task) progress.advance(task)
# Prepopulate the global ExecutorSafeFunctions class with input data required by the compile functions.
# This is required for multiprocessing to work, in presence of non-picklable inputs.
for route, component in zip(self.pages, page_components):
ExecutorSafeFunctions.COMPILE_PAGE_ARGS_BY_ROUTE[route] = (
route,
component,
self.state,
)
ExecutorSafeFunctions.COMPILE_APP_APP_ROOT = app_root
ExecutorSafeFunctions.CUSTOM_COMPONENTS = custom_components
ExecutorSafeFunctions.STYLE = self.style
# Use a forking process pool, if possible. Much faster, especially for large sites. # Use a forking process pool, if possible. Much faster, especially for large sites.
# Fallback to ThreadPoolExecutor as something that will always work. # Fallback to ThreadPoolExecutor as something that will always work.
executor = None executor = None
@ -969,36 +957,31 @@ class App(MiddlewareMixin, LifespanMixin, Base):
max_workers=environment.REFLEX_COMPILE_THREADS max_workers=environment.REFLEX_COMPILE_THREADS
) )
for route, component in zip(self.pages, page_components):
ExecutorSafeFunctions.COMPONENTS[route] = component
ExecutorSafeFunctions.STATE = self.state
with executor: with executor:
result_futures = [] result_futures = []
custom_components_future = None
def _mark_complete(_=None):
progress.advance(task)
def _submit_work(fn, *args, **kwargs): def _submit_work(fn, *args, **kwargs):
f = executor.submit(fn, *args, **kwargs) f = executor.submit(fn, *args, **kwargs)
f.add_done_callback(_mark_complete) # f = executor.apipe(fn, *args, **kwargs)
result_futures.append(f) result_futures.append(f)
# Compile all page components. # Compile the pre-compiled pages.
for route in self.pages: for route in self.pages:
_submit_work(ExecutorSafeFunctions.compile_page, route) _submit_work(
ExecutorSafeFunctions.compile_page,
# Compile the app wrapper. route,
_submit_work(ExecutorSafeFunctions.compile_app) )
# Compile the custom components.
custom_components_future = executor.submit(
ExecutorSafeFunctions.compile_custom_components,
)
custom_components_future.add_done_callback(_mark_complete)
# Compile the root stylesheet with base styles. # Compile the root stylesheet with base styles.
_submit_work(compiler.compile_root_stylesheet, self.stylesheets) _submit_work(compiler.compile_root_stylesheet, self.stylesheets)
# Compile the theme. # Compile the theme.
_submit_work(ExecutorSafeFunctions.compile_theme) _submit_work(compile_theme, self.style)
# Compile the Tailwind config. # Compile the Tailwind config.
if config.tailwind is not None: if config.tailwind is not None:
@ -1012,21 +995,34 @@ class App(MiddlewareMixin, LifespanMixin, Base):
# Wait for all compilation tasks to complete. # Wait for all compilation tasks to complete.
for future in concurrent.futures.as_completed(result_futures): for future in concurrent.futures.as_completed(result_futures):
compile_results.append(future.result()) compile_results.append(future.result())
progress.advance(task)
# Special case for custom_components, since we need the compiled imports app_root = self._app_root(app_wrappers=app_wrappers)
# to install proper frontend packages.
(
*custom_components_result,
custom_components_imports,
) = custom_components_future.result()
compile_results.append(custom_components_result)
all_imports.update(custom_components_imports)
# Get imports from AppWrap components. # Get imports from AppWrap components.
all_imports.update(app_root._get_all_imports()) all_imports.update(app_root._get_all_imports())
progress.advance(task) progress.advance(task)
# Compile the contexts.
compile_results.append(
compiler.compile_contexts(self.state, self.theme),
)
progress.advance(task)
# Compile the app root.
compile_results.append(
compiler.compile_app(app_root),
)
progress.advance(task)
# Compile custom components.
*custom_components_result, custom_components_imports = (
compiler.compile_components(custom_components)
)
compile_results.append(custom_components_result)
all_imports.update(custom_components_imports)
progress.advance(task) progress.advance(task)
progress.stop() progress.stop()

View File

@ -4,10 +4,11 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Dict, Iterable, Optional, Type, Union from typing import TYPE_CHECKING, Dict, Iterable, Optional, Tuple, Type, Union
from reflex import constants from reflex import constants
from reflex.compiler import templates, utils from reflex.compiler import templates, utils
from reflex.components.base.fragment import Fragment
from reflex.components.component import ( from reflex.components.component import (
BaseComponent, BaseComponent,
Component, Component,
@ -126,8 +127,8 @@ def _compile_contexts(state: Optional[Type[BaseState]], theme: Component | None)
def _compile_page( def _compile_page(
component: Component, component: BaseComponent,
state: Type[BaseState], state: Type[BaseState] | None,
) -> str: ) -> str:
"""Compile the component given the app state. """Compile the component given the app state.
@ -142,7 +143,7 @@ def _compile_page(
imports = utils.compile_imports(imports) imports = utils.compile_imports(imports)
# Compile the code to render the component. # Compile the code to render the component.
kwargs = {"state_name": state.get_name()} if state else {} kwargs = {"state_name": state.get_name()} if state is not None else {}
return templates.PAGE.render( return templates.PAGE.render(
imports=imports, imports=imports,
@ -424,7 +425,7 @@ def compile_contexts(
def compile_page( def compile_page(
path: str, component: Component, state: Type[BaseState] path: str, component: BaseComponent, state: Type[BaseState] | None
) -> tuple[str, str]: ) -> tuple[str, str]:
"""Compile a single page. """Compile a single page.
@ -534,6 +535,81 @@ def purge_web_pages_dir():
utils.empty_dir(get_web_dir() / constants.Dirs.PAGES, keep_files=["_app.js"]) utils.empty_dir(get_web_dir() / constants.Dirs.PAGES, keep_files=["_app.js"])
if TYPE_CHECKING:
from reflex.app import UnevaluatedPage
def compile_unevaluated_page(
route: str,
page: UnevaluatedPage,
state: Type[BaseState] | None = None,
style: ComponentStyle | None = None,
theme: Component | None = None,
) -> Tuple[Component, bool]:
"""Compiles an uncompiled page into a component and adds meta information.
Args:
route: The route of the page.
page: The uncompiled page object.
state: The state of the app.
style: The style of the page.
theme: The theme of the page.
Returns:
The compiled component and whether state should be enabled.
"""
# Generate the component if it is a callable.
component = page.component
component = component if isinstance(component, Component) else component()
# unpack components that return tuples in an rx.fragment.
if isinstance(component, tuple):
component = Fragment.create(*component)
component._add_style_recursive(style or {}, theme)
enable_state = False
# Ensure state is enabled if this page uses state.
if state is None:
if page.on_load or component._has_stateful_event_triggers():
enable_state = True
else:
for var in component._get_vars(include_children=True):
var_data = var._get_all_var_data()
if not var_data:
continue
if not var_data.state:
continue
enable_state = True
break
from reflex.app import OverlayFragment
from reflex.utils.format import make_default_page_title
component = OverlayFragment.create(component)
meta_args = {
"title": (
page.title
if page.title is not None
else make_default_page_title(get_config().app_name, route)
),
"image": page.image,
"meta": page.meta,
}
if page.description is not None:
meta_args["description"] = page.description
# Add meta information to the component.
utils.add_meta(
component,
**meta_args,
)
return component, enable_state
class ExecutorSafeFunctions: class ExecutorSafeFunctions:
"""Helper class to allow parallelisation of parts of the compilation process. """Helper class to allow parallelisation of parts of the compilation process.
@ -559,13 +635,12 @@ class ExecutorSafeFunctions:
""" """
COMPILE_PAGE_ARGS_BY_ROUTE = {} COMPONENTS: Dict[str, BaseComponent] = {}
COMPILE_APP_APP_ROOT: Component | None = None UNCOMPILED_PAGES: Dict[str, UnevaluatedPage] = {}
CUSTOM_COMPONENTS: set[CustomComponent] | None = None STATE: Optional[Type[BaseState]] = None
STYLE: ComponentStyle | None = None
@classmethod @classmethod
def compile_page(cls, route: str): def compile_page(cls, route: str) -> tuple[str, str]:
"""Compile a page. """Compile a page.
Args: Args:
@ -574,46 +649,45 @@ class ExecutorSafeFunctions:
Returns: Returns:
The path and code of the compiled page. The path and code of the compiled page.
""" """
return compile_page(*cls.COMPILE_PAGE_ARGS_BY_ROUTE[route]) return compile_page(route, cls.COMPONENTS[route], cls.STATE)
@classmethod @classmethod
def compile_app(cls): def compile_unevaluated_page(
"""Compile the app. cls,
route: str,
style: ComponentStyle,
theme: Component | None,
) -> tuple[str, Component, tuple[str, str]]:
"""Compile an unevaluated page.
Args:
route: The route of the page to compile.
style: The style of the page.
theme: The theme of the page.
Returns: Returns:
The path and code of the compiled app. The route, compiled component, and compiled page.
Raises:
ValueError: If the app root is not set.
""" """
if cls.COMPILE_APP_APP_ROOT is None: component, enable_state = compile_unevaluated_page(
raise ValueError("COMPILE_APP_APP_ROOT should be set") route, cls.UNCOMPILED_PAGES[route]
return compile_app(cls.COMPILE_APP_APP_ROOT) )
component = component if isinstance(component, Component) else component()
component._add_style_recursive(style, theme)
return route, component, compile_page(route, component, cls.STATE)
@classmethod @classmethod
def compile_custom_components(cls): def compile_theme(cls, style: ComponentStyle | None) -> tuple[str, str]:
"""Compile the custom components.
Returns:
The path and code of the compiled custom components.
Raises:
ValueError: If the custom components are not set.
"""
if cls.CUSTOM_COMPONENTS is None:
raise ValueError("CUSTOM_COMPONENTS should be set")
return compile_components(cls.CUSTOM_COMPONENTS)
@classmethod
def compile_theme(cls):
"""Compile the theme. """Compile the theme.
Args:
style: The style to compile.
Returns: Returns:
The path and code of the compiled theme. The path and code of the compiled theme.
Raises: Raises:
ValueError: If the style is not set. ValueError: If the style is not set.
""" """
if cls.STYLE is None: if style is None:
raise ValueError("STYLE should be set") raise ValueError("STYLE should be set")
return compile_theme(cls.STYLE) return compile_theme(style)

View File

@ -3,7 +3,9 @@
from __future__ import annotations from __future__ import annotations
import dataclasses import dataclasses
import enum
import importlib import importlib
import inspect
import os import os
import sys import sys
import urllib.parse import urllib.parse
@ -221,6 +223,28 @@ def interpret_path_env(value: str, field_name: str) -> Path:
return path return path
def interpret_enum_env(value: str, field_type: GenericType, field_name: str) -> Any:
"""Interpret an enum environment variable value.
Args:
value: The environment variable value.
field_type: The field type.
field_name: The field name.
Returns:
The interpreted value.
Raises:
EnvironmentVarValueError: If the value is invalid.
"""
try:
return field_type(value)
except ValueError as ve:
raise EnvironmentVarValueError(
f"Invalid enum value: {value} for {field_name}"
) from ve
def interpret_env_var_value( def interpret_env_var_value(
value: str, field_type: GenericType, field_name: str value: str, field_type: GenericType, field_name: str
) -> Any: ) -> Any:
@ -252,6 +276,8 @@ def interpret_env_var_value(
return interpret_int_env(value, field_name) return interpret_int_env(value, field_name)
elif field_type is Path: elif field_type is Path:
return interpret_path_env(value, field_name) return interpret_path_env(value, field_name)
elif inspect.isclass(field_type) and issubclass(field_type, enum.Enum):
return interpret_enum_env(value, field_type, field_name)
else: else:
raise ValueError( raise ValueError(

View File

@ -35,7 +35,6 @@ ColorType = Literal[
"amber", "amber",
"gold", "gold",
"bronze", "bronze",
"gray",
"accent", "accent",
"black", "black",
"white", "white",

View File

@ -120,7 +120,7 @@ class Node(SimpleNamespace):
# The Node version. # The Node version.
VERSION = "22.10.0" VERSION = "22.10.0"
# The minimum required node version. # The minimum required node version.
MIN_VERSION = "18.17.0" MIN_VERSION = "18.18.0"
@classproperty @classproperty
@classmethod @classmethod
@ -173,17 +173,17 @@ class PackageJson(SimpleNamespace):
PATH = "package.json" PATH = "package.json"
DEPENDENCIES = { DEPENDENCIES = {
"@babel/standalone": "7.25.8", "@babel/standalone": "7.26.0",
"@emotion/react": "11.13.3", "@emotion/react": "11.13.3",
"axios": "1.7.7", "axios": "1.7.7",
"json5": "2.2.3", "json5": "2.2.3",
"next": "14.2.15", "next": "15.0.1",
"next-sitemap": "4.2.3", "next-sitemap": "4.2.3",
"next-themes": "0.3.0", "next-themes": "0.3.0",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-focus-lock": "2.13.2", "react-focus-lock": "2.13.2",
"socket.io-client": "4.8.0", "socket.io-client": "4.8.1",
"universal-cookie": "7.2.1", "universal-cookie": "7.2.1",
} }
DEV_DEPENDENCIES = { DEV_DEPENDENCIES = {

View File

@ -182,7 +182,7 @@ class ClientStateVar(Var):
if value is not NoValue: if value is not NoValue:
# This is a hack to make it work like an EventSpec taking an arg # This is a hack to make it work like an EventSpec taking an arg
value_var = LiteralVar.create(value) value_var = LiteralVar.create(value)
_var_data = _var_data.merge(value_var._get_all_var_data()) _var_data = VarData.merge(_var_data, value_var._get_all_var_data())
value_str = str(value_var) value_str = str(value_var)
if value_str.startswith("_"): if value_str.startswith("_"):

View File

@ -151,31 +151,41 @@ class VarData:
""" """
return dict((k, list(v)) for k, v in self.imports) return dict((k, list(v)) for k, v in self.imports)
@classmethod def merge(*all: VarData | None) -> VarData | None:
def merge(cls, *others: VarData | None) -> VarData | None:
"""Merge multiple var data objects. """Merge multiple var data objects.
Args: Args:
*others: The var data objects to merge. *all: The var data objects to merge.
Returns: Returns:
The merged var data object. The merged var data object.
# noqa: DAR102 *all
""" """
state = "" all_var_datas = list(filter(None, all))
field_name = ""
_imports = {} if not all_var_datas:
hooks = {} return None
for var_data in others:
if var_data is None: if len(all_var_datas) == 1:
continue return all_var_datas[0]
state = state or var_data.state
field_name = field_name or var_data.field_name # Get the first non-empty field name or default to empty string.
_imports = imports.merge_imports(_imports, var_data.imports) field_name = next(
hooks.update( (var_data.field_name for var_data in all_var_datas if var_data.field_name),
var_data.hooks "",
if isinstance(var_data.hooks, dict) )
else {k: None for k in var_data.hooks}
) # Get the first non-empty state or default to empty string.
state = next(
(var_data.state for var_data in all_var_datas if var_data.state), ""
)
hooks = {hook: None for var_data in all_var_datas for hook in var_data.hooks}
_imports = imports.merge_imports(
*(var_data.imports for var_data in all_var_datas)
)
if state or _imports or hooks or field_name: if state or _imports or hooks or field_name:
return VarData( return VarData(
@ -184,6 +194,7 @@ class VarData:
imports=_imports, imports=_imports,
hooks=hooks, hooks=hooks,
) )
return None return None
def __bool__(self) -> bool: def __bool__(self) -> bool:

View File

@ -237,9 +237,12 @@ def test_add_page_default_route(app: App, index_page, about_page):
about_page: The about page. about_page: The about page.
""" """
assert app.pages == {} assert app.pages == {}
assert app.unevaluated_pages == {}
app.add_page(index_page) app.add_page(index_page)
app._compile_page("index")
assert app.pages.keys() == {"index"} assert app.pages.keys() == {"index"}
app.add_page(about_page) app.add_page(about_page)
app._compile_page("about")
assert app.pages.keys() == {"index", "about"} assert app.pages.keys() == {"index", "about"}
@ -252,8 +255,9 @@ def test_add_page_set_route(app: App, index_page, windows_platform: bool):
windows_platform: Whether the system is windows. windows_platform: Whether the system is windows.
""" """
route = "test" if windows_platform else "/test" route = "test" if windows_platform else "/test"
assert app.pages == {} assert app.unevaluated_pages == {}
app.add_page(index_page, route=route) app.add_page(index_page, route=route)
app._compile_page("test")
assert app.pages.keys() == {"test"} assert app.pages.keys() == {"test"}
@ -267,8 +271,9 @@ def test_add_page_set_route_dynamic(index_page, windows_platform: bool):
app = App(state=EmptyState) app = App(state=EmptyState)
assert app.state is not None assert app.state is not None
route = "/test/[dynamic]" route = "/test/[dynamic]"
assert app.pages == {} assert app.unevaluated_pages == {}
app.add_page(index_page, route=route) app.add_page(index_page, route=route)
app._compile_page("test/[dynamic]")
assert app.pages.keys() == {"test/[dynamic]"} assert app.pages.keys() == {"test/[dynamic]"}
assert "dynamic" in app.state.computed_vars assert "dynamic" in app.state.computed_vars
assert app.state.computed_vars["dynamic"]._deps(objclass=EmptyState) == { assert app.state.computed_vars["dynamic"]._deps(objclass=EmptyState) == {
@ -286,9 +291,9 @@ def test_add_page_set_route_nested(app: App, index_page, windows_platform: bool)
windows_platform: Whether the system is windows. windows_platform: Whether the system is windows.
""" """
route = "test\\nested" if windows_platform else "/test/nested" route = "test\\nested" if windows_platform else "/test/nested"
assert app.pages == {} assert app.unevaluated_pages == {}
app.add_page(index_page, route=route) app.add_page(index_page, route=route)
assert app.pages.keys() == {route.strip(os.path.sep)} assert app.unevaluated_pages.keys() == {route.strip(os.path.sep)}
def test_add_page_invalid_api_route(app: App, index_page): def test_add_page_invalid_api_route(app: App, index_page):
@ -1238,6 +1243,7 @@ def test_overlay_component(
app.add_page(rx.box("Index"), route="/test") app.add_page(rx.box("Index"), route="/test")
# overlay components are wrapped during compile only # overlay components are wrapped during compile only
app._compile_page("test")
app._setup_overlay_component() app._setup_overlay_component()
page = app.pages["test"] page = app.pages["test"]
@ -1365,6 +1371,7 @@ def test_app_state_determination():
# Add a page with `on_load` enables state. # Add a page with `on_load` enables state.
a1.add_page(rx.box("About"), route="/about", on_load=rx.console_log("")) a1.add_page(rx.box("About"), route="/about", on_load=rx.console_log(""))
a1._compile_page("about")
assert a1.state is not None assert a1.state is not None
a2 = App() a2 = App()
@ -1372,6 +1379,7 @@ def test_app_state_determination():
# Referencing a state Var enables state. # Referencing a state Var enables state.
a2.add_page(rx.box(rx.text(GenState.value)), route="/") a2.add_page(rx.box(rx.text(GenState.value)), route="/")
a2._compile_page("index")
assert a2.state is not None assert a2.state is not None
a3 = App() a3 = App()
@ -1379,6 +1387,7 @@ def test_app_state_determination():
# Referencing router enables state. # Referencing router enables state.
a3.add_page(rx.box(rx.text(State.router.page.full_path)), route="/") a3.add_page(rx.box(rx.text(State.router.page.full_path)), route="/")
a3._compile_page("index")
assert a3.state is not None assert a3.state is not None
a4 = App() a4 = App()
@ -1390,6 +1399,7 @@ def test_app_state_determination():
a4.add_page( a4.add_page(
rx.box(rx.button("Click", on_click=DynamicState.on_counter)), route="/page2" rx.box(rx.button("Click", on_click=DynamicState.on_counter)), route="/page2"
) )
a4._compile_page("page2")
assert a4.state is not None assert a4.state is not None
@ -1469,6 +1479,9 @@ def test_add_page_component_returning_tuple():
app.add_page(index) # type: ignore app.add_page(index) # type: ignore
app.add_page(page2) # type: ignore app.add_page(page2) # type: ignore
app._compile_page("index")
app._compile_page("page2")
assert isinstance((fragment_wrapper := app.pages["index"].children[0]), Fragment) assert isinstance((fragment_wrapper := app.pages["index"].children[0]), Fragment)
assert isinstance((first_text := fragment_wrapper.children[0]), Text) assert isinstance((first_text := fragment_wrapper.children[0]), Text)
assert str(first_text.children[0].contents) == '"first"' # type: ignore assert str(first_text.children[0].contents) == '"first"' # type: ignore

View File

@ -7,8 +7,13 @@ import pytest
import reflex as rx import reflex as rx
import reflex.config import reflex.config
from reflex.config import environment from reflex.config import (
from reflex.constants import Endpoint environment,
interpret_boolean_env,
interpret_enum_env,
interpret_int_env,
)
from reflex.constants import Endpoint, Env
def test_requires_app_name(): def test_requires_app_name():
@ -208,11 +213,11 @@ def test_replace_defaults(
assert getattr(c, key) == value assert getattr(c, key) == value
def reflex_dir_constant(): def reflex_dir_constant() -> Path:
return environment.REFLEX_DIR return environment.REFLEX_DIR
def test_reflex_dir_env_var(monkeypatch, tmp_path): def test_reflex_dir_env_var(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
"""Test that the REFLEX_DIR environment variable is used to set the Reflex.DIR constant. """Test that the REFLEX_DIR environment variable is used to set the Reflex.DIR constant.
Args: Args:
@ -224,3 +229,16 @@ def test_reflex_dir_env_var(monkeypatch, tmp_path):
mp_ctx = multiprocessing.get_context(method="spawn") mp_ctx = multiprocessing.get_context(method="spawn")
with mp_ctx.Pool(processes=1) as pool: with mp_ctx.Pool(processes=1) as pool:
assert pool.apply(reflex_dir_constant) == tmp_path assert pool.apply(reflex_dir_constant) == tmp_path
def test_interpret_enum_env() -> None:
assert interpret_enum_env(Env.PROD.value, Env, "REFLEX_ENV") == Env.PROD
def test_interpret_int_env() -> None:
assert interpret_int_env("3001", "FRONTEND_PORT") == 3001
@pytest.mark.parametrize("value, expected", [("true", True), ("false", False)])
def test_interpret_bool_env(value: str, expected: bool) -> None:
assert interpret_boolean_env(value, "TELEMETRY_ENABLED") == expected