Merge branch 'main' into lendemor/fix_order_of_hooks_and_memoized_evt_trigger

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

View File

@ -80,7 +80,7 @@ jobs:
fail-fast: false
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 }}

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -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]

View File

@ -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"

View File

@ -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
);

View File

@ -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")}

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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]

View File

@ -288,7 +288,7 @@ class DataEditor(NoSSRComponent):
freeze_columns: The number of columns which should remain in place when scrolling horizontally. Doesn't include rowMarkers.
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.

View File

@ -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
# },
}
)

View File

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

View File

@ -1,4 +1,4 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
"""Forms classes."""
from __future__ import annotations
@ -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):

View File

@ -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.

View File

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

View File

@ -1,4 +1,4 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
"""Media classes."""
from typing import Any, Union
@ -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):

View File

@ -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.

View File

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

View File

@ -1,4 +1,4 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
"""Other classes."""
from typing import Union
@ -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):

View File

@ -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.

View File

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

View File

@ -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.

View File

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

View File

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

View File

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

View File

@ -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"]]

View File

@ -70,7 +70,7 @@ class Image(NextComponent):
quality: The quality of the optimized image, an integer between 1 and 100, where 100 is the best quality and therefore largest file size. Defaults to 75.
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.

View File

@ -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)

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -8,6 +8,7 @@ from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spe
from reflex.vars.base import Var
from ..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()

View File

@ -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()

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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):

View File

@ -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.
"""
...

View File

@ -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"]

View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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):

View File

@ -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}."

View File

@ -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})")

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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}"

View File

@ -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)

View File

@ -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."""

View File

@ -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}"

View File

@ -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

View File

@ -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_

View File

@ -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()

View File

@ -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"

View File

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

View File

@ -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."""

View File

@ -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

View File

@ -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()}

View File

@ -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):

View File

@ -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),
)

View File

@ -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):

View File

@ -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)

View File

@ -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()

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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():

View File

@ -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(

View File

@ -1495,8 +1495,6 @@ def test_valid_var_operations(operand1_var: Var, operand2_var, operators: List[s
)
eval(f"operand1_var {operator} operand2_var")
eval(f"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(