Merge branch 'main' into lendemor/fix_order_of_hooks_and_memoized_evt_trigger
This commit is contained in:
commit
f31c6c1d9d
6
.github/workflows/benchmarks.yml
vendored
6
.github/workflows/benchmarks.yml
vendored
@ -80,7 +80,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# 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']
|
||||
exclude:
|
||||
- os: windows-latest
|
||||
@ -92,7 +92,7 @@ jobs:
|
||||
python-version: '3.9.18'
|
||||
- os: macos-latest
|
||||
python-version: '3.10.13'
|
||||
- os: macos-12
|
||||
- os: macos-latest
|
||||
python-version: '3.12.0'
|
||||
include:
|
||||
- os: windows-latest
|
||||
@ -155,7 +155,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# 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']
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
@ -58,7 +58,7 @@ jobs:
|
||||
working-directory: ./reflex-web
|
||||
run: poetry run uv pip install -r requirements.txt
|
||||
- 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
|
||||
working-directory: ./reflex-web
|
||||
run: poetry run reflex init
|
||||
|
@ -22,9 +22,9 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
state_manager: ["redis", "memory"]
|
||||
state_manager: ['redis', 'memory']
|
||||
python-version: ['3.11.5', '3.12.0', '3.13.0']
|
||||
split_index: [1, 2]
|
||||
python-version: ["3.11.5", "3.12.0"]
|
||||
fail-fast: false
|
||||
runs-on: ubuntu-22.04
|
||||
services:
|
||||
|
10
.github/workflows/integration_tests.yml
vendored
10
.github/workflows/integration_tests.yml
vendored
@ -43,7 +43,7 @@ jobs:
|
||||
matrix:
|
||||
# Show OS combos first in GUI
|
||||
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:
|
||||
- os: windows-latest
|
||||
python-version: '3.10.13'
|
||||
@ -73,7 +73,7 @@ jobs:
|
||||
run: |
|
||||
poetry run uv pip install -r requirements.txt
|
||||
- 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
|
||||
working-directory: ./reflex-examples/counter
|
||||
run: |
|
||||
@ -147,7 +147,7 @@ jobs:
|
||||
working-directory: ./reflex-web
|
||||
run: poetry run uv pip install $(grep -ivE "reflex " requirements.txt)
|
||||
- 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
|
||||
working-directory: ./reflex-web
|
||||
run: poetry run reflex init
|
||||
@ -198,7 +198,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ['3.11.5', '3.12.0']
|
||||
runs-on: macos-12
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup_build_env
|
||||
@ -216,7 +216,7 @@ jobs:
|
||||
working-directory: ./reflex-web
|
||||
run: poetry run uv pip install -r requirements.txt
|
||||
- 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
|
||||
working-directory: ./reflex-web
|
||||
run: poetry run reflex init
|
||||
|
7
.github/workflows/unit_tests.yml
vendored
7
.github/workflows/unit_tests.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
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
|
||||
exclude:
|
||||
- os: windows-latest
|
||||
@ -88,8 +88,9 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0']
|
||||
runs-on: macos-12
|
||||
# Note: py39, py310 versions chosen due to available arm64 darwin builds.
|
||||
python-version: ['3.9.13', '3.10.11', '3.11.5', '3.12.0', '3.13.0']
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup_build_env
|
||||
|
@ -52,7 +52,7 @@ FROM python:3.13-slim
|
||||
WORKDIR /app
|
||||
RUN adduser --disabled-password --home /app reflex
|
||||
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/*
|
||||
USER reflex
|
||||
ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1
|
||||
|
@ -39,7 +39,7 @@ FROM python:3.13-slim
|
||||
WORKDIR /app
|
||||
RUN adduser --disabled-password --home /app reflex
|
||||
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/*
|
||||
USER reflex
|
||||
ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1
|
||||
|
@ -15,7 +15,7 @@ services:
|
||||
|
||||
app:
|
||||
environment:
|
||||
DB_URL: postgresql+psycopg2://postgres:secret@db/postgres
|
||||
DB_URL: postgresql+psycopg://postgres:secret@db/postgres
|
||||
REDIS_URL: redis://redis:6379
|
||||
depends_on:
|
||||
- db
|
||||
|
16
poetry.lock
generated
16
poetry.lock
generated
@ -1542,18 +1542,18 @@ type = ["mypy (>=1.11.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "playwright"
|
||||
version = "1.49.0"
|
||||
version = "1.49.1"
|
||||
description = "A high-level API to automate web browsers"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "playwright-1.49.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:704532a2d8ba580ec9e1895bfeafddce2e3d52320d4eb8aa38e80376acc5cbb0"},
|
||||
{file = "playwright-1.49.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e453f02c4e5cc2db7e9759c47e7425f32e50ac76c76b7eb17c69eed72f01c4d8"},
|
||||
{file = "playwright-1.49.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:37ae985309184472946a6eb1a237e5d93c9e58a781fa73b75c8751325002a5d4"},
|
||||
{file = "playwright-1.49.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:68d94beffb3c9213e3ceaafa66171affd9a5d9162e0c8a3eed1b1132c2e57598"},
|
||||
{file = "playwright-1.49.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f12d2aecdb41fc25a624cb15f3e8391c252ebd81985e3d5c1c261fe93779345"},
|
||||
{file = "playwright-1.49.0-py3-none-win32.whl", hash = "sha256:91103de52d470594ad375b512d7143fa95d6039111ae11a93eb4fe2f2b4a4858"},
|
||||
{file = "playwright-1.49.0-py3-none-win_amd64.whl", hash = "sha256:34d28a2c2d46403368610be4339898dc9c34eb9f7c578207b4715c49743a072a"},
|
||||
{file = "playwright-1.49.1-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:1041ffb45a0d0bc44d698d3a5aa3ac4b67c9bd03540da43a0b70616ad52592b8"},
|
||||
{file = "playwright-1.49.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9f38ed3d0c1f4e0a6d1c92e73dd9a61f8855133249d6f0cec28648d38a7137be"},
|
||||
{file = "playwright-1.49.1-py3-none-macosx_11_0_universal2.whl", hash = "sha256:3be48c6d26dc819ca0a26567c1ae36a980a0303dcd4249feb6f59e115aaddfb8"},
|
||||
{file = "playwright-1.49.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:753ca90ee31b4b03d165cfd36e477309ebf2b4381953f2a982ff612d85b147d2"},
|
||||
{file = "playwright-1.49.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd9bc8dab37aa25198a01f555f0a2e2c3813fe200fef018ac34dfe86b34994b9"},
|
||||
{file = "playwright-1.49.1-py3-none-win32.whl", hash = "sha256:43b304be67f096058e587dac453ece550eff87b8fbed28de30f4f022cc1745bb"},
|
||||
{file = "playwright-1.49.1-py3-none-win_amd64.whl", hash = "sha256:47b23cb346283278f5b4d1e1990bcb6d6302f80c0aa0ca93dd0601a1400191df"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -93,7 +93,7 @@ build-backend = "poetry.core.masonry.api"
|
||||
[tool.ruff]
|
||||
target-version = "py39"
|
||||
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.pydocstyle.convention = "google"
|
||||
|
||||
|
@ -40,9 +40,6 @@ let event_processing = false;
|
||||
// Array holding pending events to be processed.
|
||||
const event_queue = [];
|
||||
|
||||
// Pending upload promises, by id
|
||||
const upload_controllers = {};
|
||||
|
||||
/**
|
||||
* Generate a UUID (Used for session tokens).
|
||||
* 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) {
|
||||
socket.emit(
|
||||
"event",
|
||||
JSON.stringify(event, (k, v) => (v === undefined ? null : v))
|
||||
event,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
@ -407,6 +404,8 @@ export const connect = async (
|
||||
transports: transports,
|
||||
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() {
|
||||
if (document.visibilityState === "visible") {
|
||||
@ -443,8 +442,7 @@ export const connect = async (
|
||||
});
|
||||
|
||||
// On each received message, queue the updates and events.
|
||||
socket.current.on("event", async (message) => {
|
||||
const update = JSON5.parse(message);
|
||||
socket.current.on("event", async (update) => {
|
||||
for (const substate in update.delta) {
|
||||
dispatch[substate](update.delta[substate]);
|
||||
}
|
||||
@ -456,7 +454,7 @@ export const connect = async (
|
||||
});
|
||||
socket.current.on("reload", async (event) => {
|
||||
event_processing = false;
|
||||
queueEvents([...initialEvents(), JSON5.parse(event)], socket);
|
||||
queueEvents([...initialEvents(), event], socket);
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", checkVisibility);
|
||||
@ -485,7 +483,9 @@ export const uploadFiles = async (
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
@ -497,7 +497,9 @@ export const uploadFiles = async (
|
||||
// Whenever called, responseText will contain the entire response so far.
|
||||
const chunks = progressEvent.event.target.responseText.trim().split("\n");
|
||||
// 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) => {
|
||||
f(chunk)
|
||||
.then(() => {
|
||||
@ -509,11 +511,17 @@ export const uploadFiles = async (
|
||||
.catch((e) => {
|
||||
if (progressEvent.progress === 1) {
|
||||
// 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;
|
||||
});
|
||||
});
|
||||
} 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.
|
||||
upload_controllers[upload_id] = controller;
|
||||
refs[upload_ref_name] = controller;
|
||||
|
||||
try {
|
||||
return await axios.post(getBackendURL(UPLOADURL), formdata, config);
|
||||
@ -557,7 +565,7 @@ export const uploadFiles = async (
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
delete upload_controllers[upload_id];
|
||||
delete refs[upload_ref_name];
|
||||
}
|
||||
};
|
||||
|
||||
@ -799,7 +807,7 @@ export const useEventLoop = (
|
||||
connect(
|
||||
socket,
|
||||
dispatch,
|
||||
["websocket", "polling"],
|
||||
["websocket"],
|
||||
setConnectErrors,
|
||||
client_storage
|
||||
);
|
||||
|
@ -17,6 +17,7 @@ import sys
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@ -363,6 +364,11 @@ class App(MiddlewareMixin, LifespanMixin):
|
||||
max_http_buffer_size=constants.POLLING_MAX_HTTP_BUFFER_SIZE,
|
||||
ping_interval=constants.Ping.INTERVAL,
|
||||
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":
|
||||
raise RuntimeError(
|
||||
@ -467,7 +473,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
||||
|
||||
def add_page(
|
||||
self,
|
||||
component: Component | ComponentCallable,
|
||||
component: Component | ComponentCallable | None = None,
|
||||
route: str | None = None,
|
||||
title: str | Var | None = None,
|
||||
description: str | Var | None = None,
|
||||
@ -490,17 +496,33 @@ class App(MiddlewareMixin, LifespanMixin):
|
||||
meta: The metadata of the page.
|
||||
|
||||
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 route is None:
|
||||
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.
|
||||
route = format.format_route(component.__name__)
|
||||
else:
|
||||
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
|
||||
verify_route_validity(route)
|
||||
|
||||
@ -516,7 +538,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
||||
if route == constants.PageNames.INDEX_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" 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.
|
||||
meta: The metadata of the page.
|
||||
"""
|
||||
if component is None:
|
||||
component = Default404Page.create()
|
||||
console.deprecate(
|
||||
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(
|
||||
component=wait_for_client_redirect(self._generate_component(component)),
|
||||
component=component,
|
||||
route=constants.Page404.SLUG,
|
||||
title=title or constants.Page404.TITLE,
|
||||
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
|
||||
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.
|
||||
self.style = evaluate_style_namespaces(self.style)
|
||||
@ -965,7 +991,6 @@ class App(MiddlewareMixin, LifespanMixin):
|
||||
|
||||
def _submit_work(fn, *args, **kwargs):
|
||||
f = executor.submit(fn, *args, **kwargs)
|
||||
# f = executor.apipe(fn, *args, **kwargs)
|
||||
result_futures.append(f)
|
||||
|
||||
# Compile the pre-compiled pages.
|
||||
@ -1270,7 +1295,7 @@ async def process(
|
||||
await asyncio.create_task(
|
||||
app.event_namespace.emit(
|
||||
"reload",
|
||||
data=format.json_dumps(event),
|
||||
data=event,
|
||||
to=sid,
|
||||
)
|
||||
)
|
||||
@ -1523,7 +1548,7 @@ class EventNamespace(AsyncNamespace):
|
||||
"""
|
||||
# Creating a task prevents the update from being blocked behind other coroutines.
|
||||
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):
|
||||
@ -1536,7 +1561,7 @@ class EventNamespace(AsyncNamespace):
|
||||
sid: The Socket.IO session id.
|
||||
data: The event data.
|
||||
"""
|
||||
fields = json.loads(data)
|
||||
fields = data
|
||||
# Get the event.
|
||||
event = Event(
|
||||
**{k: v for k, v in fields.items() if k not in ("handler", "event_actions")}
|
||||
|
@ -653,7 +653,6 @@ class Component(BaseComponent, ABC):
|
||||
|
||||
Returns:
|
||||
The event triggers.
|
||||
|
||||
"""
|
||||
default_triggers: Dict[str, types.ArgsSpec | Sequence[types.ArgsSpec]] = {
|
||||
EventTriggers.ON_FOCUS: no_args_event_spec,
|
||||
|
@ -24,7 +24,7 @@ class ClientSideRouting(Component):
|
||||
library = "$/utils/client_side_routing"
|
||||
tag = "useClientSideRouting"
|
||||
|
||||
def add_hooks(self) -> list[str]:
|
||||
def add_hooks(self) -> list[str | Var]:
|
||||
"""Get the hooks to render.
|
||||
|
||||
Returns:
|
||||
@ -66,4 +66,4 @@ class Default404Page(Component):
|
||||
tag = "Error"
|
||||
is_default = True
|
||||
|
||||
status_code: Var[int] = 404 # type: ignore
|
||||
status_code: Var[int] = Var.create(404)
|
||||
|
@ -13,7 +13,7 @@ from reflex.vars.base import Var
|
||||
route_not_found: Var
|
||||
|
||||
class ClientSideRouting(Component):
|
||||
def add_hooks(self) -> list[str]: ...
|
||||
def add_hooks(self) -> list[str | Var]: ...
|
||||
def render(self) -> str: ...
|
||||
@overload
|
||||
@classmethod
|
||||
|
@ -29,7 +29,7 @@ from reflex.event import (
|
||||
from reflex.utils import format
|
||||
from reflex.utils.imports import ImportVar
|
||||
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
|
||||
|
||||
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
|
||||
# into the shared global refs object to make it accessible outside a React
|
||||
# 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:
|
||||
@ -120,7 +121,8 @@ def cancel_upload(upload_id: str) -> EventSpec:
|
||||
Returns:
|
||||
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:
|
||||
|
@ -51,27 +51,6 @@ class GridColumnIcons(Enum):
|
||||
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):
|
||||
"""The theme for the DataEditor component."""
|
||||
|
||||
@ -229,7 +208,7 @@ class DataEditor(NoSSRComponent):
|
||||
header_height: Var[int]
|
||||
|
||||
# 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.
|
||||
max_column_auto_width: Var[int]
|
||||
|
@ -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.
|
||||
group_header_height: Controls the header of the group 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.
|
||||
min_column_width: The minimum width a column can be resized to.
|
||||
row_height: Determins the height of each row.
|
||||
|
@ -490,17 +490,17 @@ class ShikiJsTransformer(ShikiBaseTransformers):
|
||||
},
|
||||
# White Space
|
||||
# ".tab, .space": {
|
||||
# "position": "relative",
|
||||
# "position": "relative", # noqa: ERA001
|
||||
# },
|
||||
# ".tab::before": {
|
||||
# "content": "'⇥'",
|
||||
# "position": "absolute",
|
||||
# "opacity": "0.3",
|
||||
# "content": "'⇥'", # noqa: ERA001
|
||||
# "position": "absolute", # noqa: ERA001
|
||||
# "opacity": "0.3",# noqa: ERA001
|
||||
# },
|
||||
# ".space::before": {
|
||||
# "content": "'·'",
|
||||
# "position": "absolute",
|
||||
# "opacity": "0.3",
|
||||
# "content": "'·'", # noqa: ERA001
|
||||
# "position": "absolute", # noqa: ERA001
|
||||
# "opacity": "0.3", # noqa: ERA001
|
||||
# },
|
||||
}
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
||||
"""Base classes."""
|
||||
|
||||
from typing import Union
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
||||
"""Forms classes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@ -18,6 +18,7 @@ from reflex.event import (
|
||||
prevent_default,
|
||||
)
|
||||
from reflex.utils.imports import ImportDict
|
||||
from reflex.utils.types import is_optional
|
||||
from reflex.vars import VarData
|
||||
from reflex.vars.base import LiteralVar, Var
|
||||
|
||||
@ -84,7 +85,6 @@ class Datalist(BaseHTML):
|
||||
"""Display the datalist element."""
|
||||
|
||||
tag = "datalist"
|
||||
# No unique attributes, only common ones are inherited
|
||||
|
||||
|
||||
class Fieldset(Element):
|
||||
@ -250,7 +250,6 @@ class Form(BaseHTML):
|
||||
_js_expr=f"getRefValue({ref_var!s})",
|
||||
_var_data=VarData.merge(ref_var._get_all_var_data()),
|
||||
)
|
||||
# print(repr(form_refs))
|
||||
return form_refs
|
||||
|
||||
def _get_vars(self, include_children: bool = True) -> Iterator[Var]:
|
||||
@ -384,6 +383,33 @@ class Input(BaseHTML):
|
||||
# Fired when a key is released
|
||||
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):
|
||||
"""Display the label element."""
|
||||
@ -401,7 +427,6 @@ class Legend(BaseHTML):
|
||||
"""Display the legend element."""
|
||||
|
||||
tag = "legend"
|
||||
# No unique attributes, only common ones are inherited
|
||||
|
||||
|
||||
class Meter(BaseHTML):
|
||||
|
@ -189,7 +189,7 @@ class Datalist(BaseHTML):
|
||||
|
||||
Args:
|
||||
*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.
|
||||
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.
|
||||
@ -512,7 +512,7 @@ class Input(BaseHTML):
|
||||
on_unmount: Optional[EventType[[], BASE_STATE]] = None,
|
||||
**props,
|
||||
) -> "Input":
|
||||
"""Create the component.
|
||||
"""Create an Input component.
|
||||
|
||||
Args:
|
||||
*children: The children of the component.
|
||||
@ -576,7 +576,7 @@ class Input(BaseHTML):
|
||||
class_name: The class name for the component.
|
||||
autofocus: Whether the component should take the focus once the page is loaded
|
||||
custom_attrs: custom attribute
|
||||
**props: The props of the component.
|
||||
**props: The properties of the component.
|
||||
|
||||
Returns:
|
||||
The component.
|
||||
@ -730,7 +730,7 @@ class Legend(BaseHTML):
|
||||
|
||||
Args:
|
||||
*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.
|
||||
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.
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
||||
"""Inline classes."""
|
||||
|
||||
from typing import Union
|
||||
|
||||
|
@ -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
|
||||
|
||||
@ -129,7 +129,6 @@ class Img(BaseHTML):
|
||||
|
||||
Returns:
|
||||
The component.
|
||||
|
||||
"""
|
||||
return (
|
||||
super().create(src=children[0], **props)
|
||||
@ -274,14 +273,12 @@ class Picture(BaseHTML):
|
||||
"""Display the picture element."""
|
||||
|
||||
tag = "picture"
|
||||
# No unique attributes, only common ones are inherited
|
||||
|
||||
|
||||
class Portal(BaseHTML):
|
||||
"""Display the portal element."""
|
||||
|
||||
tag = "portal"
|
||||
# No unique attributes, only common ones are inherited
|
||||
|
||||
|
||||
class Source(BaseHTML):
|
||||
|
@ -340,7 +340,6 @@ class Img(BaseHTML):
|
||||
|
||||
Returns:
|
||||
The component.
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
@ -987,7 +986,7 @@ class Picture(BaseHTML):
|
||||
|
||||
Args:
|
||||
*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.
|
||||
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.
|
||||
@ -1073,7 +1072,7 @@ class Portal(BaseHTML):
|
||||
|
||||
Args:
|
||||
*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.
|
||||
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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
||||
"""Other classes."""
|
||||
|
||||
from typing import Union
|
||||
|
||||
@ -26,31 +26,39 @@ class Dialog(BaseHTML):
|
||||
|
||||
|
||||
class Summary(BaseHTML):
|
||||
"""Display the summary element."""
|
||||
"""Display the summary element.
|
||||
|
||||
Used as a summary or caption for a <details> element.
|
||||
"""
|
||||
|
||||
tag = "summary"
|
||||
# No unique attributes, only common ones are inherited; used as a summary or caption for a <details> element
|
||||
|
||||
|
||||
class Slot(BaseHTML):
|
||||
"""Display the slot element."""
|
||||
"""Display the slot element.
|
||||
|
||||
Used as a placeholder inside a web component.
|
||||
"""
|
||||
|
||||
tag = "slot"
|
||||
# No unique attributes, only common ones are inherited; used as a placeholder inside a web component
|
||||
|
||||
|
||||
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"
|
||||
# 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):
|
||||
"""Display the math element."""
|
||||
"""Display the math element.
|
||||
|
||||
Represents a mathematical expression.
|
||||
"""
|
||||
|
||||
tag = "math"
|
||||
# No unique attributes, only common ones are inherited; used for displaying mathematical expressions
|
||||
|
||||
|
||||
class Html(BaseHTML):
|
||||
|
@ -244,7 +244,7 @@ class Summary(BaseHTML):
|
||||
|
||||
Args:
|
||||
*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.
|
||||
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.
|
||||
@ -330,7 +330,7 @@ class Slot(BaseHTML):
|
||||
|
||||
Args:
|
||||
*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.
|
||||
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.
|
||||
@ -416,7 +416,7 @@ class Template(BaseHTML):
|
||||
|
||||
Args:
|
||||
*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.
|
||||
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.
|
||||
@ -502,7 +502,7 @@ class Math(BaseHTML):
|
||||
|
||||
Args:
|
||||
*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.
|
||||
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.
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
||||
"""Scripts classes."""
|
||||
|
||||
from typing import Union
|
||||
|
||||
@ -17,7 +17,6 @@ class Noscript(BaseHTML):
|
||||
"""Display the noscript element."""
|
||||
|
||||
tag = "noscript"
|
||||
# No unique attributes, only common ones are inherited
|
||||
|
||||
|
||||
class Script(BaseHTML):
|
||||
|
@ -154,7 +154,7 @@ class Noscript(BaseHTML):
|
||||
|
||||
Args:
|
||||
*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.
|
||||
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.
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
||||
"""Sectioning classes."""
|
||||
|
||||
from .base import BaseHTML
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
||||
"""Tables classes."""
|
||||
|
||||
from typing import Union
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
||||
"""Typography classes."""
|
||||
|
||||
from typing import Union
|
||||
|
||||
|
@ -47,7 +47,7 @@ class Image(NextComponent):
|
||||
placeholder: Var[str]
|
||||
|
||||
# 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.
|
||||
loading: Var[Literal["lazy", "eager"]]
|
||||
|
@ -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.
|
||||
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.
|
||||
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".
|
||||
on_load: Fires when the image has loaded.
|
||||
on_error: Fires when the image has an error.
|
||||
|
@ -188,7 +188,7 @@ class Slider(ComponentNamespace):
|
||||
else:
|
||||
children = [
|
||||
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)
|
||||
|
@ -53,7 +53,7 @@ LiteralAccentColor = Literal[
|
||||
class CommonMarginProps(Component):
|
||||
"""Many radix-themes elements accept shorthand margin props."""
|
||||
|
||||
# Margin: "0" - "9"
|
||||
# Margin: "0" - "9" # noqa: ERA001
|
||||
m: Var[LiteralSpacing]
|
||||
|
||||
# Margin horizontal: "0" - "9"
|
||||
@ -78,7 +78,7 @@ class CommonMarginProps(Component):
|
||||
class CommonPaddingProps(Component):
|
||||
"""Many radix-themes elements accept shorthand padding props."""
|
||||
|
||||
# Padding: "0" - "9"
|
||||
# Padding: "0" - "9" # noqa: ERA001
|
||||
p: Var[Responsive[LiteralSpacing]]
|
||||
|
||||
# Padding horizontal: "0" - "9"
|
||||
@ -140,11 +140,6 @@ class RadixThemesComponent(Component):
|
||||
if component.library is None:
|
||||
component.library = RadixThemesComponent.__fields__["library"].default
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
|
@ -427,7 +427,7 @@ class ColorModeSwitch(Switch):
|
||||
color_scheme: Override theme color for switch
|
||||
high_contrast: Whether to render the switch with higher contrast color against background
|
||||
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.
|
||||
key: A unique key for the component.
|
||||
id: The id for the component.
|
||||
|
@ -153,7 +153,7 @@ class Checkbox(RadixThemesComponent):
|
||||
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: 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.
|
||||
key: A unique key for the component.
|
||||
id: The id for the component.
|
||||
@ -302,7 +302,7 @@ class HighLevelCheckbox(RadixThemesComponent):
|
||||
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: 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.
|
||||
key: A unique key for the component.
|
||||
id: The id for the component.
|
||||
@ -449,7 +449,7 @@ class CheckboxNamespace(ComponentNamespace):
|
||||
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: 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.
|
||||
key: A unique key for the component.
|
||||
id: The id for the component.
|
||||
|
@ -8,6 +8,7 @@ from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spe
|
||||
from reflex.vars.base import Var
|
||||
|
||||
from ..base import LiteralAccentColor, RadixThemesComponent
|
||||
from .checkbox import Checkbox
|
||||
|
||||
LiteralDirType = Literal["ltr", "rtl"]
|
||||
|
||||
@ -232,6 +233,15 @@ class ContextMenuSeparator(RadixThemesComponent):
|
||||
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):
|
||||
"""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)
|
||||
item = staticmethod(ContextMenuItem.create)
|
||||
separator = staticmethod(ContextMenuSeparator.create)
|
||||
checkbox = staticmethod(ContextMenuCheckbox.create)
|
||||
|
||||
|
||||
context_menu = ContextMenu()
|
||||
|
@ -12,6 +12,7 @@ from reflex.style import Style
|
||||
from reflex.vars.base import Var
|
||||
|
||||
from ..base import RadixThemesComponent
|
||||
from .checkbox import Checkbox
|
||||
|
||||
LiteralDirType = Literal["ltr", "rtl"]
|
||||
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):
|
||||
root = staticmethod(ContextMenuRoot.create)
|
||||
trigger = staticmethod(ContextMenuTrigger.create)
|
||||
@ -681,5 +835,6 @@ class ContextMenu(ComponentNamespace):
|
||||
sub_content = staticmethod(ContextMenuSubContent.create)
|
||||
item = staticmethod(ContextMenuItem.create)
|
||||
separator = staticmethod(ContextMenuSeparator.create)
|
||||
checkbox = staticmethod(ContextMenuCheckbox.create)
|
||||
|
||||
context_menu = ContextMenu()
|
||||
|
@ -148,7 +148,7 @@ class RadioGroupRoot(RadixThemesComponent):
|
||||
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.
|
||||
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.
|
||||
key: A unique key for the component.
|
||||
id: The id for the component.
|
||||
|
@ -81,7 +81,7 @@ class SelectRoot(RadixThemesComponent):
|
||||
name: The name of the select control when submitting the form.
|
||||
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.
|
||||
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.
|
||||
style: The style of 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.
|
||||
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.
|
||||
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.
|
||||
style: The style of 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.
|
||||
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.
|
||||
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.
|
||||
style: The style of the component.
|
||||
key: A unique key for the component.
|
||||
|
@ -195,7 +195,7 @@ class Slider(RadixThemesComponent):
|
||||
step: The step value of the slider.
|
||||
disabled: Whether the slider is disabled
|
||||
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.
|
||||
style: The style of the component.
|
||||
key: A unique key for the component.
|
||||
|
@ -157,7 +157,7 @@ class Switch(RadixThemesComponent):
|
||||
color_scheme: Override theme color for switch
|
||||
high_contrast: Whether to render the switch with higher contrast color against background
|
||||
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.
|
||||
key: A unique key for the component.
|
||||
id: The id for the component.
|
||||
|
@ -72,7 +72,7 @@ class TabsRoot(RadixThemesComponent):
|
||||
orientation: The orientation 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.
|
||||
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.
|
||||
key: A unique key for the component.
|
||||
id: The id for the component.
|
||||
@ -374,7 +374,7 @@ class Tabs(ComponentNamespace):
|
||||
orientation: The orientation 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.
|
||||
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.
|
||||
key: A unique key for the component.
|
||||
id: The id for the component.
|
||||
|
@ -9,7 +9,9 @@ from reflex.components.core.breakpoints import Responsive
|
||||
from reflex.components.core.debounce import DebounceInput
|
||||
from reflex.components.el import elements
|
||||
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.number import ternary_operation
|
||||
|
||||
from ..base import LiteralAccentColor, LiteralRadius, RadixThemesComponent
|
||||
|
||||
@ -96,6 +98,19 @@ class TextFieldRoot(elements.Div, RadixThemesComponent):
|
||||
Returns:
|
||||
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)
|
||||
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
|
||||
|
@ -64,7 +64,6 @@ class BaseList(Component, MarkdownComponentMap):
|
||||
|
||||
Returns:
|
||||
The list component.
|
||||
|
||||
"""
|
||||
items = props.pop("items", None)
|
||||
list_style_type = props.pop("list_style_type", "none")
|
||||
@ -114,7 +113,6 @@ class UnorderedList(BaseList, Ul):
|
||||
|
||||
Returns:
|
||||
The list component.
|
||||
|
||||
"""
|
||||
items = props.pop("items", None)
|
||||
list_style_type = props.pop("list_style_type", "disc")
|
||||
@ -144,7 +142,6 @@ class OrderedList(BaseList, Ol):
|
||||
|
||||
Returns:
|
||||
The list component.
|
||||
|
||||
"""
|
||||
items = props.pop("items", None)
|
||||
list_style_type = props.pop("list_style_type", "decimal")
|
||||
@ -168,7 +165,6 @@ class ListItem(Li, MarkdownComponentMap):
|
||||
|
||||
Returns:
|
||||
The list item component.
|
||||
|
||||
"""
|
||||
for child in children:
|
||||
if isinstance(child, Text):
|
||||
|
@ -118,7 +118,6 @@ class BaseList(Component, MarkdownComponentMap):
|
||||
|
||||
Returns:
|
||||
The list component.
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
@ -252,7 +251,6 @@ class UnorderedList(BaseList, Ul):
|
||||
|
||||
Returns:
|
||||
The list component.
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
@ -390,7 +388,6 @@ class OrderedList(BaseList, Ol):
|
||||
|
||||
Returns:
|
||||
The list component.
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
@ -477,7 +474,6 @@ class ListItem(Li, MarkdownComponentMap):
|
||||
|
||||
Returns:
|
||||
The list item component.
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
@ -571,7 +567,6 @@ class List(ComponentNamespace):
|
||||
|
||||
Returns:
|
||||
The list component.
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
|
@ -416,7 +416,7 @@ class Bar(Cartesian):
|
||||
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.
|
||||
# active_bar: Var[Union[bool, Dict[str, Any]]]
|
||||
# active_bar: Var[Union[bool, Dict[str, Any]]] #noqa: ERA001
|
||||
|
||||
# Valid children components
|
||||
_valid_children: List[str] = ["Cell", "LabelList", "ErrorBar"]
|
||||
|
@ -136,7 +136,7 @@ class Radar(Recharts):
|
||||
# Fill color. Default: rx.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)
|
||||
|
||||
# The type of icon in legend. If set to 'none', no legend item will be rendered. Default: "rect"
|
||||
|
@ -204,7 +204,7 @@ class Radar(Recharts):
|
||||
dot: If false set, dots will not be drawn. Default: True
|
||||
stroke: Stoke color. Default: rx.color("accent", 9)
|
||||
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"
|
||||
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
|
||||
|
@ -98,7 +98,7 @@ class ToastProps(PropsBase, NoExtrasAllowedProps):
|
||||
|
||||
# TODO: fix serialization of icons for toast? (might not be possible yet)
|
||||
# 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
|
||||
# Renders a primary button, clicking it will close the toast.
|
||||
@ -364,9 +364,7 @@ class Toaster(Component):
|
||||
return super().create(*children, **props)
|
||||
|
||||
|
||||
# TODO: figure out why loading toast stay open forever
|
||||
# def toast_loading(message: str, **kwargs):
|
||||
# return _toast(message, level="loading", **kwargs)
|
||||
# TODO: figure out why loading toast stay open forever when using level="loading" in toast()
|
||||
|
||||
|
||||
class ToastNamespace(ComponentNamespace):
|
||||
@ -379,7 +377,6 @@ class ToastNamespace(ComponentNamespace):
|
||||
error = staticmethod(Toaster.toast_error)
|
||||
success = staticmethod(Toaster.toast_success)
|
||||
dismiss = staticmethod(Toaster.toast_dismiss)
|
||||
# loading = staticmethod(toast_loading)
|
||||
__call__ = staticmethod(Toaster.send_toast)
|
||||
|
||||
|
||||
|
@ -116,7 +116,7 @@ class Editor(NoSSRComponent):
|
||||
# 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"
|
||||
# default: "en".
|
||||
lang: Var[
|
||||
Union[
|
||||
Literal[
|
||||
@ -172,7 +172,7 @@ class Editor(NoSSRComponent):
|
||||
set_options: Var[Dict]
|
||||
|
||||
# Whether all SunEditor plugins should be loaded.
|
||||
# default: True
|
||||
# default: True.
|
||||
set_all_plugins: Var[bool]
|
||||
|
||||
# Set the content of the editor.
|
||||
@ -191,19 +191,19 @@ class Editor(NoSSRComponent):
|
||||
set_default_style: Var[str]
|
||||
|
||||
# Disable the editor
|
||||
# default: False
|
||||
# default: False.
|
||||
disable: Var[bool]
|
||||
|
||||
# Hide the editor
|
||||
# default: False
|
||||
# default: False.
|
||||
hide: Var[bool]
|
||||
|
||||
# Hide the editor toolbar
|
||||
# default: False
|
||||
# default: False.
|
||||
hide_toolbar: Var[bool]
|
||||
|
||||
# Disable the editor toolbar
|
||||
# default: False
|
||||
# default: False.
|
||||
disable_toolbar: Var[bool]
|
||||
|
||||
# Fired when the editor content changes.
|
||||
|
@ -172,7 +172,7 @@ class Editor(NoSSRComponent):
|
||||
|
||||
Args:
|
||||
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.
|
||||
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%
|
||||
@ -180,14 +180,14 @@ class Editor(NoSSRComponent):
|
||||
placeholder: Sets the placeholder of the editor.
|
||||
auto_focus: Should the editor receive focus when initialized?
|
||||
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.
|
||||
append_contents: Append editor content
|
||||
set_default_style: Sets the default style of the editor's edit area
|
||||
disable: Disable the editor default: False
|
||||
hide: Hide the editor default: False
|
||||
hide_toolbar: Hide the editor toolbar default: False
|
||||
disable_toolbar: Disable the editor toolbar default: False
|
||||
disable: Disable the editor default: False.
|
||||
hide: Hide the editor default: False.
|
||||
hide_toolbar: Hide the editor toolbar default: False.
|
||||
disable_toolbar: Disable the editor toolbar default: False.
|
||||
on_change: Fired when the editor content changes.
|
||||
on_input: Fired when the something is inputted in the editor.
|
||||
on_blur: Fired when the editor loses focus.
|
||||
|
@ -82,7 +82,7 @@ class DBConfig(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def postgresql_psycopg2(
|
||||
def postgresql_psycopg(
|
||||
cls,
|
||||
database: str,
|
||||
username: str,
|
||||
@ -90,7 +90,7 @@ class DBConfig(Base):
|
||||
host: str | None = None,
|
||||
port: int | None = 5432,
|
||||
) -> DBConfig:
|
||||
"""Create an instance with postgresql+psycopg2 engine.
|
||||
"""Create an instance with postgresql+psycopg engine.
|
||||
|
||||
Args:
|
||||
database: Database name.
|
||||
@ -103,7 +103,7 @@ class DBConfig(Base):
|
||||
DBConfig instance.
|
||||
"""
|
||||
return cls(
|
||||
engine="postgresql+psycopg2",
|
||||
engine="postgresql+psycopg",
|
||||
username=username,
|
||||
password=password,
|
||||
host=host,
|
||||
@ -684,6 +684,9 @@ class Config(Base):
|
||||
# Maximum expiration lock time for redis state manager
|
||||
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
|
||||
redis_token_expiration: int = constants.Expiration.TOKEN
|
||||
|
||||
|
@ -29,6 +29,8 @@ class Expiration(SimpleNamespace):
|
||||
LOCK = 10000
|
||||
# The PING timeout
|
||||
PING = 120
|
||||
# The maximum time in milliseconds to hold a lock before throwing a warning.
|
||||
LOCK_WARNING_THRESHOLD = 1000
|
||||
|
||||
|
||||
class GitIgnore(SimpleNamespace):
|
||||
|
@ -1222,7 +1222,7 @@ def call_event_handler(
|
||||
except TypeError:
|
||||
# TODO: In 0.7.0, remove this block and raise the exception
|
||||
# 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
|
||||
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}."
|
||||
|
@ -242,4 +242,5 @@ class ClientStateVar(Var):
|
||||
"""
|
||||
if not self._global_ref:
|
||||
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})")
|
||||
|
@ -33,12 +33,6 @@ class Sidebar(Box, MemoizationLeaf):
|
||||
Returns:
|
||||
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(
|
||||
Box.create(*children, **props), # sidebar for content
|
||||
Box.create(width=props.get("width")), # spacer for layout
|
||||
|
119
reflex/state.py
119
reflex/state.py
@ -11,6 +11,7 @@ import inspect
|
||||
import json
|
||||
import pickle
|
||||
import sys
|
||||
import time
|
||||
import typing
|
||||
import uuid
|
||||
from abc import ABC, abstractmethod
|
||||
@ -39,6 +40,7 @@ from typing import (
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from redis.asyncio.client import PubSub
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from typing_extensions import Self
|
||||
|
||||
@ -69,6 +71,11 @@ try:
|
||||
except ModuleNotFoundError:
|
||||
BaseModelV1 = BaseModelV2
|
||||
|
||||
try:
|
||||
from pydantic.v1 import validator
|
||||
except ModuleNotFoundError:
|
||||
from pydantic import validator
|
||||
|
||||
import wrapt
|
||||
from redis.asyncio import Redis
|
||||
from redis.exceptions import ResponseError
|
||||
@ -92,6 +99,7 @@ from reflex.utils.exceptions import (
|
||||
DynamicRouteArgShadowsStateVar,
|
||||
EventHandlerShadowsBuiltInStateMethod,
|
||||
ImmutableStateError,
|
||||
InvalidLockWarningThresholdError,
|
||||
InvalidStateManagerMode,
|
||||
LockExpiredError,
|
||||
ReflexRuntimeError,
|
||||
@ -1095,6 +1103,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
||||
if (
|
||||
not field.required
|
||||
and field.default is None
|
||||
and field.default_factory is None
|
||||
and not types.is_optional(prop._var_type)
|
||||
):
|
||||
# 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_data", None)
|
||||
# Never serialize parent_state or substates.
|
||||
state["__dict__"]["parent_state"] = None
|
||||
state["__dict__"]["substates"] = {}
|
||||
state["__dict__"].pop("parent_state", None)
|
||||
state["__dict__"].pop("substates", None)
|
||||
state["__dict__"].pop("_was_touched", None)
|
||||
# Remove all inherited vars.
|
||||
for inherited_var_name in self.inherited_vars:
|
||||
state["__dict__"].pop(inherited_var_name, None)
|
||||
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(
|
||||
self,
|
||||
pickle_state_size: int,
|
||||
@ -2819,6 +2840,7 @@ class StateManager(Base, ABC):
|
||||
redis=redis,
|
||||
token_expiration=config.redis_token_expiration,
|
||||
lock_expiration=config.redis_lock_expiration,
|
||||
lock_warning_threshold=config.redis_lock_warning_threshold,
|
||||
)
|
||||
raise InvalidStateManagerMode(
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
"""A state manager that stores states in redis."""
|
||||
|
||||
@ -3200,6 +3231,11 @@ class StateManagerRedis(StateManager):
|
||||
# The maximum time to hold a lock (ms).
|
||||
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
|
||||
_redis_notify_keyspace_events: str = (
|
||||
"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}) "
|
||||
"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)
|
||||
# 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:
|
||||
@ -3436,6 +3483,27 @@ class StateManagerRedis(StateManager):
|
||||
yield state
|
||||
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
|
||||
def _lock_key(token: str) -> bytes:
|
||||
"""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
|
||||
)
|
||||
|
||||
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:
|
||||
"""Wait for a redis lock to be released via pubsub.
|
||||
|
||||
@ -3479,7 +3576,6 @@ class StateManagerRedis(StateManager):
|
||||
Raises:
|
||||
ResponseError: when the keyspace config cannot be set.
|
||||
"""
|
||||
state_is_locked = False
|
||||
lock_key_channel = f"__keyspace@0__:{lock_key.decode()}"
|
||||
# Enable keyspace notifications for the lock key, so we know when it is available.
|
||||
try:
|
||||
@ -3493,20 +3589,13 @@ class StateManagerRedis(StateManager):
|
||||
raise
|
||||
async with self.redis.pubsub() as pubsub:
|
||||
await pubsub.psubscribe(lock_key_channel)
|
||||
while not state_is_locked:
|
||||
# wait for the lock to be released
|
||||
while True:
|
||||
if not await self.redis.exists(lock_key):
|
||||
break # key was removed, try to get the lock again
|
||||
message = await pubsub.get_message(
|
||||
ignore_subscribe_messages=True,
|
||||
timeout=self.lock_expiration / 1000.0,
|
||||
)
|
||||
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)
|
||||
# fast path
|
||||
if await self._try_get_lock(lock_key, lock_id):
|
||||
return
|
||||
# wait for lock events
|
||||
await self._get_pubsub_message(pubsub)
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def _lock(self, token: str):
|
||||
|
@ -138,9 +138,6 @@ def convert_item(
|
||||
if isinstance(style_item, Var):
|
||||
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.
|
||||
new_var = LiteralVar.create(style_item)
|
||||
var_data = new_var._get_all_var_data() if new_var is not None else None
|
||||
|
@ -206,7 +206,7 @@ class AppHarness:
|
||||
The full state name
|
||||
"""
|
||||
# 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]
|
||||
return ".".join(path)
|
||||
|
||||
@ -436,7 +436,6 @@ class AppHarness:
|
||||
|
||||
Returns:
|
||||
The rendered app global code.
|
||||
|
||||
"""
|
||||
if not inspect.isclass(value) and not inspect.isfunction(value):
|
||||
return f"{key} = {value!r}"
|
||||
|
@ -20,6 +20,24 @@ _EMITTED_DEPRECATION_WARNINGS = set()
|
||||
# Info messages which have been printed.
|
||||
_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):
|
||||
"""Set the log level.
|
||||
@ -55,25 +73,37 @@ def is_debug() -> bool:
|
||||
return _LOG_LEVEL <= LogLevel.DEBUG
|
||||
|
||||
|
||||
def print(msg: str, **kwargs):
|
||||
def print(msg: str, dedupe: bool = False, **kwargs):
|
||||
"""Print a message.
|
||||
|
||||
Args:
|
||||
msg: The message to print.
|
||||
dedupe: If True, suppress multiple console logs of print message.
|
||||
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)
|
||||
|
||||
|
||||
def debug(msg: str, **kwargs):
|
||||
def debug(msg: str, dedupe: bool = False, **kwargs):
|
||||
"""Print a debug message.
|
||||
|
||||
Args:
|
||||
msg: The debug message.
|
||||
dedupe: If True, suppress multiple console logs of debug message.
|
||||
kwargs: Keyword arguments to pass to the print function.
|
||||
"""
|
||||
if is_debug():
|
||||
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):
|
||||
progress.console.print(msg_, **kwargs)
|
||||
else:
|
||||
@ -97,25 +127,37 @@ def info(msg: str, dedupe: bool = False, **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.
|
||||
|
||||
Args:
|
||||
msg: The success message.
|
||||
dedupe: If True, suppress multiple console logs of success message.
|
||||
kwargs: Keyword arguments to pass to the print function.
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
def log(msg: str, **kwargs):
|
||||
def log(msg: str, dedupe: bool = False, **kwargs):
|
||||
"""Takes a string and logs it to the console.
|
||||
|
||||
Args:
|
||||
msg: The message to log.
|
||||
dedupe: If True, suppress multiple console logs of log message.
|
||||
kwargs: Keyword arguments to pass to the print function.
|
||||
"""
|
||||
if _LOG_LEVEL <= LogLevel.INFO:
|
||||
if dedupe:
|
||||
if msg in _EMITTED_LOGS:
|
||||
return
|
||||
else:
|
||||
_EMITTED_LOGS.add(msg)
|
||||
_console.log(msg, **kwargs)
|
||||
|
||||
|
||||
@ -129,14 +171,20 @@ def rule(title: str, **kwargs):
|
||||
_console.rule(title, **kwargs)
|
||||
|
||||
|
||||
def warn(msg: str, **kwargs):
|
||||
def warn(msg: str, dedupe: bool = False, **kwargs):
|
||||
"""Print a warning message.
|
||||
|
||||
Args:
|
||||
msg: The warning message.
|
||||
dedupe: If True, suppress multiple console logs of warning message.
|
||||
kwargs: Keyword arguments to pass to the print function.
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
@ -169,14 +217,20 @@ def deprecate(
|
||||
_EMITTED_DEPRECATION_WARNINGS.add(feature_name)
|
||||
|
||||
|
||||
def error(msg: str, **kwargs):
|
||||
def error(msg: str, dedupe: bool = False, **kwargs):
|
||||
"""Print an error message.
|
||||
|
||||
Args:
|
||||
msg: The error message.
|
||||
dedupe: If True, suppress multiple console logs of error message.
|
||||
kwargs: Keyword arguments to pass to the print function.
|
||||
"""
|
||||
if _LOG_LEVEL <= LogLevel.ERROR:
|
||||
if dedupe:
|
||||
if msg in _EMITTED_ERRORS:
|
||||
return
|
||||
else:
|
||||
_EMITTED_ERRORS.add(msg)
|
||||
print(f"[red]{msg}[/red]", **kwargs)
|
||||
|
||||
|
||||
|
@ -63,6 +63,10 @@ class UploadValueError(ReflexError, ValueError):
|
||||
"""Custom ValueError for upload related errors."""
|
||||
|
||||
|
||||
class PageValueError(ReflexError, ValueError):
|
||||
"""Custom ValueError for page related errors."""
|
||||
|
||||
|
||||
class RouteValueError(ReflexError, ValueError):
|
||||
"""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."
|
||||
+ (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."""
|
||||
|
@ -664,18 +664,22 @@ def format_library_name(library_fullname: str):
|
||||
return lib
|
||||
|
||||
|
||||
def json_dumps(obj: Any) -> str:
|
||||
def json_dumps(obj: Any, **kwargs) -> str:
|
||||
"""Takes an object and returns a jsonified string.
|
||||
|
||||
Args:
|
||||
obj: The object to be serialized.
|
||||
kwargs: Additional keyword arguments to pass to json.dumps.
|
||||
|
||||
Returns:
|
||||
A string
|
||||
"""
|
||||
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]:
|
||||
@ -712,7 +716,6 @@ def format_array_ref(refs: str, idx: Var | None) -> str:
|
||||
"""
|
||||
clean_ref = re.sub(r"[^\w]+", "_", refs)
|
||||
if idx is not None:
|
||||
# idx._var_is_local = True
|
||||
return f"refs_{clean_ref}[{idx!s}]"
|
||||
return f"refs_{clean_ref}"
|
||||
|
||||
|
@ -196,12 +196,7 @@ def _get_type_hint(value, type_hint_globals, is_optional=True) -> str:
|
||||
elif isinstance(value, str):
|
||||
ev = eval(value, type_hint_globals)
|
||||
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 f"Optional[{', '.join(hints)}]"
|
||||
|
||||
if rx_types.is_union(ev):
|
||||
res = [
|
||||
@ -260,8 +255,15 @@ def _generate_docstrings(clzs: list[Type[Component]], props: list[str]) -> str:
|
||||
# We've reached the functions, so stop.
|
||||
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
|
||||
if line.strip().startswith("#"):
|
||||
# Remove noqa from the comments.
|
||||
line = line.partition(" # noqa")[0]
|
||||
comments.append(line)
|
||||
continue
|
||||
|
||||
|
@ -97,7 +97,6 @@ StateIterVar = Union[list, set, tuple]
|
||||
if TYPE_CHECKING:
|
||||
from reflex.vars.base import Var
|
||||
|
||||
# ArgsSpec = Callable[[Var], list[Var]]
|
||||
ArgsSpec = (
|
||||
Callable[[], 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_
|
||||
if isinstance(type_, ModelField):
|
||||
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.
|
||||
type_ = Optional[type_]
|
||||
return type_
|
||||
|
@ -6,6 +6,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import reflex.app
|
||||
from reflex.config import environment
|
||||
from reflex.testing import AppHarness, AppHarnessProd
|
||||
|
||||
@ -76,3 +77,25 @@ def app_harness_env(request):
|
||||
The AppHarness class to use for the test.
|
||||
"""
|
||||
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()
|
||||
|
@ -637,8 +637,7 @@ async def test_client_side_state(
|
||||
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.
|
||||
state_var_input = driver.find_element(By.ID, "state_var")
|
||||
state_var_input.send_keys("re-triggering")
|
||||
set_sub("c1", "c1 post expire")
|
||||
|
||||
# get new references to all cookie and local storage elements (again)
|
||||
c1 = driver.find_element(By.ID, "c1")
|
||||
@ -659,7 +658,7 @@ async def test_client_side_state(
|
||||
l1s = driver.find_element(By.ID, "l1s")
|
||||
s1s = driver.find_element(By.ID, "s1s")
|
||||
|
||||
assert c1.text == "c1 value"
|
||||
assert c1.text == "c1 post expire"
|
||||
assert c2.text == "c2 value"
|
||||
assert c3.text == "" # temporary cookie expired after reset state!
|
||||
assert c4.text == "c4 value"
|
||||
@ -690,11 +689,11 @@ async def test_client_side_state(
|
||||
|
||||
async def poll_for_c1_set():
|
||||
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)
|
||||
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.c3 == ""
|
||||
assert sub_state.c4 == "c4 value"
|
||||
|
@ -106,7 +106,6 @@ def ComputedVars():
|
||||
),
|
||||
)
|
||||
|
||||
# raise Exception(State.count3._deps(objclass=State))
|
||||
app = rx.App()
|
||||
app.add_page(index)
|
||||
|
||||
|
@ -13,6 +13,8 @@ from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
from reflex.testing import AppHarness, AppHarnessProd
|
||||
|
||||
pytestmark = [pytest.mark.ignore_console_error]
|
||||
|
||||
|
||||
def TestApp():
|
||||
"""A test app for event exception handler integration."""
|
||||
|
@ -183,6 +183,6 @@ async def test_fully_controlled_input(fully_controlled_input: AppHarness):
|
||||
clear_button.click()
|
||||
assert AppHarness._poll_for(lambda: on_change_input.get_attribute("value") == "")
|
||||
# potential bug: clearing the on_change field doesn't itself trigger on_change
|
||||
# assert backend_state.text == ""
|
||||
# assert debounce_input.get_attribute("value") == ""
|
||||
# assert value_input.get_attribute("value") == ""
|
||||
# assert backend_state.text == "" #noqa: ERA001
|
||||
# assert debounce_input.get_attribute("value") == "" #noqa: ERA001
|
||||
# assert value_input.get_attribute("value") == "" #noqa: ERA001
|
||||
|
@ -381,9 +381,22 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive
|
||||
await asyncio.sleep(0.3)
|
||||
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)
|
||||
assert state.substates[state_name].progress_dicts
|
||||
file_data = state.substates[state_name]._file_data
|
||||
assert isinstance(file_data, dict)
|
||||
normalized_file_data = {Path(k).name: v for k, v in file_data.items()}
|
||||
|
@ -13,9 +13,6 @@ STATE_VAR = Var(_js_expr="default_state.name")
|
||||
("{}", '{"{}"}'),
|
||||
(None, '{""}'),
|
||||
(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):
|
||||
|
@ -1,8 +1,10 @@
|
||||
from typing import Dict, List, Set, Tuple, Union
|
||||
|
||||
import pydantic.v1
|
||||
import pytest
|
||||
|
||||
from reflex import el
|
||||
from reflex.base import Base
|
||||
from reflex.components.component import Component
|
||||
from reflex.components.core.foreach import (
|
||||
Foreach,
|
||||
@ -18,6 +20,12 @@ from reflex.vars.number import NumberVar
|
||||
from reflex.vars.sequence import ArrayVar
|
||||
|
||||
|
||||
class ForEachTag(Base):
|
||||
"""A tag for testing the ForEach component."""
|
||||
|
||||
name: str = ""
|
||||
|
||||
|
||||
class ForEachState(BaseState):
|
||||
"""A state for testing the ForEach component."""
|
||||
|
||||
@ -46,6 +54,8 @@ class ForEachState(BaseState):
|
||||
bad_annotation_list: list = [["red", "orange"], ["yellow", "blue"]]
|
||||
color_index_tuple: Tuple[int, str] = (0, "red")
|
||||
|
||||
default_factory_list: list[ForEachTag] = pydantic.v1.Field(default_factory=list)
|
||||
|
||||
|
||||
class ComponentStateTest(ComponentState):
|
||||
"""A test component state."""
|
||||
@ -290,3 +300,11 @@ def test_foreach_component_state():
|
||||
ForEachState.colors_list,
|
||||
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),
|
||||
)
|
||||
|
@ -42,7 +42,7 @@ def test_set_src_str():
|
||||
"`pic2.jpeg`",
|
||||
)
|
||||
# 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):
|
||||
|
@ -918,17 +918,17 @@ def test_invalid_event_handler_args(component2, test_state):
|
||||
# # Event Handler types must match
|
||||
# with pytest.raises(EventHandlerArgTypeMismatch):
|
||||
# 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):
|
||||
# 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):
|
||||
# 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_bool)
|
||||
# component2.create(on_user_visited_count_changed=test_state.do_something_with_int)
|
||||
# component2.create(on_user_list_changed=test_state.do_something_with_list_str)
|
||||
# component2.create(on_open=test_state.do_something_with_int) #noqa: ERA001
|
||||
# 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) #noqa: ERA001
|
||||
# component2.create(on_user_list_changed=test_state.do_something_with_list_str) #noqa: ERA001
|
||||
|
||||
# lambda cannot return weird values.
|
||||
with pytest.raises(ValueError):
|
||||
@ -1437,8 +1437,6 @@ def test_get_vars(component, exp_vars):
|
||||
comp_vars,
|
||||
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)
|
||||
|
||||
|
||||
|
@ -899,8 +899,6 @@ class DynamicState(BaseState):
|
||||
loaded: int = 0
|
||||
counter: int = 0
|
||||
|
||||
# side_effect_counter: int = 0
|
||||
|
||||
def on_load(self):
|
||||
"""Event handler for page on_load, should trigger for all navigation events."""
|
||||
self.loaded = self.loaded + 1
|
||||
@ -917,7 +915,6 @@ class DynamicState(BaseState):
|
||||
Returns:
|
||||
same as self.dynamic
|
||||
"""
|
||||
# self.side_effect_counter = self.side_effect_counter + 1
|
||||
return self.dynamic
|
||||
|
||||
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,
|
||||
f"comp_{arg_name}": exp_val,
|
||||
constants.CompileVars.IS_HYDRATED: False,
|
||||
# "side_effect_counter": exp_index,
|
||||
"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)
|
||||
assert state.loaded == 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):
|
||||
await app.state_manager.close()
|
||||
|
@ -3,11 +3,19 @@ from __future__ import annotations
|
||||
from typing import Dict, List, Optional, Type, Union
|
||||
|
||||
import attrs
|
||||
import pydantic.v1
|
||||
import pytest
|
||||
import sqlalchemy
|
||||
import sqlmodel
|
||||
from sqlalchemy import JSON, TypeDecorator
|
||||
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
|
||||
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)
|
||||
test_id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey("test.id"))
|
||||
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):
|
||||
@ -104,9 +116,64 @@ class SQLAClass(SQLABase):
|
||||
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):
|
||||
"""Test reflex model."""
|
||||
|
||||
no_default: Optional[int] = sqlmodel.Field(nullable=True)
|
||||
count: int = 0
|
||||
name: str = "test"
|
||||
int_list: List[int] = []
|
||||
@ -115,6 +182,7 @@ class ModelClass(rx.Model):
|
||||
sqla_tag: Optional[SQLATag] = None
|
||||
labels: List[SQLALabel] = []
|
||||
dict_str_str: Dict[str, str] = {}
|
||||
default_factory: List[int] = sqlmodel.Field(default_factory=list)
|
||||
|
||||
@property
|
||||
def str_property(self) -> str:
|
||||
@ -147,6 +215,7 @@ class ModelClass(rx.Model):
|
||||
class BaseClass(rx.Base):
|
||||
"""Test rx.Base class."""
|
||||
|
||||
no_default: Optional[int] = pydantic.v1.Field(required=False)
|
||||
count: int = 0
|
||||
name: str = "test"
|
||||
int_list: List[int] = []
|
||||
@ -155,6 +224,7 @@ class BaseClass(rx.Base):
|
||||
sqla_tag: Optional[SQLATag] = None
|
||||
labels: List[SQLALabel] = []
|
||||
dict_str_str: Dict[str, str] = {}
|
||||
default_factory: List[int] = pydantic.v1.Field(default_factory=list)
|
||||
|
||||
@property
|
||||
def str_property(self) -> str:
|
||||
@ -236,6 +306,7 @@ class AttrClass:
|
||||
sqla_tag: Optional[SQLATag] = None
|
||||
labels: List[SQLALabel] = []
|
||||
dict_str_str: Dict[str, str] = {}
|
||||
default_factory: List[int] = attrs.field(factory=list)
|
||||
|
||||
@property
|
||||
def str_property(self) -> str:
|
||||
@ -265,27 +336,17 @@ class AttrClass:
|
||||
return self.labels[0] if self.labels else None
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
params=[
|
||||
@pytest.mark.parametrize(
|
||||
"cls",
|
||||
[
|
||||
SQLAClass,
|
||||
SQLAClassDataclass,
|
||||
BaseClass,
|
||||
BareClass,
|
||||
ModelClass,
|
||||
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(
|
||||
"attr, expected",
|
||||
[
|
||||
@ -311,3 +372,38 @@ def test_get_attribute_access_type(cls: type, attr: str, expected: GenericType)
|
||||
expected: Expected type.
|
||||
"""
|
||||
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]
|
||||
|
@ -164,7 +164,7 @@ def test_constructor_postgresql(username, password, host, port, database, expect
|
||||
"localhost",
|
||||
5432,
|
||||
"db",
|
||||
"postgresql+psycopg2://user:pass@localhost:5432/db",
|
||||
"postgresql+psycopg://user:pass@localhost:5432/db",
|
||||
),
|
||||
(
|
||||
"user",
|
||||
@ -172,17 +172,17 @@ def test_constructor_postgresql(username, password, host, port, database, expect
|
||||
"localhost",
|
||||
None,
|
||||
"db",
|
||||
"postgresql+psycopg2://user@localhost/db",
|
||||
"postgresql+psycopg://user@localhost/db",
|
||||
),
|
||||
("user", "", "", None, "db", "postgresql+psycopg2://user@/db"),
|
||||
("", "", "localhost", 5432, "db", "postgresql+psycopg2://localhost:5432/db"),
|
||||
("", "", "", None, "db", "postgresql+psycopg2:///db"),
|
||||
("user", "", "", None, "db", "postgresql+psycopg://user@/db"),
|
||||
("", "", "localhost", 5432, "db", "postgresql+psycopg://localhost:5432/db"),
|
||||
("", "", "", None, "db", "postgresql+psycopg:///db"),
|
||||
],
|
||||
)
|
||||
def test_constructor_postgresql_psycopg2(
|
||||
def test_constructor_postgresql_psycopg(
|
||||
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:
|
||||
username: Database username.
|
||||
@ -192,10 +192,10 @@ def test_constructor_postgresql_psycopg2(
|
||||
database: Database name.
|
||||
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
|
||||
)
|
||||
assert db_config.engine == "postgresql+psycopg2"
|
||||
assert db_config.engine == "postgresql+psycopg"
|
||||
assert db_config.username == username
|
||||
assert db_config.password == password
|
||||
assert db_config.host == host
|
||||
|
@ -209,10 +209,6 @@ def test_event_redirect(input, output):
|
||||
assert isinstance(spec, EventSpec)
|
||||
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
|
||||
|
||||
|
||||
|
@ -127,8 +127,8 @@ def test_automigration(
|
||||
assert result[0].b == 4.2
|
||||
|
||||
# No-op
|
||||
# assert Model.migrate(autogenerate=True)
|
||||
# assert len(list(versions.glob("*.py"))) == 4
|
||||
# assert Model.migrate(autogenerate=True) #noqa: ERA001
|
||||
# assert len(list(versions.glob("*.py"))) == 4 #noqa: ERA001
|
||||
|
||||
# drop table (AlembicSecond)
|
||||
model_registry.get_metadata().clear()
|
||||
|
@ -56,6 +56,7 @@ from reflex.state import (
|
||||
from reflex.testing import chdir
|
||||
from reflex.utils import format, prerequisites, types
|
||||
from reflex.utils.exceptions import (
|
||||
InvalidLockWarningThresholdError,
|
||||
ReflexRuntimeError,
|
||||
SetUndefinedStateVarError,
|
||||
StateSerializationError,
|
||||
@ -67,7 +68,9 @@ from tests.units.states.mutation import MutableSQLAModel, MutableTestState
|
||||
from .states import GenState
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -791,7 +794,6 @@ async def test_process_event_simple(test_state):
|
||||
assert test_state.num1 == 69
|
||||
|
||||
# The delta should contain the changes, including computed vars.
|
||||
# assert update.delta == {"test_state": {"num1": 69, "sum": 72.14}}
|
||||
assert update.delta == {
|
||||
TestState.get_full_name(): {"num1": 69, "sum": 72.14, "upper": ""},
|
||||
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.
|
||||
"""
|
||||
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):
|
||||
await asyncio.sleep(0.01)
|
||||
@ -1812,6 +1815,7 @@ async def test_state_manager_lock_expire_contend(
|
||||
unexp_num1 = 666
|
||||
|
||||
state_manager_redis.lock_expiration = LOCK_EXPIRATION
|
||||
state_manager_redis.lock_warning_threshold = LOCK_WARNING_THRESHOLD
|
||||
|
||||
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
|
||||
|
||||
|
||||
@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")
|
||||
def mock_app_simple(monkeypatch) -> rx.App:
|
||||
"""Simple Mock app fixture.
|
||||
@ -1857,7 +1912,7 @@ def mock_app_simple(monkeypatch) -> rx.App:
|
||||
|
||||
setattr(app_module, CompileVars.APP, app)
|
||||
app.state = TestState
|
||||
app.event_namespace.emit = AsyncMock() # type: ignore
|
||||
app.event_namespace.emit = CopyingAsyncMock() # type: ignore
|
||||
|
||||
def _mock_get_app(*args, **kwargs):
|
||||
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()
|
||||
mcall = mock_app.event_namespace.emit.mock_calls[0]
|
||||
assert mcall.args[0] == str(SocketEvent.EVENT)
|
||||
assert json.loads(mcall.args[1]) == dataclasses.asdict(
|
||||
StateUpdate(
|
||||
assert mcall.args[1] == StateUpdate(
|
||||
delta={
|
||||
parent_state.get_full_name(): {
|
||||
"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
|
||||
|
||||
|
||||
@ -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
|
||||
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 (
|
||||
first_ws_message["delta"][BackgroundTaskState.get_full_name()].pop("router")
|
||||
first_ws_message.delta[BackgroundTaskState.get_full_name()].pop("router")
|
||||
is not None
|
||||
)
|
||||
assert first_ws_message == {
|
||||
"delta": {
|
||||
assert first_ws_message == StateUpdate(
|
||||
delta={
|
||||
BackgroundTaskState.get_full_name(): {
|
||||
"order": ["background_task:start"],
|
||||
"computed_order": ["background_task:start"],
|
||||
}
|
||||
},
|
||||
"events": [],
|
||||
"final": True,
|
||||
}
|
||||
events=[],
|
||||
final=True,
|
||||
)
|
||||
for call in emit_mock.mock_calls[1:5]:
|
||||
assert json.loads(call.args[1]) == {
|
||||
"delta": {
|
||||
assert call.args[1] == StateUpdate(
|
||||
delta={
|
||||
BackgroundTaskState.get_full_name(): {
|
||||
"computed_order": ["background_task:start"],
|
||||
}
|
||||
},
|
||||
"events": [],
|
||||
"final": True,
|
||||
}
|
||||
assert json.loads(emit_mock.mock_calls[-2].args[1]) == {
|
||||
"delta": {
|
||||
events=[],
|
||||
final=True,
|
||||
)
|
||||
assert emit_mock.mock_calls[-2].args[1] == StateUpdate(
|
||||
delta={
|
||||
BackgroundTaskState.get_full_name(): {
|
||||
"order": exp_order,
|
||||
"computed_order": exp_order,
|
||||
"dict_list": {},
|
||||
}
|
||||
},
|
||||
"events": [],
|
||||
"final": True,
|
||||
}
|
||||
assert json.loads(emit_mock.mock_calls[-1].args[1]) == {
|
||||
"delta": {
|
||||
events=[],
|
||||
final=True,
|
||||
)
|
||||
assert emit_mock.mock_calls[-1].args[1] == StateUpdate(
|
||||
delta={
|
||||
BackgroundTaskState.get_full_name(): {
|
||||
"computed_order": exp_order,
|
||||
},
|
||||
},
|
||||
"events": [],
|
||||
"final": True,
|
||||
}
|
||||
events=[],
|
||||
final=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -3238,12 +3291,42 @@ async def test_setvar_async_setter():
|
||||
@pytest.mark.parametrize(
|
||||
"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},
|
||||
(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):
|
||||
@ -3273,6 +3356,44 @@ config = rx.Config(
|
||||
state_manager = StateManager.create(state=State)
|
||||
assert state_manager.lock_expiration == expected_values[0] # 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):
|
||||
@ -3490,10 +3611,10 @@ def test_mutable_models():
|
||||
state.dirty_vars.clear()
|
||||
|
||||
# Not yet supported ENG-4083
|
||||
# assert isinstance(state.dc, MutableProxy)
|
||||
# state.dc.foo = "baz"
|
||||
# assert state.dirty_vars == {"dc"}
|
||||
# state.dirty_vars.clear()
|
||||
# assert isinstance(state.dc, MutableProxy) #noqa: ERA001
|
||||
# state.dc.foo = "baz" #noqa: ERA001
|
||||
# assert state.dirty_vars == {"dc"} #noqa: ERA001
|
||||
# state.dirty_vars.clear() #noqa: ERA001
|
||||
|
||||
|
||||
def test_get_value():
|
||||
|
@ -34,12 +34,6 @@ def test_disable():
|
||||
@pytest.mark.parametrize("event", ["init", "reinit", "run-dev", "run-prod", "export"])
|
||||
def test_send(mocker, event):
|
||||
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
|
||||
pathlib_path_read_text_mock = mocker.patch(
|
||||
|
@ -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"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(
|
||||
@ -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}")
|
||||
with pytest.raises(TypeError):
|
||||
print(eval(f"operand1_var {operator} operand2_var"))
|
||||
# operand1_var.operation(op=operator, other=operand2_var)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
print(eval(f"operand2_var {operator} operand1_var"))
|
||||
# operand1_var.operation(op=operator, other=operand2_var, flip=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
Loading…
Reference in New Issue
Block a user