Merge remote-tracking branch 'upstream/main' into more-env-var-cleanup

This commit is contained in:
Benedikt Bartscher 2024-10-26 13:34:46 +02:00
commit 85be850636
No known key found for this signature in database
48 changed files with 801 additions and 444 deletions

View File

@ -51,6 +51,7 @@ jobs:
SCREENSHOT_DIR: /tmp/screenshots SCREENSHOT_DIR: /tmp/screenshots
REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }} REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }}
run: | run: |
poetry run playwright install --with-deps
poetry run pytest tests/integration poetry run pytest tests/integration
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
name: Upload failed test screenshots name: Upload failed test screenshots

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]

75
poetry.lock generated
View File

@ -521,6 +521,21 @@ files = [
{file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"}, {file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"},
] ]
[[package]]
name = "dill"
version = "0.3.9"
description = "serialize all of Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"},
{file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"},
]
[package.extras]
graph = ["objgraph (>=1.7.2)"]
profile = ["gprof2dot (>=2022.7.29)"]
[[package]] [[package]]
name = "distlib" name = "distlib"
version = "0.3.9" version = "0.3.9"
@ -1977,20 +1992,6 @@ files = [
[package.dependencies] [package.dependencies]
six = ">=1.5" six = ">=1.5"
[[package]]
name = "python-dotenv"
version = "1.0.1"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.8"
files = [
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
]
[package.extras]
cli = ["click (>=5.0)"]
[[package]] [[package]]
name = "python-engineio" name = "python-engineio"
version = "4.10.1" version = "4.10.1"
@ -2163,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]
@ -2286,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]]
@ -3047,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 = "c5da15520cef58124f6699007c81158036840469d4f9972592d72bd456c45e7e" 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 = [
@ -33,7 +33,6 @@ jinja2 = ">=3.1.2,<4.0"
psutil = ">=5.9.4,<7.0" psutil = ">=5.9.4,<7.0"
pydantic = ">=1.10.2,<3.0" pydantic = ">=1.10.2,<3.0"
python-multipart = ">=0.0.5,<0.1" python-multipart = ">=0.0.5,<0.1"
python-dotenv = ">=1.0.1"
python-socketio = ">=5.7.0,<6.0" python-socketio = ">=5.7.0,<6.0"
redis = ">=4.3.5,<6.0" redis = ">=4.3.5,<6.0"
rich = ">=13.0.0,<14.0" rich = ">=13.0.0,<14.0"
@ -66,10 +65,11 @@ pytest = ">=7.1.2,<9.0"
pytest-mock = ">=3.10.0,<4.0" pytest-mock = ">=3.10.0,<4.0"
pyright = ">=1.1.229,<1.1.335" pyright = ">=1.1.229,<1.1.335"
darglint = ">=1.8.1,<2.0" darglint = ">=1.8.1,<2.0"
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"

View File

@ -1,11 +1,11 @@
{% extends "web/pages/base_page.js.jinja2" %} {% extends "web/pages/base_page.js.jinja2" %}
{% block early_imports %} {% block early_imports %}
import '/styles/styles.css' import '$/styles/styles.css'
{% endblock %} {% endblock %}
{% block declaration %} {% block declaration %}
import { EventLoopProvider, StateProvider, defaultColorMode } from "/utils/context.js"; import { EventLoopProvider, StateProvider, defaultColorMode } from "$/utils/context.js";
import { ThemeProvider } from 'next-themes' import { ThemeProvider } from 'next-themes'
{% for library_alias, library_path in window_libraries %} {% for library_alias, library_path in window_libraries %}
import * as {{library_alias}} from "{{library_path}}"; import * as {{library_alias}} from "{{library_path}}";

View File

@ -1,5 +1,5 @@
import { createContext, useContext, useMemo, useReducer, useState } from "react" import { createContext, useContext, useMemo, useReducer, useState } from "react"
import { applyDelta, Event, hydrateClientStorage, useEventLoop, refs } from "/utils/state.js" import { applyDelta, Event, hydrateClientStorage, useEventLoop, refs } from "$/utils/state.js"
{% if initial_state %} {% if initial_state %}
export const initialState = {{ initial_state|json_dumps }} export const initialState = {{ initial_state|json_dumps }}
@ -59,6 +59,8 @@ export const initialEvents = () => [
{% else %} {% else %}
export const state_name = undefined export const state_name = undefined
export const exception_state_name = undefined
export const onLoadInternalEvent = () => [] export const onLoadInternalEvent = () => []
export const initialEvents = () => [] export const initialEvents = () => []

View File

@ -4,8 +4,8 @@ import {
ColorModeContext, ColorModeContext,
defaultColorMode, defaultColorMode,
isDevMode, isDevMode,
lastCompiledTimeStamp lastCompiledTimeStamp,
} from "/utils/context.js"; } from "$/utils/context.js";
export default function RadixThemesColorModeProvider({ children }) { export default function RadixThemesColorModeProvider({ children }) {
const { theme, resolvedTheme, setTheme } = useTheme(); const { theme, resolvedTheme, setTheme } = useTheme();
@ -37,7 +37,7 @@ export default function RadixThemesColorModeProvider({ children }) {
const allowedModes = ["light", "dark", "system"]; const allowedModes = ["light", "dark", "system"];
if (!allowedModes.includes(mode)) { if (!allowedModes.includes(mode)) {
console.error( console.error(
`Invalid color mode "${mode}". Defaulting to "${defaultColorMode}".`, `Invalid color mode "${mode}". Defaulting to "${defaultColorMode}".`
); );
mode = defaultColorMode; mode = defaultColorMode;
} }

View File

@ -2,6 +2,7 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"$/*": ["*"],
"@/*": ["public/*"] "@/*": ["public/*"]
} }
} }

View File

