Merge branch 'main' into lendemor/fix_order_of_hooks_and_memoized_evt_trigger

This commit is contained in:
Lendemor 2024-12-13 16:31:02 +01:00
commit f31c6c1d9d
120 changed files with 1131 additions and 524 deletions

View File

@ -80,7 +80,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, macos-12] os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0'] python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0']
exclude: exclude:
- os: windows-latest - os: windows-latest
@ -92,7 +92,7 @@ jobs:
python-version: '3.9.18' python-version: '3.9.18'
- os: macos-latest - os: macos-latest
python-version: '3.10.13' python-version: '3.10.13'
- os: macos-12 - os: macos-latest
python-version: '3.12.0' python-version: '3.12.0'
include: include:
- os: windows-latest - os: windows-latest
@ -155,7 +155,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, macos-12] os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.11.5'] python-version: ['3.11.5']
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}

View File

@ -58,7 +58,7 @@ jobs:
working-directory: ./reflex-web working-directory: ./reflex-web
run: poetry run uv pip install -r requirements.txt run: poetry run uv pip install -r requirements.txt
- name: Install additional dependencies for DB access - name: Install additional dependencies for DB access
run: poetry run uv pip install psycopg2-binary run: poetry run uv pip install psycopg
- name: Init Website for reflex-web - name: Init Website for reflex-web
working-directory: ./reflex-web working-directory: ./reflex-web
run: poetry run reflex init run: poetry run reflex init

View File

@ -22,9 +22,9 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
strategy: strategy:
matrix: matrix:
state_manager: ["redis", "memory"] state_manager: ['redis', 'memory']
python-version: ['3.11.5', '3.12.0', '3.13.0']
split_index: [1, 2] split_index: [1, 2]
python-version: ["3.11.5", "3.12.0"]
fail-fast: false fail-fast: false
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
services: services:

View File

@ -43,7 +43,7 @@ jobs:
matrix: matrix:
# Show OS combos first in GUI # Show OS combos first in GUI
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0'] python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0', '3.13.0']
exclude: exclude:
- os: windows-latest - os: windows-latest
python-version: '3.10.13' python-version: '3.10.13'
@ -73,7 +73,7 @@ jobs:
run: | run: |
poetry run uv pip install -r requirements.txt poetry run uv pip install -r requirements.txt
- name: Install additional dependencies for DB access - name: Install additional dependencies for DB access
run: poetry run uv pip install psycopg2-binary run: poetry run uv pip install psycopg
- name: Check export --backend-only before init for counter example - name: Check export --backend-only before init for counter example
working-directory: ./reflex-examples/counter working-directory: ./reflex-examples/counter
run: | run: |
@ -147,7 +147,7 @@ jobs:
working-directory: ./reflex-web working-directory: ./reflex-web
run: poetry run uv pip install $(grep -ivE "reflex " requirements.txt) run: poetry run uv pip install $(grep -ivE "reflex " requirements.txt)
- name: Install additional dependencies for DB access - name: Install additional dependencies for DB access
run: poetry run uv pip install psycopg2-binary run: poetry run uv pip install psycopg
- name: Init Website for reflex-web - name: Init Website for reflex-web
working-directory: ./reflex-web working-directory: ./reflex-web
run: poetry run reflex init run: poetry run reflex init
@ -198,7 +198,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ['3.11.5', '3.12.0'] python-version: ['3.11.5', '3.12.0']
runs-on: macos-12 runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: ./.github/actions/setup_build_env - uses: ./.github/actions/setup_build_env
@ -216,7 +216,7 @@ jobs:
working-directory: ./reflex-web working-directory: ./reflex-web
run: poetry run uv pip install -r requirements.txt run: poetry run uv pip install -r requirements.txt
- name: Install additional dependencies for DB access - name: Install additional dependencies for DB access
run: poetry run uv pip install psycopg2-binary run: poetry run uv pip install psycopg
- name: Init Website for reflex-web - name: Init Website for reflex-web
working-directory: ./reflex-web working-directory: ./reflex-web
run: poetry run reflex init run: poetry run reflex init

View File

@ -28,7 +28,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0'] python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0', '3.13.0']
# Windows is a bit behind on Python version availability in Github # Windows is a bit behind on Python version availability in Github
exclude: exclude:
- os: windows-latest - os: windows-latest
@ -88,8 +88,9 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0'] # Note: py39, py310 versions chosen due to available arm64 darwin builds.
runs-on: macos-12 python-version: ['3.9.13', '3.10.11', '3.11.5', '3.12.0', '3.13.0']
runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: ./.github/actions/setup_build_env - uses: ./.github/actions/setup_build_env

View File

