merging
This commit is contained in:
commit
4bfad2c4fa
2
.github/workflows/integration_tests.yml
vendored
2
.github/workflows/integration_tests.yml
vendored
@ -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:
|
||||
|
@ -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]
|
||||
|
50
poetry.lock
generated
50
poetry.lock
generated
@ -1348,8 +1348,8 @@ files = [
|
||||
|
||||
[package.dependencies]
|
||||
numpy = [
|
||||
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
|
||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
|
||||
{version = ">=1.22.4", markers = "python_version < \"3.11\""},
|
||||
]
|
||||
python-dateutil = ">=2.8.2"
|
||||
@ -1667,8 +1667,8 @@ files = [
|
||||
annotated-types = ">=0.6.0"
|
||||
pydantic-core = "2.23.4"
|
||||
typing-extensions = [
|
||||
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
|
||||
{version = ">=4.12.2", markers = "python_version >= \"3.13\""},
|
||||
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@ -2164,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]
|
||||
@ -2287,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]]
|
||||
@ -3048,4 +3048,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "e03374b85bf10f0a7bb857969b2d6714f25affa63e14a48a88be9fa154b24326"
|
||||
content-hash = "547fdabf7a030c2a7c8d63eb5b2a3c5e821afa86390f08b895db038d30013904"
|
||||
|
@ -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 = [
|
||||
@ -69,7 +69,7 @@ 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"
|
||||
|
234
reflex/app.py
234
reflex/app.py
@ -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, should_skip_compile
|
||||
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, self.style, self.theme
|
||||
)
|
||||
|
||||
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.
|
||||
|
||||
@ -827,13 +833,33 @@ 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()
|
||||
|
||||
# Fix up the style.
|
||||
self.style = evaluate_style_namespaces(self.style)
|
||||
|
||||
# Add the app wrappers.
|
||||
app_wrappers: Dict[tuple[int, str], Component] = {
|
||||
# Default app wrap component renders {children}
|
||||
(0, "AppWrap"): AppWrap.create()
|
||||
}
|
||||
|
||||
if self.theme is not None:
|
||||
# If a theme component was provided, wrap the app with it
|
||||
app_wrappers[(20, "Theme")] = self.theme
|
||||
# Fix #2992 by removing the top-level appearance prop
|
||||
self.theme.appearance = None
|
||||
|
||||
for route in self.unevaluated_pages:
|
||||
self._compile_page(route)
|
||||
|
||||
# Add the optional endpoints (_upload)
|
||||
self._add_optional_endpoints()
|
||||
|
||||
@ -868,28 +894,15 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
||||
# Store the compile results.
|
||||
compile_results = []
|
||||
|
||||
# Add the app wrappers.
|
||||
app_wrappers: Dict[tuple[int, str], Component] = {
|
||||
# Default app wrap component renders {children}
|
||||
(0, "AppWrap"): AppWrap.create()
|
||||
}
|
||||
if self.theme is not None:
|
||||
# If a theme component was provided, wrap the app with it
|
||||
app_wrappers[(20, "Theme")] = self.theme
|
||||
|
||||
progress.advance(task)
|
||||
|
||||
# Fix up the style.
|
||||
self.style = evaluate_style_namespaces(self.style)
|
||||
|
||||
# Track imports and custom components found.
|
||||
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)
|
||||
|
||||
# This has to happen before compiling stateful components as that
|
||||
# prevents recursive functions from reaching all components.
|
||||
for component in self.pages.values():
|
||||
# Add component._get_all_imports() to all_imports.
|
||||
all_imports.update(component._get_all_imports())
|
||||
|
||||
@ -899,8 +912,6 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
||||
# Add the custom components from the page to the set.
|
||||
custom_components |= component._get_all_custom_components()
|
||||
|
||||
progress.advance(task)
|
||||
|
||||
# Perform auto-memoization of stateful components.
|
||||
(
|
||||
stateful_components_path,
|
||||
@ -927,31 +938,8 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
||||
)
|
||||
)
|
||||
|
||||
# Compile the contexts before fork.
|
||||
compile_results.append(
|
||||
compiler.compile_contexts(self.state, self.theme),
|
||||
)
|
||||
# Fix #2992 by removing the top-level appearance prop
|
||||
if self.theme is not None:
|
||||
self.theme.appearance = None
|
||||
|
||||
app_root = self._app_root(app_wrappers=app_wrappers)
|
||||
|
||||
progress.advance(task)
|
||||
|
||||
# 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 +957,31 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
||||
max_workers=environment.REFLEX_COMPILE_THREADS
|
||||
)
|
||||
|
||||
for route, component in zip(self.pages, page_components):
|
||||
ExecutorSafeFunctions.COMPONENTS[route] = component
|
||||
|
||||
ExecutorSafeFunctions.STATE = self.state
|
||||
|
||||
with executor:
|
||||
result_futures = []
|
||||
custom_components_future = None
|
||||
|
||||
def _mark_complete(_=None):
|
||||
progress.advance(task)
|
||||
|
||||
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.
|
||||
# Compile the pre-compiled pages.
|
||||
for route in self.pages:
|
||||
_submit_work(ExecutorSafeFunctions.compile_page, route)
|
||||
|
||||
# Compile the app wrapper.
|
||||
_submit_work(ExecutorSafeFunctions.compile_app)
|
||||
|
||||
# Compile the custom components.
|
||||
custom_components_future = executor.submit(
|
||||
ExecutorSafeFunctions.compile_custom_components,
|
||||
_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 +995,34 @@ 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.
|
||||
(
|
||||
*custom_components_result,
|
||||
custom_components_imports,
|
||||
) = custom_components_future.result()
|
||||
compile_results.append(custom_components_result)
|
||||
all_imports.update(custom_components_imports)
|
||||
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()
|
||||
|
||||
|
@ -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,
|
||||
@ -126,8 +127,8 @@ def _compile_contexts(state: Optional[Type[BaseState]], theme: Component | None)
|
||||
|
||||
|
||||
def _compile_page(
|
||||
component: Component,
|
||||
state: Type[BaseState],
|
||||
component: BaseComponent,
|
||||
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,
|
||||
@ -424,7 +425,7 @@ def compile_contexts(
|
||||
|
||||
|
||||
def compile_page(
|
||||
path: str, component: Component, state: Type[BaseState]
|
||||
path: str, component: BaseComponent, state: Type[BaseState] | None
|
||||
) -> tuple[str, str]:
|
||||
"""Compile a single page.
|
||||
|
||||
@ -534,6 +535,81 @@ def purge_web_pages_dir():
|
||||
utils.empty_dir(get_web_dir() / constants.Dirs.PAGES, keep_files=["_app.js"])
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from reflex.app import UnevaluatedPage
|
||||
|
||||
|
||||
def compile_unevaluated_page(
|
||||
route: str,
|
||||
page: UnevaluatedPage,
|
||||
state: Type[BaseState] | None = None,
|
||||
style: ComponentStyle | None = None,
|
||||
theme: Component | None = None,
|
||||
) -> Tuple[Component, bool]:
|
||||
"""Compiles an uncompiled page into a component and adds meta information.
|
||||
|
||||
Args:
|
||||
route: The route of the page.
|
||||
page: The uncompiled page object.
|
||||
state: The state of the app.
|
||||
style: The style of the page.
|
||||
theme: The theme of the page.
|
||||
|
||||
Returns:
|
||||
The compiled component and whether state should be enabled.
|
||||
"""
|
||||
# Generate the component if it is a callable.
|
||||
component = page.component
|
||||
component = component if isinstance(component, Component) else component()
|
||||
|
||||
# unpack components that return tuples in an rx.fragment.
|
||||
if isinstance(component, tuple):
|
||||
component = Fragment.create(*component)
|
||||
|
||||
component._add_style_recursive(style or {}, theme)
|
||||
|
||||
enable_state = False
|
||||
# Ensure state is enabled if this page uses state.
|
||||
if state is None:
|
||||
if page.on_load or component._has_stateful_event_triggers():
|
||||
enable_state = True
|
||||
else:
|
||||
for var in component._get_vars(include_children=True):
|
||||
var_data = var._get_all_var_data()
|
||||
if not var_data:
|
||||
continue
|
||||
if not var_data.state:
|
||||
continue
|
||||
enable_state = True
|
||||
break
|
||||
|
||||
from reflex.app import OverlayFragment
|
||||
from reflex.utils.format import make_default_page_title
|
||||
|
||||
component = OverlayFragment.create(component)
|
||||
|
||||
meta_args = {
|
||||
"title": (
|
||||
page.title
|
||||
if page.title is not None
|
||||
else make_default_page_title(get_config().app_name, route)
|
||||
),
|
||||
"image": page.image,
|
||||
"meta": page.meta,
|
||||
}
|
||||
|
||||
if page.description is not None:
|
||||
meta_args["description"] = page.description
|
||||
|
||||
# Add meta information to the component.
|
||||
utils.add_meta(
|
||||
component,
|
||||
**meta_args,
|
||||
)
|
||||
|
||||
return component, enable_state
|
||||
|
||||
|
||||
class ExecutorSafeFunctions:
|
||||
"""Helper class to allow parallelisation of parts of the compilation process.
|
||||
|
||||
@ -559,13 +635,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, BaseComponent] = {}
|
||||
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 +649,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)
|
||||
|
@ -3,7 +3,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
import importlib
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import urllib.parse
|
||||
@ -221,6 +223,28 @@ def interpret_path_env(value: str, field_name: str) -> Path:
|
||||
return path
|
||||
|
||||
|
||||
def interpret_enum_env(value: str, field_type: GenericType, field_name: str) -> Any:
|
||||
"""Interpret an enum environment variable value.
|
||||
|
||||
Args:
|
||||
value: The environment variable value.
|
||||
field_type: The field type.
|
||||
field_name: The field name.
|
||||
|
||||
Returns:
|
||||
The interpreted value.
|
||||
|
||||
Raises:
|
||||
EnvironmentVarValueError: If the value is invalid.
|
||||
"""
|
||||
try:
|
||||
return field_type(value)
|
||||
except ValueError as ve:
|
||||
raise EnvironmentVarValueError(
|
||||
f"Invalid enum value: {value} for {field_name}"
|
||||
) from ve
|
||||
|
||||
|
||||
def interpret_env_var_value(
|
||||
value: str, field_type: GenericType, field_name: str
|
||||
) -> Any:
|
||||
@ -252,6 +276,8 @@ def interpret_env_var_value(
|
||||
return interpret_int_env(value, field_name)
|
||||
elif field_type is Path:
|
||||
return interpret_path_env(value, field_name)
|
||||
elif inspect.isclass(field_type) and issubclass(field_type, enum.Enum):
|
||||
return interpret_enum_env(value, field_type, field_name)
|
||||
|
||||
else:
|
||||
raise ValueError(
|
||||
|
@ -35,7 +35,6 @@ ColorType = Literal[
|
||||
"amber",
|
||||
"gold",
|
||||
"bronze",
|
||||
"gray",
|
||||
"accent",
|
||||
"black",
|
||||
"white",
|
||||
|
@ -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 = {
|
||||
|
@ -182,7 +182,7 @@ class ClientStateVar(Var):
|
||||
if value is not NoValue:
|
||||
# This is a hack to make it work like an EventSpec taking an arg
|
||||
value_var = LiteralVar.create(value)
|
||||
_var_data = _var_data.merge(value_var._get_all_var_data())
|
||||
_var_data = VarData.merge(_var_data, value_var._get_all_var_data())
|
||||
value_str = str(value_var)
|
||||
|
||||
if value_str.startswith("_"):
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -7,8 +7,13 @@ import pytest
|
||||
|
||||
import reflex as rx
|
||||
import reflex.config
|
||||
from reflex.config import environment
|
||||
from reflex.constants import Endpoint
|
||||
from reflex.config import (
|
||||
environment,
|
||||
interpret_boolean_env,
|
||||
interpret_enum_env,
|
||||
interpret_int_env,
|
||||
)
|
||||
from reflex.constants import Endpoint, Env
|
||||
|
||||
|
||||
def test_requires_app_name():
|
||||
@ -208,11 +213,11 @@ def test_replace_defaults(
|
||||
assert getattr(c, key) == value
|
||||
|
||||
|
||||
def reflex_dir_constant():
|
||||
def reflex_dir_constant() -> Path:
|
||||
return environment.REFLEX_DIR
|
||||
|
||||
|
||||
def test_reflex_dir_env_var(monkeypatch, tmp_path):
|
||||
def test_reflex_dir_env_var(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
"""Test that the REFLEX_DIR environment variable is used to set the Reflex.DIR constant.
|
||||
|
||||
Args:
|
||||
@ -224,3 +229,16 @@ def test_reflex_dir_env_var(monkeypatch, tmp_path):
|
||||
mp_ctx = multiprocessing.get_context(method="spawn")
|
||||
with mp_ctx.Pool(processes=1) as pool:
|
||||
assert pool.apply(reflex_dir_constant) == tmp_path
|
||||
|
||||
|
||||
def test_interpret_enum_env() -> None:
|
||||
assert interpret_enum_env(Env.PROD.value, Env, "REFLEX_ENV") == Env.PROD
|
||||
|
||||
|
||||
def test_interpret_int_env() -> None:
|
||||
assert interpret_int_env("3001", "FRONTEND_PORT") == 3001
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value, expected", [("true", True), ("false", False)])
|
||||
def test_interpret_bool_env(value: str, expected: bool) -> None:
|
||||
assert interpret_boolean_env(value, "TELEMETRY_ENABLED") == expected
|
||||
|
Loading…
Reference in New Issue
Block a user