Merge remote-tracking branch 'origin/main' into lendemor/bump_ruff

This commit is contained in:
Masen Furer 2025-01-31 13:06:21 -08:00
commit 0cc005b0bb
No known key found for this signature in database
GPG Key ID: 2AE2BD5531FF94F4
36 changed files with 751 additions and 74 deletions

34
.github/workflows/performance.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: performance-tests
on:
push:
branches:
- "main" # or "master"
paths-ignore:
- "**/*.md"
pull_request:
workflow_dispatch:
env:
TELEMETRY_ENABLED: false
NODE_OPTIONS: "--max_old_space_size=8192"
PR_TITLE: ${{ github.event.pull_request.title }}
APP_HARNESS_HEADLESS: 1
PYTHONUNBUFFERED: 1
jobs:
benchmarks:
name: Run benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup_build_env
with:
python-version: 3.12.8
run-poetry-install: true
create-venv-at-path: .venv
- name: Run benchmarks
uses: CodSpeedHQ/action@v3
with:
token: ${{ secrets.CODSPEED_TOKEN }}
run: poetry run pytest benchmarks/test_evaluate.py --codspeed

1
.gitignore vendored
View File

@ -15,3 +15,4 @@ requirements.txt
.pyi_generator_last_run
.pyi_generator_diff
reflex.db
.codspeed

231
benchmarks/test_evaluate.py Normal file
View File

@ -0,0 +1,231 @@
from dataclasses import dataclass
from typing import cast
import pytest
import reflex as rx
class SideBarState(rx.State):
"""State for the side bar."""
current_page: rx.Field[str] = rx.field("/")
@dataclass(frozen=True)
class SideBarPage:
"""A page in the side bar."""
title: str
href: str
@dataclass(frozen=True)
class SideBarSection:
"""A section in the side bar."""
name: str
icon: str
pages: tuple[SideBarPage, ...]
@dataclass(frozen=True)
class Category:
"""A category in the side bar."""
name: str
href: str
sections: tuple[SideBarSection, ...]
SIDE_BAR = (
Category(
name="General",
href="/",
sections=(
SideBarSection(
name="Home",
icon="home",
pages=(
SideBarPage(title="Home", href="/"),
SideBarPage(title="Contact", href="/contact"),
),
),
SideBarSection(
name="About",
icon="info",
pages=(
SideBarPage(title="About", href="/about"),
SideBarPage(title="FAQ", href="/faq"),
),
),
),
),
Category(
name="Projects",
href="/projects",
sections=(
SideBarSection(
name="Python",
icon="worm",
pages=(
SideBarPage(title="Python", href="/projects/python"),
SideBarPage(title="Django", href="/projects/django"),
SideBarPage(title="Flask", href="/projects/flask"),
SideBarPage(title="FastAPI", href="/projects/fastapi"),
SideBarPage(title="Pyramid", href="/projects/pyramid"),
SideBarPage(title="Tornado", href="/projects/tornado"),
SideBarPage(title="TurboGears", href="/projects/turbogears"),
SideBarPage(title="Web2py", href="/projects/web2py"),
SideBarPage(title="Zope", href="/projects/zope"),
SideBarPage(title="Plone", href="/projects/plone"),
SideBarPage(title="Quixote", href="/projects/quixote"),
SideBarPage(title="Bottle", href="/projects/bottle"),
SideBarPage(title="CherryPy", href="/projects/cherrypy"),
SideBarPage(title="Falcon", href="/projects/falcon"),
SideBarPage(title="Sanic", href="/projects/sanic"),
SideBarPage(title="Starlette", href="/projects/starlette"),
),
),
SideBarSection(
name="JavaScript",
icon="banana",
pages=(
SideBarPage(title="JavaScript", href="/projects/javascript"),
SideBarPage(title="Angular", href="/projects/angular"),
SideBarPage(title="React", href="/projects/react"),
SideBarPage(title="Vue", href="/projects/vue"),
SideBarPage(title="Ember", href="/projects/ember"),
SideBarPage(title="Backbone", href="/projects/backbone"),
SideBarPage(title="Meteor", href="/projects/meteor"),
SideBarPage(title="Svelte", href="/projects/svelte"),
SideBarPage(title="Preact", href="/projects/preact"),
SideBarPage(title="Mithril", href="/projects/mithril"),
SideBarPage(title="Aurelia", href="/projects/aurelia"),
SideBarPage(title="Polymer", href="/projects/polymer"),
SideBarPage(title="Knockout", href="/projects/knockout"),
SideBarPage(title="Dojo", href="/projects/dojo"),
SideBarPage(title="Riot", href="/projects/riot"),
SideBarPage(title="Alpine", href="/projects/alpine"),
SideBarPage(title="Stimulus", href="/projects/stimulus"),
SideBarPage(title="Marko", href="/projects/marko"),
SideBarPage(title="Sapper", href="/projects/sapper"),
SideBarPage(title="Nuxt", href="/projects/nuxt"),
SideBarPage(title="Next", href="/projects/next"),
SideBarPage(title="Gatsby", href="/projects/gatsby"),
SideBarPage(title="Gridsome", href="/projects/gridsome"),
SideBarPage(title="Nest", href="/projects/nest"),
SideBarPage(title="Express", href="/projects/express"),
SideBarPage(title="Koa", href="/projects/koa"),
SideBarPage(title="Hapi", href="/projects/hapi"),
SideBarPage(title="LoopBack", href="/projects/loopback"),
SideBarPage(title="Feathers", href="/projects/feathers"),
SideBarPage(title="Sails", href="/projects/sails"),
SideBarPage(title="Adonis", href="/projects/adonis"),
SideBarPage(title="Meteor", href="/projects/meteor"),
SideBarPage(title="Derby", href="/projects/derby"),
SideBarPage(title="Socket.IO", href="/projects/socketio"),
),
),
),
),
)
def side_bar_page(page: SideBarPage):
return rx.box(
rx.link(
page.title,
href=page.href,
)
)
def side_bar_section(section: SideBarSection):
return rx.accordion.item(
rx.accordion.header(
rx.accordion.trigger(
rx.hstack(
rx.hstack(
rx.icon(section.icon),
section.name,
align="center",
),
rx.accordion.icon(),
width="100%",
justify="between",
)
)
),
rx.accordion.content(
rx.vstack(
*map(side_bar_page, section.pages),
),
border_inline_start="1px solid",
padding_inline_start="1em",
margin_inline_start="1.5em",
),
value=section.name,
width="100%",
variant="ghost",
)
def side_bar_category(category: Category):
selected_section = cast(
rx.Var,
rx.match(
SideBarState.current_page,
*[
(
section.name,
section.name,
)
for section in category.sections
],
None,
),
)
return rx.vstack(
rx.heading(
rx.link(
category.name,
href=category.href,
),
size="5",
),
rx.accordion.root(
*map(side_bar_section, category.sections),
default_value=selected_section.to(str),
variant="ghost",
width="100%",
collapsible=True,
type="multiple",
),
width="100%",
)
def side_bar():
return rx.vstack(
*map(side_bar_category, SIDE_BAR),
width="fit-content",
)
LOREM_IPSUM = "Lorem ipsum dolor sit amet, dolor ut dolore pariatur aliqua enim tempor sed. Labore excepteur sed exercitation. Ullamco aliquip lorem sunt enim in incididunt. Magna anim officia sint cillum labore. Ut eu non dolore minim nostrud magna eu, aute ex in incididunt irure eu. Fugiat et magna magna est excepteur eiusmod minim. Quis eiusmod et non pariatur dolor veniam incididunt, eiusmod irure enim sed dolor lorem pariatur do. Occaecat duis irure excepteur dolore. Proident ut laborum pariatur sit sit, nisi nostrud voluptate magna commodo laborum esse velit. Voluptate non minim deserunt adipiscing irure deserunt cupidatat. Laboris veniam commodo incididunt veniam lorem occaecat, fugiat ipsum dolor cupidatat. Ea officia sed eu excepteur culpa adipiscing, tempor consectetur ullamco eu. Anim ex proident nulla sunt culpa, voluptate veniam proident est adipiscing sint elit velit. Laboris adipiscing est culpa cillum magna. Sit veniam nulla nulla, aliqua eiusmod commodo lorem cupidatat commodo occaecat. Fugiat cillum dolor incididunt mollit eiusmod sint. Non lorem dolore labore excepteur minim laborum sed. Irure nisi do lorem nulla sunt commodo, deserunt quis mollit consectetur minim et esse est, proident nostrud officia enim sed reprehenderit. Magna cillum consequat aute reprehenderit duis sunt ullamco. Labore qui mollit voluptate. Duis dolor sint aute amet aliquip officia, est non mollit tempor enim quis fugiat, eu do culpa consectetur magna. Do ullamco aliqua voluptate culpa excepteur reprehenderit reprehenderit. Occaecat nulla sit est magna. Deserunt ea voluptate veniam cillum. Amet cupidatat duis est tempor fugiat ex eu, officia est sunt consectetur labore esse exercitation. Nisi cupidatat irure est nisi. Officia amet eu veniam reprehenderit. In amet incididunt tempor commodo ea labore. Mollit dolor aliquip excepteur, voluptate aute occaecat id officia proident. Ullamco est amet tempor. Proident aliquip proident mollit do aliquip ipsum, culpa quis aute id irure. Velit excepteur cillum cillum ut cupidatat. Occaecat qui elit esse nulla minim. Consequat velit id ad pariatur tempor. Eiusmod deserunt aliqua ex sed quis non. Dolor sint commodo ex in deserunt nostrud excepteur, pariatur ex aliqua anim adipiscing amet proident. Laboris eu laborum magna lorem ipsum fugiat velit."
def complicated_page():
return rx.hstack(
side_bar(),
rx.box(
rx.heading("Complicated Page", size="1"),
rx.text(LOREM_IPSUM),
),
)
@pytest.mark.benchmark
def test_component_init():
complicated_page()

