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
REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }}
run: |
poetry run playwright install --with-deps
poetry run pytest tests/integration
- uses: actions/upload-artifact@v4
name: Upload failed test screenshots

View File

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

View File

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

75
poetry.lock generated
View File

@ -521,6 +521,21 @@ files = [
{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]]
name = "distlib"
version = "0.3.9"
@ -1977,20 +1992,6 @@ files = [
[package.dependencies]
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]]
name = "python-engineio"
version = "4.10.1"
@ -2163,13 +2164,13 @@ md = ["cmarkgfm (>=0.8.0)"]
[[package]]
name = "redis"
version = "5.1.1"
version = "5.2.0"
description = "Python client for Redis database and key-value store"
optional = false
python-versions = ">=3.8"
files = [
{file = "redis-5.1.1-py3-none-any.whl", hash = "sha256:f8ea06b7482a668c6475ae202ed8d9bcaa409f6e87fb77ed1043d912afd62e24"},
{file = "redis-5.1.1.tar.gz", hash = "sha256:f6c997521fedbae53387307c5d0bf784d9acc28d9f1d058abeac566ec4dbed72"},
{file = "redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897"},
{file = "redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0"},
]
[package.dependencies]
@ -2286,29 +2287,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.7.0"
version = "0.7.1"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.7.0-py3-none-linux_armv6l.whl", hash = "sha256:0cdf20c2b6ff98e37df47b2b0bd3a34aaa155f59a11182c1303cce79be715628"},
{file = "ruff-0.7.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:496494d350c7fdeb36ca4ef1c9f21d80d182423718782222c29b3e72b3512737"},
{file = "ruff-0.7.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:214b88498684e20b6b2b8852c01d50f0651f3cc6118dfa113b4def9f14faaf06"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630fce3fefe9844e91ea5bbf7ceadab4f9981f42b704fae011bb8efcaf5d84be"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:211d877674e9373d4bb0f1c80f97a0201c61bcd1e9d045b6e9726adc42c156aa"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:194d6c46c98c73949a106425ed40a576f52291c12bc21399eb8f13a0f7073495"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:82c2579b82b9973a110fab281860403b397c08c403de92de19568f32f7178598"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9af971fe85dcd5eaed8f585ddbc6bdbe8c217fb8fcf510ea6bca5bdfff56040e"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b641c7f16939b7d24b7bfc0be4102c56562a18281f84f635604e8a6989948914"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d71672336e46b34e0c90a790afeac8a31954fd42872c1f6adaea1dff76fd44f9"},
{file = "ruff-0.7.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ab7d98c7eed355166f367597e513a6c82408df4181a937628dbec79abb2a1fe4"},
{file = "ruff-0.7.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1eb54986f770f49edb14f71d33312d79e00e629a57387382200b1ef12d6a4ef9"},
{file = "ruff-0.7.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dc452ba6f2bb9cf8726a84aa877061a2462afe9ae0ea1d411c53d226661c601d"},
{file = "ruff-0.7.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4b406c2dce5be9bad59f2de26139a86017a517e6bcd2688da515481c05a2cb11"},
{file = "ruff-0.7.0-py3-none-win32.whl", hash = "sha256:f6c968509f767776f524a8430426539587d5ec5c662f6addb6aa25bc2e8195ec"},
{file = "ruff-0.7.0-py3-none-win_amd64.whl", hash = "sha256:ff4aabfbaaba880e85d394603b9e75d32b0693152e16fa659a3064a85df7fce2"},
{file = "ruff-0.7.0-py3-none-win_arm64.whl", hash = "sha256:10842f69c245e78d6adec7e1db0a7d9ddc2fff0621d730e61657b64fa36f207e"},
{file = "ruff-0.7.0.tar.gz", hash = "sha256:47a86360cf62d9cd53ebfb0b5eb0e882193fc191c6d717e8bef4462bc3b9ea2b"},
{file = "ruff-0.7.1-py3-none-linux_armv6l.whl", hash = "sha256:cb1bc5ed9403daa7da05475d615739cc0212e861b7306f314379d958592aaa89"},
{file = "ruff-0.7.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27c1c52a8d199a257ff1e5582d078eab7145129aa02721815ca8fa4f9612dc35"},
{file = "ruff-0.7.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:588a34e1ef2ea55b4ddfec26bbe76bc866e92523d8c6cdec5e8aceefeff02d99"},
{file = "ruff-0.7.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94fc32f9cdf72dc75c451e5f072758b118ab8100727168a3df58502b43a599ca"},
{file = "ruff-0.7.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:985818742b833bffa543a84d1cc11b5e6871de1b4e0ac3060a59a2bae3969250"},
{file = "ruff-0.7.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32f1e8a192e261366c702c5fb2ece9f68d26625f198a25c408861c16dc2dea9c"},
{file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:699085bf05819588551b11751eff33e9ca58b1b86a6843e1b082a7de40da1565"},
{file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:344cc2b0814047dc8c3a8ff2cd1f3d808bb23c6658db830d25147339d9bf9ea7"},
{file = "ruff-0.7.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4316bbf69d5a859cc937890c7ac7a6551252b6a01b1d2c97e8fc96e45a7c8b4a"},
{file = "ruff-0.7.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79d3af9dca4c56043e738a4d6dd1e9444b6d6c10598ac52d146e331eb155a8ad"},
{file = "ruff-0.7.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5c121b46abde94a505175524e51891f829414e093cd8326d6e741ecfc0a9112"},
{file = "ruff-0.7.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8422104078324ea250886954e48f1373a8fe7de59283d747c3a7eca050b4e378"},
{file = "ruff-0.7.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:56aad830af8a9db644e80098fe4984a948e2b6fc2e73891538f43bbe478461b8"},
{file = "ruff-0.7.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:658304f02f68d3a83c998ad8bf91f9b4f53e93e5412b8f2388359d55869727fd"},
{file = "ruff-0.7.1-py3-none-win32.whl", hash = "sha256:b517a2011333eb7ce2d402652ecaa0ac1a30c114fbbd55c6b8ee466a7f600ee9"},
{file = "ruff-0.7.1-py3-none-win_amd64.whl", hash = "sha256:f38c41fcde1728736b4eb2b18850f6d1e3eedd9678c914dede554a70d5241307"},
{file = "ruff-0.7.1-py3-none-win_arm64.whl", hash = "sha256:19aa200ec824c0f36d0c9114c8ec0087082021732979a359d6f3c390a6ff2a37"},
{file = "ruff-0.7.1.tar.gz", hash = "sha256:9d8a41d4aa2dad1575adb98a82870cf5db5f76b2938cf2206c22c940034a36f4"},
]
[[package]]
@ -3047,4 +3048,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "c5da15520cef58124f6699007c81158036840469d4f9972592d72bd456c45e7e"
content-hash = "547fdabf7a030c2a7c8d63eb5b2a3c5e821afa86390f08b895db038d30013904"

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "reflex"
version = "0.6.4dev1"
version = "0.6.5dev1"
description = "Web apps in pure Python."
license = "Apache-2.0"
authors = [
@ -33,7 +33,6 @@ jinja2 = ">=3.1.2,<4.0"
psutil = ">=5.9.4,<7.0"
pydantic = ">=1.10.2,<3.0"
python-multipart = ">=0.0.5,<0.1"
python-dotenv = ">=1.0.1"
python-socketio = ">=5.7.0,<6.0"
redis = ">=4.3.5,<6.0"
rich = ">=13.0.0,<14.0"
@ -66,10 +65,11 @@ pytest = ">=7.1.2,<9.0"
pytest-mock = ">=3.10.0,<4.0"
pyright = ">=1.1.229,<1.1.335"
darglint = ">=1.8.1,<2.0"
dill = ">=0.3.8"
toml = ">=0.10.2,<1.0"
pytest-asyncio = ">=0.24.0"
pytest-cov = ">=4.0.0,<6.0"
ruff = "^0.7.0"
ruff = "0.7.1"
pandas = ">=2.1.1,<3.0"
pillow = ">=10.0.0,<12.0"
plotly = ">=5.13.0,<6.0"

View File

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

View File

@ -1,5 +1,5 @@
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 %}
export const initialState = {{ initial_state|json_dumps }}
@ -59,6 +59,8 @@ export const initialEvents = () => [
{% else %}
export const state_name = undefined
export const exception_state_name = undefined
export const onLoadInternalEvent = () => []
export const initialEvents = () => []

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import asyncio
import concurrent.futures
import contextlib
import copy
import dataclasses
import functools
import inspect
import io
@ -18,6 +19,7 @@ import traceback
from datetime import datetime
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
AsyncIterator,
Callable,
@ -47,7 +49,10 @@ from reflex.app_mixins import AppMixin, LifespanMixin, MiddlewareMixin
from reflex.base import Base
from reflex.compiler import compiler
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.error_boundary import ErrorBoundary
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.imports import ImportVar
if TYPE_CHECKING:
from reflex.vars import Var
# Define custom types.
ComponentCallable = Callable[[], Component]
Reducer = Callable[[Event], Coroutine[Any, Any, StateUpdate]]
@ -170,6 +178,21 @@ class OverlayFragment(Fragment):
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):
"""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.
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.
pages: Dict[str, Component] = {}
@ -381,8 +407,8 @@ class App(MiddlewareMixin, LifespanMixin, Base):
def _add_optional_endpoints(self):
"""Add optional api endpoints (_upload)."""
# To upload files.
if Upload.is_used:
# To upload files.
self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
# To access uploaded files.
@ -442,8 +468,8 @@ class App(MiddlewareMixin, LifespanMixin, Base):
self,
component: Component | ComponentCallable,
route: str | None = None,
title: str | None = None,
description: str | None = None,
title: str | Var | None = None,
description: str | Var | None = None,
image: str = constants.DefaultPage.IMAGE,
on_load: (
EventHandler | EventSpec | list[EventHandler | EventSpec] | None
@ -479,13 +505,13 @@ class App(MiddlewareMixin, LifespanMixin, Base):
# Check if the route given is valid
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
# the latest render function of a route.This applies typically to decorated pages
# 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 = (
f"`{route}` or `/`"
if route == constants.PageNames.INDEX_ROUTE
@ -501,58 +527,38 @@ class App(MiddlewareMixin, LifespanMixin, Base):
state = self.state if self.state else State
state.setup_dynamic_args(get_route_args(route))
# Generate the component if it is a callable.
component = self._generate_component(component)
# 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,
if on_load:
self.load_events[route] = (
on_load if isinstance(on_load, list) else [on_load]
)
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.
self._check_routes_conflict(route)
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]:
"""Get the load events for a route.
@ -679,7 +685,7 @@ class App(MiddlewareMixin, LifespanMixin, Base):
for i, tags in imports.items()
if i not in constants.PackageJson.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 any(tag.install for tag in tags)
}
@ -827,13 +833,18 @@ class App(MiddlewareMixin, LifespanMixin, Base):
"""
from reflex.utils.exceptions import ReflexRuntimeError
self.pages = {}
def get_compilation_time() -> str:
return str(datetime.now().time()).split(".")[0]
# 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()
for route in self.unevaluated_pages:
self._compile_page(route)
# Add the optional endpoints (_upload)
self._add_optional_endpoints()
@ -857,7 +868,7 @@ class App(MiddlewareMixin, LifespanMixin, Base):
progress.start()
task = progress.add_task(
f"[{get_compilation_time()}] Compiling:",
total=len(self.pages)
total=len(self.unevaluated_pages)
+ fixed_pages_within_executor
+ adhoc_steps_without_executor,
)
@ -886,38 +897,8 @@ class App(MiddlewareMixin, LifespanMixin, Base):
all_imports = {}
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)
# 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_results.append(
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
if self.theme is not None:
self.theme.appearance = None
app_root = self._app_root(app_wrappers=app_wrappers)
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.
# Fallback to ThreadPoolExecutor as something that will always work.
executor = None
@ -969,36 +931,55 @@ class App(MiddlewareMixin, LifespanMixin, Base):
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:
result_futures = []
custom_components_future = None
def _mark_complete(_=None):
progress.advance(task)
pages_futures = []
def _submit_work(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)
# Compile all page components.
for route in self.pages:
_submit_work(ExecutorSafeFunctions.compile_page, route)
for route in self.unevaluated_pages:
if route in self.pages:
continue
# Compile the app wrapper.
_submit_work(ExecutorSafeFunctions.compile_app)
# Compile the custom components.
custom_components_future = executor.submit(
ExecutorSafeFunctions.compile_custom_components,
f = executor.submit(
ExecutorSafeFunctions.compile_unevaluated_page,
route,
self.style,
self.theme,
)
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.
_submit_work(compiler.compile_root_stylesheet, self.stylesheets)
# Compile the theme.
_submit_work(ExecutorSafeFunctions.compile_theme)
_submit_work(compile_theme, self.style)
# Compile the Tailwind config.
if config.tailwind is not None:
@ -1012,21 +993,70 @@ class App(MiddlewareMixin, LifespanMixin, Base):
# Wait for all compilation tasks to complete.
for future in concurrent.futures.as_completed(result_futures):
compile_results.append(future.result())
progress.advance(task)
# Special case for custom_components, since we need the compiled imports
# to install proper frontend packages.
for future in concurrent.futures.as_completed(pages_futures):
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,
custom_components_imports,
) = custom_components_future.result()
compile_results.append(custom_components_result)
all_imports.update(custom_components_imports)
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))
app_root = self._app_root(app_wrappers=app_wrappers)
# Get imports from AppWrap components.
all_imports.update(app_root._get_all_imports())
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.stop()

View File

@ -4,10 +4,11 @@ from __future__ import annotations
from datetime import datetime
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.compiler import templates, utils
from reflex.components.base.fragment import Fragment
from reflex.components.component import (
BaseComponent,
Component,
@ -67,8 +68,8 @@ def _compile_app(app_root: Component) -> str:
window_libraries = [
(_normalize_library_name(name), name) for name in bundled_libraries
] + [
("utils_context", f"/{constants.Dirs.UTILS}/context"),
("utils_state", f"/{constants.Dirs.UTILS}/state"),
("utils_context", f"$/{constants.Dirs.UTILS}/context"),
("utils_state", f"$/{constants.Dirs.UTILS}/state"),
]
return templates.APP_ROOT.render(
@ -127,7 +128,7 @@ def _compile_contexts(state: Optional[Type[BaseState]], theme: Component | None)
def _compile_page(
component: Component,
state: Type[BaseState],
state: Type[BaseState] | None,
) -> str:
"""Compile the component given the app state.
@ -142,7 +143,7 @@ def _compile_page(
imports = utils.compile_imports(imports)
# 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(
imports=imports,
@ -228,7 +229,7 @@ def _compile_components(
"""
imports = {
"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 = []
@ -315,7 +316,7 @@ def _compile_stateful_components(
# Don't import from the file that we're about to create.
all_imports = utils.merge_imports(*all_import_dicts)
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(
@ -424,7 +425,7 @@ def compile_contexts(
def compile_page(
path: str, component: Component, state: Type[BaseState]
path: str, component: Component, state: Type[BaseState] | None
) -> tuple[str, str]:
"""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"])
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:
"""Helper class to allow parallelisation of parts of the compilation process.
@ -559,13 +627,12 @@ class ExecutorSafeFunctions:
"""
COMPILE_PAGE_ARGS_BY_ROUTE = {}
COMPILE_APP_APP_ROOT: Component | None = None
CUSTOM_COMPONENTS: set[CustomComponent] | None = None
STYLE: ComponentStyle | None = None
COMPONENTS: Dict[str, Component] = {}
UNCOMPILED_PAGES: Dict[str, UnevaluatedPage] = {}
STATE: Optional[Type[BaseState]] = None
@classmethod
def compile_page(cls, route: str):
def compile_page(cls, route: str) -> tuple[str, str]:
"""Compile a page.
Args:
@ -574,46 +641,45 @@ class ExecutorSafeFunctions:
Returns:
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
def compile_app(cls):
"""Compile the app.
def compile_unevaluated_page(
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:
The path and code of the compiled app.
Raises:
ValueError: If the app root is not set.
The route, compiled component, and compiled page.
"""
if cls.COMPILE_APP_APP_ROOT is None:
raise ValueError("COMPILE_APP_APP_ROOT should be set")
return compile_app(cls.COMPILE_APP_APP_ROOT)
component, enable_state = compile_unevaluated_page(
route, cls.UNCOMPILED_PAGES[route]
)
component = component if isinstance(component, Component) else component()
component._add_style_recursive(style, theme)
return route, component, compile_page(route, component, cls.STATE)
@classmethod
def compile_custom_components(cls):
"""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):
def compile_theme(cls, style: ComponentStyle | None) -> tuple[str, str]:
"""Compile the theme.
Args:
style: The style to compile.
Returns:
The path and code of the compiled theme.
Raises:
ValueError: If the style is not set.
"""
if cls.STYLE is None:
if style is None:
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
)
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(
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.event import (
EventCallback,
EventChain,
EventChainVar,
EventHandler,
@ -1126,6 +1127,8 @@ class Component(BaseComponent, ABC):
for trigger in self.event_triggers.values():
if isinstance(trigger, EventChain):
for event in trigger.events:
if isinstance(event, EventCallback):
continue
if isinstance(event, EventSpec):
if event.handler.state_full_name:
return True
@ -1305,7 +1308,9 @@ class Component(BaseComponent, ABC):
if self._get_ref_hook():
# Handle hooks needed for attaching react refs to DOM nodes.
_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():
# Handle hooks for `on_mount` / `on_unmount`.
@ -1662,7 +1667,7 @@ class CustomComponent(Component):
"""A custom user-defined component."""
# Use the components library.
library = f"/{Dirs.COMPONENTS_PATH}"
library = f"$/{Dirs.COMPONENTS_PATH}"
# The function that creates the component.
component_fn: Callable[..., Component] = Component.create
@ -2230,7 +2235,7 @@ class StatefulComponent(BaseComponent):
"""
if self.rendered_as_shared:
return {
f"/{Dirs.UTILS}/{PageNames.STATEFUL_COMPONENTS}": [
f"$/{Dirs.UTILS}/{PageNames.STATEFUL_COMPONENTS}": [
ImportVar(tag=self.tag)
]
}

View File

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

View File

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

View File

@ -67,7 +67,7 @@ class Clipboard(Fragment):
The import dict for the component.
"""
return {
"/utils/helpers/paste.js": ImportVar(
"$/utils/helpers/paste.js": ImportVar(
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
_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_data=VarData(
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.children = child.children
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
def _render(self):

View File

@ -29,7 +29,7 @@ DEFAULT_UPLOAD_ID: str = "default"
upload_files_context_var_data: VarData = VarData(
imports={
"react": "useContext",
f"/{Dirs.CONTEXTS_PATH}": "UploadFilesContext",
f"$/{Dirs.CONTEXTS_PATH}": "UploadFilesContext",
},
hooks={
"const [filesById, setFilesById] = useContext(UploadFilesContext);": None,
@ -134,8 +134,8 @@ uploaded_files_url_prefix = Var(
_js_expr="getBackendURL(env.UPLOAD)",
_var_data=VarData(
imports={
f"/{Dirs.STATE_PATH}": "getBackendURL",
"/env.json": ImportVar(tag="env", is_default=True),
f"$/{Dirs.STATE_PATH}": "getBackendURL",
"$/env.json": ImportVar(tag="env", is_default=True),
}
),
).to(str)
@ -170,7 +170,7 @@ def _on_drop_spec(files: Var) -> Tuple[Var[Any]]:
class UploadFilesProvider(Component):
"""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"

View File

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

View File

@ -344,7 +344,7 @@ class DataEditor(NoSSRComponent):
return {
"": f"{format.format_library_name(self.library)}/dist/index.css",
self.library: "GridCellKind",
"/utils/helpers/dataeditor.js": ImportVar(
"$/utils/helpers/dataeditor.js": ImportVar(
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():
formatted_lib_name = format_library_name(lib)
if (
not lib.startswith((".", "/"))
not lib.startswith((".", "/", "$/"))
and not lib.startswith("http")
and formatted_lib_name not in libs_in_window
):
@ -106,7 +106,7 @@ def load_dynamic_serializer():
# Rewrite imports from `/` to destructure from window
for ix, line in enumerate(module_code_lines[:]):
if line.startswith("import "):
if 'from "/' in line:
if 'from "$/' in line or 'from "/' in line:
module_code_lines[ix] = (
line.replace("import ", "const ", 1).replace(
" from ", " = window['__reflex'][", 1
@ -157,7 +157,7 @@ def load_dynamic_serializer():
merge_var_data=VarData.merge(
VarData(
imports={
f"/{constants.Dirs.STATE_PATH}": [
f"$/{constants.Dirs.STATE_PATH}": [
imports.ImportVar(tag="evalReactComponent"),
],
"react": [

View File

@ -187,7 +187,7 @@ class Form(BaseHTML):
"""
return {
"react": "useCallback",
f"/{Dirs.STATE_PATH}": ["getRefValue", "getRefValues"],
f"$/{Dirs.STATE_PATH}": ["getRefValue", "getRefValues"],
}
def add_hooks(self) -> list[str]:
@ -615,6 +615,42 @@ class Textarea(BaseHTML):
# Fired when a key is released
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]:
return super()._exclude_props() + [
"auto_height",
@ -634,28 +670,6 @@ class Textarea(BaseHTML):
custom_code.add(ENTER_KEY_SUBMIT_JS)
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
fieldset = Fieldset.create

View File

@ -1376,10 +1376,10 @@ class Textarea(BaseHTML):
on_unmount: Optional[EventType[[]]] = None,
**props,
) -> "Textarea":
"""Create the component.
"""Create a textarea component.
Args:
*children: The children of the component.
*children: The children of the textarea.
auto_complete: Whether the form control should have autocomplete enabled
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)
@ -1419,10 +1419,13 @@ class Textarea(BaseHTML):
class_name: The class name for the component.
autofocus: Whether the component should take the focus once the page is loaded
custom_attrs: custom attribute
**props: The props of the component.
**props: The properties of the textarea.
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
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.utils.imports import ImportDict
from reflex.vars.base import Var
from reflex.vars.base import LiteralVar, Var
@dataclasses.dataclass(frozen=True)
@ -92,6 +92,9 @@ class Moment(NoSSRComponent):
# Display the date in the given timezone.
tz: Var[str]
# The locale to use when rendering.
locale: Var[str]
# Fires when the date changes.
on_change: EventHandler[identity_event(str)]
@ -101,22 +104,15 @@ class Moment(NoSSRComponent):
Returns:
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:
return {"moment-timezone": ""}
return {}
imports["moment-timezone"] = ""
@classmethod
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
return imports

View File

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

View File

@ -221,7 +221,7 @@ class Theme(RadixThemesComponent):
The import dict.
"""
_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:
# When tailwind is disabled, import the radix-ui styles directly because they will
@ -265,7 +265,7 @@ class ThemePanel(RadixThemesComponent):
class RadixThemesColorModeProvider(Component):
"""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"
is_default = True

View File

@ -251,7 +251,7 @@ class Toaster(Component):
_js_expr=f"{toast_ref} = toast",
_var_data=VarData(
imports={
"/utils/state": [ImportVar(tag="refs")],
"$/utils/state": [ImportVar(tag="refs")],
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.
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):
"""The config defines runtime settings for the app.
@ -602,10 +608,15 @@ class Config(Base):
The updated config values.
"""
if self.env_file:
from dotenv import load_dotenv
try:
from dotenv import load_dotenv # type: ignore
# load env file if exists
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 = {}
# Iterate over the fields.

View File

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

View File

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

View File

@ -16,6 +16,7 @@ from typing import (
Generic,
List,
Optional,
Sequence,
Tuple,
Type,
TypeVar,
@ -389,7 +390,9 @@ class CallableEventSpec(EventSpec):
class EventChain(EventActionsMixin):
"""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)
@ -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")
Q = ParamSpec("Q")
T = TypeVar("T")
V = TypeVar("V")
V2 = TypeVar("V2")
@ -1473,55 +1471,73 @@ if sys.version_info >= (3, 10):
"""
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
def __get__(
self: EventCallback[[V], T], instance: None, owner
) -> Callable[[Union[Var[V], V]], EventSpec]: ...
def __call__(
self: EventCallback[Concatenate[V, Q], T], value: V | Var[V]
) -> 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
def __get__(
self: EventCallback[[V, V2], T], instance: None, owner
) -> Callable[[Union[Var[V], V], Union[Var[V2], V2]], EventSpec]: ...
@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,
]: ...
self: EventCallback[P, T], instance: None, owner
) -> EventCallback[P, T]: ...
@overload
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.
Args:
@ -1548,6 +1564,9 @@ if sys.version_info >= (3, 10):
return func # type: ignore
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]:
"""Wrap a function to be used as an event.
@ -1560,6 +1579,17 @@ else:
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):
"""A namespace for event related classes."""

View File

@ -21,7 +21,7 @@ NoValue = object()
_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
else self._setter_name
)
_var_data = VarData(imports=_refs_import if self._global_ref else {})
if value is not NoValue:
# 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("_"):
# remove patterns of ["*"] from the value_str using regex
@ -190,7 +193,7 @@ class ClientStateVar(Var):
setter = f"(() => {setter}({value_str}))"
return Var(
_js_expr=setter,
_var_data=VarData(imports=_refs_import if self._global_ref else {}),
_var_data=_var_data,
).to(FunctionVar, EventChain)
@property

View File

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

View File

@ -220,6 +220,7 @@ class EventHandlerSetVar(EventHandler):
Raises:
AttributeError: If the given Var name does not exist on the state.
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
@ -228,11 +229,20 @@ class EventHandlerSetVar(EventHandler):
raise EventHandlerValueError(
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.
if getattr(self.state_cls, constants.SETTER_PREFIX + args[0], None) is None:
if handler is None:
raise AttributeError(
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)
@ -2053,11 +2063,23 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
"""
try:
return pickle.dumps((self._to_schema(), self))
except pickle.PicklingError:
console.warn(
except (pickle.PicklingError, AttributeError) as og_pickle_error:
error = (
f"Failed to serialize state {self.get_full_name()} due to unpicklable object. "
"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""
@classmethod
@ -2895,9 +2917,13 @@ class StateManagerDisk(StateManager):
for substate in state.get_substates():
substate_token = _substate_key(client_token, substate)
fresh_instance = await root_state.get_state(substate)
instance = await self.load_state(substate_token)
if instance is None:
instance = await root_state.get_state(substate)
if instance is not None:
# 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
instance.parent_state = state

View File

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

View File

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

View File

@ -23,6 +23,12 @@ def merge_imports(
for lib, fields in (
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)):
all_imports[lib].extend(
(

View File

@ -151,30 +151,40 @@ class VarData:
"""
return dict((k, list(v)) for k, v in self.imports)
@classmethod
def merge(cls, *others: VarData | None) -> VarData | None:
def merge(*all: VarData | None) -> VarData | None:
"""Merge multiple var data objects.
Args:
*others: The var data objects to merge.
*all: The var data objects to merge.
Returns:
The merged var data object.
# noqa: DAR102 *all
"""
state = ""
field_name = ""
_imports = {}
hooks = {}
for var_data in others:
if var_data is None:
continue
state = state or var_data.state
field_name = field_name or var_data.field_name
_imports = imports.merge_imports(_imports, var_data.imports)
hooks.update(
var_data.hooks
if isinstance(var_data.hooks, dict)
else {k: None for k in var_data.hooks}
all_var_datas = list(filter(None, all))
if not all_var_datas:
return None
if len(all_var_datas) == 1:
return all_var_datas[0]
# Get the first non-empty field name or default to empty string.
field_name = next(
(var_data.field_name for var_data in all_var_datas if var_data.field_name),
"",
)
# 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:
@ -184,6 +194,7 @@ class VarData:
imports=_imports,
hooks=hooks,
)
return None
def __bool__(self) -> bool:
@ -217,7 +228,7 @@ class VarData:
): None
},
imports={
f"/{constants.Dirs.CONTEXTS_PATH}": [ImportVar(tag="StateContexts")],
f"$/{constants.Dirs.CONTEXTS_PATH}": [ImportVar(tag="StateContexts")],
"react": [ImportVar(tag="useContext")],
},
)
@ -956,7 +967,7 @@ class Var(Generic[VAR_TYPE]):
_js_expr="refs",
_var_data=VarData(
imports={
f"/{constants.Dirs.STATE_PATH}": [imports.ImportVar(tag="refs")]
f"$/{constants.Dirs.STATE_PATH}": [imports.ImportVar(tag="refs")]
}
),
).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_data = VarData(
imports={
f"/{constants.Dirs.STATE_PATH}": {ImportVar(tag="generateUUID")}, # type: ignore
f"$/{constants.Dirs.STATE_PATH}": {ImportVar(tag="generateUUID")}, # type: ignore
"react": "useMemo",
},
hooks={f"const {unique_uuid_var} = useMemo(generateUUID, [])": None},

View File

@ -1090,7 +1090,7 @@ boolean_types = Union[BooleanVar, bool]
_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
import pytest
from selenium.webdriver.common.by import By
from playwright.sync_api import Page
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():
"""App using table component."""
@ -17,11 +25,6 @@ def Table():
@app.add_page
def index():
return rx.center(
rx.input(
id="token",
value=rx.State.router.session.client_token,
is_read_only=True,
),
rx.table.root(
rx.table.header(
rx.table.row(
@ -53,7 +56,7 @@ def Table():
@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.
Args:
@ -71,47 +74,27 @@ def table(tmp_path_factory) -> Generator[AppHarness, None, None]:
yield harness
@pytest.fixture
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):
def test_table(page: Page, table_app: AppHarness):
"""Test that a table component is rendered properly.
Args:
driver: Selenium WebDriver open to the app
table: Harness for Table app
table_app: 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")
# poll till page is fully loaded.
table.poll_for_content(element=thead)
# check headers
assert thead.find_element(By.TAG_NAME, "tr").text == "Name Age Location"
# check first row value
assert (
driver.find_element(By.TAG_NAME, "tbody")
.find_elements(By.TAG_NAME, "tr")[0]
.text
== "John 30 New York"
)
page.goto(table_app.frontend_url)
table = page.get_by_role("table")
# Check column headers
headers = table.get_by_role("columnheader").all_inner_texts()
assert headers == expected_col_headers
# Check rows headers
rows = table.get_by_role("rowheader").all_inner_texts()
assert rows == expected_row_headers
# 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()
assert var_data is not None
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(
(
"react",
"/utils/context",
"/utils/state",
"$/utils/context",
"$/utils/state",
"@radix-ui/themes@^3.0.0",
"/env.json",
"$/env.json",
)
)
@ -40,10 +40,10 @@ def test_connection_modal():
assert sorted(tuple(_imports)) == sorted(
(
"react",
"/utils/context",
"/utils/state",
"$/utils/context",
"$/utils/state",
"@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.
"""
assert app.pages == {}
assert app.unevaluated_pages == {}
app.add_page(index_page)
app._compile_page("index")
assert app.pages.keys() == {"index"}
app.add_page(about_page)
app._compile_page("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.
"""
route = "test" if windows_platform else "/test"
assert app.pages == {}
assert app.unevaluated_pages == {}
app.add_page(index_page, route=route)
app._compile_page("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)
assert app.state is not None
route = "/test/[dynamic]"
assert app.pages == {}
assert app.unevaluated_pages == {}
app.add_page(index_page, route=route)
app._compile_page("test/[dynamic]")
assert app.pages.keys() == {"test/[dynamic]"}
assert "dynamic" in app.state.computed_vars
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.
"""
route = "test\\nested" if windows_platform else "/test/nested"
assert app.pages == {}
assert app.unevaluated_pages == {}
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):
@ -1238,6 +1243,7 @@ def test_overlay_component(
app.add_page(rx.box("Index"), route="/test")
# overlay components are wrapped during compile only
app._compile_page("test")
app._setup_overlay_component()
page = app.pages["test"]
@ -1365,6 +1371,7 @@ def test_app_state_determination():
# Add a page with `on_load` enables state.
a1.add_page(rx.box("About"), route="/about", on_load=rx.console_log(""))
a1._compile_page("about")
assert a1.state is not None
a2 = App()
@ -1372,6 +1379,7 @@ def test_app_state_determination():
# Referencing a state Var enables state.
a2.add_page(rx.box(rx.text(GenState.value)), route="/")
a2._compile_page("index")
assert a2.state is not None
a3 = App()
@ -1379,6 +1387,7 @@ def test_app_state_determination():
# Referencing router enables state.
a3.add_page(rx.box(rx.text(State.router.page.full_path)), route="/")
a3._compile_page("index")
assert a3.state is not None
a4 = App()
@ -1390,6 +1399,7 @@ def test_app_state_determination():
a4.add_page(
rx.box(rx.button("Click", on_click=DynamicState.on_counter)), route="/page2"
)
a4._compile_page("page2")
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(page2) # type: ignore
app._compile_page("index")
app._compile_page("page2")
assert isinstance((fragment_wrapper := app.pages["index"].children[0]), Fragment)
assert isinstance((first_text := fragment_wrapper.children[0]), Text)
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"),
("REDIS_URL", "redis://localhost:6379"),
("TIMEOUT", 600),
("TELEMETRY_ENABLED", False),
("TELEMETRY_ENABLED", True),
(environment.TELEMETRY_ENABLED.name, False),
(environment.TELEMETRY_ENABLED.name, True),
],
)
def test_update_from_env(

View File

@ -106,6 +106,7 @@ class TestState(BaseState):
fig: Figure = Figure()
dt: datetime.datetime = datetime.datetime.fromisoformat("1989-11-09T18:53:00+01:00")
_backend: int = 0
asynctest: int = 0
@ComputedVar
def sum(self) -> float:
@ -129,6 +130,14 @@ class TestState(BaseState):
"""Do something."""
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):
"""A child state fixture."""
@ -313,6 +322,7 @@ def test_class_vars(test_state):
"upper",
"fig",
"dt",
"asynctest",
}
@ -733,6 +743,7 @@ def test_reset(test_state, child_state):
"mapping",
"dt",
"_backend",
"asynctest",
}
# The dirty vars should be reset.
@ -3179,6 +3190,13 @@ async def test_setvar(mock_app: rx.App, token: str):
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.parametrize(
"expiration_kwargs, expected_values",
@ -3313,3 +3331,68 @@ def test_assignment_to_undeclared_vars():
state.handle_supported_regular_vars()
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,
"upper": "",
"router": formatted_router,
"asynctest": 0,
},
ChildState.get_full_name(): {
"count": 23,