@ -2,7 +2,7 @@
import axios from "axios"; import axios from "axios";
import io from "socket.io-client"; import io from "socket.io-client";
import JSON5 from "json5"; import JSON5 from "json5";
import env from "/env.json"; import env from "$/env.json";
import Cookies from "universal-cookie"; import Cookies from "universal-cookie";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import Router, { useRouter } from "next/router"; import Router, { useRouter } from "next/router";
@ -12,9 +12,9 @@ import {
onLoadInternalEvent, onLoadInternalEvent,
state_name, state_name,
exception_state_name, exception_state_name,
} from "utils/context.js"; } from "$/utils/context.js";
import debounce from "/utils/helpers/debounce"; import debounce from "$/utils/helpers/debounce";
import throttle from "/utils/helpers/throttle"; import throttle from "$/utils/helpers/throttle";
import * as Babel from "@babel/standalone"; import * as Babel from "@babel/standalone";
// Endpoint URLs. // Endpoint URLs.

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 from reflex.utils.exec import is_prod_mode, is_testing_env
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.
if isinstance(component, tuple):
component = Fragment.create(*component)
# Ensure state is enabled if this page uses state.
if self.state is None:
if on_load or component._has_stateful_event_triggers():
self._enable_state()
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,
) )
self.unevaluated_pages[route] = UnevaluatedPage(
component=component,
route=route,
title=title,
description=description,
image=image,
on_load=on_load,
meta=meta,
)
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
)
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.
@ -679,7 +685,7 @@ class App(MiddlewareMixin, LifespanMixin, Base):
for i, tags in imports.items() for i, tags in imports.items()
if i not in constants.PackageJson.DEPENDENCIES if i not in constants.PackageJson.DEPENDENCIES
and i not in constants.PackageJson.DEV_DEPENDENCIES and i not in constants.PackageJson.DEV_DEPENDENCIES
and not any(i.startswith(prefix) for prefix in ["/", ".", "next/"]) and not any(i.startswith(prefix) for prefix in ["/", "$/", ".", "next/"])
and i != "" and i != ""
and any(tag.install for tag in tags) and any(tag.install for tag in tags)
} }
@ -827,13 +833,18 @@ 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()
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()
@ -857,7 +868,7 @@ class App(MiddlewareMixin, LifespanMixin, Base):
progress.start() progress.start()
task = progress.add_task( task = progress.add_task(
f"[{get_compilation_time()}] Compiling:", f"[{get_compilation_time()}] Compiling:",
total=len(self.pages) total=len(self.unevaluated_pages)
+ fixed_pages_within_executor + fixed_pages_within_executor
+ adhoc_steps_without_executor, + adhoc_steps_without_executor,
) )
@ -886,38 +897,8 @@ class App(MiddlewareMixin, LifespanMixin, Base):
all_imports = {} all_imports = {}
custom_components = set() custom_components = set()
for _route, component in self.pages.items():
# Merge the component style with the app style.
component._add_style_recursive(self.style, self.theme)
# Add component._get_all_imports() to all_imports.
all_imports.update(component._get_all_imports())
# Add the app wrappers from this component.
app_wrappers.update(component._get_all_app_wrap_components())
# Add the custom components from the page to the set.
custom_components |= component._get_all_custom_components()
progress.advance(task) progress.advance(task)
# Perform auto-memoization of stateful components.
(
stateful_components_path,
stateful_components_code,
page_components,
) = compiler.compile_stateful_components(self.pages.values())
progress.advance(task)
# Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
if code_uses_state_contexts(stateful_components_code) and self.state is None:
raise ReflexRuntimeError(
"To access rx.State in frontend components, at least one "
"subclass of rx.State must be defined in the app."
)
compile_results.append((stateful_components_path, stateful_components_code))
# Compile the root document before fork. # Compile the root document before fork.
compile_results.append( compile_results.append(
compiler.compile_document_root( compiler.compile_document_root(
@ -927,31 +908,12 @@ 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 # Fix #2992 by removing the top-level appearance prop
if self.theme is not None: if self.theme is not None:
self.theme.appearance = 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 +931,55 @@ class App(MiddlewareMixin, LifespanMixin, Base):
max_workers=environment.REFLEX_COMPILE_THREADS.get max_workers=environment.REFLEX_COMPILE_THREADS.get
) )
for route, component in self.pages.items():
component._add_style_recursive(self.style, self.theme)
ExecutorSafeFunctions.COMPONENTS[route] = component
for route, page in self.unevaluated_pages.items():
if route in self.pages:
continue
ExecutorSafeFunctions.UNCOMPILED_PAGES[route] = page
ExecutorSafeFunctions.STATE = self.state
pages_results = []
with executor: with executor:
result_futures = [] result_futures = []
custom_components_future = None pages_futures = []
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 all page components.
for route in self.pages: for route in self.unevaluated_pages:
_submit_work(ExecutorSafeFunctions.compile_page, route) if route in self.pages:
continue
# Compile the app wrapper. f = executor.submit(
_submit_work(ExecutorSafeFunctions.compile_app) ExecutorSafeFunctions.compile_unevaluated_page,
route,
# Compile the custom components. self.style,
custom_components_future = executor.submit( self.theme,
ExecutorSafeFunctions.compile_custom_components, )
pages_futures.append(f)
# Compile the pre-compiled pages.
for route in self.pages:
_submit_work(
ExecutorSafeFunctions.compile_page,
route,
) )
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 +993,70 @@ 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 for future in concurrent.futures.as_completed(pages_futures):
# to install proper frontend packages. pages_results.append(future.result())
progress.advance(task)
for route, component, compiled_page in pages_results:
self._check_routes_conflict(route)
self.pages[route] = component
compile_results.append(compiled_page)
for _, component in self.pages.items():
# Add component._get_all_imports() to all_imports.
all_imports.update(component._get_all_imports())
# Add the app wrappers from this component.
app_wrappers.update(component._get_all_app_wrap_components())
# Add the custom components from the page to the set.
custom_components |= component._get_all_custom_components()
# Perform auto-memoization of stateful components.
( (
*custom_components_result, stateful_components_path,
custom_components_imports, stateful_components_code,
) = custom_components_future.result() page_components,
compile_results.append(custom_components_result) ) = compiler.compile_stateful_components(self.pages.values())
all_imports.update(custom_components_imports)
progress.advance(task)
# Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
if code_uses_state_contexts(stateful_components_code) and self.state is None:
raise ReflexRuntimeError(
"To access rx.State in frontend components, at least one "
"subclass of rx.State must be defined in the app."
)
compile_results.append((stateful_components_path, stateful_components_code))
app_root = self._app_root(app_wrappers=app_wrappers)
# 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,
@ -67,8 +68,8 @@ def _compile_app(app_root: Component) -> str:
window_libraries = [ window_libraries = [
(_normalize_library_name(name), name) for name in bundled_libraries (_normalize_library_name(name), name) for name in bundled_libraries
] + [ ] + [
("utils_context", f"/{constants.Dirs.UTILS}/context"), ("utils_context", f"$/{constants.Dirs.UTILS}/context"),
("utils_state", f"/{constants.Dirs.UTILS}/state"), ("utils_state", f"$/{constants.Dirs.UTILS}/state"),
] ]
return templates.APP_ROOT.render( return templates.APP_ROOT.render(
@ -127,7 +128,7 @@ def _compile_contexts(state: Optional[Type[BaseState]], theme: Component | None)
def _compile_page( def _compile_page(
component: Component, component: Component,
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,
@ -228,7 +229,7 @@ def _compile_components(
""" """
imports = { imports = {
"react": [ImportVar(tag="memo")], "react": [ImportVar(tag="memo")],
f"/{constants.Dirs.STATE_PATH}": [ImportVar(tag="E"), ImportVar(tag="isTrue")], f"$/{constants.Dirs.STATE_PATH}": [ImportVar(tag="E"), ImportVar(tag="isTrue")],
} }
component_renders = [] component_renders = []
@ -315,7 +316,7 @@ def _compile_stateful_components(
# Don't import from the file that we're about to create. # Don't import from the file that we're about to create.
all_imports = utils.merge_imports(*all_import_dicts) all_imports = utils.merge_imports(*all_import_dicts)
all_imports.pop( all_imports.pop(
f"/{constants.Dirs.UTILS}/{constants.PageNames.STATEFUL_COMPONENTS}", None f"$/{constants.Dirs.UTILS}/{constants.PageNames.STATEFUL_COMPONENTS}", None
) )
return templates.STATEFUL_COMPONENTS.render( return templates.STATEFUL_COMPONENTS.render(
@ -424,7 +425,7 @@ def compile_contexts(
def compile_page( def compile_page(
path: str, component: Component, state: Type[BaseState] path: str, component: Component, state: Type[BaseState] | None
) -> tuple[str, str]: ) -> tuple[str, str]:
"""Compile a single page. """Compile a single page.
@ -534,6 +535,73 @@ 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
) -> 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.
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)
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 +627,12 @@ class ExecutorSafeFunctions:
""" """
COMPILE_PAGE_ARGS_BY_ROUTE = {} COMPONENTS: Dict[str, Component] = {}
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 +641,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

@ -83,6 +83,12 @@ def validate_imports(import_dict: ParsedImportDict):
f"{_import.tag}/{_import.alias}" if _import.alias else _import.tag f"{_import.tag}/{_import.alias}" if _import.alias else _import.tag
) )
if import_name in used_tags: if import_name in used_tags:
already_imported = used_tags[import_name]
if (already_imported[0] == "$" and already_imported[1:] == lib) or (
lib[0] == "$" and lib[1:] == already_imported
):
used_tags[import_name] = lib if lib[0] == "$" else already_imported
continue
raise ValueError( raise ValueError(
f"Can not compile, the tag {import_name} is used multiple time from {lib} and {used_tags[import_name]}" f"Can not compile, the tag {import_name} is used multiple time from {lib} and {used_tags[import_name]}"
) )

View File

@ -38,6 +38,7 @@ from reflex.constants import (
) )
from reflex.constants.compiler import SpecialAttributes from reflex.constants.compiler import SpecialAttributes
from reflex.event import ( from reflex.event import (
EventCallback,
EventChain, EventChain,
EventChainVar, EventChainVar,
EventHandler, EventHandler,
@ -1126,6 +1127,8 @@ class Component(BaseComponent, ABC):
for trigger in self.event_triggers.values(): for trigger in self.event_triggers.values():
if isinstance(trigger, EventChain): if isinstance(trigger, EventChain):
for event in trigger.events: for event in trigger.events:
if isinstance(event, EventCallback):
continue
if isinstance(event, EventSpec): if isinstance(event, EventSpec):
if event.handler.state_full_name: if event.handler.state_full_name:
return True return True
@ -1305,7 +1308,9 @@ class Component(BaseComponent, ABC):
if self._get_ref_hook(): if self._get_ref_hook():
# Handle hooks needed for attaching react refs to DOM nodes. # Handle hooks needed for attaching react refs to DOM nodes.
_imports.setdefault("react", set()).add(ImportVar(tag="useRef")) _imports.setdefault("react", set()).add(ImportVar(tag="useRef"))
_imports.setdefault(f"/{Dirs.STATE_PATH}", set()).add(ImportVar(tag="refs")) _imports.setdefault(f"$/{Dirs.STATE_PATH}", set()).add(
ImportVar(tag="refs")
)
if self._get_mount_lifecycle_hook(): if self._get_mount_lifecycle_hook():
# Handle hooks for `on_mount` / `on_unmount`. # Handle hooks for `on_mount` / `on_unmount`.
@ -1662,7 +1667,7 @@ class CustomComponent(Component):
"""A custom user-defined component.""" """A custom user-defined component."""
# Use the components library. # Use the components library.
library = f"/{Dirs.COMPONENTS_PATH}" library = f"$/{Dirs.COMPONENTS_PATH}"
# The function that creates the component. # The function that creates the component.
component_fn: Callable[..., Component] = Component.create component_fn: Callable[..., Component] = Component.create
@ -2230,7 +2235,7 @@ class StatefulComponent(BaseComponent):
""" """
if self.rendered_as_shared: if self.rendered_as_shared:
return { return {
f"/{Dirs.UTILS}/{PageNames.STATEFUL_COMPONENTS}": [ f"$/{Dirs.UTILS}/{PageNames.STATEFUL_COMPONENTS}": [
ImportVar(tag=self.tag) ImportVar(tag=self.tag)
] ]
} }

View File

@ -66,8 +66,8 @@ class WebsocketTargetURL(Var):
_js_expr="getBackendURL(env.EVENT).href", _js_expr="getBackendURL(env.EVENT).href",
_var_data=VarData( _var_data=VarData(
imports={ imports={
"/env.json": [ImportVar(tag="env", is_default=True)], "$/env.json": [ImportVar(tag="env", is_default=True)],
f"/{Dirs.STATE_PATH}": [ImportVar(tag="getBackendURL")], f"$/{Dirs.STATE_PATH}": [ImportVar(tag="getBackendURL")],
}, },
), ),
_var_type=WebsocketTargetURL, _var_type=WebsocketTargetURL,

View File

@ -21,7 +21,7 @@ route_not_found: Var = Var(_js_expr=constants.ROUTE_NOT_FOUND)
class ClientSideRouting(Component): class ClientSideRouting(Component):
"""The client-side routing component.""" """The client-side routing component."""
library = "/utils/client_side_routing" library = "$/utils/client_side_routing"
tag = "useClientSideRouting" tag = "useClientSideRouting"
def add_hooks(self) -> list[str]: def add_hooks(self) -> list[str]:

View File

@ -67,7 +67,7 @@ class Clipboard(Fragment):
The import dict for the component. The import dict for the component.
""" """
return { return {
"/utils/helpers/paste.js": ImportVar( "$/utils/helpers/paste.js": ImportVar(
tag="usePasteHandler", is_default=True tag="usePasteHandler", is_default=True
), ),
} }

View File

@ -15,7 +15,7 @@ from reflex.vars.base import LiteralVar, Var
from reflex.vars.number import ternary_operation from reflex.vars.number import ternary_operation
_IS_TRUE_IMPORT: ImportDict = { _IS_TRUE_IMPORT: ImportDict = {
f"/{Dirs.STATE_PATH}": [ImportVar(tag="isTrue")], f"$/{Dirs.STATE_PATH}": [ImportVar(tag="isTrue")],
} }

View File

@ -118,7 +118,7 @@ class DebounceInput(Component):
_var_type=Type[Component], _var_type=Type[Component],
_var_data=VarData( _var_data=VarData(
imports=child._get_imports(), imports=child._get_imports(),
hooks=child._get_hooks_internal(), hooks=child._get_all_hooks(),
), ),
), ),
) )
@ -128,6 +128,10 @@ class DebounceInput(Component):
component.event_triggers.update(child.event_triggers) component.event_triggers.update(child.event_triggers)
component.children = child.children component.children = child.children
component._rename_props = child._rename_props component._rename_props = child._rename_props
outer_get_all_custom_code = component._get_all_custom_code
component._get_all_custom_code = lambda: outer_get_all_custom_code().union(
child._get_all_custom_code()
)
return component return component
def _render(self): def _render(self):

View File

@ -29,7 +29,7 @@ DEFAULT_UPLOAD_ID: str = "default"
upload_files_context_var_data: VarData = VarData( upload_files_context_var_data: VarData = VarData(
imports={ imports={
"react": "useContext", "react": "useContext",
f"/{Dirs.CONTEXTS_PATH}": "UploadFilesContext", f"$/{Dirs.CONTEXTS_PATH}": "UploadFilesContext",
}, },
hooks={ hooks={
"const [filesById, setFilesById] = useContext(UploadFilesContext);": None, "const [filesById, setFilesById] = useContext(UploadFilesContext);": None,
@ -134,8 +134,8 @@ uploaded_files_url_prefix = Var(
_js_expr="getBackendURL(env.UPLOAD)", _js_expr="getBackendURL(env.UPLOAD)",
_var_data=VarData( _var_data=VarData(
imports={ imports={
f"/{Dirs.STATE_PATH}": "getBackendURL", f"$/{Dirs.STATE_PATH}": "getBackendURL",
"/env.json": ImportVar(tag="env", is_default=True), "$/env.json": ImportVar(tag="env", is_default=True),
} }
), ),
).to(str) ).to(str)
@ -170,7 +170,7 @@ def _on_drop_spec(files: Var) -> Tuple[Var[Any]]:
class UploadFilesProvider(Component): class UploadFilesProvider(Component):
"""AppWrap component that provides a dict of selected files by ID via useContext.""" """AppWrap component that provides a dict of selected files by ID via useContext."""
library = f"/{Dirs.CONTEXTS_PATH}" library = f"$/{Dirs.CONTEXTS_PATH}"
tag = "UploadFilesProvider" tag = "UploadFilesProvider"

View File

@ -34,8 +34,8 @@ uploaded_files_url_prefix = Var(
_js_expr="getBackendURL(env.UPLOAD)", _js_expr="getBackendURL(env.UPLOAD)",
_var_data=VarData( _var_data=VarData(
imports={ imports={
f"/{Dirs.STATE_PATH}": "getBackendURL", f"$/{Dirs.STATE_PATH}": "getBackendURL",
"/env.json": ImportVar(tag="env", is_default=True), "$/env.json": ImportVar(tag="env", is_default=True),
} }
), ),
).to(str) ).to(str)

View File

@ -344,7 +344,7 @@ class DataEditor(NoSSRComponent):
return { return {
"": f"{format.format_library_name(self.library)}/dist/index.css", "": f"{format.format_library_name(self.library)}/dist/index.css",
self.library: "GridCellKind", self.library: "GridCellKind",
"/utils/helpers/dataeditor.js": ImportVar( "$/utils/helpers/dataeditor.js": ImportVar(
tag="formatDataEditorCells", is_default=False, install=False tag="formatDataEditorCells", is_default=False, install=False
), ),
} }

View File

@ -90,7 +90,7 @@ def load_dynamic_serializer():
for lib, names in component._get_all_imports().items(): for lib, names in component._get_all_imports().items():
formatted_lib_name = format_library_name(lib) formatted_lib_name = format_library_name(lib)
if ( if (
not lib.startswith((".", "/")) not lib.startswith((".", "/", "$/"))
and not lib.startswith("http") and not lib.startswith("http")
and formatted_lib_name not in libs_in_window and formatted_lib_name not in libs_in_window
): ):
@ -106,7 +106,7 @@ def load_dynamic_serializer():
# Rewrite imports from `/` to destructure from window # Rewrite imports from `/` to destructure from window
for ix, line in enumerate(module_code_lines[:]): for ix, line in enumerate(module_code_lines[:]):
if line.startswith("import "): if line.startswith("import "):
if 'from "/' in line: if 'from "$/' in line or 'from "/' in line:
module_code_lines[ix] = ( module_code_lines[ix] = (
line.replace("import ", "const ", 1).replace( line.replace("import ", "const ", 1).replace(
" from ", " = window['__reflex'][", 1 " from ", " = window['__reflex'][", 1
@ -157,7 +157,7 @@ def load_dynamic_serializer():
merge_var_data=VarData.merge( merge_var_data=VarData.merge(
VarData( VarData(
imports={ imports={
f"/{constants.Dirs.STATE_PATH}": [ f"$/{constants.Dirs.STATE_PATH}": [
imports.ImportVar(tag="evalReactComponent"), imports.ImportVar(tag="evalReactComponent"),
], ],
"react": [ "react": [

View File

@ -187,7 +187,7 @@ class Form(BaseHTML):
""" """
return { return {
"react": "useCallback", "react": "useCallback",
f"/{Dirs.STATE_PATH}": ["getRefValue", "getRefValues"], f"$/{Dirs.STATE_PATH}": ["getRefValue", "getRefValues"],
} }
def add_hooks(self) -> list[str]: def add_hooks(self) -> list[str]:
@ -615,6 +615,42 @@ class Textarea(BaseHTML):
# Fired when a key is released # Fired when a key is released
on_key_up: EventHandler[key_event] on_key_up: EventHandler[key_event]
@classmethod
def create(cls, *children, **props):
"""Create a textarea component.
Args:
*children: The children of the textarea.
**props: The properties of the textarea.
Returns:
The textarea component.
Raises:
ValueError: when `enter_key_submit` is combined with `on_key_down`.
"""
enter_key_submit = props.get("enter_key_submit")
auto_height = props.get("auto_height")
custom_attrs = props.setdefault("custom_attrs", {})
if enter_key_submit is not None:
enter_key_submit = Var.create(enter_key_submit)
if "on_key_down" in props:
raise ValueError(
"Cannot combine `enter_key_submit` with `on_key_down`.",
)
custom_attrs["on_key_down"] = Var(
_js_expr=f"(e) => enterKeySubmitOnKeyDown(e, {str(enter_key_submit)})",
_var_data=VarData.merge(enter_key_submit._get_all_var_data()),
)
if auto_height is not None:
auto_height = Var.create(auto_height)
custom_attrs["on_input"] = Var(
_js_expr=f"(e) => autoHeightOnInput(e, {str(auto_height)})",
_var_data=VarData.merge(auto_height._get_all_var_data()),
)
return super().create(*children, **props)
def _exclude_props(self) -> list[str]: def _exclude_props(self) -> list[str]:
return super()._exclude_props() + [ return super()._exclude_props() + [
"auto_height", "auto_height",
@ -634,28 +670,6 @@ class Textarea(BaseHTML):
custom_code.add(ENTER_KEY_SUBMIT_JS) custom_code.add(ENTER_KEY_SUBMIT_JS)
return custom_code return custom_code
def _render(self) -> Tag:
tag = super()._render()
if self.enter_key_submit is not None:
if "on_key_down" in self.event_triggers:
raise ValueError(
"Cannot combine `enter_key_submit` with `on_key_down`.",
)
tag.add_props(
on_key_down=Var(
_js_expr=f"(e) => enterKeySubmitOnKeyDown(e, {str(self.enter_key_submit)})",
_var_data=VarData.merge(self.enter_key_submit._get_all_var_data()),
)
)
if self.auto_height is not None:
tag.add_props(
on_input=Var(
_js_expr=f"(e) => autoHeightOnInput(e, {str(self.auto_height)})",
_var_data=VarData.merge(self.auto_height._get_all_var_data()),
)
)
return tag
button = Button.create button = Button.create
fieldset = Fieldset.create fieldset = Fieldset.create

View File

@ -1376,10 +1376,10 @@ class Textarea(BaseHTML):
on_unmount: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None,
**props, **props,
) -> "Textarea": ) -> "Textarea":
"""Create the component. """Create a textarea component.
Args: Args:
*children: The children of the component. *children: The children of the textarea.
auto_complete: Whether the form control should have autocomplete enabled auto_complete: Whether the form control should have autocomplete enabled
auto_focus: Automatically focuses the textarea when the page loads auto_focus: Automatically focuses the textarea when the page loads
auto_height: Automatically fit the content height to the text (use min-height with this prop) auto_height: Automatically fit the content height to the text (use min-height with this prop)
@ -1419,10 +1419,13 @@ class Textarea(BaseHTML):
class_name: The class name for the component. class_name: The class name for the component.
autofocus: Whether the component should take the focus once the page is loaded autofocus: Whether the component should take the focus once the page is loaded
custom_attrs: custom attribute custom_attrs: custom attribute
**props: The props of the component. **props: The properties of the textarea.
Returns: Returns:
The component. The textarea component.
Raises:
ValueError: when `enter_key_submit` is combined with `on_key_down`.
""" """
... ...

View File

@ -3,10 +3,10 @@
import dataclasses import dataclasses
from typing import List, Optional from typing import List, Optional
from reflex.components.component import Component, NoSSRComponent from reflex.components.component import NoSSRComponent
from reflex.event import EventHandler, identity_event from reflex.event import EventHandler, identity_event
from reflex.utils.imports import ImportDict from reflex.utils.imports import ImportDict
from reflex.vars.base import Var from reflex.vars.base import LiteralVar, Var
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
@ -92,6 +92,9 @@ class Moment(NoSSRComponent):
# Display the date in the given timezone. # Display the date in the given timezone.
tz: Var[str] tz: Var[str]
# The locale to use when rendering.
locale: Var[str]
# Fires when the date changes. # Fires when the date changes.
on_change: EventHandler[identity_event(str)] on_change: EventHandler[identity_event(str)]
@ -101,22 +104,15 @@ class Moment(NoSSRComponent):
Returns: Returns:
The import dict for the component. The import dict for the component.
""" """
imports = {}
if isinstance(self.locale, LiteralVar):
imports[""] = f"moment/locale/{self.locale._var_value}"
elif self.locale is not None:
# If the user is using a variable for the locale, we can't know the
# value at compile time so import all locales available.
imports[""] = "moment/min/locales"
if self.tz is not None: if self.tz is not None:
return {"moment-timezone": ""} imports["moment-timezone"] = ""
return {}
@classmethod return imports
def create(cls, *children, **props) -> Component:
"""Create a Moment component.
Args:
*children: The children of the component.
**props: The properties of the component.
Returns:
The Moment Component.
"""
comp = super().create(*children, **props)
if "tz" in props:
comp.lib_dependencies.append("moment-timezone")
return comp

View File

@ -51,6 +51,7 @@ class Moment(NoSSRComponent):
unix: Optional[Union[Var[bool], bool]] = None, unix: Optional[Union[Var[bool], bool]] = None,
local: Optional[Union[Var[bool], bool]] = None, local: Optional[Union[Var[bool], bool]] = None,
tz: Optional[Union[Var[str], str]] = None, tz: Optional[Union[Var[str], str]] = None,
locale: Optional[Union[Var[str], str]] = None,
style: Optional[Style] = None, style: Optional[Style] = None,
key: Optional[Any] = None, key: Optional[Any] = None,
id: Optional[Any] = None, id: Optional[Any] = None,
@ -75,7 +76,7 @@ class Moment(NoSSRComponent):
on_unmount: Optional[EventType[[]]] = None, on_unmount: Optional[EventType[[]]] = None,
**props, **props,
) -> "Moment": ) -> "Moment":
"""Create a Moment component. """Create the component.
Args: Args:
*children: The children of the component. *children: The children of the component.
@ -99,15 +100,16 @@ class Moment(NoSSRComponent):
unix: Tells Moment to parse the given date value as a unix timestamp. unix: Tells Moment to parse the given date value as a unix timestamp.
local: Outputs the result in local time. local: Outputs the result in local time.
tz: Display the date in the given timezone. tz: Display the date in the given timezone.
locale: The locale to use when rendering.
style: The style of the component. style: The style of the component.
key: A unique key for the component. key: A unique key for the component.
id: The id for the component. id: The id for the component.
class_name: The class name for the component. class_name: The class name for the component.
autofocus: Whether the component should take the focus once the page is loaded autofocus: Whether the component should take the focus once the page is loaded
custom_attrs: custom attribute custom_attrs: custom attribute
**props: The properties of the component. **props: The props of the component.
Returns: Returns:
The Moment Component. The component.
""" """
... ...

View File

@ -221,7 +221,7 @@ class Theme(RadixThemesComponent):
The import dict. The import dict.
""" """
_imports: ImportDict = { _imports: ImportDict = {
"/utils/theme.js": [ImportVar(tag="theme", is_default=True)], "$/utils/theme.js": [ImportVar(tag="theme", is_default=True)],
} }
if get_config().tailwind is None: if get_config().tailwind is None:
# When tailwind is disabled, import the radix-ui styles directly because they will # When tailwind is disabled, import the radix-ui styles directly because they will
@ -265,7 +265,7 @@ class ThemePanel(RadixThemesComponent):
class RadixThemesColorModeProvider(Component): class RadixThemesColorModeProvider(Component):
"""Next-themes integration for radix themes components.""" """Next-themes integration for radix themes components."""
library = "/components/reflex/radix_themes_color_mode_provider.js" library = "$/components/reflex/radix_themes_color_mode_provider.js"
tag = "RadixThemesColorModeProvider" tag = "RadixThemesColorModeProvider"
is_default = True is_default = True

View File

@ -251,7 +251,7 @@ class Toaster(Component):
_js_expr=f"{toast_ref} = toast", _js_expr=f"{toast_ref} = toast",
_var_data=VarData( _var_data=VarData(
imports={ imports={
"/utils/state": [ImportVar(tag="refs")], "$/utils/state": [ImportVar(tag="refs")],
self.library: [ImportVar(tag="toast", install=False)], self.library: [ImportVar(tag="toast", install=False)],
} }
), ),

View File

@ -442,6 +442,12 @@ class EnvironmentVariables:
# Whether to run the frontend only. Exclusive with REFLEX_BACKEND_ONLY. # Whether to run the frontend only. Exclusive with REFLEX_BACKEND_ONLY.
REFLEX_FRONTEND_ONLY: EnvVar[bool] = env_var(False) REFLEX_FRONTEND_ONLY: EnvVar[bool] = env_var(False)
# Whether to send telemetry data to Reflex.
TELEMETRY_ENABLED: EnvVar[bool] = env_var(True)
environment = EnvironmentVariables()
class Config(Base): class Config(Base):
"""The config defines runtime settings for the app. """The config defines runtime settings for the app.
@ -602,10 +608,15 @@ class Config(Base):
The updated config values. The updated config values.
""" """
if self.env_file: if self.env_file:
from dotenv import load_dotenv try:
from dotenv import load_dotenv # type: ignore
# load env file if exists # load env file if exists
load_dotenv(self.env_file, override=True) load_dotenv(self.env_file, override=True)
except ImportError:
console.error(
"""The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.0.1"`."""
)
updated_values = {} updated_values = {}
# Iterate over the fields. # Iterate over the fields.

View File

@ -114,8 +114,8 @@ class Imports(SimpleNamespace):
EVENTS = { EVENTS = {
"react": [ImportVar(tag="useContext")], "react": [ImportVar(tag="useContext")],
f"/{Dirs.CONTEXTS_PATH}": [ImportVar(tag="EventLoopContext")], f"$/{Dirs.CONTEXTS_PATH}": [ImportVar(tag="EventLoopContext")],
f"/{Dirs.STATE_PATH}": [ImportVar(tag=CompileVars.TO_EVENT)], f"$/{Dirs.STATE_PATH}": [ImportVar(tag=CompileVars.TO_EVENT)],
} }

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

@ -16,6 +16,7 @@ from typing import (
Generic, Generic,
List, List,
Optional, Optional,
Sequence,
Tuple, Tuple,
Type, Type,
TypeVar, TypeVar,
@ -389,7 +390,9 @@ class CallableEventSpec(EventSpec):
class EventChain(EventActionsMixin): class EventChain(EventActionsMixin):
"""Container for a chain of events that will be executed in order.""" """Container for a chain of events that will be executed in order."""
events: List[Union[EventSpec, EventVar]] = dataclasses.field(default_factory=list) events: Sequence[Union[EventSpec, EventVar, EventCallback]] = dataclasses.field(
default_factory=list
)
args_spec: Optional[Callable] = dataclasses.field(default=None) args_spec: Optional[Callable] = dataclasses.field(default=None)
@ -1445,13 +1448,8 @@ class LiteralEventChainVar(ArgsFunctionOperation, LiteralVar, EventChainVar):
) )
G = ParamSpec("G")
IndividualEventType = Union[EventSpec, EventHandler, Callable[G, Any], Var[Any]]
EventType = Union[IndividualEventType[G], List[IndividualEventType[G]]]
P = ParamSpec("P") P = ParamSpec("P")
Q = ParamSpec("Q")
T = TypeVar("T") T = TypeVar("T")
V = TypeVar("V") V = TypeVar("V")
V2 = TypeVar("V2") V2 = TypeVar("V2")
@ -1473,55 +1471,73 @@ if sys.version_info >= (3, 10):
""" """
self.func = func self.func = func
@property
def prevent_default(self):
"""Prevent default behavior.
Returns:
The event callback with prevent default behavior.
"""
return self
@property
def stop_propagation(self):
"""Stop event propagation.
Returns:
The event callback with stop propagation behavior.
"""
return self
@overload @overload
def __get__( def __call__(
self: EventCallback[[V], T], instance: None, owner self: EventCallback[Concatenate[V, Q], T], value: V | Var[V]
) -> Callable[[Union[Var[V], V]], EventSpec]: ... ) -> EventCallback[Q, T]: ...
@overload
def __call__(
self: EventCallback[Concatenate[V, V2, Q], T],
value: V | Var[V],
value2: V2 | Var[V2],
) -> EventCallback[Q, T]: ...
@overload
def __call__(
self: EventCallback[Concatenate[V, V2, V3, Q], T],
value: V | Var[V],
value2: V2 | Var[V2],
value3: V3 | Var[V3],
) -> EventCallback[Q, T]: ...
@overload
def __call__(
self: EventCallback[Concatenate[V, V2, V3, V4, Q], T],
value: V | Var[V],
value2: V2 | Var[V2],
value3: V3 | Var[V3],
value4: V4 | Var[V4],
) -> EventCallback[Q, T]: ...
def __call__(self, *values) -> EventCallback: # type: ignore
"""Call the function with the values.
Args:
*values: The values to call the function with.
Returns:
The function with the values.
"""
return self.func(*values) # type: ignore
@overload @overload
def __get__( def __get__(
self: EventCallback[[V, V2], T], instance: None, owner self: EventCallback[P, T], instance: None, owner
) -> Callable[[Union[Var[V], V], Union[Var[V2], V2]], EventSpec]: ... ) -> EventCallback[P, T]: ...
@overload
def __get__(
self: EventCallback[[V, V2, V3], T], instance: None, owner
) -> Callable[
[Union[Var[V], V], Union[Var[V2], V2], Union[Var[V3], V3]],
EventSpec,
]: ...
@overload
def __get__(
self: EventCallback[[V, V2, V3, V4], T], instance: None, owner
) -> Callable[
[
Union[Var[V], V],
Union[Var[V2], V2],
Union[Var[V3], V3],
Union[Var[V4], V4],
],
EventSpec,
]: ...
@overload
def __get__(
self: EventCallback[[V, V2, V3, V4, V5], T], instance: None, owner
) -> Callable[
[
Union[Var[V], V],
Union[Var[V2], V2],
Union[Var[V3], V3],
Union[Var[V4], V4],
Union[Var[V5], V5],
],
EventSpec,
]: ...
@overload @overload
def __get__(self, instance, owner) -> Callable[P, T]: ... def __get__(self, instance, owner) -> Callable[P, T]: ...
def __get__(self, instance, owner) -> Callable: def __get__(self, instance, owner) -> Callable: # type: ignore
"""Get the function with the instance bound to it. """Get the function with the instance bound to it.
Args: Args:
@ -1548,6 +1564,9 @@ if sys.version_info >= (3, 10):
return func # type: ignore return func # type: ignore
else: else:
class EventCallback(Generic[P, T]):
"""A descriptor that wraps a function to be used as an event."""
def event_handler(func: Callable[P, T]) -> Callable[P, T]: def event_handler(func: Callable[P, T]) -> Callable[P, T]:
"""Wrap a function to be used as an event. """Wrap a function to be used as an event.
@ -1560,6 +1579,17 @@ else:
return func return func
G = ParamSpec("G")
IndividualEventType = Union[
EventSpec, EventHandler, Callable[G, Any], EventCallback[G, Any], Var[Any]
]
ItemOrList = Union[V, List[V]]
EventType = ItemOrList[IndividualEventType[G]]
class EventNamespace(types.SimpleNamespace): class EventNamespace(types.SimpleNamespace):
"""A namespace for event related classes.""" """A namespace for event related classes."""

View File

@ -21,7 +21,7 @@ NoValue = object()
_refs_import = { _refs_import = {
f"/{constants.Dirs.STATE_PATH}": [ImportVar(tag="refs")], f"$/{constants.Dirs.STATE_PATH}": [ImportVar(tag="refs")],
} }
@ -178,9 +178,12 @@ class ClientStateVar(Var):
if self._global_ref if self._global_ref
else self._setter_name else self._setter_name
) )
_var_data = VarData(imports=_refs_import if self._global_ref else {})
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_str = str(LiteralVar.create(value)) value_var = LiteralVar.create(value)
_var_data = VarData.merge(_var_data, value_var._get_all_var_data())
value_str = str(value_var)
if value_str.startswith("_"): if value_str.startswith("_"):
# remove patterns of ["*"] from the value_str using regex # remove patterns of ["*"] from the value_str using regex
@ -190,7 +193,7 @@ class ClientStateVar(Var):
setter = f"(() => {setter}({value_str}))" setter = f"(() => {setter}({value_str}))"
return Var( return Var(
_js_expr=setter, _js_expr=setter,
_var_data=VarData(imports=_refs_import if self._global_ref else {}), _var_data=_var_data,
).to(FunctionVar, EventChain) ).to(FunctionVar, EventChain)
@property @property

View File

@ -38,7 +38,7 @@ def get_engine(url: str | None = None) -> sqlalchemy.engine.Engine:
url = url or conf.db_url url = url or conf.db_url
if url is None: if url is None:
raise ValueError("No database url configured") raise ValueError("No database url configured")
if environment.ALEMBIC_CONFIG.get.exists(): if not environment.ALEMBIC_CONFIG.get.exists():
console.warn( console.warn(
"Database is not initialized, run [bold]reflex db init[/bold] first." "Database is not initialized, run [bold]reflex db init[/bold] first."
) )

View File

@ -220,6 +220,7 @@ class EventHandlerSetVar(EventHandler):
Raises: Raises:
AttributeError: If the given Var name does not exist on the state. AttributeError: If the given Var name does not exist on the state.
EventHandlerValueError: If the given Var name is not a str EventHandlerValueError: If the given Var name is not a str
NotImplementedError: If the setter for the given Var is async
""" """
from reflex.utils.exceptions import EventHandlerValueError from reflex.utils.exceptions import EventHandlerValueError
@ -228,11 +229,20 @@ class EventHandlerSetVar(EventHandler):
raise EventHandlerValueError( raise EventHandlerValueError(
f"Var name must be passed as a string, got {args[0]!r}" f"Var name must be passed as a string, got {args[0]!r}"
) )
handler = getattr(self.state_cls, constants.SETTER_PREFIX + args[0], None)
# Check that the requested Var setter exists on the State at compile time. # Check that the requested Var setter exists on the State at compile time.
if getattr(self.state_cls, constants.SETTER_PREFIX + args[0], None) is None: if handler is None:
raise AttributeError( raise AttributeError(
f"Variable `{args[0]}` cannot be set on `{self.state_cls.get_full_name()}`" f"Variable `{args[0]}` cannot be set on `{self.state_cls.get_full_name()}`"
) )
if asyncio.iscoroutinefunction(handler.fn):
raise NotImplementedError(
f"Setter for {args[0]} is async, which is not supported."
)
return super().__call__(*args) return super().__call__(*args)
@ -2053,11 +2063,23 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
""" """
try: try:
return pickle.dumps((self._to_schema(), self)) return pickle.dumps((self._to_schema(), self))
except pickle.PicklingError: except (pickle.PicklingError, AttributeError) as og_pickle_error:
console.warn( error = (
f"Failed to serialize state {self.get_full_name()} due to unpicklable object. " f"Failed to serialize state {self.get_full_name()} due to unpicklable object. "
"This state will not be persisted. " "This state will not be persisted. "
) )
try:
import dill
return dill.dumps((self._to_schema(), self))
except ImportError:
error += (
f"Pickle error: {og_pickle_error}. "
"Consider `pip install 'dill>=0.3.8'` for more exotic serialization support."
)
except (pickle.PicklingError, TypeError, ValueError) as ex:
error += f"Dill was also unable to pickle the state: {ex}"
console.warn(error)
return b"" return b""
@classmethod @classmethod
@ -2895,9 +2917,13 @@ class StateManagerDisk(StateManager):
for substate in state.get_substates(): for substate in state.get_substates():
substate_token = _substate_key(client_token, substate) substate_token = _substate_key(client_token, substate)
fresh_instance = await root_state.get_state(substate)
instance = await self.load_state(substate_token) instance = await self.load_state(substate_token)
if instance is None: if instance is not None:
instance = await root_state.get_state(substate) # Ensure all substates exist, even if they weren't serialized previously.
instance.substates = fresh_instance.substates
else:
instance = fresh_instance
state.substates[substate.get_name()] = instance state.substates[substate.get_name()] = instance
instance.parent_state = state instance.parent_state = state

View File

@ -23,7 +23,7 @@ LiteralColorMode = Literal["system", "light", "dark"]
# Reference the global ColorModeContext # Reference the global ColorModeContext
color_mode_imports = { color_mode_imports = {
f"/{constants.Dirs.CONTEXTS_PATH}": [ImportVar(tag="ColorModeContext")], f"$/{constants.Dirs.CONTEXTS_PATH}": [ImportVar(tag="ColorModeContext")],
"react": [ImportVar(tag="useContext")], "react": [ImportVar(tag="useContext")],
} }

View File

@ -252,7 +252,7 @@ class AppHarness:
# disable telemetry reporting for tests # disable telemetry reporting for tests
from reflex.config import environment from reflex.config import environment
environment.TELEMETRY_ENABLED = False environment.TELEMETRY_ENABLED.set(False)
self.app_path.mkdir(parents=True, exist_ok=True) self.app_path.mkdir(parents=True, exist_ok=True)
if self.app_source is not None: if self.app_source is not None:
app_globals = self._get_globals_from_signature(self.app_source) app_globals = self._get_globals_from_signature(self.app_source)

View File

@ -23,6 +23,12 @@ def merge_imports(
for lib, fields in ( for lib, fields in (
import_dict if isinstance(import_dict, tuple) else import_dict.items() import_dict if isinstance(import_dict, tuple) else import_dict.items()
): ):
# If the lib is an absolute path, we need to prefix it with a $
lib = (
"$" + lib
if lib.startswith(("/utils/", "/components/", "/styles/", "/public/"))
else lib
)
if isinstance(fields, (list, tuple, set)): if isinstance(fields, (list, tuple, set)):
all_imports[lib].extend( all_imports[lib].extend(
( (

View File

@ -151,30 +151,40 @@ 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:
@ -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:
@ -217,7 +228,7 @@ class VarData:
): None ): None
}, },
imports={ imports={
f"/{constants.Dirs.CONTEXTS_PATH}": [ImportVar(tag="StateContexts")], f"$/{constants.Dirs.CONTEXTS_PATH}": [ImportVar(tag="StateContexts")],
"react": [ImportVar(tag="useContext")], "react": [ImportVar(tag="useContext")],
}, },
) )
@ -956,7 +967,7 @@ class Var(Generic[VAR_TYPE]):
_js_expr="refs", _js_expr="refs",
_var_data=VarData( _var_data=VarData(
imports={ imports={
f"/{constants.Dirs.STATE_PATH}": [imports.ImportVar(tag="refs")] f"$/{constants.Dirs.STATE_PATH}": [imports.ImportVar(tag="refs")]
} }
), ),
).to(ObjectVar, Dict[str, str]) ).to(ObjectVar, Dict[str, str])
@ -2530,7 +2541,7 @@ def get_uuid_string_var() -> Var:
unique_uuid_var = get_unique_variable_name() unique_uuid_var = get_unique_variable_name()
unique_uuid_var_data = VarData( unique_uuid_var_data = VarData(
imports={ imports={
f"/{constants.Dirs.STATE_PATH}": {ImportVar(tag="generateUUID")}, # type: ignore f"$/{constants.Dirs.STATE_PATH}": {ImportVar(tag="generateUUID")}, # type: ignore
"react": "useMemo", "react": "useMemo",
}, },
hooks={f"const {unique_uuid_var} = useMemo(generateUUID, [])": None}, hooks={f"const {unique_uuid_var} = useMemo(generateUUID, [])": None},

View File

@ -1090,7 +1090,7 @@ boolean_types = Union[BooleanVar, bool]
_IS_TRUE_IMPORT: ImportDict = { _IS_TRUE_IMPORT: ImportDict = {
f"/{Dirs.STATE_PATH}": [ImportVar(tag="isTrue")], f"$/{Dirs.STATE_PATH}": [ImportVar(tag="isTrue")],
} }

View File

@ -0,0 +1,59 @@
"""Integration tests for a stateless app."""
from typing import Generator
import httpx
import pytest
from playwright.sync_api import Page, expect
import reflex as rx
from reflex.testing import AppHarness
def StatelessApp():
"""A stateless app that renders a heading."""
import reflex as rx
def index():
return rx.heading("This is a stateless app")
app = rx.App()
app.add_page(index)
@pytest.fixture(scope="module")
def stateless_app(tmp_path_factory) -> Generator[AppHarness, None, None]:
"""Create a stateless app AppHarness.
Args:
tmp_path_factory: pytest fixture for creating temporary directories.
Yields:
AppHarness: A harness for testing the stateless app.
"""
with AppHarness.create(
root=tmp_path_factory.mktemp("stateless_app"),
app_source=StatelessApp, # type: ignore
) as harness:
yield harness
def test_statelessness(stateless_app: AppHarness, page: Page):
"""Test that the stateless app renders a heading but backend/_event is not mounted.
Args:
stateless_app: A harness for testing the stateless app.
page: A Playwright page.
"""
assert stateless_app.frontend_url is not None
assert stateless_app.backend is not None
assert stateless_app.backend.started
res = httpx.get(rx.config.get_config().api_url + "/_event")
assert res.status_code == 404
res2 = httpx.get(rx.config.get_config().api_url + "/ping")
assert res2.status_code == 200
page.goto(stateless_app.frontend_url)
expect(page.get_by_role("heading")).to_have_text("This is a stateless app")

View File

@ -3,10 +3,18 @@
from typing import Generator from typing import Generator
import pytest import pytest
from selenium.webdriver.common.by import By from playwright.sync_api import Page
from reflex.testing import AppHarness from reflex.testing import AppHarness
expected_col_headers = ["Name", "Age", "Location"]
expected_row_headers = ["John", "Jane", "Joe"]
expected_cells_data = [
["30", "New York"],
["31", "San Fransisco"],
["32", "Los Angeles"],
]
def Table(): def Table():
"""App using table component.""" """App using table component."""
@ -17,11 +25,6 @@ def Table():
@app.add_page @app.add_page
def index(): def index():
return rx.center( return rx.center(
rx.input(
id="token",
value=rx.State.router.session.client_token,
is_read_only=True,
),
rx.table.root( rx.table.root(
rx.table.header( rx.table.header(
rx.table.row( rx.table.row(
@ -53,7 +56,7 @@ def Table():
@pytest.fixture() @pytest.fixture()
def table(tmp_path_factory) -> Generator[AppHarness, None, None]: def table_app(tmp_path_factory) -> Generator[AppHarness, None, None]:
"""Start Table app at tmp_path via AppHarness. """Start Table app at tmp_path via AppHarness.
Args: Args:
@ -71,47 +74,27 @@ def table(tmp_path_factory) -> Generator[AppHarness, None, None]:
yield harness yield harness
@pytest.fixture def test_table(page: Page, table_app: AppHarness):
def driver(table: AppHarness):
"""GEt an instance of the browser open to the table app.
Args:
table: harness for Table app
Yields:
WebDriver instance.
"""
driver = table.frontend()
try:
token_input = driver.find_element(By.ID, "token")
assert token_input
# wait for the backend connection to send the token
token = table.poll_for_value(token_input)
assert token is not None
yield driver
finally:
driver.quit()
def test_table(driver, table: AppHarness):
"""Test that a table component is rendered properly. """Test that a table component is rendered properly.
Args: Args:
driver: Selenium WebDriver open to the app table_app: Harness for Table app
table: Harness for Table app page: Playwright page instance
""" """
assert table.app_instance is not None, "app is not running" assert table_app.frontend_url is not None, "frontend url is not available"
thead = driver.find_element(By.TAG_NAME, "thead") page.goto(table_app.frontend_url)
# poll till page is fully loaded. table = page.get_by_role("table")
table.poll_for_content(element=thead)
# check headers # Check column headers
assert thead.find_element(By.TAG_NAME, "tr").text == "Name Age Location" headers = table.get_by_role("columnheader").all_inner_texts()
# check first row value assert headers == expected_col_headers
assert (
driver.find_element(By.TAG_NAME, "tbody") # Check rows headers
.find_elements(By.TAG_NAME, "tr")[0] rows = table.get_by_role("rowheader").all_inner_texts()
.text assert rows == expected_row_headers
== "John 30 New York"
) # Check cells
rows = table.get_by_role("cell").all_inner_texts()
for i, expected_row in enumerate(expected_cells_data):
assert [rows[idx := i * 2], rows[idx + 1]] == expected_row

View File

@ -12,7 +12,7 @@ def test_websocket_target_url():
var_data = url._get_all_var_data() var_data = url._get_all_var_data()
assert var_data is not None assert var_data is not None
assert sorted(tuple((key for key, _ in var_data.imports))) == sorted( assert sorted(tuple((key for key, _ in var_data.imports))) == sorted(
("/utils/state", "/env.json") ("$/utils/state", "$/env.json")
) )
@ -22,10 +22,10 @@ def test_connection_banner():
assert sorted(tuple(_imports)) == sorted( assert sorted(tuple(_imports)) == sorted(
( (
"react", "react",
"/utils/context", "$/utils/context",
"/utils/state", "$/utils/state",
"@radix-ui/themes@^3.0.0", "@radix-ui/themes@^3.0.0",
"/env.json", "$/env.json",
) )
) )
@ -40,10 +40,10 @@ def test_connection_modal():
assert sorted(tuple(_imports)) == sorted( assert sorted(tuple(_imports)) == sorted(
( (
"react", "react",
"/utils/context", "$/utils/context",
"/utils/state", "$/utils/state",
"@radix-ui/themes@^3.0.0", "@radix-ui/themes@^3.0.0",
"/env.json", "$/env.json",
) )
) )

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

@ -40,8 +40,8 @@ def test_set_app_name(base_config_values):
("DB_URL", "postgresql://user:pass@localhost:5432/db"), ("DB_URL", "postgresql://user:pass@localhost:5432/db"),
("REDIS_URL", "redis://localhost:6379"), ("REDIS_URL", "redis://localhost:6379"),
("TIMEOUT", 600), ("TIMEOUT", 600),
("TELEMETRY_ENABLED", False), (environment.TELEMETRY_ENABLED.name, False),
("TELEMETRY_ENABLED", True), (environment.TELEMETRY_ENABLED.name, True),
], ],
) )
def test_update_from_env( def test_update_from_env(

View File

@ -106,6 +106,7 @@ class TestState(BaseState):
fig: Figure = Figure() fig: Figure = Figure()
dt: datetime.datetime = datetime.datetime.fromisoformat("1989-11-09T18:53:00+01:00") dt: datetime.datetime = datetime.datetime.fromisoformat("1989-11-09T18:53:00+01:00")
_backend: int = 0 _backend: int = 0
asynctest: int = 0
@ComputedVar @ComputedVar
def sum(self) -> float: def sum(self) -> float:
@ -129,6 +130,14 @@ class TestState(BaseState):
"""Do something.""" """Do something."""
pass pass
async def set_asynctest(self, value: int):
"""Set the asynctest value. Intentionally overwrite the default setter with an async one.
Args:
value: The new value.
"""
self.asynctest = value
class ChildState(TestState): class ChildState(TestState):
"""A child state fixture.""" """A child state fixture."""
@ -313,6 +322,7 @@ def test_class_vars(test_state):
"upper", "upper",
"fig", "fig",
"dt", "dt",
"asynctest",
} }
@ -733,6 +743,7 @@ def test_reset(test_state, child_state):
"mapping", "mapping",
"dt", "dt",
"_backend", "_backend",
"asynctest",
} }
# The dirty vars should be reset. # The dirty vars should be reset.
@ -3179,6 +3190,13 @@ async def test_setvar(mock_app: rx.App, token: str):
TestState.setvar(42, 42) TestState.setvar(42, 42)
@pytest.mark.asyncio
async def test_setvar_async_setter():
"""Test that overridden async setters raise Exception when used with setvar."""
with pytest.raises(NotImplementedError):
TestState.setvar("asynctest", 42)
@pytest.mark.skipif("REDIS_URL" not in os.environ, reason="Test requires redis") @pytest.mark.skipif("REDIS_URL" not in os.environ, reason="Test requires redis")
@pytest.mark.parametrize( @pytest.mark.parametrize(
"expiration_kwargs, expected_values", "expiration_kwargs, expected_values",
@ -3313,3 +3331,68 @@ def test_assignment_to_undeclared_vars():
state.handle_supported_regular_vars() state.handle_supported_regular_vars()
state.handle_non_var() state.handle_non_var()
@pytest.mark.asyncio
async def test_deserialize_gc_state_disk(token):
"""Test that a state can be deserialized from disk with a grandchild state.
Args:
token: A token.
"""
class Root(BaseState):
pass
class State(Root):
num: int = 42
class Child(State):
foo: str = "bar"
dsm = StateManagerDisk(state=Root)
async with dsm.modify_state(token) as root:
s = await root.get_state(State)
s.num += 1
c = await root.get_state(Child)
assert s._get_was_touched()
assert not c._get_was_touched()
dsm2 = StateManagerDisk(state=Root)
root = await dsm2.get_state(token)
s = await root.get_state(State)
assert s.num == 43
c = await root.get_state(Child)
assert c.foo == "bar"
class Obj(Base):
"""A object containing a callable for testing fallback pickle."""
_f: Callable
def test_fallback_pickle():
"""Test that state serialization will fall back to dill."""
class DillState(BaseState):
_o: Optional[Obj] = None
_f: Optional[Callable] = None
_g: Any = None
state = DillState(_reflex_internal_init=True) # type: ignore
state._o = Obj(_f=lambda: 42)
state._f = lambda: 420
pk = state._serialize()
unpickled_state = BaseState._deserialize(pk)
assert unpickled_state._f() == 420
assert unpickled_state._o._f() == 42
# Some object, like generator, are still unpicklable with dill.
state._g = (i for i in range(10))
pk = state._serialize()
assert len(pk) == 0
with pytest.raises(EOFError):
BaseState._deserialize(pk)

View File

@ -601,6 +601,7 @@ formatted_router = {
"sum": 3.14, "sum": 3.14,
"upper": "", "upper": "",
"router": formatted_router, "router": formatted_router,
"asynctest": 0,
}, },
ChildState.get_full_name(): { ChildState.get_full_name(): {
"count": 23, "count": 23,