85
poetry.lock generated
View File

@ -164,15 +164,15 @@ virtualenv = ["virtualenv (>=20.0.35)"]
[[package]]
name = "certifi"
version = "2024.12.14"
version = "2025.1.31"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"},
{file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"},
{file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
]
[[package]]
@ -251,7 +251,7 @@ files = [
{file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
]
markers = {main = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")", dev = "os_name == \"nt\" and implementation_name != \"pypy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")"}
markers = {main = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")", dev = "python_version <= \"3.11\" or python_version >= \"3.12\""}
[package.dependencies]
pycparser = "*"
@ -495,7 +495,6 @@ files = [
{file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"},
{file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"},
{file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"},
{file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"},
{file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"},
{file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"},
{file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"},
@ -506,7 +505,6 @@ files = [
{file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"},
{file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"},
{file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"},
{file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"},
{file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"},
{file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"},
{file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"},
@ -620,15 +618,15 @@ test = ["pytest (>=6)"]
[[package]]
name = "fastapi"
version = "0.115.7"
version = "0.115.8"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "fastapi-0.115.7-py3-none-any.whl", hash = "sha256:eb6a8c8bf7f26009e8147111ff15b5177a0e19bb4a45bc3486ab14804539d21e"},
{file = "fastapi-0.115.7.tar.gz", hash = "sha256:0f106da6c01d88a6786b3248fb4d7a940d071f6f488488898ad5d354b25ed015"},
{file = "fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"},
{file = "fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9"},
]
[package.dependencies]
@ -1101,7 +1099,7 @@ version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.8"
groups = ["main"]
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
@ -1199,7 +1197,7 @@ version = "0.1.2"
description = "Markdown URL utilities"
optional = false
python-versions = ">=3.7"
groups = ["main"]
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
@ -1690,7 +1688,7 @@ files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
]
markers = {main = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")", dev = "os_name == \"nt\" and implementation_name != \"pypy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")"}
markers = {main = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")", dev = "python_version <= \"3.11\" or python_version >= \"3.12\""}
[[package]]
name = "pydantic"
@ -1853,7 +1851,7 @@ version = "2.19.1"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
groups = ["main"]
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
@ -1878,15 +1876,15 @@ files = [
[[package]]
name = "pyright"
version = "1.1.392.post0"
version = "1.1.393"
description = "Command line wrapper for pyright"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pyright-1.1.392.post0-py3-none-any.whl", hash = "sha256:252f84458a46fa2f0fd4e2f91fc74f50b9ca52c757062e93f6c250c0d8329eb2"},
{file = "pyright-1.1.392.post0.tar.gz", hash = "sha256:3b7f88de74a28dcfa90c7d90c782b6569a48c2be5f9d4add38472bdaac247ebd"},
{file = "pyright-1.1.393-py3-none-any.whl", hash = "sha256:8320629bb7a44ca90944ba599390162bf59307f3d9fb6e27da3b7011b8c17ae5"},
{file = "pyright-1.1.393.tar.gz", hash = "sha256:aeeb7ff4e0364775ef416a80111613f91a05c8e01e58ecfefc370ca0db7aed9c"},
]
[package.dependencies]
@ -1998,6 +1996,39 @@ aspect = ["aspectlib"]
elasticsearch = ["elasticsearch"]
histogram = ["pygal", "pygaljs", "setuptools"]
[[package]]
name = "pytest-codspeed"
version = "3.2.0"
description = "Pytest plugin to create CodSpeed benchmarks"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5165774424c7ab8db7e7acdb539763a0e5657996effefdf0664d7fd95158d34"},
{file = "pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bd55f92d772592c04a55209950c50880413ae46876e66bd349ef157075ca26c"},
{file = "pytest_codspeed-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf6f56067538f4892baa8d7ab5ef4e45bb59033be1ef18759a2c7fc55b32035"},
{file = "pytest_codspeed-3.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39a687b05c3d145642061b45ea78e47e12f13ce510104d1a2cda00eee0e36f58"},
{file = "pytest_codspeed-3.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46a1afaaa1ac4c2ca5b0700d31ac46d80a27612961d031067d73c6ccbd8d3c2b"},
{file = "pytest_codspeed-3.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c48ce3af3dfa78413ed3d69d1924043aa1519048dbff46edccf8f35a25dab3c2"},
{file = "pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66692506d33453df48b36a84703448cb8b22953eea51f03fbb2eb758dc2bdc4f"},
{file = "pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:479774f80d0bdfafa16112700df4dbd31bf2a6757fac74795fd79c0a7b3c389b"},
{file = "pytest_codspeed-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:109f9f4dd1088019c3b3f887d003b7d65f98a7736ca1d457884f5aa293e8e81c"},
{file = "pytest_codspeed-3.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2f69a03b52c9bb041aec1b8ee54b7b6c37a6d0a948786effa4c71157765b6da"},
{file = "pytest_codspeed-3.2.0-py3-none-any.whl", hash = "sha256:54b5c2e986d6a28e7b0af11d610ea57bd5531cec8326abe486f1b55b09d91c39"},
{file = "pytest_codspeed-3.2.0.tar.gz", hash = "sha256:f9d1b1a3b2c69cdc0490a1e8b1ced44bffbd0e8e21d81a7160cfdd923f6e8155"},
]
[package.dependencies]
cffi = ">=1.17.1"
pytest = ">=3.8"
rich = ">=13.8.1"
[package.extras]
compat = ["pytest-benchmark (>=5.0.0,<5.1.0)", "pytest-xdist (>=3.6.1,<3.7.0)"]
lint = ["mypy (>=1.11.2,<1.12.0)", "ruff (>=0.6.5,<0.7.0)"]
test = ["pytest (>=7.0,<8.0)", "pytest-cov (>=4.0.0,<4.1.0)"]
[[package]]
name = "pytest-cov"
version = "6.0.0"
@ -2039,15 +2070,15 @@ dev = ["pre-commit", "pytest-asyncio", "tox"]
[[package]]
name = "pytest-playwright"
version = "0.6.2"
version = "0.7.0"
description = "A pytest wrapper with fixtures for Playwright to automate web browsers"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pytest_playwright-0.6.2-py3-none-any.whl", hash = "sha256:0eff73bebe497b0158befed91e2f5fe94cfa17181f8b3acf575beed84e7e9043"},
{file = "pytest_playwright-0.6.2.tar.gz", hash = "sha256:ff4054b19aa05df096ac6f74f0572591566aaf0f6d97f6cb9674db8a4d4ed06c"},
{file = "pytest_playwright-0.7.0-py3-none-any.whl", hash = "sha256:2516d0871fa606634bfe32afbcc0342d68da2dbff97fe3459849e9c428486da2"},
{file = "pytest_playwright-0.7.0.tar.gz", hash = "sha256:b3f2ea514bbead96d26376fac182f68dcd6571e7cb41680a89ff1673c05d60b6"},
]
[package.dependencies]
@ -2149,15 +2180,15 @@ docs = ["sphinx"]
[[package]]
name = "pytz"
version = "2024.2"
version = "2025.1"
description = "World timezone definitions, modern and historical"
optional = false
python-versions = "*"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"},
{file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"},
{file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"},
{file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"},
]
[[package]]
@ -2280,15 +2311,15 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"
[[package]]
name = "reflex-hosting-cli"
version = "0.1.33"
version = "0.1.34"
description = "Reflex Hosting CLI"
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "reflex_hosting_cli-0.1.33-py3-none-any.whl", hash = "sha256:3fe72fc448a231c61de4ac646f42c936c70e91330f616a23aec658f905d53bc4"},
{file = "reflex_hosting_cli-0.1.33.tar.gz", hash = "sha256:81c4a896b106eea99f1cab53ea23a6e19802592ce0468cc38d93d440bc95263a"},
{file = "reflex_hosting_cli-0.1.34-py3-none-any.whl", hash = "sha256:eabc4dc7bf68e022a9388614c1a35b5ab36b01021df063d0c3356eda0e245264"},
{file = "reflex_hosting_cli-0.1.34.tar.gz", hash = "sha256:07be37fda6dcede0a5d4bc1fd1786d9a3df5ad4e49dc1b6ba335418563cfecec"},
]
[package.dependencies]
@ -2362,7 +2393,7 @@ version = "13.9.4"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.8.0"
groups = ["main"]
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"},
@ -3152,4 +3183,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10, <4.0"
content-hash = "7ff4505a4a1c1a36ce5f4f53869d7cc42be845261a3bd505226241982f06d2e7"
content-hash = "25e6ea21f5acb616cbec4a7967bd6de619b684e6828f3d04381352353793e56b"

View File

@ -71,6 +71,7 @@ selenium = ">=4.11.0,<5.0"
pytest-benchmark = ">=4.0.0,<6.0"
playwright = ">=1.46.0"
pytest-playwright = ">=0.5.1"
pytest-codspeed = "^3.1.2"
[tool.poetry.scripts]
reflex = "reflex.reflex:cli"

View File

@ -86,11 +86,11 @@
{% for condition in case[:-1] %}
case JSON.stringify({{ condition._js_expr }}):
{% endfor %}
return {{ case[-1] }};
return {{ render(case[-1]) }};
break;
{% endfor %}
default:
return {{ component.default }};
return {{ render(component.default) }};
break;
}
})()

View File

@ -106,6 +106,18 @@ export const getBackendURL = (url_str) => {
return endpoint;
};
/**
* Check if the backend is disabled.
*
* @returns True if the backend is disabled, false otherwise.
*/
export const isBackendDisabled = () => {
const cookie = document.cookie
.split("; ")
.find((row) => row.startsWith("backend-enabled="));
return cookie !== undefined && cookie.split("=")[1] == "false";
};
/**
* Determine if any event in the event queue is stateful.
*
@ -301,10 +313,7 @@ export const applyEvent = async (event, socket) => {
// Send the event to the server.
if (socket) {
socket.emit(
"event",
event,
);
socket.emit("event", event);
return true;
}
@ -497,7 +506,7 @@ export const uploadFiles = async (
return false;
}
const upload_ref_name = `__upload_controllers_${upload_id}`
const upload_ref_name = `__upload_controllers_${upload_id}`;
if (refs[upload_ref_name]) {
console.log("Upload already in progress for ", upload_id);
@ -815,7 +824,7 @@ export const useEventLoop = (
return;
}
// only use websockets if state is present
if (Object.keys(initialState).length > 1) {
if (Object.keys(initialState).length > 1 && !isBackendDisabled()) {
// Initialize the websocket connection.
if (!socket.current) {
connect(

View File

@ -59,7 +59,11 @@ from reflex.components.component import (
ComponentStyle,
evaluate_style_namespaces,
)
from reflex.components.core.banner import connection_pulser, connection_toaster
from reflex.components.core.banner import (
backend_disabled,
connection_pulser,
connection_toaster,
)
from reflex.components.core.breakpoints import set_breakpoints
from reflex.components.core.client_side_routing import (
Default404Page,
@ -158,9 +162,12 @@ def default_overlay_component() -> Component:
Returns:
The default overlay_component, which is a connection_modal.
"""
config = get_config()
return Fragment.create(
connection_pulser(),
connection_toaster(),
*([backend_disabled()] if config.is_reflex_cloud else []),
*codespaces.codespaces_auto_redirect(),
)

View File

@ -1943,7 +1943,7 @@ class StatefulComponent(BaseComponent):
if not should_memoize:
# Determine if any Vars have associated data.
for prop_var in component._get_vars():
for prop_var in component._get_vars(include_children=True):
if prop_var._get_all_var_data():
should_memoize = True
break
@ -2326,8 +2326,8 @@ class MemoizationLeaf(Component):
"""
comp = super().create(*children, **props)
if comp._get_all_hooks():
comp._memoization_mode = cls._memoization_mode.copy(
update={"disposition": MemoizationDisposition.ALWAYS}
comp._memoization_mode = dataclasses.replace(
comp._memoization_mode, disposition=MemoizationDisposition.ALWAYS
)
return comp
@ -2388,7 +2388,7 @@ def render_dict_to_var(tag: dict | Component | str, imported_names: set[str]) ->
if tag["name"] == "match":
element = tag["cond"]
conditionals = tag["default"]
conditionals = render_dict_to_var(tag["default"], imported_names)
for case in tag["match_cases"][::-1]:
condition = case[0].to_string() == element.to_string()
@ -2397,7 +2397,7 @@ def render_dict_to_var(tag: dict | Component | str, imported_names: set[str]) ->
conditionals = ternary_operation(
condition,
case[-1],
render_dict_to_var(case[-1], imported_names),
conditionals,
)

View File

@ -4,8 +4,10 @@ from __future__ import annotations
from typing import Optional
from reflex import constants
from reflex.components.component import Component
from reflex.components.core.cond import cond
from reflex.components.datadisplay.logo import svg_logo
from reflex.components.el.elements.typography import Div
from reflex.components.lucide.icon import Icon
from reflex.components.radix.themes.components.dialog import (
@ -293,7 +295,84 @@ class ConnectionPulser(Div):
)
class BackendDisabled(Div):
"""A component that displays a message when the backend is disabled."""
@classmethod
def create(cls, **props) -> Component:
"""Create a backend disabled component.
Args:
**props: The properties of the component.
Returns:
The backend disabled component.
"""
import reflex as rx
is_backend_disabled = Var(
"backendDisabled",
_var_type=bool,
_var_data=VarData(
hooks={
"const [backendDisabled, setBackendDisabled] = useState(false);": None,
"useEffect(() => { setBackendDisabled(isBackendDisabled()); }, []);": None,
},
imports={
f"$/{constants.Dirs.STATE_PATH}": [
ImportVar(tag="isBackendDisabled")
],
},
),
)
return super().create(
rx.cond(
is_backend_disabled,
rx.box(
rx.box(
rx.card(
rx.vstack(
svg_logo(),
rx.text(
"You ran out of compute credits.",
),
rx.callout(
rx.fragment(
"Please upgrade your plan or raise your compute credits at ",
rx.link(
"Reflex Cloud.",
href="https://cloud.reflex.dev/",
),
),
width="100%",
icon="info",
variant="surface",
),
),
font_size="20px",
font_family='"Inter", "Helvetica", "Arial", sans-serif',
variant="classic",
),
position="fixed",
top="50%",
left="50%",
transform="translate(-50%, -50%)",
width="40ch",
max_width="90vw",
),
position="fixed",
z_index=9999,
backdrop_filter="grayscale(1) blur(5px)",
width="100dvw",
height="100dvh",
),
)
)
connection_banner = ConnectionBanner.create
connection_modal = ConnectionModal.create
connection_toaster = ConnectionToaster.create
connection_pulser = ConnectionPulser.create
backend_disabled = BackendDisabled.create

View File

@ -350,7 +350,93 @@ class ConnectionPulser(Div):
"""
...
class BackendDisabled(Div):
@overload
@classmethod
def create( # type: ignore
cls,
*children,
access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
auto_capitalize: Optional[
Union[Var[Union[bool, int, str]], bool, int, str]
] = None,
content_editable: Optional[
Union[Var[Union[bool, int, str]], bool, int, str]
] = None,
context_menu: Optional[
Union[Var[Union[bool, int, str]], bool, int, str]
] = None,
dir: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
draggable: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
enter_key_hint: Optional[
Union[Var[Union[bool, int, str]], bool, int, str]
] = None,
hidden: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
input_mode: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
item_prop: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
role: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
slot: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
spell_check: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
tab_index: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
title: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
style: Optional[Style] = None,
key: Optional[Any] = None,
id: Optional[Any] = None,
class_name: Optional[Any] = None,
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None,
on_blur: Optional[EventType[[], BASE_STATE]] = None,
on_click: Optional[EventType[[], BASE_STATE]] = None,
on_context_menu: Optional[EventType[[], BASE_STATE]] = None,
on_double_click: Optional[EventType[[], BASE_STATE]] = None,
on_focus: Optional[EventType[[], BASE_STATE]] = None,
on_mount: Optional[EventType[[], BASE_STATE]] = None,
on_mouse_down: Optional[EventType[[], BASE_STATE]] = None,
on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None,
on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None,
on_mouse_move: Optional[EventType[[], BASE_STATE]] = None,
on_mouse_out: Optional[EventType[[], BASE_STATE]] = None,
on_mouse_over: Optional[EventType[[], BASE_STATE]] = None,
on_mouse_up: Optional[EventType[[], BASE_STATE]] = None,
on_scroll: Optional[EventType[[], BASE_STATE]] = None,
on_unmount: Optional[EventType[[], BASE_STATE]] = None,
**props,
) -> "BackendDisabled":
"""Create a backend disabled component.
Args:
access_key: Provides a hint for generating a keyboard shortcut for the current element.
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
content_editable: Indicates whether the element's content is editable.
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
dir: Defines the text direction. Allowed values are ltr (Left-To-Right) or rtl (Right-To-Left)
draggable: Defines whether the element can be dragged.
enter_key_hint: Hints what media types the media element is able to play.
hidden: Defines whether the element is hidden.
input_mode: Defines the type of the element.
item_prop: Defines the name of the element for metadata purposes.
lang: Defines the language used in the element.
role: Defines the role of the element.
slot: Assigns a slot in a shadow DOM shadow tree to an element.
spell_check: Defines whether the element may be checked for spelling errors.
tab_index: Defines the position of the current element in the tabbing order.
title: Defines a tooltip for the element.
style: The style of the component.
key: A unique key for the component.
id: The id for the component.
class_name: The class name for the component.
autofocus: Whether the component should take the focus once the page is loaded
custom_attrs: custom attribute
**props: The properties of the component.
Returns:
The backend disabled component.
"""
...
connection_banner = ConnectionBanner.create
connection_modal = ConnectionModal.create
connection_toaster = ConnectionToaster.create
connection_pulser = ConnectionPulser.create
backend_disabled = BackendDisabled.create

View File

@ -2,6 +2,7 @@
from __future__ import annotations
import functools
import inspect
from typing import Any, Callable, Iterable
@ -97,9 +98,20 @@ class Foreach(Component):
# Determine the index var name based on the params accepted by render_fn.
props["index_var_name"] = params[1].name
else:
render_fn = self.render_fn
# Otherwise, use a deterministic index, based on the render function bytecode.
code_hash = (
hash(self.render_fn.__code__)
hash(
getattr(
render_fn,
"__code__",
(
repr(self.render_fn)
if not isinstance(render_fn, functools.partial)
else render_fn.func.__code__
),
)
)
.to_bytes(
length=8,
byteorder="big",

View File

@ -10,6 +10,7 @@ from reflex.components.core.cond import cond
from reflex.components.lucide.icon import Icon
from reflex.components.radix.primitives.base import RadixPrimitiveComponent
from reflex.components.radix.themes.base import LiteralAccentColor, LiteralRadius
from reflex.constants.compiler import MemoizationMode
from reflex.event import EventHandler
from reflex.style import Style
from reflex.vars import get_uuid_string_var
@ -342,6 +343,8 @@ class AccordionTrigger(AccordionComponent):
alias = "RadixAccordionTrigger"
_memoization_mode = MemoizationMode(recursive=False)
@classmethod
def create(cls, *children, **props) -> Component:
"""Create the Accordion trigger component.

View File

@ -10,6 +10,7 @@ from reflex.components.component import Component, ComponentNamespace
from reflex.components.radix.primitives.base import RadixPrimitiveComponent
from reflex.components.radix.themes.base import Theme
from reflex.components.radix.themes.layout.flex import Flex
from reflex.constants.compiler import MemoizationMode
from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec
from reflex.vars.base import Var
@ -85,6 +86,8 @@ class DrawerTrigger(DrawerComponent):
# Defaults to true, if the first child acts as the trigger.
as_child: Var[bool] = Var.create(True)
_memoization_mode = MemoizationMode(recursive=False)
@classmethod
def create(cls, *children: Any, **props: Any) -> Component:
"""Create a new DrawerTrigger instance.

View File

@ -5,6 +5,7 @@ from typing import Literal
from reflex.components.component import ComponentNamespace
from reflex.components.core.breakpoints import Responsive
from reflex.components.el import elements
from reflex.constants.compiler import MemoizationMode
from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec
from reflex.vars.base import Var
@ -33,6 +34,8 @@ class AlertDialogTrigger(RadixThemesTriggerComponent):
tag = "AlertDialog.Trigger"
_memoization_mode = MemoizationMode(recursive=False)
class AlertDialogContent(elements.Div, RadixThemesComponent):
"""Contains the content of the dialog. This component is based on the div element."""

View File

@ -20,7 +20,7 @@ class Card(elements.Div, RadixThemesComponent):
# Card size: "1" - "5"
size: Var[Responsive[Literal["1", "2", "3", "4", "5"],]]
# Variant of Card: "solid" | "soft" | "outline" | "ghost"
# Variant of Card: "surface" | "classic" | "ghost"
variant: Var[Literal["surface", "classic", "ghost"]]

View File

@ -94,7 +94,7 @@ class Card(elements.Div, RadixThemesComponent):
*children: Child components.
as_child: Change the default rendered element for the one passed as a child, merging their props and behavior.
size: Card size: "1" - "5"
variant: Variant of Card: "solid" | "soft" | "outline" | "ghost"
variant: Variant of Card: "surface" | "classic" | "ghost"
access_key: Provides a hint for generating a keyboard shortcut for the current element.
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
content_editable: Indicates whether the element's content is editable.

View File

@ -4,6 +4,7 @@ from typing import Dict, List, Literal, Union
from reflex.components.component import ComponentNamespace
from reflex.components.core.breakpoints import Responsive
from reflex.constants.compiler import MemoizationMode
from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec
from reflex.vars.base import Var
@ -55,6 +56,8 @@ class ContextMenuTrigger(RadixThemesComponent):
_invalid_children: List[str] = ["ContextMenuContent"]
_memoization_mode = MemoizationMode(recursive=False)
class ContextMenuContent(RadixThemesComponent):
"""The component that pops out when the context menu is open."""
@ -153,6 +156,8 @@ class ContextMenuSubTrigger(RadixThemesComponent):
_valid_parents: List[str] = ["ContextMenuContent", "ContextMenuSub"]
_memoization_mode = MemoizationMode(recursive=False)
class ContextMenuSubContent(RadixThemesComponent):
"""The component that pops out when a submenu is open."""

View File

@ -5,6 +5,7 @@ from typing import Literal
from reflex.components.component import ComponentNamespace
from reflex.components.core.breakpoints import Responsive
from reflex.components.el import elements
from reflex.constants.compiler import MemoizationMode
from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec
from reflex.vars.base import Var
@ -31,6 +32,8 @@ class DialogTrigger(RadixThemesTriggerComponent):
tag = "Dialog.Trigger"
_memoization_mode = MemoizationMode(recursive=False)
class DialogTitle(RadixThemesComponent):
"""Title component to display inside a Dialog modal."""

View File

@ -4,6 +4,7 @@ from typing import Dict, List, Literal, Union
from reflex.components.component import ComponentNamespace
from reflex.components.core.breakpoints import Responsive
from reflex.constants.compiler import MemoizationMode
from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec
from reflex.vars.base import Var
@ -60,6 +61,8 @@ class DropdownMenuTrigger(RadixThemesTriggerComponent):
_invalid_children: List[str] = ["DropdownMenuContent"]
_memoization_mode = MemoizationMode(recursive=False)
class DropdownMenuContent(RadixThemesComponent):
"""The Dropdown Menu Content component that pops out when the dropdown menu is open."""
@ -143,6 +146,8 @@ class DropdownMenuSubTrigger(RadixThemesTriggerComponent):
_valid_parents: List[str] = ["DropdownMenuContent", "DropdownMenuSub"]
_memoization_mode = MemoizationMode(recursive=False)
class DropdownMenuSub(RadixThemesComponent):
"""Contains all the parts of a submenu."""

View File

@ -5,6 +5,7 @@ from typing import Dict, Literal, Union
from reflex.components.component import ComponentNamespace
from reflex.components.core.breakpoints import Responsive
from reflex.components.el import elements
from reflex.constants.compiler import MemoizationMode
from reflex.event import EventHandler, passthrough_event_spec
from reflex.vars.base import Var
@ -37,6 +38,8 @@ class HoverCardTrigger(RadixThemesTriggerComponent):
tag = "HoverCard.Trigger"
_memoization_mode = MemoizationMode(recursive=False)
class HoverCardContent(elements.Div, RadixThemesComponent):
"""Contains the content of the open hover card."""

View File

@ -5,6 +5,7 @@ from typing import Dict, Literal, Union
from reflex.components.component import ComponentNamespace
from reflex.components.core.breakpoints import Responsive
from reflex.components.el import elements
from reflex.constants.compiler import MemoizationMode
from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec
from reflex.vars.base import Var
@ -34,6 +35,8 @@ class PopoverTrigger(RadixThemesTriggerComponent):
tag = "Popover.Trigger"
_memoization_mode = MemoizationMode(recursive=False)
class PopoverContent(elements.Div, RadixThemesComponent):
"""Contains content to be rendered in the open popover."""

View File

@ -5,6 +5,7 @@ from typing import List, Literal, Union
import reflex as rx
from reflex.components.component import Component, ComponentNamespace
from reflex.components.core.breakpoints import Responsive
from reflex.constants.compiler import MemoizationMode
from reflex.event import no_args_event_spec, passthrough_event_spec
from reflex.vars.base import Var
@ -69,6 +70,8 @@ class SelectTrigger(RadixThemesComponent):
_valid_parents: List[str] = ["SelectRoot"]
_memoization_mode = MemoizationMode(recursive=False)
class SelectContent(RadixThemesComponent):
"""The component that pops out when the select is open."""

View File

@ -7,6 +7,7 @@ from typing import Any, Dict, List, Literal
from reflex.components.component import Component, ComponentNamespace
from reflex.components.core.breakpoints import Responsive
from reflex.components.core.colors import color
from reflex.constants.compiler import MemoizationMode
from reflex.event import EventHandler, passthrough_event_spec
from reflex.vars.base import Var
@ -95,6 +96,8 @@ class TabsTrigger(RadixThemesComponent):
_valid_parents: List[str] = ["TabsList"]
_memoization_mode = MemoizationMode(recursive=False)
@classmethod
def create(cls, *children, **props) -> Component:
"""Create a TabsTrigger component.

View File

@ -3,13 +3,33 @@
from __future__ import annotations
import dataclasses
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Optional, Sequence, Union
from reflex.event import EventChain
from reflex.utils import format, types
from reflex.vars.base import LiteralVar, Var
def render_prop(value: Any) -> Any:
"""Render the prop.
Args:
value: The value to render.
Returns:
The rendered value.
"""
from reflex.components.component import BaseComponent
if isinstance(value, BaseComponent):
return value.render()
if isinstance(value, Sequence) and not isinstance(value, str):
return [render_prop(v) for v in value]
if callable(value) and not isinstance(value, Var):
return None
return value
@dataclasses.dataclass()
class Tag:
"""A React tag."""
@ -66,7 +86,9 @@ class Tag:
Tuple[str, Any]: The field name and value.
"""
for field in dataclasses.fields(self):
yield field.name, getattr(self, field.name)
rendered_value = render_prop(getattr(self, field.name))
if rendered_value is not None:
yield field.name, rendered_value
def add_props(self, **kwargs: Optional[Any]) -> Tag:
"""Add props to the tag.

View File

@ -703,6 +703,9 @@ class Config(Base):
# Path to file containing key-values pairs to override in the environment; Dotenv format.
env_file: Optional[str] = None
# Whether the app is running in the reflex cloud environment.
is_reflex_cloud: bool = False
def __init__(self, *args, **kwargs):
"""Initialize the config values.

View File

@ -1,10 +1,10 @@
"""Compiler variables."""
import dataclasses
import enum
from enum import Enum
from types import SimpleNamespace
from reflex.base import Base
from reflex.constants import Dirs
from reflex.utils.imports import ImportVar
@ -151,7 +151,8 @@ class MemoizationDisposition(enum.Enum):
NEVER = "never"
class MemoizationMode(Base):
@dataclasses.dataclass(frozen=True)
class MemoizationMode:
"""The mode for memoizing a Component."""
# The conditions under which the component should be memoized.

View File

@ -12,6 +12,7 @@ from reflex.components.radix.themes.components.icon_button import IconButton
from reflex.components.radix.themes.layout.box import Box
from reflex.components.radix.themes.layout.container import Container
from reflex.components.radix.themes.layout.stack import HStack
from reflex.constants.compiler import MemoizationMode
from reflex.event import run_script
from reflex.experimental import hooks
from reflex.state import ComponentState
@ -146,6 +147,8 @@ sidebar_trigger_style = {
class SidebarTrigger(Fragment):
"""A component that renders the sidebar trigger."""
_memoization_mode = MemoizationMode(recursive=False)
@classmethod
def create(cls, sidebar: Component, **props):
"""Create the sidebar trigger component.

View File

@ -303,8 +303,16 @@ class Style(dict):
Returns:
The combined style.
"""
_var_data = VarData.merge(self._var_data, getattr(other, "_var_data", None))
return Style(super().__or__(self, other), _var_data=_var_data) # pyright: ignore [reportGeneralTypeIssues, reportCallIssue]
other_var_data = None
if not isinstance(other, Style):
other_dict, other_var_data = convert(other)
else:
other_dict, other_var_data = other, other._var_data
new_style = Style(super().__or__(other_dict))
if self._var_data or other_var_data:
new_style._var_data = VarData.merge(self._var_data, other_var_data)
return new_style
def _format_emotion_style_pseudo_selector(key: str) -> str:

View File

@ -445,7 +445,12 @@ class Var(Generic[VAR_TYPE]):
_default_var_type: ClassVar[GenericType] = default_type
ToVarOperation.__name__ = f"To{cls.__name__.removesuffix('Var')}Operation"
new_to_var_operation_name = f"To{cls.__name__.removesuffix('Var')}Operation"
ToVarOperation.__qualname__ = (
ToVarOperation.__qualname__.removesuffix(ToVarOperation.__name__)
+ new_to_var_operation_name
)
ToVarOperation.__name__ = new_to_var_operation_name
_var_subclasses.append(VarSubclassEntry(cls, ToVarOperation, python_types))
@ -1385,7 +1390,7 @@ class LiteralVar(Var):
TypeError: If the value is not a supported type for LiteralVar.
"""
from .object import LiteralObjectVar
from .sequence import LiteralStringVar
from .sequence import ArrayVar, LiteralStringVar
if isinstance(value, Var):
if _var_data is None:
@ -1441,6 +1446,9 @@ class LiteralVar(Var):
_var_data=_var_data,
)
if isinstance(value, range):
return ArrayVar.range(value.start, value.stop, value.step)
raise TypeError(
f"Unsupported type {type(value)} for LiteralVar. Tried to create a LiteralVar from {value}."
)

View File

@ -1593,7 +1593,7 @@ def array_range_operation(
The range of numbers.
"""
return var_operation_return(
js_expression=f"Array.from({{ length: ({stop!s} - {start!s}) / {step!s} }}, (_, i) => {start!s} + i * {step!s})",
js_expression=f"Array.from({{ length: Math.ceil(({stop!s} - {start!s}) / {step!s}) }}, (_, i) => {start!s} + i * {step!s})",
var_type=List[int],
)

View File

@ -1,5 +1,6 @@
"""Test case for displaying the connection banner when the websocket drops."""
import functools
from typing import Generator
import pytest
@ -11,12 +12,19 @@ from reflex.testing import AppHarness, WebDriver
from .utils import SessionStorage
def ConnectionBanner():
"""App with a connection banner."""
def ConnectionBanner(is_reflex_cloud: bool = False):
"""App with a connection banner.
Args:
is_reflex_cloud: The value for config.is_reflex_cloud.
"""
import asyncio
import reflex as rx
# Simulate reflex cloud deploy
rx.config.get_config().is_reflex_cloud = is_reflex_cloud
class State(rx.State):
foo: int = 0
@ -40,19 +48,43 @@ def ConnectionBanner():
app.add_page(index)
@pytest.fixture(
params=[False, True], ids=["reflex_cloud_disabled", "reflex_cloud_enabled"]
)
def simulate_is_reflex_cloud(request) -> bool:
"""Fixture to simulate reflex cloud deployment.
Args:
request: pytest request fixture.
Returns:
True if reflex cloud is enabled, False otherwise.
"""
return request.param
@pytest.fixture()
def connection_banner(tmp_path) -> Generator[AppHarness, None, None]:
def connection_banner(
tmp_path,
simulate_is_reflex_cloud: bool,
) -> Generator[AppHarness, None, None]:
"""Start ConnectionBanner app at tmp_path via AppHarness.
Args:
tmp_path: pytest tmp_path fixture
simulate_is_reflex_cloud: Whether is_reflex_cloud is set for the app.
Yields:
running AppHarness instance
"""
with AppHarness.create(
root=tmp_path,
app_source=ConnectionBanner,
app_source=functools.partial(
ConnectionBanner, is_reflex_cloud=simulate_is_reflex_cloud
),
app_name="connection_banner_reflex_cloud"
if simulate_is_reflex_cloud
else "connection_banner",
) as harness:
yield harness
@ -77,6 +109,38 @@ def has_error_modal(driver: WebDriver) -> bool:
return True
def has_cloud_banner(driver: WebDriver) -> bool:
"""Check if the cloud banner is displayed.
Args:
driver: Selenium webdriver instance.
Returns:
True if the banner is displayed, False otherwise.
"""
try:
driver.find_element(
By.XPATH, "//*[ contains(text(), 'You ran out of compute credits.') ]"
)
except NoSuchElementException:
return False
else:
return True
def _assert_token(connection_banner, driver):
"""Poll for backend to be up.
Args:
connection_banner: AppHarness instance.
driver: Selenium webdriver instance.
"""
ss = SessionStorage(driver)
assert connection_banner._poll_for(lambda: ss.get("token") is not None), (
"token not found"
)
@pytest.mark.asyncio
async def test_connection_banner(connection_banner: AppHarness):
"""Test that the connection banner is displayed when the websocket drops.
@ -88,11 +152,7 @@ async def test_connection_banner(connection_banner: AppHarness):
assert connection_banner.backend is not None
driver = connection_banner.frontend()
ss = SessionStorage(driver)
assert connection_banner._poll_for(lambda: ss.get("token") is not None), (
"token not found"
)
_assert_token(connection_banner, driver)
assert connection_banner._poll_for(lambda: not has_error_modal(driver))
delay_button = driver.find_element(By.ID, "delay")
@ -132,3 +192,36 @@ async def test_connection_banner(connection_banner: AppHarness):
# Count should have incremented after coming back up
assert connection_banner.poll_for_value(counter_element, exp_not_equal="1") == "2"
@pytest.mark.asyncio
async def test_cloud_banner(
connection_banner: AppHarness, simulate_is_reflex_cloud: bool
):
"""Test that the connection banner is displayed when the websocket drops.
Args:
connection_banner: AppHarness instance.
simulate_is_reflex_cloud: Whether is_reflex_cloud is set for the app.
"""
assert connection_banner.app_instance is not None
assert connection_banner.backend is not None
driver = connection_banner.frontend()
driver.add_cookie({"name": "backend-enabled", "value": "truly"})
driver.refresh()
_assert_token(connection_banner, driver)
assert connection_banner._poll_for(lambda: not has_cloud_banner(driver))
driver.add_cookie({"name": "backend-enabled", "value": "false"})
driver.refresh()
if simulate_is_reflex_cloud:
assert connection_banner._poll_for(lambda: has_cloud_banner(driver))
else:
_assert_token(connection_banner, driver)
assert connection_banner._poll_for(lambda: not has_cloud_banner(driver))
driver.delete_cookie("backend-enabled")
driver.refresh()
_assert_token(connection_banner, driver)
assert connection_banner._poll_for(lambda: not has_cloud_banner(driver))

View File

@ -600,6 +600,11 @@ def VarOperations():
),
id="foreach_in_match",
),
# Literal range var in a foreach
rx.box(rx.foreach(range(42, 80, 27), rx.text.span), id="range_in_foreach1"),
rx.box(rx.foreach(range(42, 80, 3), rx.text.span), id="range_in_foreach2"),
rx.box(rx.foreach(range(42, 20, -6), rx.text.span), id="range_in_foreach3"),
rx.box(rx.foreach(range(42, 43, 5), rx.text.span), id="range_in_foreach4"),
)
@ -799,6 +804,11 @@ def test_var_operations(driver, var_operations: AppHarness):
("memo_comp_nested", "345"),
# foreach in a match
("foreach_in_match", "first\nsecond\nthird"),
# literal range in a foreach
("range_in_foreach1", "4269"),
("range_in_foreach2", "42454851545760636669727578"),
("range_in_foreach3", "42363024"),
("range_in_foreach4", "42"),
]
for tag, expected in tests:

View File

@ -42,7 +42,7 @@ def test_match_components():
assert match_cases[0][0]._js_expr == "1"
assert match_cases[0][0]._var_type is int
first_return_value_render = match_cases[0][1].render()
first_return_value_render = match_cases[0][1]
assert first_return_value_render["name"] == "RadixThemesText"
assert first_return_value_render["children"][0]["contents"] == '{"first value"}'
@ -50,35 +50,35 @@ def test_match_components():
assert match_cases[1][0]._var_type is int
assert match_cases[1][1]._js_expr == "3"
assert match_cases[1][1]._var_type is int
second_return_value_render = match_cases[1][2].render()
second_return_value_render = match_cases[1][2]
assert second_return_value_render["name"] == "RadixThemesText"
assert second_return_value_render["children"][0]["contents"] == '{"second value"}'
assert match_cases[2][0]._js_expr == "[1, 2]"
assert match_cases[2][0]._var_type == List[int]
third_return_value_render = match_cases[2][1].render()
third_return_value_render = match_cases[2][1]
assert third_return_value_render["name"] == "RadixThemesText"
assert third_return_value_render["children"][0]["contents"] == '{"third value"}'
assert match_cases[3][0]._js_expr == '"random"'
assert match_cases[3][0]._var_type is str
fourth_return_value_render = match_cases[3][1].render()
fourth_return_value_render = match_cases[3][1]
assert fourth_return_value_render["name"] == "RadixThemesText"
assert fourth_return_value_render["children"][0]["contents"] == '{"fourth value"}'
assert match_cases[4][0]._js_expr == '({ ["foo"] : "bar" })'
assert match_cases[4][0]._var_type == Mapping[str, str]
fifth_return_value_render = match_cases[4][1].render()
fifth_return_value_render = match_cases[4][1]
assert fifth_return_value_render["name"] == "RadixThemesText"
assert fifth_return_value_render["children"][0]["contents"] == '{"fifth value"}'
assert match_cases[5][0]._js_expr == f"({MatchState.get_name()}.num + 1)"
assert match_cases[5][0]._var_type is int
fifth_return_value_render = match_cases[5][1].render()
fifth_return_value_render = match_cases[5][1]
assert fifth_return_value_render["name"] == "RadixThemesText"
assert fifth_return_value_render["children"][0]["contents"] == '{"sixth value"}'
default = match_child["default"].render()
default = match_child["default"]
assert default["name"] == "RadixThemesText"
assert default["children"][0]["contents"] == '{"default value"}'
@ -153,7 +153,7 @@ def test_match_on_component_without_default():
match_comp = Match.create(MatchState.value, *match_case_tuples)
default = match_comp.render()["children"][0]["default"]
assert isinstance(default, Fragment)
assert isinstance(default, dict) and default["name"] == Fragment.__name__
def test_match_on_var_no_default():

View File

@ -541,3 +541,7 @@ def test_style_update_with_var_data():
assert s2._var_data is not None
assert "const red = true" in s2._var_data.hooks
assert "const blue = true" in s2._var_data.hooks
s3 = s1 | s2
assert s3._var_data is not None
assert "_varData" not in s3

View File

@ -1076,19 +1076,19 @@ def test_array_operations():
assert str(array_var.reverse()) == "[1, 2, 3, 4, 5].slice().reverse()"
assert (
str(ArrayVar.range(10))
== "Array.from({ length: (10 - 0) / 1 }, (_, i) => 0 + i * 1)"
== "Array.from({ length: Math.ceil((10 - 0) / 1) }, (_, i) => 0 + i * 1)"
)
assert (
str(ArrayVar.range(1, 10))
== "Array.from({ length: (10 - 1) / 1 }, (_, i) => 1 + i * 1)"
== "Array.from({ length: Math.ceil((10 - 1) / 1) }, (_, i) => 1 + i * 1)"
)
assert (
str(ArrayVar.range(1, 10, 2))
== "Array.from({ length: (10 - 1) / 2 }, (_, i) => 1 + i * 2)"
== "Array.from({ length: Math.ceil((10 - 1) / 2) }, (_, i) => 1 + i * 2)"
)
assert (
str(ArrayVar.range(1, 10, -1))
== "Array.from({ length: (10 - 1) / -1 }, (_, i) => 1 + i * -1)"
== "Array.from({ length: Math.ceil((10 - 1) / -1) }, (_, i) => 1 + i * -1)"
)