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
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# Show OS combos first in GUI
|
# Show OS combos first in GUI
|
||||||
os: [ubuntu-latest, windows-latest]
|
os: [ubuntu-latest]
|
||||||
python-version: ['3.10.11', '3.11.4']
|
python-version: ['3.10.11', '3.11.4']
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
@ -3,7 +3,7 @@ fail_fast: true
|
|||||||
repos:
|
repos:
|
||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
rev: v0.7.0
|
rev: v0.7.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
args: [reflex, tests]
|
args: [reflex, tests]
|
||||||
|
50
poetry.lock
generated
50
poetry.lock
generated
@ -1348,8 +1348,8 @@ files = [
|
|||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
numpy = [
|
numpy = [
|
||||||
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
|
|
||||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||||
|
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
|
||||||
{version = ">=1.22.4", markers = "python_version < \"3.11\""},
|
{version = ">=1.22.4", markers = "python_version < \"3.11\""},
|
||||||
]
|
]
|
||||||
python-dateutil = ">=2.8.2"
|
python-dateutil = ">=2.8.2"
|
||||||
@ -1667,8 +1667,8 @@ files = [
|
|||||||
annotated-types = ">=0.6.0"
|
annotated-types = ">=0.6.0"
|
||||||
pydantic-core = "2.23.4"
|
pydantic-core = "2.23.4"
|
||||||
typing-extensions = [
|
typing-extensions = [
|
||||||
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
|
|
||||||
{version = ">=4.12.2", markers = "python_version >= \"3.13\""},
|
{version = ">=4.12.2", markers = "python_version >= \"3.13\""},
|
||||||
|
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
@ -2164,13 +2164,13 @@ md = ["cmarkgfm (>=0.8.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redis"
|
name = "redis"
|
||||||
version = "5.1.1"
|
version = "5.2.0"
|
||||||
description = "Python client for Redis database and key-value store"
|
description = "Python client for Redis database and key-value store"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "redis-5.1.1-py3-none-any.whl", hash = "sha256:f8ea06b7482a668c6475ae202ed8d9bcaa409f6e87fb77ed1043d912afd62e24"},
|
{file = "redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897"},
|
||||||
{file = "redis-5.1.1.tar.gz", hash = "sha256:f6c997521fedbae53387307c5d0bf784d9acc28d9f1d058abeac566ec4dbed72"},
|
{file = "redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -2287,29 +2287,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.7.0-py3-none-linux_armv6l.whl", hash = "sha256:0cdf20c2b6ff98e37df47b2b0bd3a34aaa155f59a11182c1303cce79be715628"},
|
{file = "ruff-0.7.1-py3-none-linux_armv6l.whl", hash = "sha256:cb1bc5ed9403daa7da05475d615739cc0212e861b7306f314379d958592aaa89"},
|
||||||
{file = "ruff-0.7.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:496494d350c7fdeb36ca4ef1c9f21d80d182423718782222c29b3e72b3512737"},
|
{file = "ruff-0.7.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27c1c52a8d199a257ff1e5582d078eab7145129aa02721815ca8fa4f9612dc35"},
|
||||||
{file = "ruff-0.7.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:214b88498684e20b6b2b8852c01d50f0651f3cc6118dfa113b4def9f14faaf06"},
|
{file = "ruff-0.7.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:588a34e1ef2ea55b4ddfec26bbe76bc866e92523d8c6cdec5e8aceefeff02d99"},
|
||||||
{file = "ruff-0.7.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630fce3fefe9844e91ea5bbf7ceadab4f9981f42b704fae011bb8efcaf5d84be"},
|
{file = "ruff-0.7.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94fc32f9cdf72dc75c451e5f072758b118ab8100727168a3df58502b43a599ca"},
|
||||||
{file = "ruff-0.7.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:211d877674e9373d4bb0f1c80f97a0201c61bcd1e9d045b6e9726adc42c156aa"},
|
{file = "ruff-0.7.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:985818742b833bffa543a84d1cc11b5e6871de1b4e0ac3060a59a2bae3969250"},
|
||||||
{file = "ruff-0.7.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:194d6c46c98c73949a106425ed40a576f52291c12bc21399eb8f13a0f7073495"},
|
{file = "ruff-0.7.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32f1e8a192e261366c702c5fb2ece9f68d26625f198a25c408861c16dc2dea9c"},
|
||||||
{file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:82c2579b82b9973a110fab281860403b397c08c403de92de19568f32f7178598"},
|
{file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:699085bf05819588551b11751eff33e9ca58b1b86a6843e1b082a7de40da1565"},
|
||||||
{file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9af971fe85dcd5eaed8f585ddbc6bdbe8c217fb8fcf510ea6bca5bdfff56040e"},
|
{file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:344cc2b0814047dc8c3a8ff2cd1f3d808bb23c6658db830d25147339d9bf9ea7"},
|
||||||
{file = "ruff-0.7.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b641c7f16939b7d24b7bfc0be4102c56562a18281f84f635604e8a6989948914"},
|
{file = "ruff-0.7.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4316bbf69d5a859cc937890c7ac7a6551252b6a01b1d2c97e8fc96e45a7c8b4a"},
|
||||||
{file = "ruff-0.7.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d71672336e46b34e0c90a790afeac8a31954fd42872c1f6adaea1dff76fd44f9"},
|
{file = "ruff-0.7.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79d3af9dca4c56043e738a4d6dd1e9444b6d6c10598ac52d146e331eb155a8ad"},
|
||||||
{file = "ruff-0.7.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ab7d98c7eed355166f367597e513a6c82408df4181a937628dbec79abb2a1fe4"},
|
{file = "ruff-0.7.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5c121b46abde94a505175524e51891f829414e093cd8326d6e741ecfc0a9112"},
|
||||||
{file = "ruff-0.7.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1eb54986f770f49edb14f71d33312d79e00e629a57387382200b1ef12d6a4ef9"},
|
{file = "ruff-0.7.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8422104078324ea250886954e48f1373a8fe7de59283d747c3a7eca050b4e378"},
|
||||||
{file = "ruff-0.7.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dc452ba6f2bb9cf8726a84aa877061a2462afe9ae0ea1d411c53d226661c601d"},
|
{file = "ruff-0.7.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:56aad830af8a9db644e80098fe4984a948e2b6fc2e73891538f43bbe478461b8"},
|
||||||
{file = "ruff-0.7.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4b406c2dce5be9bad59f2de26139a86017a517e6bcd2688da515481c05a2cb11"},
|
{file = "ruff-0.7.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:658304f02f68d3a83c998ad8bf91f9b4f53e93e5412b8f2388359d55869727fd"},
|
||||||
{file = "ruff-0.7.0-py3-none-win32.whl", hash = "sha256:f6c968509f767776f524a8430426539587d5ec5c662f6addb6aa25bc2e8195ec"},
|
{file = "ruff-0.7.1-py3-none-win32.whl", hash = "sha256:b517a2011333eb7ce2d402652ecaa0ac1a30c114fbbd55c6b8ee466a7f600ee9"},
|
||||||
{file = "ruff-0.7.0-py3-none-win_amd64.whl", hash = "sha256:ff4aabfbaaba880e85d394603b9e75d32b0693152e16fa659a3064a85df7fce2"},
|
{file = "ruff-0.7.1-py3-none-win_amd64.whl", hash = "sha256:f38c41fcde1728736b4eb2b18850f6d1e3eedd9678c914dede554a70d5241307"},
|
||||||
{file = "ruff-0.7.0-py3-none-win_arm64.whl", hash = "sha256:10842f69c245e78d6adec7e1db0a7d9ddc2fff0621d730e61657b64fa36f207e"},
|
{file = "ruff-0.7.1-py3-none-win_arm64.whl", hash = "sha256:19aa200ec824c0f36d0c9114c8ec0087082021732979a359d6f3c390a6ff2a37"},
|
||||||
{file = "ruff-0.7.0.tar.gz", hash = "sha256:47a86360cf62d9cd53ebfb0b5eb0e882193fc191c6d717e8bef4462bc3b9ea2b"},
|
{file = "ruff-0.7.1.tar.gz", hash = "sha256:9d8a41d4aa2dad1575adb98a82870cf5db5f76b2938cf2206c22c940034a36f4"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3048,4 +3048,4 @@ type = ["pytest-mypy"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.9"
|
python-versions = "^3.9"
|
||||||
content-hash = "e03374b85bf10f0a7bb857969b2d6714f25affa63e14a48a88be9fa154b24326"
|
content-hash = "547fdabf7a030c2a7c8d63eb5b2a3c5e821afa86390f08b895db038d30013904"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "reflex"
|
name = "reflex"
|
||||||
version = "0.6.4dev1"
|
version = "0.6.5dev1"
|
||||||
description = "Web apps in pure Python."
|
description = "Web apps in pure Python."
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
authors = [
|
authors = [
|
||||||
@ -69,7 +69,7 @@ dill = ">=0.3.8"
|
|||||||
toml = ">=0.10.2,<1.0"
|
toml = ">=0.10.2,<1.0"
|
||||||
pytest-asyncio = ">=0.24.0"
|
pytest-asyncio = ">=0.24.0"
|
||||||
pytest-cov = ">=4.0.0,<6.0"
|
pytest-cov = ">=4.0.0,<6.0"
|
||||||
ruff = "^0.7.0"
|
ruff = "0.7.1"
|
||||||
pandas = ">=2.1.1,<3.0"
|
pandas = ">=2.1.1,<3.0"
|
||||||
pillow = ">=10.0.0,<12.0"
|
pillow = ">=10.0.0,<12.0"
|
||||||
plotly = ">=5.13.0,<6.0"
|
plotly = ">=5.13.0,<6.0"
|
||||||
@ -104,4 +104,4 @@ lint.pydocstyle.convention = "google"
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
asyncio_default_fixture_loop_scope = "function"
|
asyncio_default_fixture_loop_scope = "function"
|
||||||
asyncio_mode = "auto"
|
asyncio_mode = "auto"
|
||||||
|
234
reflex/app.py
234
reflex/app.py
@ -6,6 +6,7 @@ import asyncio
|
|||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import contextlib
|
import contextlib
|
||||||
import copy
|
import copy
|
||||||
|
import dataclasses
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import io
|
import io
|
||||||
@ -18,6 +19,7 @@ import traceback
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import (
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
AsyncIterator,
|
AsyncIterator,
|
||||||
Callable,
|
Callable,
|
||||||
@ -47,7 +49,10 @@ from reflex.app_mixins import AppMixin, LifespanMixin, MiddlewareMixin
|
|||||||
from reflex.base import Base
|
from reflex.base import Base
|
||||||
from reflex.compiler import compiler
|
from reflex.compiler import compiler
|
||||||
from reflex.compiler import utils as compiler_utils
|
from reflex.compiler import utils as compiler_utils
|
||||||
from reflex.compiler.compiler import ExecutorSafeFunctions
|
from reflex.compiler.compiler import (
|
||||||
|
ExecutorSafeFunctions,
|
||||||
|
compile_theme,
|
||||||
|
)
|
||||||
from reflex.components.base.app_wrap import AppWrap
|
from reflex.components.base.app_wrap import AppWrap
|
||||||
from reflex.components.base.error_boundary import ErrorBoundary
|
from reflex.components.base.error_boundary import ErrorBoundary
|
||||||
from reflex.components.base.fragment import Fragment
|
from reflex.components.base.fragment import Fragment
|
||||||
@ -88,6 +93,9 @@ from reflex.utils import codespaces, console, exceptions, format, prerequisites,
|
|||||||
from reflex.utils.exec import is_prod_mode, is_testing_env, should_skip_compile
|
from reflex.utils.exec import is_prod_mode, is_testing_env, should_skip_compile
|
||||||
from reflex.utils.imports import ImportVar
|
from reflex.utils.imports import ImportVar
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from reflex.vars import Var
|
||||||
|
|
||||||
# Define custom types.
|
# Define custom types.
|
||||||
ComponentCallable = Callable[[], Component]
|
ComponentCallable = Callable[[], Component]
|
||||||
Reducer = Callable[[Event], Coroutine[Any, Any, StateUpdate]]
|
Reducer = Callable[[Event], Coroutine[Any, Any, StateUpdate]]
|
||||||
@ -170,6 +178,21 @@ class OverlayFragment(Fragment):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(
|
||||||
|
frozen=True,
|
||||||
|
)
|
||||||
|
class UnevaluatedPage:
|
||||||
|
"""An uncompiled page."""
|
||||||
|
|
||||||
|
component: Union[Component, ComponentCallable]
|
||||||
|
route: str
|
||||||
|
title: Union[Var, str, None]
|
||||||
|
description: Union[Var, str, None]
|
||||||
|
image: str
|
||||||
|
on_load: Union[EventHandler, EventSpec, List[Union[EventHandler, EventSpec]], None]
|
||||||
|
meta: List[Dict[str, str]]
|
||||||
|
|
||||||
|
|
||||||
class App(MiddlewareMixin, LifespanMixin, Base):
|
class App(MiddlewareMixin, LifespanMixin, Base):
|
||||||
"""The main Reflex app that encapsulates the backend and frontend.
|
"""The main Reflex app that encapsulates the backend and frontend.
|
||||||
|
|
||||||
@ -220,6 +243,9 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
|||||||
# Attributes to add to the html root tag of every page.
|
# Attributes to add to the html root tag of every page.
|
||||||
html_custom_attrs: Optional[Dict[str, str]] = None
|
html_custom_attrs: Optional[Dict[str, str]] = None
|
||||||
|
|
||||||
|
# A map from a route to an unevaluated page. PRIVATE.
|
||||||
|
unevaluated_pages: Dict[str, UnevaluatedPage] = {}
|
||||||
|
|
||||||
# A map from a page route to the component to render. Users should use `add_page`. PRIVATE.
|
# A map from a page route to the component to render. Users should use `add_page`. PRIVATE.
|
||||||
pages: Dict[str, Component] = {}
|
pages: Dict[str, Component] = {}
|
||||||
|
|
||||||
@ -381,8 +407,8 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
|||||||
|
|
||||||
def _add_optional_endpoints(self):
|
def _add_optional_endpoints(self):
|
||||||
"""Add optional api endpoints (_upload)."""
|
"""Add optional api endpoints (_upload)."""
|
||||||
# To upload files.
|
|
||||||
if Upload.is_used:
|
if Upload.is_used:
|
||||||
|
# To upload files.
|
||||||
self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
|
self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
|
||||||
|
|
||||||
# To access uploaded files.
|
# To access uploaded files.
|
||||||
@ -442,8 +468,8 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
|||||||
self,
|
self,
|
||||||
component: Component | ComponentCallable,
|
component: Component | ComponentCallable,
|
||||||
route: str | None = None,
|
route: str | None = None,
|
||||||
title: str | None = None,
|
title: str | Var | None = None,
|
||||||
description: str | None = None,
|
description: str | Var | None = None,
|
||||||
image: str = constants.DefaultPage.IMAGE,
|
image: str = constants.DefaultPage.IMAGE,
|
||||||
on_load: (
|
on_load: (
|
||||||
EventHandler | EventSpec | list[EventHandler | EventSpec] | None
|
EventHandler | EventSpec | list[EventHandler | EventSpec] | None
|
||||||
@ -479,13 +505,13 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
|||||||
# Check if the route given is valid
|
# Check if the route given is valid
|
||||||
verify_route_validity(route)
|
verify_route_validity(route)
|
||||||
|
|
||||||
if route in self.pages and os.getenv(constants.RELOAD_CONFIG):
|
if route in self.unevaluated_pages and os.getenv(constants.RELOAD_CONFIG):
|
||||||
# when the app is reloaded(typically for app harness tests), we should maintain
|
# when the app is reloaded(typically for app harness tests), we should maintain
|
||||||
# the latest render function of a route.This applies typically to decorated pages
|
# the latest render function of a route.This applies typically to decorated pages
|
||||||
# since they are only added when app._compile is called.
|
# since they are only added when app._compile is called.
|
||||||
self.pages.pop(route)
|
self.unevaluated_pages.pop(route)
|
||||||
|
|
||||||
if route in self.pages:
|
if route in self.unevaluated_pages:
|
||||||
route_name = (
|
route_name = (
|
||||||
f"`{route}` or `/`"
|
f"`{route}` or `/`"
|
||||||
if route == constants.PageNames.INDEX_ROUTE
|
if route == constants.PageNames.INDEX_ROUTE
|
||||||
@ -501,58 +527,38 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
|||||||
state = self.state if self.state else State
|
state = self.state if self.state else State
|
||||||
state.setup_dynamic_args(get_route_args(route))
|
state.setup_dynamic_args(get_route_args(route))
|
||||||
|
|
||||||
# Generate the component if it is a callable.
|
if on_load:
|
||||||
component = self._generate_component(component)
|
self.load_events[route] = (
|
||||||
|
on_load if isinstance(on_load, list) else [on_load]
|
||||||
|
)
|
||||||
|
|
||||||
# unpack components that return tuples in an rx.fragment.
|
self.unevaluated_pages[route] = UnevaluatedPage(
|
||||||
if isinstance(component, tuple):
|
component=component,
|
||||||
component = Fragment.create(*component)
|
route=route,
|
||||||
|
title=title,
|
||||||
# Ensure state is enabled if this page uses state.
|
description=description,
|
||||||
if self.state is None:
|
image=image,
|
||||||
if on_load or component._has_stateful_event_triggers():
|
on_load=on_load,
|
||||||
self._enable_state()
|
meta=meta,
|
||||||
else:
|
|
||||||
for var in component._get_vars(include_children=True):
|
|
||||||
var_data = var._get_all_var_data()
|
|
||||||
if not var_data:
|
|
||||||
continue
|
|
||||||
if not var_data.state:
|
|
||||||
continue
|
|
||||||
self._enable_state()
|
|
||||||
break
|
|
||||||
|
|
||||||
component = OverlayFragment.create(component)
|
|
||||||
|
|
||||||
meta_args = {
|
|
||||||
"title": (
|
|
||||||
title
|
|
||||||
if title is not None
|
|
||||||
else format.make_default_page_title(get_config().app_name, route)
|
|
||||||
),
|
|
||||||
"image": image,
|
|
||||||
"meta": meta,
|
|
||||||
}
|
|
||||||
|
|
||||||
if description is not None:
|
|
||||||
meta_args["description"] = description
|
|
||||||
|
|
||||||
# Add meta information to the component.
|
|
||||||
compiler_utils.add_meta(
|
|
||||||
component,
|
|
||||||
**meta_args,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _compile_page(self, route: str):
|
||||||
|
"""Compile a page.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
route: The route of the page to compile.
|
||||||
|
"""
|
||||||
|
component, enable_state = compiler.compile_unevaluated_page(
|
||||||
|
route, self.unevaluated_pages[route], self.state, self.style, self.theme
|
||||||
|
)
|
||||||
|
|
||||||
|
if enable_state:
|
||||||
|
self._enable_state()
|
||||||
|
|
||||||
# Add the page.
|
# Add the page.
|
||||||
self._check_routes_conflict(route)
|
self._check_routes_conflict(route)
|
||||||
self.pages[route] = component
|
self.pages[route] = component
|
||||||
|
|
||||||
# Add the load events.
|
|
||||||
if on_load:
|
|
||||||
if not isinstance(on_load, list):
|
|
||||||
on_load = [on_load]
|
|
||||||
self.load_events[route] = on_load
|
|
||||||
|
|
||||||
def get_load_events(self, route: str) -> list[EventHandler | EventSpec]:
|
def get_load_events(self, route: str) -> list[EventHandler | EventSpec]:
|
||||||
"""Get the load events for a route.
|
"""Get the load events for a route.
|
||||||
|
|
||||||
@ -827,13 +833,33 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
|||||||
"""
|
"""
|
||||||
from reflex.utils.exceptions import ReflexRuntimeError
|
from reflex.utils.exceptions import ReflexRuntimeError
|
||||||
|
|
||||||
|
self.pages = {}
|
||||||
|
|
||||||
def get_compilation_time() -> str:
|
def get_compilation_time() -> str:
|
||||||
return str(datetime.now().time()).split(".")[0]
|
return str(datetime.now().time()).split(".")[0]
|
||||||
|
|
||||||
# Render a default 404 page if the user didn't supply one
|
# Render a default 404 page if the user didn't supply one
|
||||||
if constants.Page404.SLUG not in self.pages:
|
if constants.Page404.SLUG not in self.unevaluated_pages:
|
||||||
self.add_custom_404_page()
|
self.add_custom_404_page()
|
||||||
|
|
||||||
|
# Fix up the style.
|
||||||
|
self.style = evaluate_style_namespaces(self.style)
|
||||||
|
|
||||||
|
# Add the app wrappers.
|
||||||
|
app_wrappers: Dict[tuple[int, str], Component] = {
|
||||||
|
# Default app wrap component renders {children}
|
||||||
|
(0, "AppWrap"): AppWrap.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.theme is not None:
|
||||||
|
# If a theme component was provided, wrap the app with it
|
||||||
|
app_wrappers[(20, "Theme")] = self.theme
|
||||||
|
# Fix #2992 by removing the top-level appearance prop
|
||||||
|
self.theme.appearance = None
|
||||||
|
|
||||||
|
for route in self.unevaluated_pages:
|
||||||
|
self._compile_page(route)
|
||||||
|
|
||||||
# Add the optional endpoints (_upload)
|
# Add the optional endpoints (_upload)
|
||||||
self._add_optional_endpoints()
|
self._add_optional_endpoints()
|
||||||
|
|
||||||
@ -868,28 +894,15 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
|||||||
# Store the compile results.
|
# Store the compile results.
|
||||||
compile_results = []
|
compile_results = []
|
||||||
|
|
||||||
# Add the app wrappers.
|
|
||||||
app_wrappers: Dict[tuple[int, str], Component] = {
|
|
||||||
# Default app wrap component renders {children}
|
|
||||||
(0, "AppWrap"): AppWrap.create()
|
|
||||||
}
|
|
||||||
if self.theme is not None:
|
|
||||||
# If a theme component was provided, wrap the app with it
|
|
||||||
app_wrappers[(20, "Theme")] = self.theme
|
|
||||||
|
|
||||||
progress.advance(task)
|
progress.advance(task)
|
||||||
|
|
||||||
# Fix up the style.
|
|
||||||
self.style = evaluate_style_namespaces(self.style)
|
|
||||||
|
|
||||||
# Track imports and custom components found.
|
# Track imports and custom components found.
|
||||||
all_imports = {}
|
all_imports = {}
|
||||||
custom_components = set()
|
custom_components = set()
|
||||||
|
|
||||||
for _route, component in self.pages.items():
|
# This has to happen before compiling stateful components as that
|
||||||
# Merge the component style with the app style.
|
# prevents recursive functions from reaching all components.
|
||||||
component._add_style_recursive(self.style, self.theme)
|
for component in self.pages.values():
|
||||||
|
|
||||||
# Add component._get_all_imports() to all_imports.
|
# Add component._get_all_imports() to all_imports.
|
||||||
all_imports.update(component._get_all_imports())
|
all_imports.update(component._get_all_imports())
|
||||||
|
|
||||||
@ -899,8 +912,6 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
|||||||
# Add the custom components from the page to the set.
|
# Add the custom components from the page to the set.
|
||||||
custom_components |= component._get_all_custom_components()
|
custom_components |= component._get_all_custom_components()
|
||||||
|
|
||||||
progress.advance(task)
|
|
||||||
|
|
||||||
# Perform auto-memoization of stateful components.
|
# Perform auto-memoization of stateful components.
|
||||||
(
|
(
|
||||||
stateful_components_path,
|
stateful_components_path,
|
||||||
@ -927,31 +938,8 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Compile the contexts before fork.
|
|
||||||
compile_results.append(
|
|
||||||
compiler.compile_contexts(self.state, self.theme),
|
|
||||||
)
|
|
||||||
# Fix #2992 by removing the top-level appearance prop
|
|
||||||
if self.theme is not None:
|
|
||||||
self.theme.appearance = None
|
|
||||||
|
|
||||||
app_root = self._app_root(app_wrappers=app_wrappers)
|
|
||||||
|
|
||||||
progress.advance(task)
|
progress.advance(task)
|
||||||
|
|
||||||
# Prepopulate the global ExecutorSafeFunctions class with input data required by the compile functions.
|
|
||||||
# This is required for multiprocessing to work, in presence of non-picklable inputs.
|
|
||||||
for route, component in zip(self.pages, page_components):
|
|
||||||
ExecutorSafeFunctions.COMPILE_PAGE_ARGS_BY_ROUTE[route] = (
|
|
||||||
route,
|
|
||||||
component,
|
|
||||||
self.state,
|
|
||||||
)
|
|
||||||
|
|
||||||
ExecutorSafeFunctions.COMPILE_APP_APP_ROOT = app_root
|
|
||||||
ExecutorSafeFunctions.CUSTOM_COMPONENTS = custom_components
|
|
||||||
ExecutorSafeFunctions.STYLE = self.style
|
|
||||||
|
|
||||||
# Use a forking process pool, if possible. Much faster, especially for large sites.
|
# Use a forking process pool, if possible. Much faster, especially for large sites.
|
||||||
# Fallback to ThreadPoolExecutor as something that will always work.
|
# Fallback to ThreadPoolExecutor as something that will always work.
|
||||||
executor = None
|
executor = None
|
||||||
@ -969,36 +957,31 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
|||||||
max_workers=environment.REFLEX_COMPILE_THREADS
|
max_workers=environment.REFLEX_COMPILE_THREADS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for route, component in zip(self.pages, page_components):
|
||||||
|
ExecutorSafeFunctions.COMPONENTS[route] = component
|
||||||
|
|
||||||
|
ExecutorSafeFunctions.STATE = self.state
|
||||||
|
|
||||||
with executor:
|
with executor:
|
||||||
result_futures = []
|
result_futures = []
|
||||||
custom_components_future = None
|
|
||||||
|
|
||||||
def _mark_complete(_=None):
|
|
||||||
progress.advance(task)
|
|
||||||
|
|
||||||
def _submit_work(fn, *args, **kwargs):
|
def _submit_work(fn, *args, **kwargs):
|
||||||
f = executor.submit(fn, *args, **kwargs)
|
f = executor.submit(fn, *args, **kwargs)
|
||||||
f.add_done_callback(_mark_complete)
|
# f = executor.apipe(fn, *args, **kwargs)
|
||||||
result_futures.append(f)
|
result_futures.append(f)
|
||||||
|
|
||||||
# Compile all page components.
|
# Compile the pre-compiled pages.
|
||||||
for route in self.pages:
|
for route in self.pages:
|
||||||
_submit_work(ExecutorSafeFunctions.compile_page, route)
|
_submit_work(
|
||||||
|
ExecutorSafeFunctions.compile_page,
|
||||||
# Compile the app wrapper.
|
route,
|
||||||
_submit_work(ExecutorSafeFunctions.compile_app)
|
)
|
||||||
|
|
||||||
# Compile the custom components.
|
|
||||||
custom_components_future = executor.submit(
|
|
||||||
ExecutorSafeFunctions.compile_custom_components,
|
|
||||||
)
|
|
||||||
custom_components_future.add_done_callback(_mark_complete)
|
|
||||||
|
|
||||||
# Compile the root stylesheet with base styles.
|
# Compile the root stylesheet with base styles.
|
||||||
_submit_work(compiler.compile_root_stylesheet, self.stylesheets)
|
_submit_work(compiler.compile_root_stylesheet, self.stylesheets)
|
||||||
|
|
||||||
# Compile the theme.
|
# Compile the theme.
|
||||||
_submit_work(ExecutorSafeFunctions.compile_theme)
|
_submit_work(compile_theme, self.style)
|
||||||
|
|
||||||
# Compile the Tailwind config.
|
# Compile the Tailwind config.
|
||||||
if config.tailwind is not None:
|
if config.tailwind is not None:
|
||||||
@ -1012,21 +995,34 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
|||||||
# Wait for all compilation tasks to complete.
|
# Wait for all compilation tasks to complete.
|
||||||
for future in concurrent.futures.as_completed(result_futures):
|
for future in concurrent.futures.as_completed(result_futures):
|
||||||
compile_results.append(future.result())
|
compile_results.append(future.result())
|
||||||
|
progress.advance(task)
|
||||||
|
|
||||||
# Special case for custom_components, since we need the compiled imports
|
app_root = self._app_root(app_wrappers=app_wrappers)
|
||||||
# to install proper frontend packages.
|
|
||||||
(
|
|
||||||
*custom_components_result,
|
|
||||||
custom_components_imports,
|
|
||||||
) = custom_components_future.result()
|
|
||||||
compile_results.append(custom_components_result)
|
|
||||||
all_imports.update(custom_components_imports)
|
|
||||||
|
|
||||||
# Get imports from AppWrap components.
|
# Get imports from AppWrap components.
|
||||||
all_imports.update(app_root._get_all_imports())
|
all_imports.update(app_root._get_all_imports())
|
||||||
|
|
||||||
progress.advance(task)
|
progress.advance(task)
|
||||||
|
|
||||||
|
# Compile the contexts.
|
||||||
|
compile_results.append(
|
||||||
|
compiler.compile_contexts(self.state, self.theme),
|
||||||
|
)
|
||||||
|
progress.advance(task)
|
||||||
|
|
||||||
|
# Compile the app root.
|
||||||
|
compile_results.append(
|
||||||
|
compiler.compile_app(app_root),
|
||||||
|
)
|
||||||
|
progress.advance(task)
|
||||||
|
|
||||||
|
# Compile custom components.
|
||||||
|
*custom_components_result, custom_components_imports = (
|
||||||
|
compiler.compile_components(custom_components)
|
||||||
|
)
|
||||||
|
compile_results.append(custom_components_result)
|
||||||
|
all_imports.update(custom_components_imports)
|
||||||
|
|
||||||
progress.advance(task)
|
progress.advance(task)
|
||||||
progress.stop()
|
progress.stop()
|
||||||
|
|
||||||
|
@ -4,10 +4,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Iterable, Optional, Type, Union
|
from typing import TYPE_CHECKING, Dict, Iterable, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
from reflex import constants
|
from reflex import constants
|
||||||
from reflex.compiler import templates, utils
|
from reflex.compiler import templates, utils
|
||||||
|
from reflex.components.base.fragment import Fragment
|
||||||
from reflex.components.component import (
|
from reflex.components.component import (
|
||||||
BaseComponent,
|
BaseComponent,
|
||||||
Component,
|
Component,
|
||||||
@ -126,8 +127,8 @@ def _compile_contexts(state: Optional[Type[BaseState]], theme: Component | None)
|
|||||||
|
|
||||||
|
|
||||||
def _compile_page(
|
def _compile_page(
|
||||||
component: Component,
|
component: BaseComponent,
|
||||||
state: Type[BaseState],
|
state: Type[BaseState] | None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Compile the component given the app state.
|
"""Compile the component given the app state.
|
||||||
|
|
||||||
@ -142,7 +143,7 @@ def _compile_page(
|
|||||||
imports = utils.compile_imports(imports)
|
imports = utils.compile_imports(imports)
|
||||||
|
|
||||||
# Compile the code to render the component.
|
# Compile the code to render the component.
|
||||||
kwargs = {"state_name": state.get_name()} if state else {}
|
kwargs = {"state_name": state.get_name()} if state is not None else {}
|
||||||
|
|
||||||
return templates.PAGE.render(
|
return templates.PAGE.render(
|
||||||
imports=imports,
|
imports=imports,
|
||||||
@ -424,7 +425,7 @@ def compile_contexts(
|
|||||||
|
|
||||||
|
|
||||||
def compile_page(
|
def compile_page(
|
||||||
path: str, component: Component, state: Type[BaseState]
|
path: str, component: BaseComponent, state: Type[BaseState] | None
|
||||||
) -> tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
"""Compile a single page.
|
"""Compile a single page.
|
||||||
|
|
||||||
@ -534,6 +535,81 @@ def purge_web_pages_dir():
|
|||||||
utils.empty_dir(get_web_dir() / constants.Dirs.PAGES, keep_files=["_app.js"])
|
utils.empty_dir(get_web_dir() / constants.Dirs.PAGES, keep_files=["_app.js"])
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from reflex.app import UnevaluatedPage
|
||||||
|
|
||||||
|
|
||||||
|
def compile_unevaluated_page(
|
||||||
|
route: str,
|
||||||
|
page: UnevaluatedPage,
|
||||||
|
state: Type[BaseState] | None = None,
|
||||||
|
style: ComponentStyle | None = None,
|
||||||
|
theme: Component | None = None,
|
||||||
|
) -> Tuple[Component, bool]:
|
||||||
|
"""Compiles an uncompiled page into a component and adds meta information.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
route: The route of the page.
|
||||||
|
page: The uncompiled page object.
|
||||||
|
state: The state of the app.
|
||||||
|
style: The style of the page.
|
||||||
|
theme: The theme of the page.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The compiled component and whether state should be enabled.
|
||||||
|
"""
|
||||||
|
# Generate the component if it is a callable.
|
||||||
|
component = page.component
|
||||||
|
component = component if isinstance(component, Component) else component()
|
||||||
|
|
||||||
|
# unpack components that return tuples in an rx.fragment.
|
||||||
|
if isinstance(component, tuple):
|
||||||
|
component = Fragment.create(*component)
|
||||||
|
|
||||||
|
component._add_style_recursive(style or {}, theme)
|
||||||
|
|
||||||
|
enable_state = False
|
||||||
|
# Ensure state is enabled if this page uses state.
|
||||||
|
if state is None:
|
||||||
|
if page.on_load or component._has_stateful_event_triggers():
|
||||||
|
enable_state = True
|
||||||
|
else:
|
||||||
|
for var in component._get_vars(include_children=True):
|
||||||
|
var_data = var._get_all_var_data()
|
||||||
|
if not var_data:
|
||||||
|
continue
|
||||||
|
if not var_data.state:
|
||||||
|
continue
|
||||||
|
enable_state = True
|
||||||
|
break
|
||||||
|
|
||||||
|
from reflex.app import OverlayFragment
|
||||||
|
from reflex.utils.format import make_default_page_title
|
||||||
|
|
||||||
|
component = OverlayFragment.create(component)
|
||||||
|
|
||||||
|
meta_args = {
|
||||||
|
"title": (
|
||||||
|
page.title
|
||||||
|
if page.title is not None
|
||||||
|
else make_default_page_title(get_config().app_name, route)
|
||||||
|
),
|
||||||
|
"image": page.image,
|
||||||
|
"meta": page.meta,
|
||||||
|
}
|
||||||
|
|
||||||
|
if page.description is not None:
|
||||||
|
meta_args["description"] = page.description
|
||||||
|
|
||||||
|
# Add meta information to the component.
|
||||||
|
utils.add_meta(
|
||||||
|
component,
|
||||||
|
**meta_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
return component, enable_state
|
||||||
|
|
||||||
|
|
||||||
class ExecutorSafeFunctions:
|
class ExecutorSafeFunctions:
|
||||||
"""Helper class to allow parallelisation of parts of the compilation process.
|
"""Helper class to allow parallelisation of parts of the compilation process.
|
||||||
|
|
||||||
@ -559,13 +635,12 @@ class ExecutorSafeFunctions:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
COMPILE_PAGE_ARGS_BY_ROUTE = {}
|
COMPONENTS: Dict[str, BaseComponent] = {}
|
||||||
COMPILE_APP_APP_ROOT: Component | None = None
|
UNCOMPILED_PAGES: Dict[str, UnevaluatedPage] = {}
|
||||||
CUSTOM_COMPONENTS: set[CustomComponent] | None = None
|
STATE: Optional[Type[BaseState]] = None
|
||||||
STYLE: ComponentStyle | None = None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def compile_page(cls, route: str):
|
def compile_page(cls, route: str) -> tuple[str, str]:
|
||||||
"""Compile a page.
|
"""Compile a page.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -574,46 +649,45 @@ class ExecutorSafeFunctions:
|
|||||||
Returns:
|
Returns:
|
||||||
The path and code of the compiled page.
|
The path and code of the compiled page.
|
||||||
"""
|
"""
|
||||||
return compile_page(*cls.COMPILE_PAGE_ARGS_BY_ROUTE[route])
|
return compile_page(route, cls.COMPONENTS[route], cls.STATE)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def compile_app(cls):
|
def compile_unevaluated_page(
|
||||||
"""Compile the app.
|
cls,
|
||||||
|
route: str,
|
||||||
|
style: ComponentStyle,
|
||||||
|
theme: Component | None,
|
||||||
|
) -> tuple[str, Component, tuple[str, str]]:
|
||||||
|
"""Compile an unevaluated page.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
route: The route of the page to compile.
|
||||||
|
style: The style of the page.
|
||||||
|
theme: The theme of the page.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The path and code of the compiled app.
|
The route, compiled component, and compiled page.
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If the app root is not set.
|
|
||||||
"""
|
"""
|
||||||
if cls.COMPILE_APP_APP_ROOT is None:
|
component, enable_state = compile_unevaluated_page(
|
||||||
raise ValueError("COMPILE_APP_APP_ROOT should be set")
|
route, cls.UNCOMPILED_PAGES[route]
|
||||||
return compile_app(cls.COMPILE_APP_APP_ROOT)
|
)
|
||||||
|
component = component if isinstance(component, Component) else component()
|
||||||
|
component._add_style_recursive(style, theme)
|
||||||
|
return route, component, compile_page(route, component, cls.STATE)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def compile_custom_components(cls):
|
def compile_theme(cls, style: ComponentStyle | None) -> tuple[str, str]:
|
||||||
"""Compile the custom components.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The path and code of the compiled custom components.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If the custom components are not set.
|
|
||||||
"""
|
|
||||||
if cls.CUSTOM_COMPONENTS is None:
|
|
||||||
raise ValueError("CUSTOM_COMPONENTS should be set")
|
|
||||||
return compile_components(cls.CUSTOM_COMPONENTS)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def compile_theme(cls):
|
|
||||||
"""Compile the theme.
|
"""Compile the theme.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
style: The style to compile.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The path and code of the compiled theme.
|
The path and code of the compiled theme.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If the style is not set.
|
ValueError: If the style is not set.
|
||||||
"""
|
"""
|
||||||
if cls.STYLE is None:
|
if style is None:
|
||||||
raise ValueError("STYLE should be set")
|
raise ValueError("STYLE should be set")
|
||||||
return compile_theme(cls.STYLE)
|
return compile_theme(style)
|
||||||
|
@ -3,7 +3,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import enum
|
||||||
import importlib
|
import importlib
|
||||||
|
import inspect
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
@ -221,6 +223,28 @@ def interpret_path_env(value: str, field_name: str) -> Path:
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def interpret_enum_env(value: str, field_type: GenericType, field_name: str) -> Any:
|
||||||
|
"""Interpret an enum environment variable value.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The environment variable value.
|
||||||
|
field_type: The field type.
|
||||||
|
field_name: The field name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The interpreted value.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
EnvironmentVarValueError: If the value is invalid.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return field_type(value)
|
||||||
|
except ValueError as ve:
|
||||||
|
raise EnvironmentVarValueError(
|
||||||
|
f"Invalid enum value: {value} for {field_name}"
|
||||||
|
) from ve
|
||||||
|
|
||||||
|
|
||||||
def interpret_env_var_value(
|
def interpret_env_var_value(
|
||||||
value: str, field_type: GenericType, field_name: str
|
value: str, field_type: GenericType, field_name: str
|
||||||
) -> Any:
|
) -> Any:
|
||||||
@ -252,6 +276,8 @@ def interpret_env_var_value(
|
|||||||
return interpret_int_env(value, field_name)
|
return interpret_int_env(value, field_name)
|
||||||
elif field_type is Path:
|
elif field_type is Path:
|
||||||
return interpret_path_env(value, field_name)
|
return interpret_path_env(value, field_name)
|
||||||
|
elif inspect.isclass(field_type) and issubclass(field_type, enum.Enum):
|
||||||
|
return interpret_enum_env(value, field_type, field_name)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
@ -35,7 +35,6 @@ ColorType = Literal[
|
|||||||
"amber",
|
"amber",
|
||||||
"gold",
|
"gold",
|
||||||
"bronze",
|
"bronze",
|
||||||
"gray",
|
|
||||||
"accent",
|
"accent",
|
||||||
"black",
|
"black",
|
||||||
"white",
|
"white",
|
||||||
|
@ -120,7 +120,7 @@ class Node(SimpleNamespace):
|
|||||||
# The Node version.
|
# The Node version.
|
||||||
VERSION = "22.10.0"
|
VERSION = "22.10.0"
|
||||||
# The minimum required node version.
|
# The minimum required node version.
|
||||||
MIN_VERSION = "18.17.0"
|
MIN_VERSION = "18.18.0"
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -173,17 +173,17 @@ class PackageJson(SimpleNamespace):
|
|||||||
PATH = "package.json"
|
PATH = "package.json"
|
||||||
|
|
||||||
DEPENDENCIES = {
|
DEPENDENCIES = {
|
||||||
"@babel/standalone": "7.25.8",
|
"@babel/standalone": "7.26.0",
|
||||||
"@emotion/react": "11.13.3",
|
"@emotion/react": "11.13.3",
|
||||||
"axios": "1.7.7",
|
"axios": "1.7.7",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"next": "14.2.15",
|
"next": "15.0.1",
|
||||||
"next-sitemap": "4.2.3",
|
"next-sitemap": "4.2.3",
|
||||||
"next-themes": "0.3.0",
|
"next-themes": "0.3.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-focus-lock": "2.13.2",
|
"react-focus-lock": "2.13.2",
|
||||||
"socket.io-client": "4.8.0",
|
"socket.io-client": "4.8.1",
|
||||||
"universal-cookie": "7.2.1",
|
"universal-cookie": "7.2.1",
|
||||||
}
|
}
|
||||||
DEV_DEPENDENCIES = {
|
DEV_DEPENDENCIES = {
|
||||||
|
@ -182,7 +182,7 @@ class ClientStateVar(Var):
|
|||||||
if value is not NoValue:
|
if value is not NoValue:
|
||||||
# This is a hack to make it work like an EventSpec taking an arg
|
# This is a hack to make it work like an EventSpec taking an arg
|
||||||
value_var = LiteralVar.create(value)
|
value_var = LiteralVar.create(value)
|
||||||
_var_data = _var_data.merge(value_var._get_all_var_data())
|
_var_data = VarData.merge(_var_data, value_var._get_all_var_data())
|
||||||
value_str = str(value_var)
|
value_str = str(value_var)
|
||||||
|
|
||||||
if value_str.startswith("_"):
|
if value_str.startswith("_"):
|
||||||
|
@ -151,31 +151,41 @@ class VarData:
|
|||||||
"""
|
"""
|
||||||
return dict((k, list(v)) for k, v in self.imports)
|
return dict((k, list(v)) for k, v in self.imports)
|
||||||
|
|
||||||
@classmethod
|
def merge(*all: VarData | None) -> VarData | None:
|
||||||
def merge(cls, *others: VarData | None) -> VarData | None:
|
|
||||||
"""Merge multiple var data objects.
|
"""Merge multiple var data objects.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
*others: The var data objects to merge.
|
*all: The var data objects to merge.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The merged var data object.
|
The merged var data object.
|
||||||
|
|
||||||
|
# noqa: DAR102 *all
|
||||||
"""
|
"""
|
||||||
state = ""
|
all_var_datas = list(filter(None, all))
|
||||||
field_name = ""
|
|
||||||
_imports = {}
|
if not all_var_datas:
|
||||||
hooks = {}
|
return None
|
||||||
for var_data in others:
|
|
||||||
if var_data is None:
|
if len(all_var_datas) == 1:
|
||||||
continue
|
return all_var_datas[0]
|
||||||
state = state or var_data.state
|
|
||||||
field_name = field_name or var_data.field_name
|
# Get the first non-empty field name or default to empty string.
|
||||||
_imports = imports.merge_imports(_imports, var_data.imports)
|
field_name = next(
|
||||||
hooks.update(
|
(var_data.field_name for var_data in all_var_datas if var_data.field_name),
|
||||||
var_data.hooks
|
"",
|
||||||
if isinstance(var_data.hooks, dict)
|
)
|
||||||
else {k: None for k in var_data.hooks}
|
|
||||||
)
|
# Get the first non-empty state or default to empty string.
|
||||||
|
state = next(
|
||||||
|
(var_data.state for var_data in all_var_datas if var_data.state), ""
|
||||||
|
)
|
||||||
|
|
||||||
|
hooks = {hook: None for var_data in all_var_datas for hook in var_data.hooks}
|
||||||
|
|
||||||
|
_imports = imports.merge_imports(
|
||||||
|
*(var_data.imports for var_data in all_var_datas)
|
||||||
|
)
|
||||||
|
|
||||||
if state or _imports or hooks or field_name:
|
if state or _imports or hooks or field_name:
|
||||||
return VarData(
|
return VarData(
|
||||||
@ -184,6 +194,7 @@ class VarData:
|
|||||||
imports=_imports,
|
imports=_imports,
|
||||||
hooks=hooks,
|
hooks=hooks,
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __bool__(self) -> bool:
|
def __bool__(self) -> bool:
|
||||||
|
@ -237,9 +237,12 @@ def test_add_page_default_route(app: App, index_page, about_page):
|
|||||||
about_page: The about page.
|
about_page: The about page.
|
||||||
"""
|
"""
|
||||||
assert app.pages == {}
|
assert app.pages == {}
|
||||||
|
assert app.unevaluated_pages == {}
|
||||||
app.add_page(index_page)
|
app.add_page(index_page)
|
||||||
|
app._compile_page("index")
|
||||||
assert app.pages.keys() == {"index"}
|
assert app.pages.keys() == {"index"}
|
||||||
app.add_page(about_page)
|
app.add_page(about_page)
|
||||||
|
app._compile_page("about")
|
||||||
assert app.pages.keys() == {"index", "about"}
|
assert app.pages.keys() == {"index", "about"}
|
||||||
|
|
||||||
|
|
||||||
@ -252,8 +255,9 @@ def test_add_page_set_route(app: App, index_page, windows_platform: bool):
|
|||||||
windows_platform: Whether the system is windows.
|
windows_platform: Whether the system is windows.
|
||||||
"""
|
"""
|
||||||
route = "test" if windows_platform else "/test"
|
route = "test" if windows_platform else "/test"
|
||||||
assert app.pages == {}
|
assert app.unevaluated_pages == {}
|
||||||
app.add_page(index_page, route=route)
|
app.add_page(index_page, route=route)
|
||||||
|
app._compile_page("test")
|
||||||
assert app.pages.keys() == {"test"}
|
assert app.pages.keys() == {"test"}
|
||||||
|
|
||||||
|
|
||||||
@ -267,8 +271,9 @@ def test_add_page_set_route_dynamic(index_page, windows_platform: bool):
|
|||||||
app = App(state=EmptyState)
|
app = App(state=EmptyState)
|
||||||
assert app.state is not None
|
assert app.state is not None
|
||||||
route = "/test/[dynamic]"
|
route = "/test/[dynamic]"
|
||||||
assert app.pages == {}
|
assert app.unevaluated_pages == {}
|
||||||
app.add_page(index_page, route=route)
|
app.add_page(index_page, route=route)
|
||||||
|
app._compile_page("test/[dynamic]")
|
||||||
assert app.pages.keys() == {"test/[dynamic]"}
|
assert app.pages.keys() == {"test/[dynamic]"}
|
||||||
assert "dynamic" in app.state.computed_vars
|
assert "dynamic" in app.state.computed_vars
|
||||||
assert app.state.computed_vars["dynamic"]._deps(objclass=EmptyState) == {
|
assert app.state.computed_vars["dynamic"]._deps(objclass=EmptyState) == {
|
||||||
@ -286,9 +291,9 @@ def test_add_page_set_route_nested(app: App, index_page, windows_platform: bool)
|
|||||||
windows_platform: Whether the system is windows.
|
windows_platform: Whether the system is windows.
|
||||||
"""
|
"""
|
||||||
route = "test\\nested" if windows_platform else "/test/nested"
|
route = "test\\nested" if windows_platform else "/test/nested"
|
||||||
assert app.pages == {}
|
assert app.unevaluated_pages == {}
|
||||||
app.add_page(index_page, route=route)
|
app.add_page(index_page, route=route)
|
||||||
assert app.pages.keys() == {route.strip(os.path.sep)}
|
assert app.unevaluated_pages.keys() == {route.strip(os.path.sep)}
|
||||||
|
|
||||||
|
|
||||||
def test_add_page_invalid_api_route(app: App, index_page):
|
def test_add_page_invalid_api_route(app: App, index_page):
|
||||||
@ -1238,6 +1243,7 @@ def test_overlay_component(
|
|||||||
|
|
||||||
app.add_page(rx.box("Index"), route="/test")
|
app.add_page(rx.box("Index"), route="/test")
|
||||||
# overlay components are wrapped during compile only
|
# overlay components are wrapped during compile only
|
||||||
|
app._compile_page("test")
|
||||||
app._setup_overlay_component()
|
app._setup_overlay_component()
|
||||||
page = app.pages["test"]
|
page = app.pages["test"]
|
||||||
|
|
||||||
@ -1365,6 +1371,7 @@ def test_app_state_determination():
|
|||||||
|
|
||||||
# Add a page with `on_load` enables state.
|
# Add a page with `on_load` enables state.
|
||||||
a1.add_page(rx.box("About"), route="/about", on_load=rx.console_log(""))
|
a1.add_page(rx.box("About"), route="/about", on_load=rx.console_log(""))
|
||||||
|
a1._compile_page("about")
|
||||||
assert a1.state is not None
|
assert a1.state is not None
|
||||||
|
|
||||||
a2 = App()
|
a2 = App()
|
||||||
@ -1372,6 +1379,7 @@ def test_app_state_determination():
|
|||||||
|
|
||||||
# Referencing a state Var enables state.
|
# Referencing a state Var enables state.
|
||||||
a2.add_page(rx.box(rx.text(GenState.value)), route="/")
|
a2.add_page(rx.box(rx.text(GenState.value)), route="/")
|
||||||
|
a2._compile_page("index")
|
||||||
assert a2.state is not None
|
assert a2.state is not None
|
||||||
|
|
||||||
a3 = App()
|
a3 = App()
|
||||||
@ -1379,6 +1387,7 @@ def test_app_state_determination():
|
|||||||
|
|
||||||
# Referencing router enables state.
|
# Referencing router enables state.
|
||||||
a3.add_page(rx.box(rx.text(State.router.page.full_path)), route="/")
|
a3.add_page(rx.box(rx.text(State.router.page.full_path)), route="/")
|
||||||
|
a3._compile_page("index")
|
||||||
assert a3.state is not None
|
assert a3.state is not None
|
||||||
|
|
||||||
a4 = App()
|
a4 = App()
|
||||||
@ -1390,6 +1399,7 @@ def test_app_state_determination():
|
|||||||
a4.add_page(
|
a4.add_page(
|
||||||
rx.box(rx.button("Click", on_click=DynamicState.on_counter)), route="/page2"
|
rx.box(rx.button("Click", on_click=DynamicState.on_counter)), route="/page2"
|
||||||
)
|
)
|
||||||
|
a4._compile_page("page2")
|
||||||
assert a4.state is not None
|
assert a4.state is not None
|
||||||
|
|
||||||
|
|
||||||
@ -1469,6 +1479,9 @@ def test_add_page_component_returning_tuple():
|
|||||||
app.add_page(index) # type: ignore
|
app.add_page(index) # type: ignore
|
||||||
app.add_page(page2) # type: ignore
|
app.add_page(page2) # type: ignore
|
||||||
|
|
||||||
|
app._compile_page("index")
|
||||||
|
app._compile_page("page2")
|
||||||
|
|
||||||
assert isinstance((fragment_wrapper := app.pages["index"].children[0]), Fragment)
|
assert isinstance((fragment_wrapper := app.pages["index"].children[0]), Fragment)
|
||||||
assert isinstance((first_text := fragment_wrapper.children[0]), Text)
|
assert isinstance((first_text := fragment_wrapper.children[0]), Text)
|
||||||
assert str(first_text.children[0].contents) == '"first"' # type: ignore
|
assert str(first_text.children[0].contents) == '"first"' # type: ignore
|
||||||
|
@ -7,8 +7,13 @@ import pytest
|
|||||||
|
|
||||||
import reflex as rx
|
import reflex as rx
|
||||||
import reflex.config
|
import reflex.config
|
||||||
from reflex.config import environment
|
from reflex.config import (
|
||||||
from reflex.constants import Endpoint
|
environment,
|
||||||
|
interpret_boolean_env,
|
||||||
|
interpret_enum_env,
|
||||||
|
interpret_int_env,
|
||||||
|
)
|
||||||
|
from reflex.constants import Endpoint, Env
|
||||||
|
|
||||||
|
|
||||||
def test_requires_app_name():
|
def test_requires_app_name():
|
||||||
@ -208,11 +213,11 @@ def test_replace_defaults(
|
|||||||
assert getattr(c, key) == value
|
assert getattr(c, key) == value
|
||||||
|
|
||||||
|
|
||||||
def reflex_dir_constant():
|
def reflex_dir_constant() -> Path:
|
||||||
return environment.REFLEX_DIR
|
return environment.REFLEX_DIR
|
||||||
|
|
||||||
|
|
||||||
def test_reflex_dir_env_var(monkeypatch, tmp_path):
|
def test_reflex_dir_env_var(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||||
"""Test that the REFLEX_DIR environment variable is used to set the Reflex.DIR constant.
|
"""Test that the REFLEX_DIR environment variable is used to set the Reflex.DIR constant.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -224,3 +229,16 @@ def test_reflex_dir_env_var(monkeypatch, tmp_path):
|
|||||||
mp_ctx = multiprocessing.get_context(method="spawn")
|
mp_ctx = multiprocessing.get_context(method="spawn")
|
||||||
with mp_ctx.Pool(processes=1) as pool:
|
with mp_ctx.Pool(processes=1) as pool:
|
||||||
assert pool.apply(reflex_dir_constant) == tmp_path
|
assert pool.apply(reflex_dir_constant) == tmp_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_interpret_enum_env() -> None:
|
||||||
|
assert interpret_enum_env(Env.PROD.value, Env, "REFLEX_ENV") == Env.PROD
|
||||||
|
|
||||||
|
|
||||||
|
def test_interpret_int_env() -> None:
|
||||||
|
assert interpret_int_env("3001", "FRONTEND_PORT") == 3001
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value, expected", [("true", True), ("false", False)])
|
||||||
|
def test_interpret_bool_env(value: str, expected: bool) -> None:
|
||||||
|
assert interpret_boolean_env(value, "TELEMETRY_ENABLED") == expected
|
||||||
|
Loading…
Reference in New Issue
Block a user