@ -52,7 +52,7 @@ FROM python:3.13-slim
WORKDIR /app WORKDIR /app
RUN adduser --disabled-password --home /app reflex RUN adduser --disabled-password --home /app reflex
COPY --chown=reflex --from=init /app /app COPY --chown=reflex --from=init /app /app
# Install libpq-dev for psycopg2 (skip if not using postgres). # Install libpq-dev for psycopg (skip if not using postgres).
RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/* RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
USER reflex USER reflex
ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1 ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1

View File

@ -39,7 +39,7 @@ FROM python:3.13-slim
WORKDIR /app WORKDIR /app
RUN adduser --disabled-password --home /app reflex RUN adduser --disabled-password --home /app reflex
COPY --chown=reflex --from=init /app /app COPY --chown=reflex --from=init /app /app
# Install libpq-dev for psycopg2 (skip if not using postgres). # Install libpq-dev for psycopg (skip if not using postgres).
RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/* RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
USER reflex USER reflex
ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1 ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1

View File

@ -15,7 +15,7 @@ services:
app: app:
environment: environment:
DB_URL: postgresql+psycopg2://postgres:secret@db/postgres DB_URL: postgresql+psycopg://postgres:secret@db/postgres
REDIS_URL: redis://redis:6379 REDIS_URL: redis://redis:6379
depends_on: depends_on:
- db - db

16
poetry.lock generated
View File

@ -1542,18 +1542,18 @@ type = ["mypy (>=1.11.2)"]
[[package]] [[package]]
name = "playwright" name = "playwright"
version = "1.49.0" version = "1.49.1"
description = "A high-level API to automate web browsers" description = "A high-level API to automate web browsers"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "playwright-1.49.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:704532a2d8ba580ec9e1895bfeafddce2e3d52320d4eb8aa38e80376acc5cbb0"}, {file = "playwright-1.49.1-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:1041ffb45a0d0bc44d698d3a5aa3ac4b67c9bd03540da43a0b70616ad52592b8"},
{file = "playwright-1.49.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e453f02c4e5cc2db7e9759c47e7425f32e50ac76c76b7eb17c69eed72f01c4d8"}, {file = "playwright-1.49.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9f38ed3d0c1f4e0a6d1c92e73dd9a61f8855133249d6f0cec28648d38a7137be"},
{file = "playwright-1.49.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:37ae985309184472946a6eb1a237e5d93c9e58a781fa73b75c8751325002a5d4"}, {file = "playwright-1.49.1-py3-none-macosx_11_0_universal2.whl", hash = "sha256:3be48c6d26dc819ca0a26567c1ae36a980a0303dcd4249feb6f59e115aaddfb8"},
{file = "playwright-1.49.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:68d94beffb3c9213e3ceaafa66171affd9a5d9162e0c8a3eed1b1132c2e57598"}, {file = "playwright-1.49.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:753ca90ee31b4b03d165cfd36e477309ebf2b4381953f2a982ff612d85b147d2"},
{file = "playwright-1.49.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f12d2aecdb41fc25a624cb15f3e8391c252ebd81985e3d5c1c261fe93779345"}, {file = "playwright-1.49.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd9bc8dab37aa25198a01f555f0a2e2c3813fe200fef018ac34dfe86b34994b9"},
{file = "playwright-1.49.0-py3-none-win32.whl", hash = "sha256:91103de52d470594ad375b512d7143fa95d6039111ae11a93eb4fe2f2b4a4858"}, {file = "playwright-1.49.1-py3-none-win32.whl", hash = "sha256:43b304be67f096058e587dac453ece550eff87b8fbed28de30f4f022cc1745bb"},
{file = "playwright-1.49.0-py3-none-win_amd64.whl", hash = "sha256:34d28a2c2d46403368610be4339898dc9c34eb9f7c578207b4715c49743a072a"}, {file = "playwright-1.49.1-py3-none-win_amd64.whl", hash = "sha256:47b23cb346283278f5b4d1e1990bcb6d6302f80c0aa0ca93dd0601a1400191df"},
] ]
[package.dependencies] [package.dependencies]

View File

@ -93,7 +93,7 @@ build-backend = "poetry.core.masonry.api"
[tool.ruff] [tool.ruff]
target-version = "py39" target-version = "py39"
lint.isort.split-on-trailing-comma = false lint.isort.split-on-trailing-comma = false
lint.select = ["B", "D", "E", "F", "I", "SIM", "W", "RUF", "FURB"] lint.select = ["B", "D", "E", "F", "I", "SIM", "W", "RUF", "FURB", "ERA"]
lint.ignore = ["B008", "D205", "E501", "F403", "SIM115", "RUF006", "RUF012"] lint.ignore = ["B008", "D205", "E501", "F403", "SIM115", "RUF006", "RUF012"]
lint.pydocstyle.convention = "google" lint.pydocstyle.convention = "google"

View File

@ -40,9 +40,6 @@ let event_processing = false;
// Array holding pending events to be processed. // Array holding pending events to be processed.
const event_queue = []; const event_queue = [];
// Pending upload promises, by id
const upload_controllers = {};
/** /**
* Generate a UUID (Used for session tokens). * Generate a UUID (Used for session tokens).
* Taken from: https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid * Taken from: https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid
@ -300,7 +297,7 @@ export const applyEvent = async (event, socket) => {
if (socket) { if (socket) {
socket.emit( socket.emit(
"event", "event",
JSON.stringify(event, (k, v) => (v === undefined ? null : v)) event,
); );
return true; return true;
} }
@ -407,6 +404,8 @@ export const connect = async (
transports: transports, transports: transports,
autoUnref: false, autoUnref: false,
}); });
// Ensure undefined fields in events are sent as null instead of removed
socket.current.io.encoder.replacer = (k, v) => (v === undefined ? null : v)
function checkVisibility() { function checkVisibility() {
if (document.visibilityState === "visible") { if (document.visibilityState === "visible") {
@ -443,8 +442,7 @@ export const connect = async (
}); });
// On each received message, queue the updates and events. // On each received message, queue the updates and events.
socket.current.on("event", async (message) => { socket.current.on("event", async (update) => {
const update = JSON5.parse(message);
for (const substate in update.delta) { for (const substate in update.delta) {
dispatch[substate](update.delta[substate]); dispatch[substate](update.delta[substate]);
} }
@ -456,7 +454,7 @@ export const connect = async (
}); });
socket.current.on("reload", async (event) => { socket.current.on("reload", async (event) => {
event_processing = false; event_processing = false;
queueEvents([...initialEvents(), JSON5.parse(event)], socket); queueEvents([...initialEvents(), event], socket);
}); });
document.addEventListener("visibilitychange", checkVisibility); document.addEventListener("visibilitychange", checkVisibility);
@ -485,7 +483,9 @@ export const uploadFiles = async (
return false; return false;
} }
if (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); console.log("Upload already in progress for ", upload_id);
return false; return false;
} }
@ -497,7 +497,9 @@ export const uploadFiles = async (
// Whenever called, responseText will contain the entire response so far. // Whenever called, responseText will contain the entire response so far.
const chunks = progressEvent.event.target.responseText.trim().split("\n"); const chunks = progressEvent.event.target.responseText.trim().split("\n");
// So only process _new_ chunks beyond resp_idx. // So only process _new_ chunks beyond resp_idx.
chunks.slice(resp_idx).map((chunk) => { chunks.slice(resp_idx).map((chunk_json) => {
try {
const chunk = JSON5.parse(chunk_json);
event_callbacks.map((f, ix) => { event_callbacks.map((f, ix) => {
f(chunk) f(chunk)
.then(() => { .then(() => {
@ -509,11 +511,17 @@ export const uploadFiles = async (
.catch((e) => { .catch((e) => {
if (progressEvent.progress === 1) { if (progressEvent.progress === 1) {
// Chunk may be incomplete, so only report errors when full response is available. // Chunk may be incomplete, so only report errors when full response is available.
console.log("Error parsing chunk", chunk, e); console.log("Error processing chunk", chunk, e);
} }
return; return;
}); });
}); });
} catch (e) {
if (progressEvent.progress === 1) {
console.log("Error parsing chunk", chunk_json, e);
}
return;
}
}); });
}; };
@ -537,7 +545,7 @@ export const uploadFiles = async (
}); });
// Send the file to the server. // Send the file to the server.
upload_controllers[upload_id] = controller; refs[upload_ref_name] = controller;
try { try {
return await axios.post(getBackendURL(UPLOADURL), formdata, config); return await axios.post(getBackendURL(UPLOADURL), formdata, config);
@ -557,7 +565,7 @@ export const uploadFiles = async (
} }
return false; return false;
} finally { } finally {
delete upload_controllers[upload_id]; delete refs[upload_ref_name];
} }
}; };
@ -799,7 +807,7 @@ export const useEventLoop = (
connect( connect(
socket, socket,
dispatch, dispatch,
["websocket", "polling"], ["websocket"],
setConnectErrors, setConnectErrors,
client_storage client_storage
); );

View File

@ -17,6 +17,7 @@ import sys
import traceback import traceback
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from types import SimpleNamespace
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
@ -363,6 +364,11 @@ class App(MiddlewareMixin, LifespanMixin):
max_http_buffer_size=constants.POLLING_MAX_HTTP_BUFFER_SIZE, max_http_buffer_size=constants.POLLING_MAX_HTTP_BUFFER_SIZE,
ping_interval=constants.Ping.INTERVAL, ping_interval=constants.Ping.INTERVAL,
ping_timeout=constants.Ping.TIMEOUT, ping_timeout=constants.Ping.TIMEOUT,
json=SimpleNamespace(
dumps=staticmethod(format.json_dumps),
loads=staticmethod(json.loads),
),
transports=["websocket"],
) )
elif getattr(self.sio, "async_mode", "") != "asgi": elif getattr(self.sio, "async_mode", "") != "asgi":
raise RuntimeError( raise RuntimeError(
@ -467,7 +473,7 @@ class App(MiddlewareMixin, LifespanMixin):
def add_page( def add_page(
self, self,
component: Component | ComponentCallable, component: Component | ComponentCallable | None = None,
route: str | None = None, route: str | None = None,
title: str | Var | None = None, title: str | Var | None = None,
description: str | Var | None = None, description: str | Var | None = None,
@ -490,17 +496,33 @@ class App(MiddlewareMixin, LifespanMixin):
meta: The metadata of the page. meta: The metadata of the page.
Raises: Raises:
ValueError: When the specified route name already exists. PageValueError: When the component is not set for a non-404 page.
RouteValueError: When the specified route name already exists.
""" """
# If the route is not set, get it from the callable. # If the route is not set, get it from the callable.
if route is None: if route is None:
if not isinstance(component, Callable): if not isinstance(component, Callable):
raise ValueError("Route must be set if component is not a callable.") raise exceptions.RouteValueError(
"Route must be set if component is not a callable."
)
# Format the route. # Format the route.
route = format.format_route(component.__name__) route = format.format_route(component.__name__)
else: else:
route = format.format_route(route, format_case=False) route = format.format_route(route, format_case=False)
if route == constants.Page404.SLUG:
if component is None:
component = Default404Page.create()
component = wait_for_client_redirect(self._generate_component(component))
title = title or constants.Page404.TITLE
description = description or constants.Page404.DESCRIPTION
image = image or constants.Page404.IMAGE
else:
if component is None:
raise exceptions.PageValueError(
"Component must be set for a non-404 page."
)
# Check if the route given is valid # Check if the route given is valid
verify_route_validity(route) verify_route_validity(route)
@ -516,7 +538,7 @@ class App(MiddlewareMixin, LifespanMixin):
if route == constants.PageNames.INDEX_ROUTE if route == constants.PageNames.INDEX_ROUTE
else f"`{route}`" else f"`{route}`"
) )
raise ValueError( raise exceptions.RouteValueError(
f"Duplicate page route {route_name} already exists. Make sure you do not have two" f"Duplicate page route {route_name} already exists. Make sure you do not have two"
f" pages with the same route" f" pages with the same route"
) )
@ -633,10 +655,14 @@ class App(MiddlewareMixin, LifespanMixin):
on_load: The event handler(s) that will be called each time the page load. on_load: The event handler(s) that will be called each time the page load.
meta: The metadata of the page. meta: The metadata of the page.
""" """
if component is None: console.deprecate(
component = Default404Page.create() feature_name="App.add_custom_404_page",
reason=f"Use app.add_page(component, route='/{constants.Page404.SLUG}') instead.",
deprecation_version="0.6.7",
removal_version="0.8.0",
)
self.add_page( self.add_page(
component=wait_for_client_redirect(self._generate_component(component)), component=component,
route=constants.Page404.SLUG, route=constants.Page404.SLUG,
title=title or constants.Page404.TITLE, title=title or constants.Page404.TITLE,
image=image or constants.Page404.IMAGE, image=image or constants.Page404.IMAGE,
@ -837,7 +863,7 @@ class App(MiddlewareMixin, LifespanMixin):
# 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.unevaluated_pages: if constants.Page404.SLUG not in self.unevaluated_pages:
self.add_custom_404_page() self.add_page(route=constants.Page404.SLUG)
# Fix up the style. # Fix up the style.
self.style = evaluate_style_namespaces(self.style) self.style = evaluate_style_namespaces(self.style)
@ -965,7 +991,6 @@ class App(MiddlewareMixin, LifespanMixin):
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 = executor.apipe(fn, *args, **kwargs)
result_futures.append(f) result_futures.append(f)
# Compile the pre-compiled pages. # Compile the pre-compiled pages.
@ -1270,7 +1295,7 @@ async def process(
await asyncio.create_task( await asyncio.create_task(
app.event_namespace.emit( app.event_namespace.emit(
"reload", "reload",
data=format.json_dumps(event), data=event,
to=sid, to=sid,
) )
) )
@ -1523,7 +1548,7 @@ class EventNamespace(AsyncNamespace):
""" """
# Creating a task prevents the update from being blocked behind other coroutines. # Creating a task prevents the update from being blocked behind other coroutines.
await asyncio.create_task( await asyncio.create_task(
self.emit(str(constants.SocketEvent.EVENT), update.json(), to=sid) self.emit(str(constants.SocketEvent.EVENT), update, to=sid)
) )
async def on_event(self, sid, data): async def on_event(self, sid, data):
@ -1536,7 +1561,7 @@ class EventNamespace(AsyncNamespace):
sid: The Socket.IO session id. sid: The Socket.IO session id.
data: The event data. data: The event data.
""" """
fields = json.loads(data) fields = data
# Get the event. # Get the event.
event = Event( event = Event(
**{k: v for k, v in fields.items() if k not in ("handler", "event_actions")} **{k: v for k, v in fields.items() if k not in ("handler", "event_actions")}

View File

@ -653,7 +653,6 @@ class Component(BaseComponent, ABC):
Returns: Returns:
The event triggers. The event triggers.
""" """
default_triggers: Dict[str, types.ArgsSpec | Sequence[types.ArgsSpec]] = { default_triggers: Dict[str, types.ArgsSpec | Sequence[types.ArgsSpec]] = {
EventTriggers.ON_FOCUS: no_args_event_spec, EventTriggers.ON_FOCUS: no_args_event_spec,

View File

@ -24,7 +24,7 @@ class ClientSideRouting(Component):
library = "$/utils/client_side_routing" library = "$/utils/client_side_routing"
tag = "useClientSideRouting" tag = "useClientSideRouting"
def add_hooks(self) -> list[str]: def add_hooks(self) -> list[str | Var]:
"""Get the hooks to render. """Get the hooks to render.
Returns: Returns:
@ -66,4 +66,4 @@ class Default404Page(Component):
tag = "Error" tag = "Error"
is_default = True is_default = True
status_code: Var[int] = 404 # type: ignore status_code: Var[int] = Var.create(404)

View File

@ -13,7 +13,7 @@ from reflex.vars.base import Var
route_not_found: Var route_not_found: Var
class ClientSideRouting(Component): class ClientSideRouting(Component):
def add_hooks(self) -> list[str]: ... def add_hooks(self) -> list[str | Var]: ...
def render(self) -> str: ... def render(self) -> str: ...
@overload @overload
@classmethod @classmethod

View File

@ -29,7 +29,7 @@ from reflex.event import (
from reflex.utils import format from reflex.utils import format
from reflex.utils.imports import ImportVar from reflex.utils.imports import ImportVar
from reflex.vars import VarData from reflex.vars import VarData
from reflex.vars.base import CallableVar, LiteralVar, Var, get_unique_variable_name from reflex.vars.base import CallableVar, Var, get_unique_variable_name
from reflex.vars.sequence import LiteralStringVar from reflex.vars.sequence import LiteralStringVar
DEFAULT_UPLOAD_ID: str = "default" DEFAULT_UPLOAD_ID: str = "default"
@ -108,7 +108,8 @@ def clear_selected_files(id_: str = DEFAULT_UPLOAD_ID) -> EventSpec:
# UploadFilesProvider assigns a special function to clear selected files # UploadFilesProvider assigns a special function to clear selected files
# into the shared global refs object to make it accessible outside a React # into the shared global refs object to make it accessible outside a React
# component via `run_script` (otherwise backend could never clear files). # component via `run_script` (otherwise backend could never clear files).
return run_script(f"refs['__clear_selected_files']({id_!r})") func = Var("__clear_selected_files")._as_ref()
return run_script(f"{func}({id_!r})")
def cancel_upload(upload_id: str) -> EventSpec: def cancel_upload(upload_id: str) -> EventSpec:
@ -120,7 +121,8 @@ def cancel_upload(upload_id: str) -> EventSpec:
Returns: Returns:
An event spec that cancels the upload when triggered. An event spec that cancels the upload when triggered.
""" """
return run_script(f"upload_controllers[{LiteralVar.create(upload_id)!s}]?.abort()") controller = Var(f"__upload_controllers_{upload_id}")._as_ref()
return run_script(f"{controller}?.abort()")
def get_upload_dir() -> Path: def get_upload_dir() -> Path:

View File

@ -51,27 +51,6 @@ class GridColumnIcons(Enum):
VideoUri = "video_uri" VideoUri = "video_uri"
# @serializer
# def serialize_gridcolumn_icon(icon: GridColumnIcons) -> str:
# """Serialize grid column icon.
# Args:
# icon: the Icon to serialize.
# Returns:
# The serialized value.
# """
# return "prefix" + str(icon)
# class DataEditorColumn(Base):
# """Column."""
# title: str
# id: Optional[str] = None
# type_: str = "str"
class DataEditorTheme(Base): class DataEditorTheme(Base):
"""The theme for the DataEditor component.""" """The theme for the DataEditor component."""
@ -229,7 +208,7 @@ class DataEditor(NoSSRComponent):
header_height: Var[int] header_height: Var[int]
# Additional header icons: # Additional header icons:
# header_icons: Var[Any] # (TODO: must be a map of name: svg) # header_icons: Var[Any] # (TODO: must be a map of name: svg) #noqa: ERA001
# The maximum width a column can be automatically sized to. # The maximum width a column can be automatically sized to.
max_column_auto_width: Var[int] max_column_auto_width: Var[int]

View File

@ -288,7 +288,7 @@ class DataEditor(NoSSRComponent):
freeze_columns: The number of columns which should remain in place when scrolling horizontally. Doesn't include rowMarkers. freeze_columns: The number of columns which should remain in place when scrolling horizontally. Doesn't include rowMarkers.
group_header_height: Controls the header of the group header row. group_header_height: Controls the header of the group header row.
header_height: Controls the height of the header row. header_height: Controls the height of the header row.
max_column_auto_width: Additional header icons: header_icons: Var[Any] # (TODO: must be a map of name: svg) The maximum width a column can be automatically sized to. max_column_auto_width: The maximum width a column can be automatically sized to.
max_column_width: The maximum width a column can be resized to. max_column_width: The maximum width a column can be resized to.
min_column_width: The minimum width a column can be resized to. min_column_width: The minimum width a column can be resized to.
row_height: Determins the height of each row. row_height: Determins the height of each row.

View File

@ -490,17 +490,17 @@ class ShikiJsTransformer(ShikiBaseTransformers):
}, },
# White Space # White Space
# ".tab, .space": { # ".tab, .space": {
# "position": "relative", # "position": "relative", # noqa: ERA001
# }, # },
# ".tab::before": { # ".tab::before": {
# "content": "'⇥'", # "content": "'⇥'", # noqa: ERA001
# "position": "absolute", # "position": "absolute", # noqa: ERA001
# "opacity": "0.3", # "opacity": "0.3",# noqa: ERA001
# }, # },
# ".space::before": { # ".space::before": {
# "content": "'·'", # "content": "'·'", # noqa: ERA001
# "position": "absolute", # "position": "absolute", # noqa: ERA001
# "opacity": "0.3", # "opacity": "0.3", # noqa: ERA001
# }, # },
} }
) )

View File

@ -1,4 +1,4 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" """Base classes."""
from typing import Union from typing import Union

View File

@ -1,4 +1,4 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" """Forms classes."""
from __future__ import annotations from __future__ import annotations
@ -18,6 +18,7 @@ from reflex.event import (
prevent_default, prevent_default,
) )
from reflex.utils.imports import ImportDict from reflex.utils.imports import ImportDict
from reflex.utils.types import is_optional
from reflex.vars import VarData from reflex.vars import VarData
from reflex.vars.base import LiteralVar, Var from reflex.vars.base import LiteralVar, Var
@ -84,7 +85,6 @@ class Datalist(BaseHTML):
"""Display the datalist element.""" """Display the datalist element."""
tag = "datalist" tag = "datalist"
# No unique attributes, only common ones are inherited
class Fieldset(Element): class Fieldset(Element):
@ -250,7 +250,6 @@ class Form(BaseHTML):
_js_expr=f"getRefValue({ref_var!s})", _js_expr=f"getRefValue({ref_var!s})",
_var_data=VarData.merge(ref_var._get_all_var_data()), _var_data=VarData.merge(ref_var._get_all_var_data()),
) )
# print(repr(form_refs))
return form_refs return form_refs
def _get_vars(self, include_children: bool = True) -> Iterator[Var]: def _get_vars(self, include_children: bool = True) -> Iterator[Var]:
@ -384,6 +383,33 @@ class Input(BaseHTML):
# Fired when a key is released # Fired when a key is released
on_key_up: EventHandler[key_event] on_key_up: EventHandler[key_event]
@classmethod
def create(cls, *children, **props):
"""Create an Input component.
Args:
*children: The children of the component.
**props: The properties of the component.
Returns:
The component.
"""
from reflex.vars.number import ternary_operation
value = props.get("value")
# React expects an empty string(instead of null) for controlled inputs.
if value is not None and is_optional(
(value_var := Var.create(value))._var_type
):
props["value"] = ternary_operation(
(value_var != Var.create(None)) # pyright: ignore [reportGeneralTypeIssues]
& (value_var != Var(_js_expr="undefined")),
value,
Var.create(""),
)
return super().create(*children, **props)
class Label(BaseHTML): class Label(BaseHTML):
"""Display the label element.""" """Display the label element."""
@ -401,7 +427,6 @@ class Legend(BaseHTML):
"""Display the legend element.""" """Display the legend element."""
tag = "legend" tag = "legend"
# No unique attributes, only common ones are inherited
class Meter(BaseHTML): class Meter(BaseHTML):

View File

@ -189,7 +189,7 @@ class Datalist(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element. 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. 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. 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. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
@ -512,7 +512,7 @@ class Input(BaseHTML):
on_unmount: Optional[EventType[[], BASE_STATE]] = None, on_unmount: Optional[EventType[[], BASE_STATE]] = None,
**props, **props,
) -> "Input": ) -> "Input":
"""Create the component. """Create an Input component.
Args: Args:
*children: The children of the component. *children: The children of the component.
@ -576,7 +576,7 @@ class Input(BaseHTML):
class_name: The class name for the component. class_name: The class name for the component.
autofocus: Whether the component should take the focus once the page is loaded autofocus: Whether the component should take the focus once the page is loaded
custom_attrs: custom attribute custom_attrs: custom attribute
**props: The props of the component. **props: The properties of the component.
Returns: Returns:
The component. The component.
@ -730,7 +730,7 @@ class Legend(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element. 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. 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. 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. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.

View File

@ -1,4 +1,4 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" """Inline classes."""
from typing import Union from typing import Union

View File

@ -1,4 +1,4 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" """Media classes."""
from typing import Any, Union from typing import Any, Union
@ -129,7 +129,6 @@ class Img(BaseHTML):
Returns: Returns:
The component. The component.
""" """
return ( return (
super().create(src=children[0], **props) super().create(src=children[0], **props)
@ -274,14 +273,12 @@ class Picture(BaseHTML):
"""Display the picture element.""" """Display the picture element."""
tag = "picture" tag = "picture"
# No unique attributes, only common ones are inherited
class Portal(BaseHTML): class Portal(BaseHTML):
"""Display the portal element.""" """Display the portal element."""
tag = "portal" tag = "portal"
# No unique attributes, only common ones are inherited
class Source(BaseHTML): class Source(BaseHTML):

View File

@ -340,7 +340,6 @@ class Img(BaseHTML):
Returns: Returns:
The component. The component.
""" """
... ...
@ -987,7 +986,7 @@ class Picture(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element. 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. 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. 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. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
@ -1073,7 +1072,7 @@ class Portal(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element. 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. 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. 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. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.

View File

@ -1,4 +1,4 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" """Metadata classes."""
from typing import List, Union from typing import List, Union

View File

@ -1,4 +1,4 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" """Other classes."""
from typing import Union from typing import Union
@ -26,31 +26,39 @@ class Dialog(BaseHTML):
class Summary(BaseHTML): class Summary(BaseHTML):
"""Display the summary element.""" """Display the summary element.
Used as a summary or caption for a <details> element.
"""
tag = "summary" tag = "summary"
# No unique attributes, only common ones are inherited; used as a summary or caption for a <details> element
class Slot(BaseHTML): class Slot(BaseHTML):
"""Display the slot element.""" """Display the slot element.
Used as a placeholder inside a web component.
"""
tag = "slot" tag = "slot"
# No unique attributes, only common ones are inherited; used as a placeholder inside a web component
class Template(BaseHTML): class Template(BaseHTML):
"""Display the template element.""" """Display the template element.
Used for declaring fragments of HTML that can be cloned and inserted in the document.
"""
tag = "template" tag = "template"
# No unique attributes, only common ones are inherited; used for declaring fragments of HTML that can be cloned and inserted in the document
class Math(BaseHTML): class Math(BaseHTML):
"""Display the math element.""" """Display the math element.
Represents a mathematical expression.
"""
tag = "math" tag = "math"
# No unique attributes, only common ones are inherited; used for displaying mathematical expressions
class Html(BaseHTML): class Html(BaseHTML):

View File

@ -244,7 +244,7 @@ class Summary(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited; used as a summary or caption for a <details> element Provides a hint for generating a keyboard shortcut for the current element. 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. 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. 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. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
@ -330,7 +330,7 @@ class Slot(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited; used as a placeholder inside a web component Provides a hint for generating a keyboard shortcut for the current element. 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. 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. 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. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
@ -416,7 +416,7 @@ class Template(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited; used for declaring fragments of HTML that can be cloned and inserted in the document Provides a hint for generating a keyboard shortcut for the current element. 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. 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. 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. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
@ -502,7 +502,7 @@ class Math(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited; used for displaying mathematical expressions Provides a hint for generating a keyboard shortcut for the current element. 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. 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. 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. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.

View File

@ -1,4 +1,4 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" """Scripts classes."""
from typing import Union from typing import Union
@ -17,7 +17,6 @@ class Noscript(BaseHTML):
"""Display the noscript element.""" """Display the noscript element."""
tag = "noscript" tag = "noscript"
# No unique attributes, only common ones are inherited
class Script(BaseHTML): class Script(BaseHTML):

View File

@ -154,7 +154,7 @@ class Noscript(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element. 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. 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. 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. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.

View File

@ -1,4 +1,4 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" """Sectioning classes."""
from .base import BaseHTML from .base import BaseHTML

View File

@ -1,4 +1,4 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" """Tables classes."""
from typing import Union from typing import Union

View File

@ -1,4 +1,4 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" """Typography classes."""
from typing import Union from typing import Union

View File

@ -47,7 +47,7 @@ class Image(NextComponent):
placeholder: Var[str] placeholder: Var[str]
# Allows passing CSS styles to the underlying image element. # Allows passing CSS styles to the underlying image element.
# style: Var[Any] # style: Var[Any] #noqa: ERA001
# The loading behavior of the image. Defaults to lazy. Can hurt performance, recommended to use `priority` instead. # The loading behavior of the image. Defaults to lazy. Can hurt performance, recommended to use `priority` instead.
loading: Var[Literal["lazy", "eager"]] loading: Var[Literal["lazy", "eager"]]

View File

@ -70,7 +70,7 @@ class Image(NextComponent):
quality: The quality of the optimized image, an integer between 1 and 100, where 100 is the best quality and therefore largest file size. Defaults to 75. quality: The quality of the optimized image, an integer between 1 and 100, where 100 is the best quality and therefore largest file size. Defaults to 75.
priority: When true, the image will be considered high priority and preload. Lazy loading is automatically disabled for images using priority. priority: When true, the image will be considered high priority and preload. Lazy loading is automatically disabled for images using priority.
placeholder: A placeholder to use while the image is loading. Possible values are blur, empty, or data:image/.... Defaults to empty. placeholder: A placeholder to use while the image is loading. Possible values are blur, empty, or data:image/.... Defaults to empty.
loading: Allows passing CSS styles to the underlying image element. style: Var[Any] The loading behavior of the image. Defaults to lazy. Can hurt performance, recommended to use `priority` instead. loading: The loading behavior of the image. Defaults to lazy. Can hurt performance, recommended to use `priority` instead.
blurDataURL: A Data URL to be used as a placeholder image before the src image successfully loads. Only takes effect when combined with placeholder="blur". blurDataURL: A Data URL to be used as a placeholder image before the src image successfully loads. Only takes effect when combined with placeholder="blur".
on_load: Fires when the image has loaded. on_load: Fires when the image has loaded.
on_error: Fires when the image has an error. on_error: Fires when the image has an error.

View File

@ -188,7 +188,7 @@ class Slider(ComponentNamespace):
else: else:
children = [ children = [
track, track,
# Foreach.create(props.get("value"), lambda e: SliderThumb.create()), # foreach doesn't render Thumbs properly # Foreach.create(props.get("value"), lambda e: SliderThumb.create()), # foreach doesn't render Thumbs properly # noqa: ERA001
] ]
return SliderRoot.create(*children, **props) return SliderRoot.create(*children, **props)

View File

@ -53,7 +53,7 @@ LiteralAccentColor = Literal[
class CommonMarginProps(Component): class CommonMarginProps(Component):
"""Many radix-themes elements accept shorthand margin props.""" """Many radix-themes elements accept shorthand margin props."""
# Margin: "0" - "9" # Margin: "0" - "9" # noqa: ERA001
m: Var[LiteralSpacing] m: Var[LiteralSpacing]
# Margin horizontal: "0" - "9" # Margin horizontal: "0" - "9"
@ -78,7 +78,7 @@ class CommonMarginProps(Component):
class CommonPaddingProps(Component): class CommonPaddingProps(Component):
"""Many radix-themes elements accept shorthand padding props.""" """Many radix-themes elements accept shorthand padding props."""
# Padding: "0" - "9" # Padding: "0" - "9" # noqa: ERA001
p: Var[Responsive[LiteralSpacing]] p: Var[Responsive[LiteralSpacing]]
# Padding horizontal: "0" - "9" # Padding horizontal: "0" - "9"
@ -140,11 +140,6 @@ class RadixThemesComponent(Component):
if component.library is None: if component.library is None:
component.library = RadixThemesComponent.__fields__["library"].default component.library = RadixThemesComponent.__fields__["library"].default
component.alias = "RadixThemes" + (component.tag or type(component).__name__) component.alias = "RadixThemes" + (component.tag or type(component).__name__)
# value = props.get("value")
# if value is not None and component.alias == "RadixThemesSelect.Root":
# lv = LiteralVar.create(value)
# print(repr(lv))
# print(f"Warning: Value {value} is not used in {component.alias}.")
return component return component
@staticmethod @staticmethod

View File

@ -427,7 +427,7 @@ class ColorModeSwitch(Switch):
color_scheme: Override theme color for switch color_scheme: Override theme color for switch
high_contrast: Whether to render the switch with higher contrast color against background high_contrast: Whether to render the switch with higher contrast color against background
radius: Override theme radius for switch: "none" | "small" | "full" radius: Override theme radius for switch: "none" | "small" | "full"
on_change: Props to rename Fired when the value of the switch changes on_change: Fired when the value of the switch changes
style: The style of the component. style: The style of the component.
key: A unique key for the component. key: A unique key for the component.
id: The id for the component. id: The id for the component.

View File

@ -153,7 +153,7 @@ class Checkbox(RadixThemesComponent):
required: Whether the checkbox is required required: Whether the checkbox is required
name: The name of the checkbox control when submitting the form. name: The name of the checkbox control when submitting the form.
value: The value of the checkbox control when submitting the form. value: The value of the checkbox control when submitting the form.
on_change: Props to rename Fired when the checkbox is checked or unchecked. on_change: Fired when the checkbox is checked or unchecked.
style: The style of the component. style: The style of the component.
key: A unique key for the component. key: A unique key for the component.
id: The id for the component. id: The id for the component.
@ -302,7 +302,7 @@ class HighLevelCheckbox(RadixThemesComponent):
required: Whether the checkbox is required required: Whether the checkbox is required
name: The name of the checkbox control when submitting the form. name: The name of the checkbox control when submitting the form.
value: The value of the checkbox control when submitting the form. value: The value of the checkbox control when submitting the form.
on_change: Props to rename Fired when the checkbox is checked or unchecked. on_change: Fired when the checkbox is checked or unchecked.
style: The style of the component. style: The style of the component.
key: A unique key for the component. key: A unique key for the component.
id: The id for the component. id: The id for the component.
@ -449,7 +449,7 @@ class CheckboxNamespace(ComponentNamespace):
required: Whether the checkbox is required required: Whether the checkbox is required
name: The name of the checkbox control when submitting the form. name: The name of the checkbox control when submitting the form.
value: The value of the checkbox control when submitting the form. value: The value of the checkbox control when submitting the form.
on_change: Props to rename Fired when the checkbox is checked or unchecked. on_change: Fired when the checkbox is checked or unchecked.
style: The style of the component. style: The style of the component.
key: A unique key for the component. key: A unique key for the component.
id: The id for the component. id: The id for the component.

View File

@ -8,6 +8,7 @@ from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spe
from reflex.vars.base import Var from reflex.vars.base import Var
from ..base import LiteralAccentColor, RadixThemesComponent from ..base import LiteralAccentColor, RadixThemesComponent
from .checkbox import Checkbox
LiteralDirType = Literal["ltr", "rtl"] LiteralDirType = Literal["ltr", "rtl"]
@ -232,6 +233,15 @@ class ContextMenuSeparator(RadixThemesComponent):
tag = "ContextMenu.Separator" tag = "ContextMenu.Separator"
class ContextMenuCheckbox(Checkbox):
"""The component that contains the checkbox."""
tag = "ContextMenu.CheckboxItem"
# Text to render as shortcut.
shortcut: Var[str]
class ContextMenu(ComponentNamespace): class ContextMenu(ComponentNamespace):
"""Menu representing a set of actions, displayed at the origin of a pointer right-click or long-press.""" """Menu representing a set of actions, displayed at the origin of a pointer right-click or long-press."""
@ -243,6 +253,7 @@ class ContextMenu(ComponentNamespace):
sub_content = staticmethod(ContextMenuSubContent.create) sub_content = staticmethod(ContextMenuSubContent.create)
item = staticmethod(ContextMenuItem.create) item = staticmethod(ContextMenuItem.create)
separator = staticmethod(ContextMenuSeparator.create) separator = staticmethod(ContextMenuSeparator.create)
checkbox = staticmethod(ContextMenuCheckbox.create)
context_menu = ContextMenu() context_menu = ContextMenu()

View File

@ -12,6 +12,7 @@ from reflex.style import Style
from reflex.vars.base import Var from reflex.vars.base import Var
from ..base import RadixThemesComponent from ..base import RadixThemesComponent
from .checkbox import Checkbox
LiteralDirType = Literal["ltr", "rtl"] LiteralDirType = Literal["ltr", "rtl"]
LiteralSizeType = Literal["1", "2"] LiteralSizeType = Literal["1", "2"]
@ -672,6 +673,159 @@ class ContextMenuSeparator(RadixThemesComponent):
""" """
... ...
class ContextMenuCheckbox(Checkbox):
@overload
@classmethod
def create( # type: ignore
cls,
*children,
shortcut: Optional[Union[Var[str], str]] = None,
as_child: Optional[Union[Var[bool], bool]] = None,
size: Optional[
Union[
Breakpoints[str, Literal["1", "2", "3"]],
Literal["1", "2", "3"],
Var[
Union[
Breakpoints[str, Literal["1", "2", "3"]], Literal["1", "2", "3"]
]
],
]
] = None,
variant: Optional[
Union[
Literal["classic", "soft", "surface"],
Var[Literal["classic", "soft", "surface"]],
]
] = None,
color_scheme: Optional[
Union[
Literal[
"amber",
"blue",
"bronze",
"brown",
"crimson",
"cyan",
"gold",
"grass",
"gray",
"green",
"indigo",
"iris",
"jade",
"lime",
"mint",
"orange",
"pink",
"plum",
"purple",
"red",
"ruby",
"sky",
"teal",
"tomato",
"violet",
"yellow",
],
Var[
Literal[
"amber",
"blue",
"bronze",
"brown",
"crimson",
"cyan",
"gold",
"grass",
"gray",
"green",
"indigo",
"iris",
"jade",
"lime",
"mint",
"orange",
"pink",
"plum",
"purple",
"red",
"ruby",
"sky",
"teal",
"tomato",
"violet",
"yellow",
]
],
]
] = None,
high_contrast: Optional[Union[Var[bool], bool]] = None,
default_checked: Optional[Union[Var[bool], bool]] = None,
checked: Optional[Union[Var[bool], bool]] = None,
disabled: Optional[Union[Var[bool], bool]] = None,
required: Optional[Union[Var[bool], bool]] = None,
name: Optional[Union[Var[str], str]] = None,
value: Optional[Union[Var[str], 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_change: Optional[
Union[EventType[[], BASE_STATE], EventType[[bool], 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,
) -> "ContextMenuCheckbox":
"""Create a new component instance.
Will prepend "RadixThemes" to the component tag to avoid conflicts with
other UI libraries for common names, like Text and Button.
Args:
*children: Child components.
shortcut: Text to render as shortcut.
as_child: Change the default rendered element for the one passed as a child, merging their props and behavior.
size: Checkbox size "1" - "3"
variant: Variant of checkbox: "classic" | "surface" | "soft"
color_scheme: Override theme color for checkbox
high_contrast: Whether to render the checkbox with higher contrast color against background
default_checked: Whether the checkbox is checked by default
checked: Whether the checkbox is checked
disabled: Whether the checkbox is disabled
required: Whether the checkbox is required
name: The name of the checkbox control when submitting the form.
value: The value of the checkbox control when submitting the form.
on_change: Fired when the checkbox is checked or unchecked.
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: Component properties.
Returns:
A new component instance.
"""
...
class ContextMenu(ComponentNamespace): class ContextMenu(ComponentNamespace):
root = staticmethod(ContextMenuRoot.create) root = staticmethod(ContextMenuRoot.create)
trigger = staticmethod(ContextMenuTrigger.create) trigger = staticmethod(ContextMenuTrigger.create)
@ -681,5 +835,6 @@ class ContextMenu(ComponentNamespace):
sub_content = staticmethod(ContextMenuSubContent.create) sub_content = staticmethod(ContextMenuSubContent.create)
item = staticmethod(ContextMenuItem.create) item = staticmethod(ContextMenuItem.create)
separator = staticmethod(ContextMenuSeparator.create) separator = staticmethod(ContextMenuSeparator.create)
checkbox = staticmethod(ContextMenuCheckbox.create)
context_menu = ContextMenu() context_menu = ContextMenu()

View File

@ -148,7 +148,7 @@ class RadioGroupRoot(RadixThemesComponent):
disabled: Whether the radio group is disabled disabled: Whether the radio group is disabled
name: The name of the group. Submitted with its owning form as part of a name/value pair. name: The name of the group. Submitted with its owning form as part of a name/value pair.
required: Whether the radio group is required required: Whether the radio group is required
on_change: Props to rename Fired when the value of the radio group changes. on_change: Fired when the value of the radio group changes.
style: The style of the component. style: The style of the component.
key: A unique key for the component. key: A unique key for the component.
id: The id for the component. id: The id for the component.

View File

@ -81,7 +81,7 @@ class SelectRoot(RadixThemesComponent):
name: The name of the select control when submitting the form. name: The name of the select control when submitting the form.
disabled: When True, prevents the user from interacting with select. disabled: When True, prevents the user from interacting with select.
required: When True, indicates that the user must select a value before the owning form can be submitted. required: When True, indicates that the user must select a value before the owning form can be submitted.
on_change: Props to rename Fired when the value of the select changes. on_change: Fired when the value of the select changes.
on_open_change: Fired when the select is opened or closed. on_open_change: Fired when the select is opened or closed.
style: The style of the component. style: The style of the component.
key: A unique key for the component. key: A unique key for the component.
@ -732,7 +732,7 @@ class HighLevelSelect(SelectRoot):
name: The name of the select control when submitting the form. name: The name of the select control when submitting the form.
disabled: When True, prevents the user from interacting with select. disabled: When True, prevents the user from interacting with select.
required: When True, indicates that the user must select a value before the owning form can be submitted. required: When True, indicates that the user must select a value before the owning form can be submitted.
on_change: Props to rename Fired when the value of the select changes. on_change: Fired when the value of the select changes.
on_open_change: Fired when the select is opened or closed. on_open_change: Fired when the select is opened or closed.
style: The style of the component. style: The style of the component.
key: A unique key for the component. key: A unique key for the component.
@ -912,7 +912,7 @@ class Select(ComponentNamespace):
name: The name of the select control when submitting the form. name: The name of the select control when submitting the form.
disabled: When True, prevents the user from interacting with select. disabled: When True, prevents the user from interacting with select.
required: When True, indicates that the user must select a value before the owning form can be submitted. required: When True, indicates that the user must select a value before the owning form can be submitted.
on_change: Props to rename Fired when the value of the select changes. on_change: Fired when the value of the select changes.
on_open_change: Fired when the select is opened or closed. on_open_change: Fired when the select is opened or closed.
style: The style of the component. style: The style of the component.
key: A unique key for the component. key: A unique key for the component.

View File

@ -195,7 +195,7 @@ class Slider(RadixThemesComponent):
step: The step value of the slider. step: The step value of the slider.
disabled: Whether the slider is disabled disabled: Whether the slider is disabled
orientation: The orientation of the slider. orientation: The orientation of the slider.
on_change: Props to rename Fired when the value of the slider changes. on_change: Fired when the value of the slider changes.
on_value_commit: Fired when a thumb is released after being dragged. on_value_commit: Fired when a thumb is released after being dragged.
style: The style of the component. style: The style of the component.
key: A unique key for the component. key: A unique key for the component.

View File

@ -157,7 +157,7 @@ class Switch(RadixThemesComponent):
color_scheme: Override theme color for switch color_scheme: Override theme color for switch
high_contrast: Whether to render the switch with higher contrast color against background high_contrast: Whether to render the switch with higher contrast color against background
radius: Override theme radius for switch: "none" | "small" | "full" radius: Override theme radius for switch: "none" | "small" | "full"
on_change: Props to rename Fired when the value of the switch changes on_change: Fired when the value of the switch changes
style: The style of the component. style: The style of the component.
key: A unique key for the component. key: A unique key for the component.
id: The id for the component. id: The id for the component.

View File

@ -72,7 +72,7 @@ class TabsRoot(RadixThemesComponent):
orientation: The orientation of the tabs. orientation: The orientation of the tabs.
dir: Reading direction of the tabs. dir: Reading direction of the tabs.
activation_mode: The mode of activation for the tabs. "automatic" will activate the tab when focused. "manual" will activate the tab when clicked. activation_mode: The mode of activation for the tabs. "automatic" will activate the tab when focused. "manual" will activate the tab when clicked.
on_change: Props to rename Fired when the value of the tabs changes. on_change: Fired when the value of the tabs changes.
style: The style of the component. style: The style of the component.
key: A unique key for the component. key: A unique key for the component.
id: The id for the component. id: The id for the component.
@ -374,7 +374,7 @@ class Tabs(ComponentNamespace):
orientation: The orientation of the tabs. orientation: The orientation of the tabs.
dir: Reading direction of the tabs. dir: Reading direction of the tabs.
activation_mode: The mode of activation for the tabs. "automatic" will activate the tab when focused. "manual" will activate the tab when clicked. activation_mode: The mode of activation for the tabs. "automatic" will activate the tab when focused. "manual" will activate the tab when clicked.
on_change: Props to rename Fired when the value of the tabs changes. on_change: Fired when the value of the tabs changes.
style: The style of the component. style: The style of the component.
key: A unique key for the component. key: A unique key for the component.
id: The id for the component. id: The id for the component.

View File

@ -9,7 +9,9 @@ from reflex.components.core.breakpoints import Responsive
from reflex.components.core.debounce import DebounceInput from reflex.components.core.debounce import DebounceInput
from reflex.components.el import elements from reflex.components.el import elements
from reflex.event import EventHandler, input_event, key_event from reflex.event import EventHandler, input_event, key_event
from reflex.utils.types import is_optional
from reflex.vars.base import Var from reflex.vars.base import Var
from reflex.vars.number import ternary_operation
from ..base import LiteralAccentColor, LiteralRadius, RadixThemesComponent from ..base import LiteralAccentColor, LiteralRadius, RadixThemesComponent
@ -96,6 +98,19 @@ class TextFieldRoot(elements.Div, RadixThemesComponent):
Returns: Returns:
The component. The component.
""" """
value = props.get("value")
# React expects an empty string(instead of null) for controlled inputs.
if value is not None and is_optional(
(value_var := Var.create(value))._var_type
):
props["value"] = ternary_operation(
(value_var != Var.create(None)) # pyright: ignore [reportGeneralTypeIssues]
& (value_var != Var(_js_expr="undefined")),
value,
Var.create(""),
)
component = super().create(*children, **props) component = super().create(*children, **props)
if props.get("value") is not None and props.get("on_change") is not None: if props.get("value") is not None and props.get("on_change") is not None:
# create a debounced input if the user requests full control to avoid typing jank # create a debounced input if the user requests full control to avoid typing jank

View File

@ -64,7 +64,6 @@ class BaseList(Component, MarkdownComponentMap):
Returns: Returns:
The list component. The list component.
""" """
items = props.pop("items", None) items = props.pop("items", None)
list_style_type = props.pop("list_style_type", "none") list_style_type = props.pop("list_style_type", "none")
@ -114,7 +113,6 @@ class UnorderedList(BaseList, Ul):
Returns: Returns:
The list component. The list component.
""" """
items = props.pop("items", None) items = props.pop("items", None)
list_style_type = props.pop("list_style_type", "disc") list_style_type = props.pop("list_style_type", "disc")
@ -144,7 +142,6 @@ class OrderedList(BaseList, Ol):
Returns: Returns:
The list component. The list component.
""" """
items = props.pop("items", None) items = props.pop("items", None)
list_style_type = props.pop("list_style_type", "decimal") list_style_type = props.pop("list_style_type", "decimal")
@ -168,7 +165,6 @@ class ListItem(Li, MarkdownComponentMap):
Returns: Returns:
The list item component. The list item component.
""" """
for child in children: for child in children:
if isinstance(child, Text): if isinstance(child, Text):

View File

@ -118,7 +118,6 @@ class BaseList(Component, MarkdownComponentMap):
Returns: Returns:
The list component. The list component.
""" """
... ...
@ -252,7 +251,6 @@ class UnorderedList(BaseList, Ul):
Returns: Returns:
The list component. The list component.
""" """
... ...
@ -390,7 +388,6 @@ class OrderedList(BaseList, Ol):
Returns: Returns:
The list component. The list component.
""" """
... ...
@ -477,7 +474,6 @@ class ListItem(Li, MarkdownComponentMap):
Returns: Returns:
The list item component. The list item component.
""" """
... ...
@ -571,7 +567,6 @@ class List(ComponentNamespace):
Returns: Returns:
The list component. The list component.
""" """
... ...

View File

@ -416,7 +416,7 @@ class Bar(Cartesian):
radius: Var[Union[int, List[int]]] radius: Var[Union[int, List[int]]]
# The active bar is shown when a user enters a bar chart and this chart has tooltip. If set to false, no active bar will be drawn. If set to true, active bar will be drawn with the props calculated internally. If passed an object, active bar will be drawn, and the internally calculated props will be merged with the key value pairs of the passed object. # The active bar is shown when a user enters a bar chart and this chart has tooltip. If set to false, no active bar will be drawn. If set to true, active bar will be drawn with the props calculated internally. If passed an object, active bar will be drawn, and the internally calculated props will be merged with the key value pairs of the passed object.
# active_bar: Var[Union[bool, Dict[str, Any]]] # active_bar: Var[Union[bool, Dict[str, Any]]] #noqa: ERA001
# Valid children components # Valid children components
_valid_children: List[str] = ["Cell", "LabelList", "ErrorBar"] _valid_children: List[str] = ["Cell", "LabelList", "ErrorBar"]

View File

@ -136,7 +136,7 @@ class Radar(Recharts):
# Fill color. Default: rx.color("accent", 3) # Fill color. Default: rx.color("accent", 3)
fill: Var[str] = LiteralVar.create(Color("accent", 3)) fill: Var[str] = LiteralVar.create(Color("accent", 3))
# opacity. Default: 0.6 # The opacity to fill the chart. Default: 0.6
fill_opacity: Var[float] = LiteralVar.create(0.6) fill_opacity: Var[float] = LiteralVar.create(0.6)
# The type of icon in legend. If set to 'none', no legend item will be rendered. Default: "rect" # The type of icon in legend. If set to 'none', no legend item will be rendered. Default: "rect"

View File

@ -204,7 +204,7 @@ class Radar(Recharts):
dot: If false set, dots will not be drawn. Default: True dot: If false set, dots will not be drawn. Default: True
stroke: Stoke color. Default: rx.color("accent", 9) stroke: Stoke color. Default: rx.color("accent", 9)
fill: Fill color. Default: rx.color("accent", 3) fill: Fill color. Default: rx.color("accent", 3)
fill_opacity: opacity. Default: 0.6 fill_opacity: The opacity to fill the chart. Default: 0.6
legend_type: The type of icon in legend. If set to 'none', no legend item will be rendered. Default: "rect" legend_type: The type of icon in legend. If set to 'none', no legend item will be rendered. Default: "rect"
label: If false set, labels will not be drawn. Default: True label: If false set, labels will not be drawn. Default: True
is_animation_active: If set false, animation of polygon will be disabled. Default: True in CSR, and False in SSR is_animation_active: If set false, animation of polygon will be disabled. Default: True in CSR, and False in SSR

View File

@ -98,7 +98,7 @@ class ToastProps(PropsBase, NoExtrasAllowedProps):
# TODO: fix serialization of icons for toast? (might not be possible yet) # TODO: fix serialization of icons for toast? (might not be possible yet)
# Icon displayed in front of toast's text, aligned vertically. # Icon displayed in front of toast's text, aligned vertically.
# icon: Optional[Icon] = None # icon: Optional[Icon] = None # noqa: ERA001
# TODO: fix implementation for action / cancel buttons # TODO: fix implementation for action / cancel buttons
# Renders a primary button, clicking it will close the toast. # Renders a primary button, clicking it will close the toast.
@ -364,9 +364,7 @@ class Toaster(Component):
return super().create(*children, **props) return super().create(*children, **props)
# TODO: figure out why loading toast stay open forever # TODO: figure out why loading toast stay open forever when using level="loading" in toast()
# def toast_loading(message: str, **kwargs):
# return _toast(message, level="loading", **kwargs)
class ToastNamespace(ComponentNamespace): class ToastNamespace(ComponentNamespace):
@ -379,7 +377,6 @@ class ToastNamespace(ComponentNamespace):
error = staticmethod(Toaster.toast_error) error = staticmethod(Toaster.toast_error)
success = staticmethod(Toaster.toast_success) success = staticmethod(Toaster.toast_success)
dismiss = staticmethod(Toaster.toast_dismiss) dismiss = staticmethod(Toaster.toast_dismiss)
# loading = staticmethod(toast_loading)
__call__ = staticmethod(Toaster.send_toast) __call__ = staticmethod(Toaster.send_toast)

View File

@ -116,7 +116,7 @@ class Editor(NoSSRComponent):
# Please refer to the library docs for this. # Please refer to the library docs for this.
# options: "en" | "da" | "de" | "es" | "fr" | "ja" | "ko" | "pt_br" | # options: "en" | "da" | "de" | "es" | "fr" | "ja" | "ko" | "pt_br" |
# "ru" | "zh_cn" | "ro" | "pl" | "ckb" | "lv" | "se" | "ua" | "he" | "it" # "ru" | "zh_cn" | "ro" | "pl" | "ckb" | "lv" | "se" | "ua" | "he" | "it"
# default : "en" # default: "en".
lang: Var[ lang: Var[
Union[ Union[
Literal[ Literal[
@ -172,7 +172,7 @@ class Editor(NoSSRComponent):
set_options: Var[Dict] set_options: Var[Dict]
# Whether all SunEditor plugins should be loaded. # Whether all SunEditor plugins should be loaded.
# default: True # default: True.
set_all_plugins: Var[bool] set_all_plugins: Var[bool]
# Set the content of the editor. # Set the content of the editor.
@ -191,19 +191,19 @@ class Editor(NoSSRComponent):
set_default_style: Var[str] set_default_style: Var[str]
# Disable the editor # Disable the editor
# default: False # default: False.
disable: Var[bool] disable: Var[bool]
# Hide the editor # Hide the editor
# default: False # default: False.
hide: Var[bool] hide: Var[bool]
# Hide the editor toolbar # Hide the editor toolbar
# default: False # default: False.
hide_toolbar: Var[bool] hide_toolbar: Var[bool]
# Disable the editor toolbar # Disable the editor toolbar
# default: False # default: False.
disable_toolbar: Var[bool] disable_toolbar: Var[bool]
# Fired when the editor content changes. # Fired when the editor content changes.

View File

@ -172,7 +172,7 @@ class Editor(NoSSRComponent):
Args: Args:
set_options(Optional[EditorOptions]): Configuration object to further configure the instance. set_options(Optional[EditorOptions]): Configuration object to further configure the instance.
lang: Language of the editor. Alternatively to a string, a dict of your language can be passed to this prop. Please refer to the library docs for this. options: "en" | "da" | "de" | "es" | "fr" | "ja" | "ko" | "pt_br" | "ru" | "zh_cn" | "ro" | "pl" | "ckb" | "lv" | "se" | "ua" | "he" | "it" default : "en" lang: Language of the editor. Alternatively to a string, a dict of your language can be passed to this prop. Please refer to the library docs for this. options: "en" | "da" | "de" | "es" | "fr" | "ja" | "ko" | "pt_br" | "ru" | "zh_cn" | "ro" | "pl" | "ckb" | "lv" | "se" | "ua" | "he" | "it" default: "en".
name: This is used to set the HTML form name of the editor. This means on HTML form submission, it will be submitted together with contents of the editor by the name provided. name: This is used to set the HTML form name of the editor. This means on HTML form submission, it will be submitted together with contents of the editor by the name provided.
default_value: Sets the default value of the editor. This is useful if you don't want the on_change method to be called on render. If you want the on_change method to be called on render please use the set_contents prop default_value: Sets the default value of the editor. This is useful if you don't want the on_change method to be called on render. If you want the on_change method to be called on render please use the set_contents prop
width: Sets the width of the editor. px and percentage values are accepted, eg width="100%" or width="500px" default: 100% width: Sets the width of the editor. px and percentage values are accepted, eg width="100%" or width="500px" default: 100%
@ -180,14 +180,14 @@ class Editor(NoSSRComponent):
placeholder: Sets the placeholder of the editor. placeholder: Sets the placeholder of the editor.
auto_focus: Should the editor receive focus when initialized? auto_focus: Should the editor receive focus when initialized?
set_options: Pass an EditorOptions instance to modify the behaviour of Editor even more. set_options: Pass an EditorOptions instance to modify the behaviour of Editor even more.
set_all_plugins: Whether all SunEditor plugins should be loaded. default: True set_all_plugins: Whether all SunEditor plugins should be loaded. default: True.
set_contents: Set the content of the editor. Note: To set the initial contents of the editor without calling the on_change event, please use the default_value prop. set_contents is used to set the contents of the editor programmatically. You must be aware that, when the set_contents's prop changes, the on_change event is triggered. set_contents: Set the content of the editor. Note: To set the initial contents of the editor without calling the on_change event, please use the default_value prop. set_contents is used to set the contents of the editor programmatically. You must be aware that, when the set_contents's prop changes, the on_change event is triggered.
append_contents: Append editor content append_contents: Append editor content
set_default_style: Sets the default style of the editor's edit area set_default_style: Sets the default style of the editor's edit area
disable: Disable the editor default: False disable: Disable the editor default: False.
hide: Hide the editor default: False hide: Hide the editor default: False.
hide_toolbar: Hide the editor toolbar default: False hide_toolbar: Hide the editor toolbar default: False.
disable_toolbar: Disable the editor toolbar default: False disable_toolbar: Disable the editor toolbar default: False.
on_change: Fired when the editor content changes. on_change: Fired when the editor content changes.
on_input: Fired when the something is inputted in the editor. on_input: Fired when the something is inputted in the editor.
on_blur: Fired when the editor loses focus. on_blur: Fired when the editor loses focus.

View File

@ -82,7 +82,7 @@ class DBConfig(Base):
) )
@classmethod @classmethod
def postgresql_psycopg2( def postgresql_psycopg(
cls, cls,
database: str, database: str,
username: str, username: str,
@ -90,7 +90,7 @@ class DBConfig(Base):
host: str | None = None, host: str | None = None,
port: int | None = 5432, port: int | None = 5432,
) -> DBConfig: ) -> DBConfig:
"""Create an instance with postgresql+psycopg2 engine. """Create an instance with postgresql+psycopg engine.
Args: Args:
database: Database name. database: Database name.
@ -103,7 +103,7 @@ class DBConfig(Base):
DBConfig instance. DBConfig instance.
""" """
return cls( return cls(
engine="postgresql+psycopg2", engine="postgresql+psycopg",
username=username, username=username,
password=password, password=password,
host=host, host=host,
@ -684,6 +684,9 @@ class Config(Base):
# Maximum expiration lock time for redis state manager # Maximum expiration lock time for redis state manager
redis_lock_expiration: int = constants.Expiration.LOCK redis_lock_expiration: int = constants.Expiration.LOCK
# Maximum lock time before warning for redis state manager.
redis_lock_warning_threshold: int = constants.Expiration.LOCK_WARNING_THRESHOLD
# Token expiration time for redis state manager # Token expiration time for redis state manager
redis_token_expiration: int = constants.Expiration.TOKEN redis_token_expiration: int = constants.Expiration.TOKEN

View File

@ -29,6 +29,8 @@ class Expiration(SimpleNamespace):
LOCK = 10000 LOCK = 10000
# The PING timeout # The PING timeout
PING = 120 PING = 120
# The maximum time in milliseconds to hold a lock before throwing a warning.
LOCK_WARNING_THRESHOLD = 1000
class GitIgnore(SimpleNamespace): class GitIgnore(SimpleNamespace):

View File

@ -1222,7 +1222,7 @@ def call_event_handler(
except TypeError: except TypeError:
# TODO: In 0.7.0, remove this block and raise the exception # TODO: In 0.7.0, remove this block and raise the exception
# raise TypeError( # raise TypeError(
# f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_handler.fn.__qualname__} provided for {key}." # f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_handler.fn.__qualname__} provided for {key}." # noqa: ERA001
# ) from e # ) from e
console.warn( console.warn(
f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_callback.fn.__qualname__} provided for {key}." f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_callback.fn.__qualname__} provided for {key}."

View File

@ -242,4 +242,5 @@ class ClientStateVar(Var):
""" """
if not self._global_ref: if not self._global_ref:
raise ValueError("ClientStateVar must be global to push the value.") raise ValueError("ClientStateVar must be global to push the value.")
value = Var.create(value)
return run_script(f"{_client_state_ref(self._setter_name)}({value})") return run_script(f"{_client_state_ref(self._setter_name)}({value})")

View File

@ -33,12 +33,6 @@ class Sidebar(Box, MemoizationLeaf):
Returns: Returns:
The sidebar component. The sidebar component.
""" """
# props.setdefault("border_right", f"1px solid {color('accent', 12)}")
# props.setdefault("background_color", color("accent", 1))
# props.setdefault("width", "20vw")
# props.setdefault("height", "100vh")
# props.setdefault("position", "fixed")
return super().create( return super().create(
Box.create(*children, **props), # sidebar for content Box.create(*children, **props), # sidebar for content
Box.create(width=props.get("width")), # spacer for layout Box.create(width=props.get("width")), # spacer for layout

View File

@ -11,6 +11,7 @@ import inspect
import json import json
import pickle import pickle
import sys import sys
import time
import typing import typing
import uuid import uuid
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
@ -39,6 +40,7 @@ from typing import (
get_type_hints, get_type_hints,
) )
from redis.asyncio.client import PubSub
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase
from typing_extensions import Self from typing_extensions import Self
@ -69,6 +71,11 @@ try:
except ModuleNotFoundError: except ModuleNotFoundError:
BaseModelV1 = BaseModelV2 BaseModelV1 = BaseModelV2
try:
from pydantic.v1 import validator
except ModuleNotFoundError:
from pydantic import validator
import wrapt import wrapt
from redis.asyncio import Redis from redis.asyncio import Redis
from redis.exceptions import ResponseError from redis.exceptions import ResponseError
@ -92,6 +99,7 @@ from reflex.utils.exceptions import (
DynamicRouteArgShadowsStateVar, DynamicRouteArgShadowsStateVar,
EventHandlerShadowsBuiltInStateMethod, EventHandlerShadowsBuiltInStateMethod,
ImmutableStateError, ImmutableStateError,
InvalidLockWarningThresholdError,
InvalidStateManagerMode, InvalidStateManagerMode,
LockExpiredError, LockExpiredError,
ReflexRuntimeError, ReflexRuntimeError,
@ -1095,6 +1103,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
if ( if (
not field.required not field.required
and field.default is None and field.default is None
and field.default_factory is None
and not types.is_optional(prop._var_type) and not types.is_optional(prop._var_type)
): ):
# Ensure frontend uses null coalescing when accessing. # Ensure frontend uses null coalescing when accessing.
@ -2122,14 +2131,26 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
state["__dict__"].pop("router", None) state["__dict__"].pop("router", None)
state["__dict__"].pop("router_data", None) state["__dict__"].pop("router_data", None)
# Never serialize parent_state or substates. # Never serialize parent_state or substates.
state["__dict__"]["parent_state"] = None state["__dict__"].pop("parent_state", None)
state["__dict__"]["substates"] = {} state["__dict__"].pop("substates", None)
state["__dict__"].pop("_was_touched", None) state["__dict__"].pop("_was_touched", None)
# Remove all inherited vars. # Remove all inherited vars.
for inherited_var_name in self.inherited_vars: for inherited_var_name in self.inherited_vars:
state["__dict__"].pop(inherited_var_name, None) state["__dict__"].pop(inherited_var_name, None)
return state return state
def __setstate__(self, state: dict[str, Any]):
"""Set the state from redis deserialization.
This method is called by pickle to deserialize the object.
Args:
state: The state dict for deserialization.
"""
state["__dict__"]["parent_state"] = None
state["__dict__"]["substates"] = {}
super().__setstate__(state)
def _check_state_size( def _check_state_size(
self, self,
pickle_state_size: int, pickle_state_size: int,
@ -2819,6 +2840,7 @@ class StateManager(Base, ABC):
redis=redis, redis=redis,
token_expiration=config.redis_token_expiration, token_expiration=config.redis_token_expiration,
lock_expiration=config.redis_lock_expiration, lock_expiration=config.redis_lock_expiration,
lock_warning_threshold=config.redis_lock_warning_threshold,
) )
raise InvalidStateManagerMode( raise InvalidStateManagerMode(
f"Expected one of: DISK, MEMORY, REDIS, got {config.state_manager_mode}" f"Expected one of: DISK, MEMORY, REDIS, got {config.state_manager_mode}"
@ -3188,6 +3210,15 @@ def _default_lock_expiration() -> int:
return get_config().redis_lock_expiration return get_config().redis_lock_expiration
def _default_lock_warning_threshold() -> int:
"""Get the default lock warning threshold.
Returns:
The default lock warning threshold.
"""
return get_config().redis_lock_warning_threshold
class StateManagerRedis(StateManager): class StateManagerRedis(StateManager):
"""A state manager that stores states in redis.""" """A state manager that stores states in redis."""
@ -3200,6 +3231,11 @@ class StateManagerRedis(StateManager):
# The maximum time to hold a lock (ms). # The maximum time to hold a lock (ms).
lock_expiration: int = pydantic.Field(default_factory=_default_lock_expiration) lock_expiration: int = pydantic.Field(default_factory=_default_lock_expiration)
# The maximum time to hold a lock (ms) before warning.
lock_warning_threshold: int = pydantic.Field(
default_factory=_default_lock_warning_threshold
)
# The keyspace subscription string when redis is waiting for lock to be released # The keyspace subscription string when redis is waiting for lock to be released
_redis_notify_keyspace_events: str = ( _redis_notify_keyspace_events: str = (
"K" # Enable keyspace notifications (target a particular key) "K" # Enable keyspace notifications (target a particular key)
@ -3387,6 +3423,17 @@ class StateManagerRedis(StateManager):
f"`app.state_manager.lock_expiration` (currently {self.lock_expiration}) " f"`app.state_manager.lock_expiration` (currently {self.lock_expiration}) "
"or use `@rx.event(background=True)` decorator for long-running tasks." "or use `@rx.event(background=True)` decorator for long-running tasks."
) )
elif lock_id is not None:
time_taken = self.lock_expiration / 1000 - (
await self.redis.ttl(self._lock_key(token))
)
if time_taken > self.lock_warning_threshold / 1000:
console.warn(
f"Lock for token {token} was held too long {time_taken=}s, "
f"use `@rx.event(background=True)` decorator for long-running tasks.",
dedupe=True,
)
client_token, substate_name = _split_substate_key(token) client_token, substate_name = _split_substate_key(token)
# If the substate name on the token doesn't match the instance name, it cannot have a parent. # If the substate name on the token doesn't match the instance name, it cannot have a parent.
if state.parent_state is not None and state.get_full_name() != substate_name: if state.parent_state is not None and state.get_full_name() != substate_name:
@ -3436,6 +3483,27 @@ class StateManagerRedis(StateManager):
yield state yield state
await self.set_state(token, state, lock_id) await self.set_state(token, state, lock_id)
@validator("lock_warning_threshold")
@classmethod
def validate_lock_warning_threshold(cls, lock_warning_threshold: int, values):
"""Validate the lock warning threshold.
Args:
lock_warning_threshold: The lock warning threshold.
values: The validated attributes.
Returns:
The lock warning threshold.
Raises:
InvalidLockWarningThresholdError: If the lock warning threshold is invalid.
"""
if lock_warning_threshold >= (lock_expiration := values["lock_expiration"]):
raise InvalidLockWarningThresholdError(
f"The lock warning threshold({lock_warning_threshold}) must be less than the lock expiration time({lock_expiration})."
)
return lock_warning_threshold
@staticmethod @staticmethod
def _lock_key(token: str) -> bytes: def _lock_key(token: str) -> bytes:
"""Get the redis key for a token's lock. """Get the redis key for a token's lock.
@ -3467,6 +3535,35 @@ class StateManagerRedis(StateManager):
nx=True, # only set if it doesn't exist nx=True, # only set if it doesn't exist
) )
async def _get_pubsub_message(
self, pubsub: PubSub, timeout: float | None = None
) -> None:
"""Get lock release events from the pubsub.
Args:
pubsub: The pubsub to get a message from.
timeout: Remaining time to wait for a message.
Returns:
The message.
"""
if timeout is None:
timeout = self.lock_expiration / 1000.0
started = time.time()
message = await pubsub.get_message(
ignore_subscribe_messages=True,
timeout=timeout,
)
if (
message is None
or message["data"] not in self._redis_keyspace_lock_release_events
):
remaining = timeout - (time.time() - started)
if remaining <= 0:
return
await self._get_pubsub_message(pubsub, timeout=remaining)
async def _wait_lock(self, lock_key: bytes, lock_id: bytes) -> None: async def _wait_lock(self, lock_key: bytes, lock_id: bytes) -> None:
"""Wait for a redis lock to be released via pubsub. """Wait for a redis lock to be released via pubsub.
@ -3479,7 +3576,6 @@ class StateManagerRedis(StateManager):
Raises: Raises:
ResponseError: when the keyspace config cannot be set. ResponseError: when the keyspace config cannot be set.
""" """
state_is_locked = False
lock_key_channel = f"__keyspace@0__:{lock_key.decode()}" lock_key_channel = f"__keyspace@0__:{lock_key.decode()}"
# Enable keyspace notifications for the lock key, so we know when it is available. # Enable keyspace notifications for the lock key, so we know when it is available.
try: try:
@ -3493,20 +3589,13 @@ class StateManagerRedis(StateManager):
raise raise
async with self.redis.pubsub() as pubsub: async with self.redis.pubsub() as pubsub:
await pubsub.psubscribe(lock_key_channel) await pubsub.psubscribe(lock_key_channel)
while not state_is_locked:
# wait for the lock to be released # wait for the lock to be released
while True: while True:
if not await self.redis.exists(lock_key): # fast path
break # key was removed, try to get the lock again if await self._try_get_lock(lock_key, lock_id):
message = await pubsub.get_message( return
ignore_subscribe_messages=True, # wait for lock events
timeout=self.lock_expiration / 1000.0, await self._get_pubsub_message(pubsub)
)
if message is None:
continue
if message["data"] in self._redis_keyspace_lock_release_events:
break
state_is_locked = await self._try_get_lock(lock_key, lock_id)
@contextlib.asynccontextmanager @contextlib.asynccontextmanager
async def _lock(self, token: str): async def _lock(self, token: str):

View File

@ -138,9 +138,6 @@ def convert_item(
if isinstance(style_item, Var): if isinstance(style_item, Var):
return style_item, style_item._get_all_var_data() return style_item, style_item._get_all_var_data()
# if isinstance(style_item, str) and REFLEX_VAR_OPENING_TAG not in style_item:
# return style_item, None
# Otherwise, convert to Var to collapse VarData encoded in f-string. # Otherwise, convert to Var to collapse VarData encoded in f-string.
new_var = LiteralVar.create(style_item) new_var = LiteralVar.create(style_item)
var_data = new_var._get_all_var_data() if new_var is not None else None var_data = new_var._get_all_var_data() if new_var is not None else None

View File

@ -206,7 +206,7 @@ class AppHarness:
The full state name The full state name
""" """
# NOTE: using State.get_name() somehow causes trouble here # NOTE: using State.get_name() somehow causes trouble here
# path = [State.get_name()] + [self.get_state_name(p) for p in path] # path = [State.get_name()] + [self.get_state_name(p) for p in path] # noqa: ERA001
path = ["reflex___state____state"] + [self.get_state_name(p) for p in path] path = ["reflex___state____state"] + [self.get_state_name(p) for p in path]
return ".".join(path) return ".".join(path)
@ -436,7 +436,6 @@ class AppHarness:
Returns: Returns:
The rendered app global code. The rendered app global code.
""" """
if not inspect.isclass(value) and not inspect.isfunction(value): if not inspect.isclass(value) and not inspect.isfunction(value):
return f"{key} = {value!r}" return f"{key} = {value!r}"

View File

@ -20,6 +20,24 @@ _EMITTED_DEPRECATION_WARNINGS = set()
# Info messages which have been printed. # Info messages which have been printed.
_EMITTED_INFO = set() _EMITTED_INFO = set()
# Warnings which have been printed.
_EMIITED_WARNINGS = set()
# Errors which have been printed.
_EMITTED_ERRORS = set()
# Success messages which have been printed.
_EMITTED_SUCCESS = set()
# Debug messages which have been printed.
_EMITTED_DEBUG = set()
# Logs which have been printed.
_EMITTED_LOGS = set()
# Prints which have been printed.
_EMITTED_PRINTS = set()
def set_log_level(log_level: LogLevel): def set_log_level(log_level: LogLevel):
"""Set the log level. """Set the log level.
@ -55,25 +73,37 @@ def is_debug() -> bool:
return _LOG_LEVEL <= LogLevel.DEBUG return _LOG_LEVEL <= LogLevel.DEBUG
def print(msg: str, **kwargs): def print(msg: str, dedupe: bool = False, **kwargs):
"""Print a message. """Print a message.
Args: Args:
msg: The message to print. msg: The message to print.
dedupe: If True, suppress multiple console logs of print message.
kwargs: Keyword arguments to pass to the print function. kwargs: Keyword arguments to pass to the print function.
""" """
if dedupe:
if msg in _EMITTED_PRINTS:
return
else:
_EMITTED_PRINTS.add(msg)
_console.print(msg, **kwargs) _console.print(msg, **kwargs)
def debug(msg: str, **kwargs): def debug(msg: str, dedupe: bool = False, **kwargs):
"""Print a debug message. """Print a debug message.
Args: Args:
msg: The debug message. msg: The debug message.
dedupe: If True, suppress multiple console logs of debug message.
kwargs: Keyword arguments to pass to the print function. kwargs: Keyword arguments to pass to the print function.
""" """
if is_debug(): if is_debug():
msg_ = f"[purple]Debug: {msg}[/purple]" msg_ = f"[purple]Debug: {msg}[/purple]"
if dedupe:
if msg_ in _EMITTED_DEBUG:
return
else:
_EMITTED_DEBUG.add(msg_)
if progress := kwargs.pop("progress", None): if progress := kwargs.pop("progress", None):
progress.console.print(msg_, **kwargs) progress.console.print(msg_, **kwargs)
else: else:
@ -97,25 +127,37 @@ def info(msg: str, dedupe: bool = False, **kwargs):
print(f"[cyan]Info: {msg}[/cyan]", **kwargs) print(f"[cyan]Info: {msg}[/cyan]", **kwargs)
def success(msg: str, **kwargs): def success(msg: str, dedupe: bool = False, **kwargs):
"""Print a success message. """Print a success message.
Args: Args:
msg: The success message. msg: The success message.
dedupe: If True, suppress multiple console logs of success message.
kwargs: Keyword arguments to pass to the print function. kwargs: Keyword arguments to pass to the print function.
""" """
if _LOG_LEVEL <= LogLevel.INFO: if _LOG_LEVEL <= LogLevel.INFO:
if dedupe:
if msg in _EMITTED_SUCCESS:
return
else:
_EMITTED_SUCCESS.add(msg)
print(f"[green]Success: {msg}[/green]", **kwargs) print(f"[green]Success: {msg}[/green]", **kwargs)
def log(msg: str, **kwargs): def log(msg: str, dedupe: bool = False, **kwargs):
"""Takes a string and logs it to the console. """Takes a string and logs it to the console.
Args: Args:
msg: The message to log. msg: The message to log.
dedupe: If True, suppress multiple console logs of log message.
kwargs: Keyword arguments to pass to the print function. kwargs: Keyword arguments to pass to the print function.
""" """
if _LOG_LEVEL <= LogLevel.INFO: if _LOG_LEVEL <= LogLevel.INFO:
if dedupe:
if msg in _EMITTED_LOGS:
return
else:
_EMITTED_LOGS.add(msg)
_console.log(msg, **kwargs) _console.log(msg, **kwargs)
@ -129,14 +171,20 @@ def rule(title: str, **kwargs):
_console.rule(title, **kwargs) _console.rule(title, **kwargs)
def warn(msg: str, **kwargs): def warn(msg: str, dedupe: bool = False, **kwargs):
"""Print a warning message. """Print a warning message.
Args: Args:
msg: The warning message. msg: The warning message.
dedupe: If True, suppress multiple console logs of warning message.
kwargs: Keyword arguments to pass to the print function. kwargs: Keyword arguments to pass to the print function.
""" """
if _LOG_LEVEL <= LogLevel.WARNING: if _LOG_LEVEL <= LogLevel.WARNING:
if dedupe:
if msg in _EMIITED_WARNINGS:
return
else:
_EMIITED_WARNINGS.add(msg)
print(f"[orange1]Warning: {msg}[/orange1]", **kwargs) print(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
@ -169,14 +217,20 @@ def deprecate(
_EMITTED_DEPRECATION_WARNINGS.add(feature_name) _EMITTED_DEPRECATION_WARNINGS.add(feature_name)
def error(msg: str, **kwargs): def error(msg: str, dedupe: bool = False, **kwargs):
"""Print an error message. """Print an error message.
Args: Args:
msg: The error message. msg: The error message.
dedupe: If True, suppress multiple console logs of error message.
kwargs: Keyword arguments to pass to the print function. kwargs: Keyword arguments to pass to the print function.
""" """
if _LOG_LEVEL <= LogLevel.ERROR: if _LOG_LEVEL <= LogLevel.ERROR:
if dedupe:
if msg in _EMITTED_ERRORS:
return
else:
_EMITTED_ERRORS.add(msg)
print(f"[red]{msg}[/red]", **kwargs) print(f"[red]{msg}[/red]", **kwargs)

View File

@ -63,6 +63,10 @@ class UploadValueError(ReflexError, ValueError):
"""Custom ValueError for upload related errors.""" """Custom ValueError for upload related errors."""
class PageValueError(ReflexError, ValueError):
"""Custom ValueError for page related errors."""
class RouteValueError(ReflexError, ValueError): class RouteValueError(ReflexError, ValueError):
"""Custom ValueError for route related errors.""" """Custom ValueError for route related errors."""
@ -179,3 +183,7 @@ def raise_system_package_missing_error(package: str) -> NoReturn:
" Please install it through your system package manager." " Please install it through your system package manager."
+ (f" You can do so by running 'brew install {package}'." if IS_MACOS else "") + (f" You can do so by running 'brew install {package}'." if IS_MACOS else "")
) )
class InvalidLockWarningThresholdError(ReflexError):
"""Raised when an invalid lock warning threshold is provided."""

View File

@ -664,18 +664,22 @@ def format_library_name(library_fullname: str):
return lib return lib
def json_dumps(obj: Any) -> str: def json_dumps(obj: Any, **kwargs) -> str:
"""Takes an object and returns a jsonified string. """Takes an object and returns a jsonified string.
Args: Args:
obj: The object to be serialized. obj: The object to be serialized.
kwargs: Additional keyword arguments to pass to json.dumps.
Returns: Returns:
A string A string
""" """
from reflex.utils import serializers from reflex.utils import serializers
return json.dumps(obj, ensure_ascii=False, default=serializers.serialize) kwargs.setdefault("ensure_ascii", False)
kwargs.setdefault("default", serializers.serialize)
return json.dumps(obj, **kwargs)
def collect_form_dict_names(form_dict: dict[str, Any]) -> dict[str, Any]: def collect_form_dict_names(form_dict: dict[str, Any]) -> dict[str, Any]:
@ -712,7 +716,6 @@ def format_array_ref(refs: str, idx: Var | None) -> str:
""" """
clean_ref = re.sub(r"[^\w]+", "_", refs) clean_ref = re.sub(r"[^\w]+", "_", refs)
if idx is not None: if idx is not None:
# idx._var_is_local = True
return f"refs_{clean_ref}[{idx!s}]" return f"refs_{clean_ref}[{idx!s}]"
return f"refs_{clean_ref}" return f"refs_{clean_ref}"

View File

@ -196,12 +196,7 @@ def _get_type_hint(value, type_hint_globals, is_optional=True) -> str:
elif isinstance(value, str): elif isinstance(value, str):
ev = eval(value, type_hint_globals) ev = eval(value, type_hint_globals)
if rx_types.is_optional(ev): if rx_types.is_optional(ev):
# hints = {
# _get_type_hint(arg, type_hint_globals, is_optional=False)
# for arg in ev.__args__
# }
return _get_type_hint(ev, type_hint_globals, is_optional=False) return _get_type_hint(ev, type_hint_globals, is_optional=False)
# return f"Optional[{', '.join(hints)}]"
if rx_types.is_union(ev): if rx_types.is_union(ev):
res = [ res = [
@ -260,8 +255,15 @@ def _generate_docstrings(clzs: list[Type[Component]], props: list[str]) -> str:
# We've reached the functions, so stop. # We've reached the functions, so stop.
break break
if line == "":
# We hit a blank line, so clear comments to avoid commented out prop appearing in next prop docs.
comments.clear()
continue
# Get comments for prop # Get comments for prop
if line.strip().startswith("#"): if line.strip().startswith("#"):
# Remove noqa from the comments.
line = line.partition(" # noqa")[0]
comments.append(line) comments.append(line)
continue continue

View File

@ -97,7 +97,6 @@ StateIterVar = Union[list, set, tuple]
if TYPE_CHECKING: if TYPE_CHECKING:
from reflex.vars.base import Var from reflex.vars.base import Var
# ArgsSpec = Callable[[Var], list[Var]]
ArgsSpec = ( ArgsSpec = (
Callable[[], Sequence[Var]] Callable[[], Sequence[Var]]
| Callable[[Var], Sequence[Var]] | Callable[[Var], Sequence[Var]]
@ -331,7 +330,11 @@ def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None
type_ = field.outer_type_ type_ = field.outer_type_
if isinstance(type_, ModelField): if isinstance(type_, ModelField):
type_ = type_.type_ type_ = type_.type_
if not field.required and field.default is None: if (
not field.required
and field.default is None
and field.default_factory is None
):
# Ensure frontend uses null coalescing when accessing. # Ensure frontend uses null coalescing when accessing.
type_ = Optional[type_] type_ = Optional[type_]
return type_ return type_

View File

@ -6,6 +6,7 @@ from pathlib import Path
import pytest import pytest
import reflex.app
from reflex.config import environment from reflex.config import environment
from reflex.testing import AppHarness, AppHarnessProd from reflex.testing import AppHarness, AppHarnessProd
@ -76,3 +77,25 @@ def app_harness_env(request):
The AppHarness class to use for the test. The AppHarness class to use for the test.
""" """
return request.param return request.param
@pytest.fixture(autouse=True)
def raise_console_error(request, mocker):
"""Spy on calls to `console.error` used by the framework.
Help catch spurious error conditions that might otherwise go unnoticed.
If a test is marked with `ignore_console_error`, the spy will be ignored
after the test.
Args:
request: The pytest request object.
mocker: The pytest mocker object.
Yields:
control to the test function.
"""
spy = mocker.spy(reflex.app.console, "error")
yield
if "ignore_console_error" not in request.keywords:
spy.assert_not_called()

View File

@ -637,8 +637,7 @@ async def test_client_side_state(
assert await AppHarness._poll_for_async(poll_for_not_hydrated) assert await AppHarness._poll_for_async(poll_for_not_hydrated)
# Trigger event to get a new instance of the state since the old was expired. # Trigger event to get a new instance of the state since the old was expired.
state_var_input = driver.find_element(By.ID, "state_var") set_sub("c1", "c1 post expire")
state_var_input.send_keys("re-triggering")
# get new references to all cookie and local storage elements (again) # get new references to all cookie and local storage elements (again)
c1 = driver.find_element(By.ID, "c1") c1 = driver.find_element(By.ID, "c1")
@ -659,7 +658,7 @@ async def test_client_side_state(
l1s = driver.find_element(By.ID, "l1s") l1s = driver.find_element(By.ID, "l1s")
s1s = driver.find_element(By.ID, "s1s") s1s = driver.find_element(By.ID, "s1s")
assert c1.text == "c1 value" assert c1.text == "c1 post expire"
assert c2.text == "c2 value" assert c2.text == "c2 value"
assert c3.text == "" # temporary cookie expired after reset state! assert c3.text == "" # temporary cookie expired after reset state!
assert c4.text == "c4 value" assert c4.text == "c4 value"
@ -690,11 +689,11 @@ async def test_client_side_state(
async def poll_for_c1_set(): async def poll_for_c1_set():
sub_state = await get_sub_state() sub_state = await get_sub_state()
return sub_state.c1 == "c1 value" return sub_state.c1 == "c1 post expire"
assert await AppHarness._poll_for_async(poll_for_c1_set) assert await AppHarness._poll_for_async(poll_for_c1_set)
sub_state = await get_sub_state() sub_state = await get_sub_state()
assert sub_state.c1 == "c1 value" assert sub_state.c1 == "c1 post expire"
assert sub_state.c2 == "c2 value" assert sub_state.c2 == "c2 value"
assert sub_state.c3 == "" assert sub_state.c3 == ""
assert sub_state.c4 == "c4 value" assert sub_state.c4 == "c4 value"

View File

@ -106,7 +106,6 @@ def ComputedVars():
), ),
) )
# raise Exception(State.count3._deps(objclass=State))
app = rx.App() app = rx.App()
app.add_page(index) app.add_page(index)

View File

@ -13,6 +13,8 @@ from selenium.webdriver.support.ui import WebDriverWait
from reflex.testing import AppHarness, AppHarnessProd from reflex.testing import AppHarness, AppHarnessProd
pytestmark = [pytest.mark.ignore_console_error]
def TestApp(): def TestApp():
"""A test app for event exception handler integration.""" """A test app for event exception handler integration."""

View File

@ -183,6 +183,6 @@ async def test_fully_controlled_input(fully_controlled_input: AppHarness):
clear_button.click() clear_button.click()
assert AppHarness._poll_for(lambda: on_change_input.get_attribute("value") == "") assert AppHarness._poll_for(lambda: on_change_input.get_attribute("value") == "")
# potential bug: clearing the on_change field doesn't itself trigger on_change # potential bug: clearing the on_change field doesn't itself trigger on_change
# assert backend_state.text == "" # assert backend_state.text == "" #noqa: ERA001
# assert debounce_input.get_attribute("value") == "" # assert debounce_input.get_attribute("value") == "" #noqa: ERA001
# assert value_input.get_attribute("value") == "" # assert value_input.get_attribute("value") == "" #noqa: ERA001

View File

@ -381,9 +381,22 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive
await asyncio.sleep(0.3) await asyncio.sleep(0.3)
cancel_button.click() cancel_button.click()
# look up the backend state and assert on progress # Wait a bit for the upload to get cancelled.
await asyncio.sleep(0.5)
# Get interim progress dicts saved in the on_upload_progress handler.
async def _progress_dicts():
state = await upload_file.get_state(substate_token)
return state.substates[state_name].progress_dicts
# We should have _some_ progress
assert await AppHarness._poll_for_async(_progress_dicts)
# But there should never be a final progress record for a cancelled upload.
for p in await _progress_dicts():
assert p["progress"] != 1
state = await upload_file.get_state(substate_token) state = await upload_file.get_state(substate_token)
assert state.substates[state_name].progress_dicts
file_data = state.substates[state_name]._file_data file_data = state.substates[state_name]._file_data
assert isinstance(file_data, dict) assert isinstance(file_data, dict)
normalized_file_data = {Path(k).name: v for k, v in file_data.items()} normalized_file_data = {Path(k).name: v for k, v in file_data.items()}

View File

@ -13,9 +13,6 @@ STATE_VAR = Var(_js_expr="default_state.name")
("{}", '{"{}"}'), ("{}", '{"{}"}'),
(None, '{""}'), (None, '{""}'),
(STATE_VAR, "{default_state.name}"), (STATE_VAR, "{default_state.name}"),
# This behavior is now unsupported.
# ("${default_state.name}", "${default_state.name}"),
# ("{state.name}", "{state.name}"),
], ],
) )
def test_fstrings(contents, expected): def test_fstrings(contents, expected):

View File

@ -1,8 +1,10 @@
from typing import Dict, List, Set, Tuple, Union from typing import Dict, List, Set, Tuple, Union
import pydantic.v1
import pytest import pytest
from reflex import el from reflex import el
from reflex.base import Base
from reflex.components.component import Component from reflex.components.component import Component
from reflex.components.core.foreach import ( from reflex.components.core.foreach import (
Foreach, Foreach,
@ -18,6 +20,12 @@ from reflex.vars.number import NumberVar
from reflex.vars.sequence import ArrayVar from reflex.vars.sequence import ArrayVar
class ForEachTag(Base):
"""A tag for testing the ForEach component."""
name: str = ""
class ForEachState(BaseState): class ForEachState(BaseState):
"""A state for testing the ForEach component.""" """A state for testing the ForEach component."""
@ -46,6 +54,8 @@ class ForEachState(BaseState):
bad_annotation_list: list = [["red", "orange"], ["yellow", "blue"]] bad_annotation_list: list = [["red", "orange"], ["yellow", "blue"]]
color_index_tuple: Tuple[int, str] = (0, "red") color_index_tuple: Tuple[int, str] = (0, "red")
default_factory_list: list[ForEachTag] = pydantic.v1.Field(default_factory=list)
class ComponentStateTest(ComponentState): class ComponentStateTest(ComponentState):
"""A test component state.""" """A test component state."""
@ -290,3 +300,11 @@ def test_foreach_component_state():
ForEachState.colors_list, ForEachState.colors_list,
ComponentStateTest.create, ComponentStateTest.create,
) )
def test_foreach_default_factory():
"""Test that the default factory is called."""
_ = Foreach.create(
ForEachState.default_factory_list,
lambda tag: text(tag.name),
)

View File

@ -42,7 +42,7 @@ def test_set_src_str():
"`pic2.jpeg`", "`pic2.jpeg`",
) )
# For plain rx.el.img, an explicit var is not created, so the quoting happens later # For plain rx.el.img, an explicit var is not created, so the quoting happens later
# assert str(image.src) == "pic2.jpeg" # type: ignore # assert str(image.src) == "pic2.jpeg" # type: ignore #noqa: ERA001
def test_set_src_img(pil_image: Img): def test_set_src_img(pil_image: Img):

View File

@ -918,17 +918,17 @@ def test_invalid_event_handler_args(component2, test_state):
# # Event Handler types must match # # Event Handler types must match
# with pytest.raises(EventHandlerArgTypeMismatch): # with pytest.raises(EventHandlerArgTypeMismatch):
# component2.create( # component2.create(
# on_user_visited_count_changed=test_state.do_something_with_bool # on_user_visited_count_changed=test_state.do_something_with_bool # noqa: ERA001 RUF100
# ) # ) # noqa: ERA001 RUF100
# with pytest.raises(EventHandlerArgTypeMismatch): # with pytest.raises(EventHandlerArgTypeMismatch):
# component2.create(on_user_list_changed=test_state.do_something_with_int) # component2.create(on_user_list_changed=test_state.do_something_with_int) #noqa: ERA001
# with pytest.raises(EventHandlerArgTypeMismatch): # with pytest.raises(EventHandlerArgTypeMismatch):
# component2.create(on_user_list_changed=test_state.do_something_with_list_int) # component2.create(on_user_list_changed=test_state.do_something_with_list_int) #noqa: ERA001
# component2.create(on_open=test_state.do_something_with_int) # component2.create(on_open=test_state.do_something_with_int) #noqa: ERA001
# component2.create(on_open=test_state.do_something_with_bool) # component2.create(on_open=test_state.do_something_with_bool) #noqa: ERA001
# component2.create(on_user_visited_count_changed=test_state.do_something_with_int) # component2.create(on_user_visited_count_changed=test_state.do_something_with_int) #noqa: ERA001
# component2.create(on_user_list_changed=test_state.do_something_with_list_str) # component2.create(on_user_list_changed=test_state.do_something_with_list_str) #noqa: ERA001
# lambda cannot return weird values. # lambda cannot return weird values.
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -1437,8 +1437,6 @@ def test_get_vars(component, exp_vars):
comp_vars, comp_vars,
sorted(exp_vars, key=lambda v: v._js_expr), sorted(exp_vars, key=lambda v: v._js_expr),
): ):
# print(str(comp_var), str(exp_var))
# print(comp_var._get_all_var_data(), exp_var._get_all_var_data())
assert comp_var.equals(exp_var) assert comp_var.equals(exp_var)

View File

@ -899,8 +899,6 @@ class DynamicState(BaseState):
loaded: int = 0 loaded: int = 0
counter: int = 0 counter: int = 0
# side_effect_counter: int = 0
def on_load(self): def on_load(self):
"""Event handler for page on_load, should trigger for all navigation events.""" """Event handler for page on_load, should trigger for all navigation events."""
self.loaded = self.loaded + 1 self.loaded = self.loaded + 1
@ -917,7 +915,6 @@ class DynamicState(BaseState):
Returns: Returns:
same as self.dynamic same as self.dynamic
""" """
# self.side_effect_counter = self.side_effect_counter + 1
return self.dynamic return self.dynamic
on_load_internal = OnLoadInternalState.on_load_internal.fn on_load_internal = OnLoadInternalState.on_load_internal.fn
@ -1059,7 +1056,6 @@ async def test_dynamic_route_var_route_change_completed_on_load(
arg_name: exp_val, arg_name: exp_val,
f"comp_{arg_name}": exp_val, f"comp_{arg_name}": exp_val,
constants.CompileVars.IS_HYDRATED: False, constants.CompileVars.IS_HYDRATED: False,
# "side_effect_counter": exp_index,
"router": exp_router, "router": exp_router,
} }
}, },
@ -1155,8 +1151,6 @@ async def test_dynamic_route_var_route_change_completed_on_load(
state = await app.state_manager.get_state(substate_token) state = await app.state_manager.get_state(substate_token)
assert state.loaded == len(exp_vals) assert state.loaded == len(exp_vals)
assert state.counter == len(exp_vals) assert state.counter == len(exp_vals)
# print(f"Expected {exp_vals} rendering side effects, got {state.side_effect_counter}")
# assert state.side_effect_counter == len(exp_vals)
if isinstance(app.state_manager, StateManagerRedis): if isinstance(app.state_manager, StateManagerRedis):
await app.state_manager.close() await app.state_manager.close()

View File

@ -3,11 +3,19 @@ from __future__ import annotations
from typing import Dict, List, Optional, Type, Union from typing import Dict, List, Optional, Type, Union
import attrs import attrs
import pydantic.v1
import pytest import pytest
import sqlalchemy import sqlalchemy
import sqlmodel
from sqlalchemy import JSON, TypeDecorator from sqlalchemy import JSON, TypeDecorator
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship from sqlalchemy.orm import (
DeclarativeBase,
Mapped,
MappedAsDataclass,
mapped_column,
relationship,
)
import reflex as rx import reflex as rx
from reflex.utils.types import GenericType, get_attribute_access_type from reflex.utils.types import GenericType, get_attribute_access_type
@ -53,6 +61,10 @@ class SQLALabel(SQLABase):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
test_id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey("test.id")) test_id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey("test.id"))
test: Mapped[SQLAClass] = relationship(back_populates="labels") test: Mapped[SQLAClass] = relationship(back_populates="labels")
test_dataclass_id: Mapped[int] = mapped_column(
sqlalchemy.ForeignKey("test_dataclass.id")
)
test_dataclass: Mapped[SQLAClassDataclass] = relationship(back_populates="labels")
class SQLAClass(SQLABase): class SQLAClass(SQLABase):
@ -104,9 +116,64 @@ class SQLAClass(SQLABase):
return self.labels[0] if self.labels else None return self.labels[0] if self.labels else None
class SQLAClassDataclass(MappedAsDataclass, SQLABase):
"""Test sqlalchemy model."""
id: Mapped[int] = mapped_column(primary_key=True)
no_default: Mapped[int] = mapped_column(nullable=True)
count: Mapped[int] = mapped_column()
name: Mapped[str] = mapped_column()
int_list: Mapped[List[int]] = mapped_column(
sqlalchemy.types.ARRAY(item_type=sqlalchemy.INTEGER)
)
str_list: Mapped[List[str]] = mapped_column(
sqlalchemy.types.ARRAY(item_type=sqlalchemy.String)
)
optional_int: Mapped[Optional[int]] = mapped_column(nullable=True)
sqla_tag_id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey(SQLATag.id))
sqla_tag: Mapped[Optional[SQLATag]] = relationship()
labels: Mapped[List[SQLALabel]] = relationship(back_populates="test_dataclass")
# do not use lower case dict here!
# https://github.com/sqlalchemy/sqlalchemy/issues/9902
dict_str_str: Mapped[Dict[str, str]] = mapped_column()
default_factory: Mapped[List[int]] = mapped_column(
sqlalchemy.types.ARRAY(item_type=sqlalchemy.INTEGER),
default_factory=list,
)
__tablename__: str = "test_dataclass"
@property
def str_property(self) -> str:
"""String property.
Returns:
Name attribute
"""
return self.name
@hybrid_property
def str_or_int_property(self) -> Union[str, int]:
"""String or int property.
Returns:
Name attribute
"""
return self.name
@hybrid_property
def first_label(self) -> Optional[SQLALabel]:
"""First label property.
Returns:
First label
"""
return self.labels[0] if self.labels else None
class ModelClass(rx.Model): class ModelClass(rx.Model):
"""Test reflex model.""" """Test reflex model."""
no_default: Optional[int] = sqlmodel.Field(nullable=True)
count: int = 0 count: int = 0
name: str = "test" name: str = "test"
int_list: List[int] = [] int_list: List[int] = []
@ -115,6 +182,7 @@ class ModelClass(rx.Model):
sqla_tag: Optional[SQLATag] = None sqla_tag: Optional[SQLATag] = None
labels: List[SQLALabel] = [] labels: List[SQLALabel] = []
dict_str_str: Dict[str, str] = {} dict_str_str: Dict[str, str] = {}
default_factory: List[int] = sqlmodel.Field(default_factory=list)
@property @property
def str_property(self) -> str: def str_property(self) -> str:
@ -147,6 +215,7 @@ class ModelClass(rx.Model):
class BaseClass(rx.Base): class BaseClass(rx.Base):
"""Test rx.Base class.""" """Test rx.Base class."""
no_default: Optional[int] = pydantic.v1.Field(required=False)
count: int = 0 count: int = 0
name: str = "test" name: str = "test"
int_list: List[int] = [] int_list: List[int] = []
@ -155,6 +224,7 @@ class BaseClass(rx.Base):
sqla_tag: Optional[SQLATag] = None sqla_tag: Optional[SQLATag] = None
labels: List[SQLALabel] = [] labels: List[SQLALabel] = []
dict_str_str: Dict[str, str] = {} dict_str_str: Dict[str, str] = {}
default_factory: List[int] = pydantic.v1.Field(default_factory=list)
@property @property
def str_property(self) -> str: def str_property(self) -> str:
@ -236,6 +306,7 @@ class AttrClass:
sqla_tag: Optional[SQLATag] = None sqla_tag: Optional[SQLATag] = None
labels: List[SQLALabel] = [] labels: List[SQLALabel] = []
dict_str_str: Dict[str, str] = {} dict_str_str: Dict[str, str] = {}
default_factory: List[int] = attrs.field(factory=list)
@property @property
def str_property(self) -> str: def str_property(self) -> str:
@ -265,27 +336,17 @@ class AttrClass:
return self.labels[0] if self.labels else None return self.labels[0] if self.labels else None
@pytest.fixture( @pytest.mark.parametrize(
params=[ "cls",
[
SQLAClass, SQLAClass,
SQLAClassDataclass,
BaseClass, BaseClass,
BareClass, BareClass,
ModelClass, ModelClass,
AttrClass, AttrClass,
] ],
) )
def cls(request: pytest.FixtureRequest) -> type:
"""Fixture for the class to test.
Args:
request: pytest request object.
Returns:
Class to test.
"""
return request.param
@pytest.mark.parametrize( @pytest.mark.parametrize(
"attr, expected", "attr, expected",
[ [
@ -311,3 +372,38 @@ def test_get_attribute_access_type(cls: type, attr: str, expected: GenericType)
expected: Expected type. expected: Expected type.
""" """
assert get_attribute_access_type(cls, attr) == expected assert get_attribute_access_type(cls, attr) == expected
@pytest.mark.parametrize(
"cls",
[
SQLAClassDataclass,
BaseClass,
ModelClass,
AttrClass,
],
)
def test_get_attribute_access_type_default_factory(cls: type) -> None:
"""Test get_attribute_access_type returns the correct type for default factory fields.
Args:
cls: Class to test.
"""
assert get_attribute_access_type(cls, "default_factory") == List[int]
@pytest.mark.parametrize(
"cls",
[
SQLAClassDataclass,
BaseClass,
ModelClass,
],
)
def test_get_attribute_access_type_no_default(cls: type) -> None:
"""Test get_attribute_access_type returns the correct type for fields with no default which are not required.
Args:
cls: Class to test.
"""
assert get_attribute_access_type(cls, "no_default") == Optional[int]

View File

@ -164,7 +164,7 @@ def test_constructor_postgresql(username, password, host, port, database, expect
"localhost", "localhost",
5432, 5432,
"db", "db",
"postgresql+psycopg2://user:pass@localhost:5432/db", "postgresql+psycopg://user:pass@localhost:5432/db",
), ),
( (
"user", "user",
@ -172,17 +172,17 @@ def test_constructor_postgresql(username, password, host, port, database, expect
"localhost", "localhost",
None, None,
"db", "db",
"postgresql+psycopg2://user@localhost/db", "postgresql+psycopg://user@localhost/db",
), ),
("user", "", "", None, "db", "postgresql+psycopg2://user@/db"), ("user", "", "", None, "db", "postgresql+psycopg://user@/db"),
("", "", "localhost", 5432, "db", "postgresql+psycopg2://localhost:5432/db"), ("", "", "localhost", 5432, "db", "postgresql+psycopg://localhost:5432/db"),
("", "", "", None, "db", "postgresql+psycopg2:///db"), ("", "", "", None, "db", "postgresql+psycopg:///db"),
], ],
) )
def test_constructor_postgresql_psycopg2( def test_constructor_postgresql_psycopg(
username, password, host, port, database, expected_url username, password, host, port, database, expected_url
): ):
"""Test DBConfig.postgresql_psycopg2 constructor creates the instance correctly. """Test DBConfig.postgresql_psycopg constructor creates the instance correctly.
Args: Args:
username: Database username. username: Database username.
@ -192,10 +192,10 @@ def test_constructor_postgresql_psycopg2(
database: Database name. database: Database name.
expected_url: Expected database URL generated. expected_url: Expected database URL generated.
""" """
db_config = DBConfig.postgresql_psycopg2( db_config = DBConfig.postgresql_psycopg(
username=username, password=password, host=host, port=port, database=database username=username, password=password, host=host, port=port, database=database
) )
assert db_config.engine == "postgresql+psycopg2" assert db_config.engine == "postgresql+psycopg"
assert db_config.username == username assert db_config.username == username
assert db_config.password == password assert db_config.password == password
assert db_config.host == host assert db_config.host == host

View File

@ -209,10 +209,6 @@ def test_event_redirect(input, output):
assert isinstance(spec, EventSpec) assert isinstance(spec, EventSpec)
assert spec.handler.fn.__qualname__ == "_redirect" assert spec.handler.fn.__qualname__ == "_redirect"
# this asserts need comment about what it's testing (they fail with Var as input)
# assert spec.args[0][0].equals(Var(_js_expr="path"))
# assert spec.args[0][1].equals(Var(_js_expr="/path"))
assert format.format_event(spec) == output assert format.format_event(spec) == output

View File

@ -127,8 +127,8 @@ def test_automigration(
assert result[0].b == 4.2 assert result[0].b == 4.2
# No-op # No-op
# assert Model.migrate(autogenerate=True) # assert Model.migrate(autogenerate=True) #noqa: ERA001
# assert len(list(versions.glob("*.py"))) == 4 # assert len(list(versions.glob("*.py"))) == 4 #noqa: ERA001
# drop table (AlembicSecond) # drop table (AlembicSecond)
model_registry.get_metadata().clear() model_registry.get_metadata().clear()

View File

@ -56,6 +56,7 @@ from reflex.state import (
from reflex.testing import chdir from reflex.testing import chdir
from reflex.utils import format, prerequisites, types from reflex.utils import format, prerequisites, types
from reflex.utils.exceptions import ( from reflex.utils.exceptions import (
InvalidLockWarningThresholdError,
ReflexRuntimeError, ReflexRuntimeError,
SetUndefinedStateVarError, SetUndefinedStateVarError,
StateSerializationError, StateSerializationError,
@ -67,7 +68,9 @@ from tests.units.states.mutation import MutableSQLAModel, MutableTestState
from .states import GenState from .states import GenState
CI = bool(os.environ.get("CI", False)) CI = bool(os.environ.get("CI", False))
LOCK_EXPIRATION = 2000 if CI else 300 LOCK_EXPIRATION = 2500 if CI else 300
LOCK_WARNING_THRESHOLD = 1000 if CI else 100
LOCK_WARN_SLEEP = 1.5 if CI else 0.15
LOCK_EXPIRE_SLEEP = 2.5 if CI else 0.4 LOCK_EXPIRE_SLEEP = 2.5 if CI else 0.4
@ -791,7 +794,6 @@ async def test_process_event_simple(test_state):
assert test_state.num1 == 69 assert test_state.num1 == 69
# The delta should contain the changes, including computed vars. # The delta should contain the changes, including computed vars.
# assert update.delta == {"test_state": {"num1": 69, "sum": 72.14}}
assert update.delta == { assert update.delta == {
TestState.get_full_name(): {"num1": 69, "sum": 72.14, "upper": ""}, TestState.get_full_name(): {"num1": 69, "sum": 72.14, "upper": ""},
GrandchildState3.get_full_name(): {"computed": ""}, GrandchildState3.get_full_name(): {"computed": ""},
@ -1788,6 +1790,7 @@ async def test_state_manager_lock_expire(
substate_token_redis: A token + substate name for looking up in state manager. substate_token_redis: A token + substate name for looking up in state manager.
""" """
state_manager_redis.lock_expiration = LOCK_EXPIRATION state_manager_redis.lock_expiration = LOCK_EXPIRATION
state_manager_redis.lock_warning_threshold = LOCK_WARNING_THRESHOLD
async with state_manager_redis.modify_state(substate_token_redis): async with state_manager_redis.modify_state(substate_token_redis):
await asyncio.sleep(0.01) await asyncio.sleep(0.01)
@ -1812,6 +1815,7 @@ async def test_state_manager_lock_expire_contend(
unexp_num1 = 666 unexp_num1 = 666
state_manager_redis.lock_expiration = LOCK_EXPIRATION state_manager_redis.lock_expiration = LOCK_EXPIRATION
state_manager_redis.lock_warning_threshold = LOCK_WARNING_THRESHOLD
order = [] order = []
@ -1841,6 +1845,57 @@ async def test_state_manager_lock_expire_contend(
assert (await state_manager_redis.get_state(substate_token_redis)).num1 == exp_num1 assert (await state_manager_redis.get_state(substate_token_redis)).num1 == exp_num1
@pytest.mark.asyncio
async def test_state_manager_lock_warning_threshold_contend(
state_manager_redis: StateManager, token: str, substate_token_redis: str, mocker
):
"""Test that the state manager triggers a warning when lock contention exceeds the warning threshold.
Args:
state_manager_redis: A state manager instance.
token: A token.
substate_token_redis: A token + substate name for looking up in state manager.
mocker: Pytest mocker object.
"""
console_warn = mocker.patch("reflex.utils.console.warn")
state_manager_redis.lock_expiration = LOCK_EXPIRATION
state_manager_redis.lock_warning_threshold = LOCK_WARNING_THRESHOLD
order = []
async def _coro_blocker():
async with state_manager_redis.modify_state(substate_token_redis):
order.append("blocker")
await asyncio.sleep(LOCK_WARN_SLEEP)
tasks = [
asyncio.create_task(_coro_blocker()),
]
await tasks[0]
console_warn.assert_called()
assert console_warn.call_count == 7
class CopyingAsyncMock(AsyncMock):
"""An AsyncMock, but deepcopy the args and kwargs first."""
def __call__(self, *args, **kwargs):
"""Call the mock.
Args:
args: the arguments passed to the mock
kwargs: the keyword arguments passed to the mock
Returns:
The result of the mock call
"""
args = copy.deepcopy(args)
kwargs = copy.deepcopy(kwargs)
return super().__call__(*args, **kwargs)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def mock_app_simple(monkeypatch) -> rx.App: def mock_app_simple(monkeypatch) -> rx.App:
"""Simple Mock app fixture. """Simple Mock app fixture.
@ -1857,7 +1912,7 @@ def mock_app_simple(monkeypatch) -> rx.App:
setattr(app_module, CompileVars.APP, app) setattr(app_module, CompileVars.APP, app)
app.state = TestState app.state = TestState
app.event_namespace.emit = AsyncMock() # type: ignore app.event_namespace.emit = CopyingAsyncMock() # type: ignore
def _mock_get_app(*args, **kwargs): def _mock_get_app(*args, **kwargs):
return app_module return app_module
@ -1961,8 +2016,7 @@ async def test_state_proxy(grandchild_state: GrandchildState, mock_app: rx.App):
mock_app.event_namespace.emit.assert_called_once() mock_app.event_namespace.emit.assert_called_once()
mcall = mock_app.event_namespace.emit.mock_calls[0] mcall = mock_app.event_namespace.emit.mock_calls[0]
assert mcall.args[0] == str(SocketEvent.EVENT) assert mcall.args[0] == str(SocketEvent.EVENT)
assert json.loads(mcall.args[1]) == dataclasses.asdict( assert mcall.args[1] == StateUpdate(
StateUpdate(
delta={ delta={
parent_state.get_full_name(): { parent_state.get_full_name(): {
"upper": "", "upper": "",
@ -1976,7 +2030,6 @@ async def test_state_proxy(grandchild_state: GrandchildState, mock_app: rx.App):
}, },
} }
) )
)
assert mcall.kwargs["to"] == grandchild_state.router.session.session_id assert mcall.kwargs["to"] == grandchild_state.router.session.session_id
@ -2157,51 +2210,51 @@ async def test_background_task_no_block(mock_app: rx.App, token: str):
assert mock_app.event_namespace is not None assert mock_app.event_namespace is not None
emit_mock = mock_app.event_namespace.emit emit_mock = mock_app.event_namespace.emit
first_ws_message = json.loads(emit_mock.mock_calls[0].args[1]) first_ws_message = emit_mock.mock_calls[0].args[1]
assert ( assert (
first_ws_message["delta"][BackgroundTaskState.get_full_name()].pop("router") first_ws_message.delta[BackgroundTaskState.get_full_name()].pop("router")
is not None is not None
) )
assert first_ws_message == { assert first_ws_message == StateUpdate(
"delta": { delta={
BackgroundTaskState.get_full_name(): { BackgroundTaskState.get_full_name(): {
"order": ["background_task:start"], "order": ["background_task:start"],
"computed_order": ["background_task:start"], "computed_order": ["background_task:start"],
} }
}, },
"events": [], events=[],
"final": True, final=True,
} )
for call in emit_mock.mock_calls[1:5]: for call in emit_mock.mock_calls[1:5]:
assert json.loads(call.args[1]) == { assert call.args[1] == StateUpdate(
"delta": { delta={
BackgroundTaskState.get_full_name(): { BackgroundTaskState.get_full_name(): {
"computed_order": ["background_task:start"], "computed_order": ["background_task:start"],
} }
}, },
"events": [], events=[],
"final": True, final=True,
} )
assert json.loads(emit_mock.mock_calls[-2].args[1]) == { assert emit_mock.mock_calls[-2].args[1] == StateUpdate(
"delta": { delta={
BackgroundTaskState.get_full_name(): { BackgroundTaskState.get_full_name(): {
"order": exp_order, "order": exp_order,
"computed_order": exp_order, "computed_order": exp_order,
"dict_list": {}, "dict_list": {},
} }
}, },
"events": [], events=[],
"final": True, final=True,
} )
assert json.loads(emit_mock.mock_calls[-1].args[1]) == { assert emit_mock.mock_calls[-1].args[1] == StateUpdate(
"delta": { delta={
BackgroundTaskState.get_full_name(): { BackgroundTaskState.get_full_name(): {
"computed_order": exp_order, "computed_order": exp_order,
}, },
}, },
"events": [], events=[],
"final": True, final=True,
} )
@pytest.mark.asyncio @pytest.mark.asyncio
@ -3238,12 +3291,42 @@ async def test_setvar_async_setter():
@pytest.mark.parametrize( @pytest.mark.parametrize(
"expiration_kwargs, expected_values", "expiration_kwargs, expected_values",
[ [
({"redis_lock_expiration": 20000}, (20000, constants.Expiration.TOKEN)), (
{"redis_lock_expiration": 20000},
(
20000,
constants.Expiration.TOKEN,
constants.Expiration.LOCK_WARNING_THRESHOLD,
),
),
( (
{"redis_lock_expiration": 50000, "redis_token_expiration": 5600}, {"redis_lock_expiration": 50000, "redis_token_expiration": 5600},
(50000, 5600), (50000, 5600, constants.Expiration.LOCK_WARNING_THRESHOLD),
),
(
{"redis_token_expiration": 7600},
(
constants.Expiration.LOCK,
7600,
constants.Expiration.LOCK_WARNING_THRESHOLD,
),
),
(
{"redis_lock_expiration": 50000, "redis_lock_warning_threshold": 1500},
(50000, constants.Expiration.TOKEN, 1500),
),
(
{"redis_token_expiration": 5600, "redis_lock_warning_threshold": 3000},
(constants.Expiration.LOCK, 5600, 3000),
),
(
{
"redis_lock_expiration": 50000,
"redis_token_expiration": 5600,
"redis_lock_warning_threshold": 2000,
},
(50000, 5600, 2000),
), ),
({"redis_token_expiration": 7600}, (constants.Expiration.LOCK, 7600)),
], ],
) )
def test_redis_state_manager_config_knobs(tmp_path, expiration_kwargs, expected_values): def test_redis_state_manager_config_knobs(tmp_path, expiration_kwargs, expected_values):
@ -3273,6 +3356,44 @@ config = rx.Config(
state_manager = StateManager.create(state=State) state_manager = StateManager.create(state=State)
assert state_manager.lock_expiration == expected_values[0] # type: ignore assert state_manager.lock_expiration == expected_values[0] # type: ignore
assert state_manager.token_expiration == expected_values[1] # type: ignore assert state_manager.token_expiration == expected_values[1] # type: ignore
assert state_manager.lock_warning_threshold == expected_values[2] # type: ignore
@pytest.mark.skipif("REDIS_URL" not in os.environ, reason="Test requires redis")
@pytest.mark.parametrize(
"redis_lock_expiration, redis_lock_warning_threshold",
[
(10000, 10000),
(20000, 30000),
],
)
def test_redis_state_manager_config_knobs_invalid_lock_warning_threshold(
tmp_path, redis_lock_expiration, redis_lock_warning_threshold
):
proj_root = tmp_path / "project1"
proj_root.mkdir()
config_string = f"""
import reflex as rx
config = rx.Config(
app_name="project1",
redis_url="redis://localhost:6379",
state_manager_mode="redis",
redis_lock_expiration = {redis_lock_expiration},
redis_lock_warning_threshold = {redis_lock_warning_threshold},
)
"""
(proj_root / "rxconfig.py").write_text(dedent(config_string))
with chdir(proj_root):
# reload config for each parameter to avoid stale values
reflex.config.get_config(reload=True)
from reflex.state import State, StateManager
with pytest.raises(InvalidLockWarningThresholdError):
StateManager.create(state=State)
del sys.modules[constants.Config.MODULE]
class MixinState(State, mixin=True): class MixinState(State, mixin=True):
@ -3490,10 +3611,10 @@ def test_mutable_models():
state.dirty_vars.clear() state.dirty_vars.clear()
# Not yet supported ENG-4083 # Not yet supported ENG-4083
# assert isinstance(state.dc, MutableProxy) # assert isinstance(state.dc, MutableProxy) #noqa: ERA001
# state.dc.foo = "baz" # state.dc.foo = "baz" #noqa: ERA001
# assert state.dirty_vars == {"dc"} # assert state.dirty_vars == {"dc"} #noqa: ERA001
# state.dirty_vars.clear() # state.dirty_vars.clear() #noqa: ERA001
def test_get_value(): def test_get_value():

View File

@ -34,12 +34,6 @@ def test_disable():
@pytest.mark.parametrize("event", ["init", "reinit", "run-dev", "run-prod", "export"]) @pytest.mark.parametrize("event", ["init", "reinit", "run-dev", "run-prod", "export"])
def test_send(mocker, event): def test_send(mocker, event):
httpx_post_mock = mocker.patch("httpx.post") httpx_post_mock = mocker.patch("httpx.post")
# mocker.patch(
# "builtins.open",
# mocker.mock_open(
# read_data='{"project_hash": "78285505863498957834586115958872998605"}'
# ),
# )
# Mock the read_text method of Path # Mock the read_text method of Path
pathlib_path_read_text_mock = mocker.patch( pathlib_path_read_text_mock = mocker.patch(

View File

@ -1495,8 +1495,6 @@ def test_valid_var_operations(operand1_var: Var, operand2_var, operators: List[s
) )
eval(f"operand1_var {operator} operand2_var") eval(f"operand1_var {operator} operand2_var")
eval(f"operand2_var {operator} operand1_var") eval(f"operand2_var {operator} operand1_var")
# operand1_var.operation(op=operator, other=operand2_var)
# operand1_var.operation(op=operator, other=operand2_var, flip=True)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -1773,11 +1771,9 @@ def test_invalid_var_operations(operand1_var: Var, operand2_var, operators: List
print(f"testing {operator} on {operand1_var!s} and {operand2_var!s}") print(f"testing {operator} on {operand1_var!s} and {operand2_var!s}")
with pytest.raises(TypeError): with pytest.raises(TypeError):
print(eval(f"operand1_var {operator} operand2_var")) print(eval(f"operand1_var {operator} operand2_var"))
# operand1_var.operation(op=operator, other=operand2_var)
with pytest.raises(TypeError): with pytest.raises(TypeError):
print(eval(f"operand2_var {operator} operand1_var")) print(eval(f"operand2_var {operator} operand1_var"))
# operand1_var.operation(op=operator, other=operand2_var, flip=True)
@pytest.mark.parametrize( @pytest.mark.parametrize(