Merge branch 'main' into lendemor/add_PTH_rule
This commit is contained in:
commit
193b8d8e30
6
.github/workflows/benchmarks.yml
vendored
6
.github/workflows/benchmarks.yml
vendored
@ -80,7 +80,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# Show OS combos first in GUI
|
# Show OS combos first in GUI
|
||||||
os: [ubuntu-latest, windows-latest, macos-12]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0']
|
python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0']
|
||||||
exclude:
|
exclude:
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
@ -92,7 +92,7 @@ jobs:
|
|||||||
python-version: '3.9.18'
|
python-version: '3.9.18'
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python-version: '3.10.13'
|
python-version: '3.10.13'
|
||||||
- os: macos-12
|
- os: macos-latest
|
||||||
python-version: '3.12.0'
|
python-version: '3.12.0'
|
||||||
include:
|
include:
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
@ -155,7 +155,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# Show OS combos first in GUI
|
# Show OS combos first in GUI
|
||||||
os: [ubuntu-latest, windows-latest, macos-12]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
python-version: ['3.11.5']
|
python-version: ['3.11.5']
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
@ -58,7 +58,7 @@ jobs:
|
|||||||
working-directory: ./reflex-web
|
working-directory: ./reflex-web
|
||||||
run: poetry run uv pip install -r requirements.txt
|
run: poetry run uv pip install -r requirements.txt
|
||||||
- name: Install additional dependencies for DB access
|
- name: Install additional dependencies for DB access
|
||||||
run: poetry run uv pip install psycopg2-binary
|
run: poetry run uv pip install psycopg
|
||||||
- name: Init Website for reflex-web
|
- name: Init Website for reflex-web
|
||||||
working-directory: ./reflex-web
|
working-directory: ./reflex-web
|
||||||
run: poetry run reflex init
|
run: poetry run reflex init
|
||||||
|
@ -22,9 +22,9 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
state_manager: ["redis", "memory"]
|
state_manager: ['redis', 'memory']
|
||||||
|
python-version: ['3.11.5', '3.12.0', '3.13.0']
|
||||||
split_index: [1, 2]
|
split_index: [1, 2]
|
||||||
python-version: ["3.11.5", "3.12.0"]
|
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
services:
|
services:
|
||||||
|
10
.github/workflows/integration_tests.yml
vendored
10
.github/workflows/integration_tests.yml
vendored
@ -43,7 +43,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
# Show OS combos first in GUI
|
# Show OS combos first in GUI
|
||||||
os: [ubuntu-latest, windows-latest]
|
os: [ubuntu-latest, windows-latest]
|
||||||
python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0']
|
python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0', '3.13.0']
|
||||||
exclude:
|
exclude:
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python-version: '3.10.13'
|
python-version: '3.10.13'
|
||||||
@ -73,7 +73,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
poetry run uv pip install -r requirements.txt
|
poetry run uv pip install -r requirements.txt
|
||||||
- name: Install additional dependencies for DB access
|
- name: Install additional dependencies for DB access
|
||||||
run: poetry run uv pip install psycopg2-binary
|
run: poetry run uv pip install psycopg
|
||||||
- name: Check export --backend-only before init for counter example
|
- name: Check export --backend-only before init for counter example
|
||||||
working-directory: ./reflex-examples/counter
|
working-directory: ./reflex-examples/counter
|
||||||
run: |
|
run: |
|
||||||
@ -147,7 +147,7 @@ jobs:
|
|||||||
working-directory: ./reflex-web
|
working-directory: ./reflex-web
|
||||||
run: poetry run uv pip install $(grep -ivE "reflex " requirements.txt)
|
run: poetry run uv pip install $(grep -ivE "reflex " requirements.txt)
|
||||||
- name: Install additional dependencies for DB access
|
- name: Install additional dependencies for DB access
|
||||||
run: poetry run uv pip install psycopg2-binary
|
run: poetry run uv pip install psycopg
|
||||||
- name: Init Website for reflex-web
|
- name: Init Website for reflex-web
|
||||||
working-directory: ./reflex-web
|
working-directory: ./reflex-web
|
||||||
run: poetry run reflex init
|
run: poetry run reflex init
|
||||||
@ -198,7 +198,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ['3.11.5', '3.12.0']
|
python-version: ['3.11.5', '3.12.0']
|
||||||
runs-on: macos-12
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/setup_build_env
|
- uses: ./.github/actions/setup_build_env
|
||||||
@ -216,7 +216,7 @@ jobs:
|
|||||||
working-directory: ./reflex-web
|
working-directory: ./reflex-web
|
||||||
run: poetry run uv pip install -r requirements.txt
|
run: poetry run uv pip install -r requirements.txt
|
||||||
- name: Install additional dependencies for DB access
|
- name: Install additional dependencies for DB access
|
||||||
run: poetry run uv pip install psycopg2-binary
|
run: poetry run uv pip install psycopg
|
||||||
- name: Init Website for reflex-web
|
- name: Init Website for reflex-web
|
||||||
working-directory: ./reflex-web
|
working-directory: ./reflex-web
|
||||||
run: poetry run reflex init
|
run: poetry run reflex init
|
||||||
|
7
.github/workflows/unit_tests.yml
vendored
7
.github/workflows/unit_tests.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest]
|
os: [ubuntu-latest, windows-latest]
|
||||||
python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0']
|
python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0', '3.13.0']
|
||||||
# Windows is a bit behind on Python version availability in Github
|
# Windows is a bit behind on Python version availability in Github
|
||||||
exclude:
|
exclude:
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
@ -88,8 +88,9 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0']
|
# Note: py39, py310 versions chosen due to available arm64 darwin builds.
|
||||||
runs-on: macos-12
|
python-version: ['3.9.13', '3.10.11', '3.11.5', '3.12.0', '3.13.0']
|
||||||
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/setup_build_env
|
- uses: ./.github/actions/setup_build_env
|
||||||
|
@ -52,7 +52,7 @@ FROM python:3.13-slim
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN adduser --disabled-password --home /app reflex
|
RUN adduser --disabled-password --home /app reflex
|
||||||
COPY --chown=reflex --from=init /app /app
|
COPY --chown=reflex --from=init /app /app
|
||||||
# Install libpq-dev for psycopg2 (skip if not using postgres).
|
# Install libpq-dev for psycopg (skip if not using postgres).
|
||||||
RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
|
||||||
USER reflex
|
USER reflex
|
||||||
ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1
|
ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1
|
||||||
|
@ -39,7 +39,7 @@ FROM python:3.13-slim
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN adduser --disabled-password --home /app reflex
|
RUN adduser --disabled-password --home /app reflex
|
||||||
COPY --chown=reflex --from=init /app /app
|
COPY --chown=reflex --from=init /app /app
|
||||||
# Install libpq-dev for psycopg2 (skip if not using postgres).
|
# Install libpq-dev for psycopg (skip if not using postgres).
|
||||||
RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
|
||||||
USER reflex
|
USER reflex
|
||||||
ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1
|
ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1
|
||||||
|
@ -15,7 +15,7 @@ services:
|
|||||||
|
|
||||||
app:
|
app:
|
||||||
environment:
|
environment:
|
||||||
DB_URL: postgresql+psycopg2://postgres:secret@db/postgres
|
DB_URL: postgresql+psycopg://postgres:secret@db/postgres
|
||||||
REDIS_URL: redis://redis:6379
|
REDIS_URL: redis://redis:6379
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
16
poetry.lock
generated
16
poetry.lock
generated
@ -1542,18 +1542,18 @@ type = ["mypy (>=1.11.2)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "playwright"
|
name = "playwright"
|
||||||
version = "1.49.0"
|
version = "1.49.1"
|
||||||
description = "A high-level API to automate web browsers"
|
description = "A high-level API to automate web browsers"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
files = [
|
files = [
|
||||||
{file = "playwright-1.49.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:704532a2d8ba580ec9e1895bfeafddce2e3d52320d4eb8aa38e80376acc5cbb0"},
|
{file = "playwright-1.49.1-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:1041ffb45a0d0bc44d698d3a5aa3ac4b67c9bd03540da43a0b70616ad52592b8"},
|
||||||
{file = "playwright-1.49.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e453f02c4e5cc2db7e9759c47e7425f32e50ac76c76b7eb17c69eed72f01c4d8"},
|
{file = "playwright-1.49.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9f38ed3d0c1f4e0a6d1c92e73dd9a61f8855133249d6f0cec28648d38a7137be"},
|
||||||
{file = "playwright-1.49.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:37ae985309184472946a6eb1a237e5d93c9e58a781fa73b75c8751325002a5d4"},
|
{file = "playwright-1.49.1-py3-none-macosx_11_0_universal2.whl", hash = "sha256:3be48c6d26dc819ca0a26567c1ae36a980a0303dcd4249feb6f59e115aaddfb8"},
|
||||||
{file = "playwright-1.49.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:68d94beffb3c9213e3ceaafa66171affd9a5d9162e0c8a3eed1b1132c2e57598"},
|
{file = "playwright-1.49.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:753ca90ee31b4b03d165cfd36e477309ebf2b4381953f2a982ff612d85b147d2"},
|
||||||
{file = "playwright-1.49.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f12d2aecdb41fc25a624cb15f3e8391c252ebd81985e3d5c1c261fe93779345"},
|
{file = "playwright-1.49.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd9bc8dab37aa25198a01f555f0a2e2c3813fe200fef018ac34dfe86b34994b9"},
|
||||||
{file = "playwright-1.49.0-py3-none-win32.whl", hash = "sha256:91103de52d470594ad375b512d7143fa95d6039111ae11a93eb4fe2f2b4a4858"},
|
{file = "playwright-1.49.1-py3-none-win32.whl", hash = "sha256:43b304be67f096058e587dac453ece550eff87b8fbed28de30f4f022cc1745bb"},
|
||||||
{file = "playwright-1.49.0-py3-none-win_amd64.whl", hash = "sha256:34d28a2c2d46403368610be4339898dc9c34eb9f7c578207b4715c49743a072a"},
|
{file = "playwright-1.49.1-py3-none-win_amd64.whl", hash = "sha256:47b23cb346283278f5b4d1e1990bcb6d6302f80c0aa0ca93dd0601a1400191df"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -93,7 +93,19 @@ build-backend = "poetry.core.masonry.api"
|
|||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
target-version = "py39"
|
target-version = "py39"
|
||||||
lint.isort.split-on-trailing-comma = false
|
lint.isort.split-on-trailing-comma = false
|
||||||
lint.select = ["B", "D", "E", "F", "I", "SIM", "W", "PTH", "RUF", "FURB"]
|
lint.select = [
|
||||||
|
"B",
|
||||||
|
"D",
|
||||||
|
"E",
|
||||||
|
"F",
|
||||||
|
"I",
|
||||||
|
"SIM",
|
||||||
|
"W",
|
||||||
|
"PTH",
|
||||||
|
"RUF",
|
||||||
|
"FURB",
|
||||||
|
"ERA"
|
||||||
|
]
|
||||||
lint.ignore = ["B008", "D205", "E501", "F403", "SIM115", "RUF006", "RUF012"]
|
lint.ignore = ["B008", "D205", "E501", "F403", "SIM115", "RUF006", "RUF012"]
|
||||||
lint.pydocstyle.convention = "google"
|
lint.pydocstyle.convention = "google"
|
||||||
|
|
||||||
|
@ -40,9 +40,6 @@ let event_processing = false;
|
|||||||
// Array holding pending events to be processed.
|
// Array holding pending events to be processed.
|
||||||
const event_queue = [];
|
const event_queue = [];
|
||||||
|
|
||||||
// Pending upload promises, by id
|
|
||||||
const upload_controllers = {};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a UUID (Used for session tokens).
|
* Generate a UUID (Used for session tokens).
|
||||||
* Taken from: https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid
|
* Taken from: https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid
|
||||||
@ -300,7 +297,7 @@ export const applyEvent = async (event, socket) => {
|
|||||||
if (socket) {
|
if (socket) {
|
||||||
socket.emit(
|
socket.emit(
|
||||||
"event",
|
"event",
|
||||||
JSON.stringify(event, (k, v) => (v === undefined ? null : v))
|
event,
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -407,6 +404,8 @@ export const connect = async (
|
|||||||
transports: transports,
|
transports: transports,
|
||||||
autoUnref: false,
|
autoUnref: false,
|
||||||
});
|
});
|
||||||
|
// Ensure undefined fields in events are sent as null instead of removed
|
||||||
|
socket.current.io.encoder.replacer = (k, v) => (v === undefined ? null : v)
|
||||||
|
|
||||||
function checkVisibility() {
|
function checkVisibility() {
|
||||||
if (document.visibilityState === "visible") {
|
if (document.visibilityState === "visible") {
|
||||||
@ -443,8 +442,7 @@ export const connect = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
// On each received message, queue the updates and events.
|
// On each received message, queue the updates and events.
|
||||||
socket.current.on("event", async (message) => {
|
socket.current.on("event", async (update) => {
|
||||||
const update = JSON5.parse(message);
|
|
||||||
for (const substate in update.delta) {
|
for (const substate in update.delta) {
|
||||||
dispatch[substate](update.delta[substate]);
|
dispatch[substate](update.delta[substate]);
|
||||||
}
|
}
|
||||||
@ -456,7 +454,7 @@ export const connect = async (
|
|||||||
});
|
});
|
||||||
socket.current.on("reload", async (event) => {
|
socket.current.on("reload", async (event) => {
|
||||||
event_processing = false;
|
event_processing = false;
|
||||||
queueEvents([...initialEvents(), JSON5.parse(event)], socket);
|
queueEvents([...initialEvents(), event], socket);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("visibilitychange", checkVisibility);
|
document.addEventListener("visibilitychange", checkVisibility);
|
||||||
@ -485,7 +483,9 @@ export const uploadFiles = async (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (upload_controllers[upload_id]) {
|
const upload_ref_name = `__upload_controllers_${upload_id}`
|
||||||
|
|
||||||
|
if (refs[upload_ref_name]) {
|
||||||
console.log("Upload already in progress for ", upload_id);
|
console.log("Upload already in progress for ", upload_id);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -497,7 +497,9 @@ export const uploadFiles = async (
|
|||||||
// Whenever called, responseText will contain the entire response so far.
|
// Whenever called, responseText will contain the entire response so far.
|
||||||
const chunks = progressEvent.event.target.responseText.trim().split("\n");
|
const chunks = progressEvent.event.target.responseText.trim().split("\n");
|
||||||
// So only process _new_ chunks beyond resp_idx.
|
// So only process _new_ chunks beyond resp_idx.
|
||||||
chunks.slice(resp_idx).map((chunk) => {
|
chunks.slice(resp_idx).map((chunk_json) => {
|
||||||
|
try {
|
||||||
|
const chunk = JSON5.parse(chunk_json);
|
||||||
event_callbacks.map((f, ix) => {
|
event_callbacks.map((f, ix) => {
|
||||||
f(chunk)
|
f(chunk)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -509,11 +511,17 @@ export const uploadFiles = async (
|
|||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
if (progressEvent.progress === 1) {
|
if (progressEvent.progress === 1) {
|
||||||
// Chunk may be incomplete, so only report errors when full response is available.
|
// Chunk may be incomplete, so only report errors when full response is available.
|
||||||
console.log("Error parsing chunk", chunk, e);
|
console.log("Error processing chunk", chunk, e);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (progressEvent.progress === 1) {
|
||||||
|
console.log("Error parsing chunk", chunk_json, e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -537,7 +545,7 @@ export const uploadFiles = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Send the file to the server.
|
// Send the file to the server.
|
||||||
upload_controllers[upload_id] = controller;
|
refs[upload_ref_name] = controller;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await axios.post(getBackendURL(UPLOADURL), formdata, config);
|
return await axios.post(getBackendURL(UPLOADURL), formdata, config);
|
||||||
@ -557,7 +565,7 @@ export const uploadFiles = async (
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
delete upload_controllers[upload_id];
|
delete refs[upload_ref_name];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -799,7 +807,7 @@ export const useEventLoop = (
|
|||||||
connect(
|
connect(
|
||||||
socket,
|
socket,
|
||||||
dispatch,
|
dispatch,
|
||||||
["websocket", "polling"],
|
["websocket"],
|
||||||
setConnectErrors,
|
setConnectErrors,
|
||||||
client_storage
|
client_storage
|
||||||
);
|
);
|
||||||
|
@ -331,7 +331,7 @@ _MAPPING: dict = {
|
|||||||
"SessionStorage",
|
"SessionStorage",
|
||||||
],
|
],
|
||||||
"middleware": ["middleware", "Middleware"],
|
"middleware": ["middleware", "Middleware"],
|
||||||
"model": ["session", "Model"],
|
"model": ["asession", "session", "Model"],
|
||||||
"state": [
|
"state": [
|
||||||
"var",
|
"var",
|
||||||
"ComponentState",
|
"ComponentState",
|
||||||
|
@ -186,6 +186,7 @@ from .istate.wrappers import get_state as get_state
|
|||||||
from .middleware import Middleware as Middleware
|
from .middleware import Middleware as Middleware
|
||||||
from .middleware import middleware as middleware
|
from .middleware import middleware as middleware
|
||||||
from .model import Model as Model
|
from .model import Model as Model
|
||||||
|
from .model import asession as asession
|
||||||
from .model import session as session
|
from .model import session as session
|
||||||
from .page import page as page
|
from .page import page as page
|
||||||
from .state import ComponentState as ComponentState
|
from .state import ComponentState as ComponentState
|
||||||
|
@ -17,6 +17,7 @@ import sys
|
|||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
@ -363,6 +364,11 @@ class App(MiddlewareMixin, LifespanMixin):
|
|||||||
max_http_buffer_size=constants.POLLING_MAX_HTTP_BUFFER_SIZE,
|
max_http_buffer_size=constants.POLLING_MAX_HTTP_BUFFER_SIZE,
|
||||||
ping_interval=constants.Ping.INTERVAL,
|
ping_interval=constants.Ping.INTERVAL,
|
||||||
ping_timeout=constants.Ping.TIMEOUT,
|
ping_timeout=constants.Ping.TIMEOUT,
|
||||||
|
json=SimpleNamespace(
|
||||||
|
dumps=staticmethod(format.json_dumps),
|
||||||
|
loads=staticmethod(json.loads),
|
||||||
|
),
|
||||||
|
transports=["websocket"],
|
||||||
)
|
)
|
||||||
elif getattr(self.sio, "async_mode", "") != "asgi":
|
elif getattr(self.sio, "async_mode", "") != "asgi":
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@ -467,7 +473,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|||||||
|
|
||||||
def add_page(
|
def add_page(
|
||||||
self,
|
self,
|
||||||
component: Component | ComponentCallable,
|
component: Component | ComponentCallable | None = None,
|
||||||
route: str | None = None,
|
route: str | None = None,
|
||||||
title: str | Var | None = None,
|
title: str | Var | None = None,
|
||||||
description: str | Var | None = None,
|
description: str | Var | None = None,
|
||||||
@ -490,17 +496,33 @@ class App(MiddlewareMixin, LifespanMixin):
|
|||||||
meta: The metadata of the page.
|
meta: The metadata of the page.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: When the specified route name already exists.
|
PageValueError: When the component is not set for a non-404 page.
|
||||||
|
RouteValueError: When the specified route name already exists.
|
||||||
"""
|
"""
|
||||||
# If the route is not set, get it from the callable.
|
# If the route is not set, get it from the callable.
|
||||||
if route is None:
|
if route is None:
|
||||||
if not isinstance(component, Callable):
|
if not isinstance(component, Callable):
|
||||||
raise ValueError("Route must be set if component is not a callable.")
|
raise exceptions.RouteValueError(
|
||||||
|
"Route must be set if component is not a callable."
|
||||||
|
)
|
||||||
# Format the route.
|
# Format the route.
|
||||||
route = format.format_route(component.__name__)
|
route = format.format_route(component.__name__)
|
||||||
else:
|
else:
|
||||||
route = format.format_route(route, format_case=False)
|
route = format.format_route(route, format_case=False)
|
||||||
|
|
||||||
|
if route == constants.Page404.SLUG:
|
||||||
|
if component is None:
|
||||||
|
component = Default404Page.create()
|
||||||
|
component = wait_for_client_redirect(self._generate_component(component))
|
||||||
|
title = title or constants.Page404.TITLE
|
||||||
|
description = description or constants.Page404.DESCRIPTION
|
||||||
|
image = image or constants.Page404.IMAGE
|
||||||
|
else:
|
||||||
|
if component is None:
|
||||||
|
raise exceptions.PageValueError(
|
||||||
|
"Component must be set for a non-404 page."
|
||||||
|
)
|
||||||
|
|
||||||
# Check if the route given is valid
|
# Check if the route given is valid
|
||||||
verify_route_validity(route)
|
verify_route_validity(route)
|
||||||
|
|
||||||
@ -516,7 +538,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|||||||
if route == constants.PageNames.INDEX_ROUTE
|
if route == constants.PageNames.INDEX_ROUTE
|
||||||
else f"`{route}`"
|
else f"`{route}`"
|
||||||
)
|
)
|
||||||
raise ValueError(
|
raise exceptions.RouteValueError(
|
||||||
f"Duplicate page route {route_name} already exists. Make sure you do not have two"
|
f"Duplicate page route {route_name} already exists. Make sure you do not have two"
|
||||||
f" pages with the same route"
|
f" pages with the same route"
|
||||||
)
|
)
|
||||||
@ -633,10 +655,14 @@ class App(MiddlewareMixin, LifespanMixin):
|
|||||||
on_load: The event handler(s) that will be called each time the page load.
|
on_load: The event handler(s) that will be called each time the page load.
|
||||||
meta: The metadata of the page.
|
meta: The metadata of the page.
|
||||||
"""
|
"""
|
||||||
if component is None:
|
console.deprecate(
|
||||||
component = Default404Page.create()
|
feature_name="App.add_custom_404_page",
|
||||||
|
reason=f"Use app.add_page(component, route='/{constants.Page404.SLUG}') instead.",
|
||||||
|
deprecation_version="0.6.7",
|
||||||
|
removal_version="0.8.0",
|
||||||
|
)
|
||||||
self.add_page(
|
self.add_page(
|
||||||
component=wait_for_client_redirect(self._generate_component(component)),
|
component=component,
|
||||||
route=constants.Page404.SLUG,
|
route=constants.Page404.SLUG,
|
||||||
title=title or constants.Page404.TITLE,
|
title=title or constants.Page404.TITLE,
|
||||||
image=image or constants.Page404.IMAGE,
|
image=image or constants.Page404.IMAGE,
|
||||||
@ -837,7 +863,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|||||||
|
|
||||||
# Render a default 404 page if the user didn't supply one
|
# Render a default 404 page if the user didn't supply one
|
||||||
if constants.Page404.SLUG not in self.unevaluated_pages:
|
if constants.Page404.SLUG not in self.unevaluated_pages:
|
||||||
self.add_custom_404_page()
|
self.add_page(route=constants.Page404.SLUG)
|
||||||
|
|
||||||
# Fix up the style.
|
# Fix up the style.
|
||||||
self.style = evaluate_style_namespaces(self.style)
|
self.style = evaluate_style_namespaces(self.style)
|
||||||
@ -947,12 +973,12 @@ class App(MiddlewareMixin, LifespanMixin):
|
|||||||
is not None
|
is not None
|
||||||
):
|
):
|
||||||
executor = concurrent.futures.ProcessPoolExecutor(
|
executor = concurrent.futures.ProcessPoolExecutor(
|
||||||
max_workers=number_of_processes,
|
max_workers=number_of_processes or None,
|
||||||
mp_context=multiprocessing.get_context("fork"),
|
mp_context=multiprocessing.get_context("fork"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
executor = concurrent.futures.ThreadPoolExecutor(
|
executor = concurrent.futures.ThreadPoolExecutor(
|
||||||
max_workers=environment.REFLEX_COMPILE_THREADS.get()
|
max_workers=environment.REFLEX_COMPILE_THREADS.get() or None
|
||||||
)
|
)
|
||||||
|
|
||||||
for route, component in zip(self.pages, page_components):
|
for route, component in zip(self.pages, page_components):
|
||||||
@ -965,7 +991,6 @@ class App(MiddlewareMixin, LifespanMixin):
|
|||||||
|
|
||||||
def _submit_work(fn, *args, **kwargs):
|
def _submit_work(fn, *args, **kwargs):
|
||||||
f = executor.submit(fn, *args, **kwargs)
|
f = executor.submit(fn, *args, **kwargs)
|
||||||
# f = executor.apipe(fn, *args, **kwargs)
|
|
||||||
result_futures.append(f)
|
result_futures.append(f)
|
||||||
|
|
||||||
# Compile the pre-compiled pages.
|
# Compile the pre-compiled pages.
|
||||||
@ -1157,7 +1182,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|||||||
if hasattr(handler_fn, "__name__"):
|
if hasattr(handler_fn, "__name__"):
|
||||||
_fn_name = handler_fn.__name__
|
_fn_name = handler_fn.__name__
|
||||||
else:
|
else:
|
||||||
_fn_name = handler_fn.__class__.__name__
|
_fn_name = type(handler_fn).__name__
|
||||||
|
|
||||||
if isinstance(handler_fn, functools.partial):
|
if isinstance(handler_fn, functools.partial):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@ -1270,7 +1295,7 @@ async def process(
|
|||||||
await asyncio.create_task(
|
await asyncio.create_task(
|
||||||
app.event_namespace.emit(
|
app.event_namespace.emit(
|
||||||
"reload",
|
"reload",
|
||||||
data=format.json_dumps(event),
|
data=event,
|
||||||
to=sid,
|
to=sid,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -1523,7 +1548,7 @@ class EventNamespace(AsyncNamespace):
|
|||||||
"""
|
"""
|
||||||
# Creating a task prevents the update from being blocked behind other coroutines.
|
# Creating a task prevents the update from being blocked behind other coroutines.
|
||||||
await asyncio.create_task(
|
await asyncio.create_task(
|
||||||
self.emit(str(constants.SocketEvent.EVENT), update.json(), to=sid)
|
self.emit(str(constants.SocketEvent.EVENT), update, to=sid)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def on_event(self, sid, data):
|
async def on_event(self, sid, data):
|
||||||
@ -1536,7 +1561,7 @@ class EventNamespace(AsyncNamespace):
|
|||||||
sid: The Socket.IO session id.
|
sid: The Socket.IO session id.
|
||||||
data: The event data.
|
data: The event data.
|
||||||
"""
|
"""
|
||||||
fields = json.loads(data)
|
fields = data
|
||||||
# Get the event.
|
# Get the event.
|
||||||
event = Event(
|
event = Event(
|
||||||
**{k: v for k, v in fields.items() if k not in ("handler", "event_actions")}
|
**{k: v for k, v in fields.items() if k not in ("handler", "event_actions")}
|
||||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from reflex import constants
|
from reflex import constants
|
||||||
from reflex.utils.exec import is_backend_only
|
from reflex.config import EnvironmentVariables
|
||||||
|
|
||||||
|
|
||||||
def asset(
|
def asset(
|
||||||
@ -52,7 +52,7 @@ def asset(
|
|||||||
The relative URL to the asset.
|
The relative URL to the asset.
|
||||||
"""
|
"""
|
||||||
assets = constants.Dirs.APP_ASSETS
|
assets = constants.Dirs.APP_ASSETS
|
||||||
backend_only = is_backend_only()
|
backend_only = EnvironmentVariables.REFLEX_BACKEND_ONLY.get()
|
||||||
|
|
||||||
# Local asset handling
|
# Local asset handling
|
||||||
if not shared:
|
if not shared:
|
||||||
|
@ -161,7 +161,7 @@ class ComponentNamespace(SimpleNamespace):
|
|||||||
Returns:
|
Returns:
|
||||||
The hash of the namespace.
|
The hash of the namespace.
|
||||||
"""
|
"""
|
||||||
return hash(self.__class__.__name__)
|
return hash(type(self).__name__)
|
||||||
|
|
||||||
|
|
||||||
def evaluate_style_namespaces(style: ComponentStyle) -> dict:
|
def evaluate_style_namespaces(style: ComponentStyle) -> dict:
|
||||||
@ -653,7 +653,6 @@ class Component(BaseComponent, ABC):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The event triggers.
|
The event triggers.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
default_triggers: Dict[str, types.ArgsSpec | Sequence[types.ArgsSpec]] = {
|
default_triggers: Dict[str, types.ArgsSpec | Sequence[types.ArgsSpec]] = {
|
||||||
EventTriggers.ON_FOCUS: no_args_event_spec,
|
EventTriggers.ON_FOCUS: no_args_event_spec,
|
||||||
@ -2565,7 +2564,7 @@ class LiteralComponentVar(CachedVarOperation, LiteralVar, ComponentVar):
|
|||||||
Returns:
|
Returns:
|
||||||
The hash of the var.
|
The hash of the var.
|
||||||
"""
|
"""
|
||||||
return hash((self.__class__.__name__, self._js_expr))
|
return hash((type(self).__name__, self._js_expr))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(
|
def create(
|
||||||
|
@ -24,7 +24,7 @@ class ClientSideRouting(Component):
|
|||||||
library = "$/utils/client_side_routing"
|
library = "$/utils/client_side_routing"
|
||||||
tag = "useClientSideRouting"
|
tag = "useClientSideRouting"
|
||||||
|
|
||||||
def add_hooks(self) -> list[str]:
|
def add_hooks(self) -> list[str | Var]:
|
||||||
"""Get the hooks to render.
|
"""Get the hooks to render.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -66,4 +66,4 @@ class Default404Page(Component):
|
|||||||
tag = "Error"
|
tag = "Error"
|
||||||
is_default = True
|
is_default = True
|
||||||
|
|
||||||
status_code: Var[int] = 404 # type: ignore
|
status_code: Var[int] = Var.create(404)
|
||||||
|
@ -13,7 +13,7 @@ from reflex.vars.base import Var
|
|||||||
route_not_found: Var
|
route_not_found: Var
|
||||||
|
|
||||||
class ClientSideRouting(Component):
|
class ClientSideRouting(Component):
|
||||||
def add_hooks(self) -> list[str]: ...
|
def add_hooks(self) -> list[str | Var]: ...
|
||||||
def render(self) -> str: ...
|
def render(self) -> str: ...
|
||||||
@overload
|
@overload
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -49,9 +49,9 @@ class Cond(MemoizationLeaf):
|
|||||||
The conditional component.
|
The conditional component.
|
||||||
"""
|
"""
|
||||||
# Wrap everything in fragments.
|
# Wrap everything in fragments.
|
||||||
if comp1.__class__.__name__ != "Fragment":
|
if type(comp1).__name__ != "Fragment":
|
||||||
comp1 = Fragment.create(comp1)
|
comp1 = Fragment.create(comp1)
|
||||||
if comp2 is None or comp2.__class__.__name__ != "Fragment":
|
if comp2 is None or type(comp2).__name__ != "Fragment":
|
||||||
comp2 = Fragment.create(comp2) if comp2 else Fragment.create()
|
comp2 = Fragment.create(comp2) if comp2 else Fragment.create()
|
||||||
return Fragment.create(
|
return Fragment.create(
|
||||||
cls(
|
cls(
|
||||||
|
@ -29,7 +29,7 @@ from reflex.event import (
|
|||||||
from reflex.utils import format
|
from reflex.utils import format
|
||||||
from reflex.utils.imports import ImportVar
|
from reflex.utils.imports import ImportVar
|
||||||
from reflex.vars import VarData
|
from reflex.vars import VarData
|
||||||
from reflex.vars.base import CallableVar, LiteralVar, Var, get_unique_variable_name
|
from reflex.vars.base import CallableVar, Var, get_unique_variable_name
|
||||||
from reflex.vars.sequence import LiteralStringVar
|
from reflex.vars.sequence import LiteralStringVar
|
||||||
|
|
||||||
DEFAULT_UPLOAD_ID: str = "default"
|
DEFAULT_UPLOAD_ID: str = "default"
|
||||||
@ -108,7 +108,8 @@ def clear_selected_files(id_: str = DEFAULT_UPLOAD_ID) -> EventSpec:
|
|||||||
# UploadFilesProvider assigns a special function to clear selected files
|
# UploadFilesProvider assigns a special function to clear selected files
|
||||||
# into the shared global refs object to make it accessible outside a React
|
# into the shared global refs object to make it accessible outside a React
|
||||||
# component via `run_script` (otherwise backend could never clear files).
|
# component via `run_script` (otherwise backend could never clear files).
|
||||||
return run_script(f"refs['__clear_selected_files']({id_!r})")
|
func = Var("__clear_selected_files")._as_ref()
|
||||||
|
return run_script(f"{func}({id_!r})")
|
||||||
|
|
||||||
|
|
||||||
def cancel_upload(upload_id: str) -> EventSpec:
|
def cancel_upload(upload_id: str) -> EventSpec:
|
||||||
@ -120,7 +121,8 @@ def cancel_upload(upload_id: str) -> EventSpec:
|
|||||||
Returns:
|
Returns:
|
||||||
An event spec that cancels the upload when triggered.
|
An event spec that cancels the upload when triggered.
|
||||||
"""
|
"""
|
||||||
return run_script(f"upload_controllers[{LiteralVar.create(upload_id)!s}]?.abort()")
|
controller = Var(f"__upload_controllers_{upload_id}")._as_ref()
|
||||||
|
return run_script(f"{controller}?.abort()")
|
||||||
|
|
||||||
|
|
||||||
def get_upload_dir() -> Path:
|
def get_upload_dir() -> Path:
|
||||||
|
@ -51,27 +51,6 @@ class GridColumnIcons(Enum):
|
|||||||
VideoUri = "video_uri"
|
VideoUri = "video_uri"
|
||||||
|
|
||||||
|
|
||||||
# @serializer
|
|
||||||
# def serialize_gridcolumn_icon(icon: GridColumnIcons) -> str:
|
|
||||||
# """Serialize grid column icon.
|
|
||||||
|
|
||||||
# Args:
|
|
||||||
# icon: the Icon to serialize.
|
|
||||||
|
|
||||||
# Returns:
|
|
||||||
# The serialized value.
|
|
||||||
# """
|
|
||||||
# return "prefix" + str(icon)
|
|
||||||
|
|
||||||
|
|
||||||
# class DataEditorColumn(Base):
|
|
||||||
# """Column."""
|
|
||||||
|
|
||||||
# title: str
|
|
||||||
# id: Optional[str] = None
|
|
||||||
# type_: str = "str"
|
|
||||||
|
|
||||||
|
|
||||||
class DataEditorTheme(Base):
|
class DataEditorTheme(Base):
|
||||||
"""The theme for the DataEditor component."""
|
"""The theme for the DataEditor component."""
|
||||||
|
|
||||||
@ -229,7 +208,7 @@ class DataEditor(NoSSRComponent):
|
|||||||
header_height: Var[int]
|
header_height: Var[int]
|
||||||
|
|
||||||
# Additional header icons:
|
# Additional header icons:
|
||||||
# header_icons: Var[Any] # (TODO: must be a map of name: svg)
|
# header_icons: Var[Any] # (TODO: must be a map of name: svg) #noqa: ERA001
|
||||||
|
|
||||||
# The maximum width a column can be automatically sized to.
|
# The maximum width a column can be automatically sized to.
|
||||||
max_column_auto_width: Var[int]
|
max_column_auto_width: Var[int]
|
||||||
|
@ -288,7 +288,7 @@ class DataEditor(NoSSRComponent):
|
|||||||
freeze_columns: The number of columns which should remain in place when scrolling horizontally. Doesn't include rowMarkers.
|
freeze_columns: The number of columns which should remain in place when scrolling horizontally. Doesn't include rowMarkers.
|
||||||
group_header_height: Controls the header of the group header row.
|
group_header_height: Controls the header of the group header row.
|
||||||
header_height: Controls the height of the header row.
|
header_height: Controls the height of the header row.
|
||||||
max_column_auto_width: Additional header icons: header_icons: Var[Any] # (TODO: must be a map of name: svg) The maximum width a column can be automatically sized to.
|
max_column_auto_width: The maximum width a column can be automatically sized to.
|
||||||
max_column_width: The maximum width a column can be resized to.
|
max_column_width: The maximum width a column can be resized to.
|
||||||
min_column_width: The minimum width a column can be resized to.
|
min_column_width: The minimum width a column can be resized to.
|
||||||
row_height: Determins the height of each row.
|
row_height: Determins the height of each row.
|
||||||
|
@ -490,17 +490,17 @@ class ShikiJsTransformer(ShikiBaseTransformers):
|
|||||||
},
|
},
|
||||||
# White Space
|
# White Space
|
||||||
# ".tab, .space": {
|
# ".tab, .space": {
|
||||||
# "position": "relative",
|
# "position": "relative", # noqa: ERA001
|
||||||
# },
|
# },
|
||||||
# ".tab::before": {
|
# ".tab::before": {
|
||||||
# "content": "'⇥'",
|
# "content": "'⇥'", # noqa: ERA001
|
||||||
# "position": "absolute",
|
# "position": "absolute", # noqa: ERA001
|
||||||
# "opacity": "0.3",
|
# "opacity": "0.3",# noqa: ERA001
|
||||||
# },
|
# },
|
||||||
# ".space::before": {
|
# ".space::before": {
|
||||||
# "content": "'·'",
|
# "content": "'·'", # noqa: ERA001
|
||||||
# "position": "absolute",
|
# "position": "absolute", # noqa: ERA001
|
||||||
# "opacity": "0.3",
|
# "opacity": "0.3", # noqa: ERA001
|
||||||
# },
|
# },
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
"""Base classes."""
|
||||||
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
"""Forms classes."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@ -18,6 +18,7 @@ from reflex.event import (
|
|||||||
prevent_default,
|
prevent_default,
|
||||||
)
|
)
|
||||||
from reflex.utils.imports import ImportDict
|
from reflex.utils.imports import ImportDict
|
||||||
|
from reflex.utils.types import is_optional
|
||||||
from reflex.vars import VarData
|
from reflex.vars import VarData
|
||||||
from reflex.vars.base import LiteralVar, Var
|
from reflex.vars.base import LiteralVar, Var
|
||||||
|
|
||||||
@ -84,7 +85,6 @@ class Datalist(BaseHTML):
|
|||||||
"""Display the datalist element."""
|
"""Display the datalist element."""
|
||||||
|
|
||||||
tag = "datalist"
|
tag = "datalist"
|
||||||
# No unique attributes, only common ones are inherited
|
|
||||||
|
|
||||||
|
|
||||||
class Fieldset(Element):
|
class Fieldset(Element):
|
||||||
@ -250,7 +250,6 @@ class Form(BaseHTML):
|
|||||||
_js_expr=f"getRefValue({ref_var!s})",
|
_js_expr=f"getRefValue({ref_var!s})",
|
||||||
_var_data=VarData.merge(ref_var._get_all_var_data()),
|
_var_data=VarData.merge(ref_var._get_all_var_data()),
|
||||||
)
|
)
|
||||||
# print(repr(form_refs))
|
|
||||||
return form_refs
|
return form_refs
|
||||||
|
|
||||||
def _get_vars(self, include_children: bool = True) -> Iterator[Var]:
|
def _get_vars(self, include_children: bool = True) -> Iterator[Var]:
|
||||||
@ -384,6 +383,33 @@ class Input(BaseHTML):
|
|||||||
# Fired when a key is released
|
# Fired when a key is released
|
||||||
on_key_up: EventHandler[key_event]
|
on_key_up: EventHandler[key_event]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, *children, **props):
|
||||||
|
"""Create an Input component.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*children: The children of the component.
|
||||||
|
**props: The properties of the component.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The component.
|
||||||
|
"""
|
||||||
|
from reflex.vars.number import ternary_operation
|
||||||
|
|
||||||
|
value = props.get("value")
|
||||||
|
|
||||||
|
# React expects an empty string(instead of null) for controlled inputs.
|
||||||
|
if value is not None and is_optional(
|
||||||
|
(value_var := Var.create(value))._var_type
|
||||||
|
):
|
||||||
|
props["value"] = ternary_operation(
|
||||||
|
(value_var != Var.create(None)) # pyright: ignore [reportGeneralTypeIssues]
|
||||||
|
& (value_var != Var(_js_expr="undefined")),
|
||||||
|
value,
|
||||||
|
Var.create(""),
|
||||||
|
)
|
||||||
|
return super().create(*children, **props)
|
||||||
|
|
||||||
|
|
||||||
class Label(BaseHTML):
|
class Label(BaseHTML):
|
||||||
"""Display the label element."""
|
"""Display the label element."""
|
||||||
@ -401,7 +427,6 @@ class Legend(BaseHTML):
|
|||||||
"""Display the legend element."""
|
"""Display the legend element."""
|
||||||
|
|
||||||
tag = "legend"
|
tag = "legend"
|
||||||
# No unique attributes, only common ones are inherited
|
|
||||||
|
|
||||||
|
|
||||||
class Meter(BaseHTML):
|
class Meter(BaseHTML):
|
||||||
|
@ -189,7 +189,7 @@ class Datalist(BaseHTML):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
*children: The children of the component.
|
*children: The children of the component.
|
||||||
access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element.
|
access_key: Provides a hint for generating a keyboard shortcut for the current element.
|
||||||
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
||||||
content_editable: Indicates whether the element's content is editable.
|
content_editable: Indicates whether the element's content is editable.
|
||||||
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
||||||
@ -512,7 +512,7 @@ class Input(BaseHTML):
|
|||||||
on_unmount: Optional[EventType[[], BASE_STATE]] = None,
|
on_unmount: Optional[EventType[[], BASE_STATE]] = None,
|
||||||
**props,
|
**props,
|
||||||
) -> "Input":
|
) -> "Input":
|
||||||
"""Create the component.
|
"""Create an Input component.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
*children: The children of the component.
|
*children: The children of the component.
|
||||||
@ -576,7 +576,7 @@ class Input(BaseHTML):
|
|||||||
class_name: The class name for the component.
|
class_name: The class name for the component.
|
||||||
autofocus: Whether the component should take the focus once the page is loaded
|
autofocus: Whether the component should take the focus once the page is loaded
|
||||||
custom_attrs: custom attribute
|
custom_attrs: custom attribute
|
||||||
**props: The props of the component.
|
**props: The properties of the component.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The component.
|
The component.
|
||||||
@ -730,7 +730,7 @@ class Legend(BaseHTML):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
*children: The children of the component.
|
*children: The children of the component.
|
||||||
access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element.
|
access_key: Provides a hint for generating a keyboard shortcut for the current element.
|
||||||
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
||||||
content_editable: Indicates whether the element's content is editable.
|
content_editable: Indicates whether the element's content is editable.
|
||||||
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
"""Inline classes."""
|
||||||
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
"""Media classes."""
|
||||||
|
|
||||||
from typing import Any, Union
|
from typing import Any, Union
|
||||||
|
|
||||||
@ -129,7 +129,6 @@ class Img(BaseHTML):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The component.
|
The component.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
super().create(src=children[0], **props)
|
super().create(src=children[0], **props)
|
||||||
@ -274,14 +273,12 @@ class Picture(BaseHTML):
|
|||||||
"""Display the picture element."""
|
"""Display the picture element."""
|
||||||
|
|
||||||
tag = "picture"
|
tag = "picture"
|
||||||
# No unique attributes, only common ones are inherited
|
|
||||||
|
|
||||||
|
|
||||||
class Portal(BaseHTML):
|
class Portal(BaseHTML):
|
||||||
"""Display the portal element."""
|
"""Display the portal element."""
|
||||||
|
|
||||||
tag = "portal"
|
tag = "portal"
|
||||||
# No unique attributes, only common ones are inherited
|
|
||||||
|
|
||||||
|
|
||||||
class Source(BaseHTML):
|
class Source(BaseHTML):
|
||||||
|
@ -340,7 +340,6 @@ class Img(BaseHTML):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The component.
|
The component.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
@ -987,7 +986,7 @@ class Picture(BaseHTML):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
*children: The children of the component.
|
*children: The children of the component.
|
||||||
access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element.
|
access_key: Provides a hint for generating a keyboard shortcut for the current element.
|
||||||
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
||||||
content_editable: Indicates whether the element's content is editable.
|
content_editable: Indicates whether the element's content is editable.
|
||||||
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
||||||
@ -1073,7 +1072,7 @@ class Portal(BaseHTML):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
*children: The children of the component.
|
*children: The children of the component.
|
||||||
access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element.
|
access_key: Provides a hint for generating a keyboard shortcut for the current element.
|
||||||
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
||||||
content_editable: Indicates whether the element's content is editable.
|
content_editable: Indicates whether the element's content is editable.
|
||||||
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
"""Metadata classes."""
|
||||||
|
|
||||||
from typing import List, Union
|
from typing import List, Union
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
"""Other classes."""
|
||||||
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
@ -26,31 +26,39 @@ class Dialog(BaseHTML):
|
|||||||
|
|
||||||
|
|
||||||
class Summary(BaseHTML):
|
class Summary(BaseHTML):
|
||||||
"""Display the summary element."""
|
"""Display the summary element.
|
||||||
|
|
||||||
|
Used as a summary or caption for a <details> element.
|
||||||
|
"""
|
||||||
|
|
||||||
tag = "summary"
|
tag = "summary"
|
||||||
# No unique attributes, only common ones are inherited; used as a summary or caption for a <details> element
|
|
||||||
|
|
||||||
|
|
||||||
class Slot(BaseHTML):
|
class Slot(BaseHTML):
|
||||||
"""Display the slot element."""
|
"""Display the slot element.
|
||||||
|
|
||||||
|
Used as a placeholder inside a web component.
|
||||||
|
"""
|
||||||
|
|
||||||
tag = "slot"
|
tag = "slot"
|
||||||
# No unique attributes, only common ones are inherited; used as a placeholder inside a web component
|
|
||||||
|
|
||||||
|
|
||||||
class Template(BaseHTML):
|
class Template(BaseHTML):
|
||||||
"""Display the template element."""
|
"""Display the template element.
|
||||||
|
|
||||||
|
Used for declaring fragments of HTML that can be cloned and inserted in the document.
|
||||||
|
"""
|
||||||
|
|
||||||
tag = "template"
|
tag = "template"
|
||||||
# No unique attributes, only common ones are inherited; used for declaring fragments of HTML that can be cloned and inserted in the document
|
|
||||||
|
|
||||||
|
|
||||||
class Math(BaseHTML):
|
class Math(BaseHTML):
|
||||||
"""Display the math element."""
|
"""Display the math element.
|
||||||
|
|
||||||
|
Represents a mathematical expression.
|
||||||
|
"""
|
||||||
|
|
||||||
tag = "math"
|
tag = "math"
|
||||||
# No unique attributes, only common ones are inherited; used for displaying mathematical expressions
|
|
||||||
|
|
||||||
|
|
||||||
class Html(BaseHTML):
|
class Html(BaseHTML):
|
||||||
|
@ -244,7 +244,7 @@ class Summary(BaseHTML):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
*children: The children of the component.
|
*children: The children of the component.
|
||||||
access_key: No unique attributes, only common ones are inherited; used as a summary or caption for a <details> element Provides a hint for generating a keyboard shortcut for the current element.
|
access_key: Provides a hint for generating a keyboard shortcut for the current element.
|
||||||
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
||||||
content_editable: Indicates whether the element's content is editable.
|
content_editable: Indicates whether the element's content is editable.
|
||||||
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
||||||
@ -330,7 +330,7 @@ class Slot(BaseHTML):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
*children: The children of the component.
|
*children: The children of the component.
|
||||||
access_key: No unique attributes, only common ones are inherited; used as a placeholder inside a web component Provides a hint for generating a keyboard shortcut for the current element.
|
access_key: Provides a hint for generating a keyboard shortcut for the current element.
|
||||||
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
||||||
content_editable: Indicates whether the element's content is editable.
|
content_editable: Indicates whether the element's content is editable.
|
||||||
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
||||||
@ -416,7 +416,7 @@ class Template(BaseHTML):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
*children: The children of the component.
|
*children: The children of the component.
|
||||||
access_key: No unique attributes, only common ones are inherited; used for declaring fragments of HTML that can be cloned and inserted in the document Provides a hint for generating a keyboard shortcut for the current element.
|
access_key: Provides a hint for generating a keyboard shortcut for the current element.
|
||||||
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
||||||
content_editable: Indicates whether the element's content is editable.
|
content_editable: Indicates whether the element's content is editable.
|
||||||
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
||||||
@ -502,7 +502,7 @@ class Math(BaseHTML):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
*children: The children of the component.
|
*children: The children of the component.
|
||||||
access_key: No unique attributes, only common ones are inherited; used for displaying mathematical expressions Provides a hint for generating a keyboard shortcut for the current element.
|
access_key: Provides a hint for generating a keyboard shortcut for the current element.
|
||||||
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
||||||
content_editable: Indicates whether the element's content is editable.
|
content_editable: Indicates whether the element's content is editable.
|
||||||
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
"""Scripts classes."""
|
||||||
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
@ -17,7 +17,6 @@ class Noscript(BaseHTML):
|
|||||||
"""Display the noscript element."""
|
"""Display the noscript element."""
|
||||||
|
|
||||||
tag = "noscript"
|
tag = "noscript"
|
||||||
# No unique attributes, only common ones are inherited
|
|
||||||
|
|
||||||
|
|
||||||
class Script(BaseHTML):
|
class Script(BaseHTML):
|
||||||
|
@ -154,7 +154,7 @@ class Noscript(BaseHTML):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
*children: The children of the component.
|
*children: The children of the component.
|
||||||
access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element.
|
access_key: Provides a hint for generating a keyboard shortcut for the current element.
|
||||||
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
|
||||||
content_editable: Indicates whether the element's content is editable.
|
content_editable: Indicates whether the element's content is editable.
|
||||||
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
"""Sectioning classes."""
|
||||||
|
|
||||||
from .base import BaseHTML
|
from .base import BaseHTML
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
"""Tables classes."""
|
||||||
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
|
"""Typography classes."""
|
||||||
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ class Image(NextComponent):
|
|||||||
placeholder: Var[str]
|
placeholder: Var[str]
|
||||||
|
|
||||||
# Allows passing CSS styles to the underlying image element.
|
# Allows passing CSS styles to the underlying image element.
|
||||||
# style: Var[Any]
|
# style: Var[Any] #noqa: ERA001
|
||||||
|
|
||||||
# The loading behavior of the image. Defaults to lazy. Can hurt performance, recommended to use `priority` instead.
|
# The loading behavior of the image. Defaults to lazy. Can hurt performance, recommended to use `priority` instead.
|
||||||
loading: Var[Literal["lazy", "eager"]]
|
loading: Var[Literal["lazy", "eager"]]
|
||||||
|
@ -70,7 +70,7 @@ class Image(NextComponent):
|
|||||||
quality: The quality of the optimized image, an integer between 1 and 100, where 100 is the best quality and therefore largest file size. Defaults to 75.
|
quality: The quality of the optimized image, an integer between 1 and 100, where 100 is the best quality and therefore largest file size. Defaults to 75.
|
||||||
priority: When true, the image will be considered high priority and preload. Lazy loading is automatically disabled for images using priority.
|
priority: When true, the image will be considered high priority and preload. Lazy loading is automatically disabled for images using priority.
|
||||||
placeholder: A placeholder to use while the image is loading. Possible values are blur, empty, or data:image/.... Defaults to empty.
|
placeholder: A placeholder to use while the image is loading. Possible values are blur, empty, or data:image/.... Defaults to empty.
|
||||||
loading: Allows passing CSS styles to the underlying image element. style: Var[Any] The loading behavior of the image. Defaults to lazy. Can hurt performance, recommended to use `priority` instead.
|
loading: The loading behavior of the image. Defaults to lazy. Can hurt performance, recommended to use `priority` instead.
|
||||||
blurDataURL: A Data URL to be used as a placeholder image before the src image successfully loads. Only takes effect when combined with placeholder="blur".
|
blurDataURL: A Data URL to be used as a placeholder image before the src image successfully loads. Only takes effect when combined with placeholder="blur".
|
||||||
on_load: Fires when the image has loaded.
|
on_load: Fires when the image has loaded.
|
||||||
on_error: Fires when the image has an error.
|
on_error: Fires when the image has an error.
|
||||||
|
@ -188,7 +188,7 @@ class Slider(ComponentNamespace):
|
|||||||
else:
|
else:
|
||||||
children = [
|
children = [
|
||||||
track,
|
track,
|
||||||
# Foreach.create(props.get("value"), lambda e: SliderThumb.create()), # foreach doesn't render Thumbs properly
|
# Foreach.create(props.get("value"), lambda e: SliderThumb.create()), # foreach doesn't render Thumbs properly # noqa: ERA001
|
||||||
]
|
]
|
||||||
|
|
||||||
return SliderRoot.create(*children, **props)
|
return SliderRoot.create(*children, **props)
|
||||||
|
@ -53,7 +53,7 @@ LiteralAccentColor = Literal[
|
|||||||
class CommonMarginProps(Component):
|
class CommonMarginProps(Component):
|
||||||
"""Many radix-themes elements accept shorthand margin props."""
|
"""Many radix-themes elements accept shorthand margin props."""
|
||||||
|
|
||||||
# Margin: "0" - "9"
|
# Margin: "0" - "9" # noqa: ERA001
|
||||||
m: Var[LiteralSpacing]
|
m: Var[LiteralSpacing]
|
||||||
|
|
||||||
# Margin horizontal: "0" - "9"
|
# Margin horizontal: "0" - "9"
|
||||||
@ -78,7 +78,7 @@ class CommonMarginProps(Component):
|
|||||||
class CommonPaddingProps(Component):
|
class CommonPaddingProps(Component):
|
||||||
"""Many radix-themes elements accept shorthand padding props."""
|
"""Many radix-themes elements accept shorthand padding props."""
|
||||||
|
|
||||||
# Padding: "0" - "9"
|
# Padding: "0" - "9" # noqa: ERA001
|
||||||
p: Var[Responsive[LiteralSpacing]]
|
p: Var[Responsive[LiteralSpacing]]
|
||||||
|
|
||||||
# Padding horizontal: "0" - "9"
|
# Padding horizontal: "0" - "9"
|
||||||
@ -139,14 +139,7 @@ class RadixThemesComponent(Component):
|
|||||||
component = super().create(*children, **props)
|
component = super().create(*children, **props)
|
||||||
if component.library is None:
|
if component.library is None:
|
||||||
component.library = RadixThemesComponent.__fields__["library"].default
|
component.library = RadixThemesComponent.__fields__["library"].default
|
||||||
component.alias = "RadixThemes" + (
|
component.alias = "RadixThemes" + (component.tag or type(component).__name__)
|
||||||
component.tag or component.__class__.__name__
|
|
||||||
)
|
|
||||||
# value = props.get("value")
|
|
||||||
# if value is not None and component.alias == "RadixThemesSelect.Root":
|
|
||||||
# lv = LiteralVar.create(value)
|
|
||||||
# print(repr(lv))
|
|
||||||
# print(f"Warning: Value {value} is not used in {component.alias}.")
|
|
||||||
return component
|
return component
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -268,6 +261,7 @@ class Theme(RadixThemesComponent):
|
|||||||
_js_expr="{...theme.styles.global[':root'], ...theme.styles.global.body}"
|
_js_expr="{...theme.styles.global[':root'], ...theme.styles.global.body}"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
tag.remove_props("appearance")
|
||||||
return tag
|
return tag
|
||||||
|
|
||||||
|
|
||||||
|
@ -427,7 +427,7 @@ class ColorModeSwitch(Switch):
|
|||||||
color_scheme: Override theme color for switch
|
color_scheme: Override theme color for switch
|
||||||
high_contrast: Whether to render the switch with higher contrast color against background
|
high_contrast: Whether to render the switch with higher contrast color against background
|
||||||
radius: Override theme radius for switch: "none" | "small" | "full"
|
radius: Override theme radius for switch: "none" | "small" | "full"
|
||||||
on_change: Props to rename Fired when the value of the switch changes
|
on_change: Fired when the value of the switch changes
|
||||||
style: The style of the component.
|
style: The style of the component.
|
||||||
key: A unique key for the component.
|
key: A unique key for the component.
|
||||||
id: The id for the component.
|
id: The id for the component.
|
||||||
|
@ -153,7 +153,7 @@ class Checkbox(RadixThemesComponent):
|
|||||||
required: Whether the checkbox is required
|
required: Whether the checkbox is required
|
||||||
name: The name of the checkbox control when submitting the form.
|
name: The name of the checkbox control when submitting the form.
|
||||||
value: The value of the checkbox control when submitting the form.
|
value: The value of the checkbox control when submitting the form.
|
||||||
on_change: Props to rename Fired when the checkbox is checked or unchecked.
|
on_change: Fired when the checkbox is checked or unchecked.
|
||||||
style: The style of the component.
|
style: The style of the component.
|
||||||
key: A unique key for the component.
|
key: A unique key for the component.
|
||||||
id: The id for the component.
|
id: The id for the component.
|
||||||
@ -302,7 +302,7 @@ class HighLevelCheckbox(RadixThemesComponent):
|
|||||||
required: Whether the checkbox is required
|
required: Whether the checkbox is required
|
||||||
name: The name of the checkbox control when submitting the form.
|
name: The name of the checkbox control when submitting the form.
|
||||||
value: The value of the checkbox control when submitting the form.
|
value: The value of the checkbox control when submitting the form.
|
||||||
on_change: Props to rename Fired when the checkbox is checked or unchecked.
|
on_change: Fired when the checkbox is checked or unchecked.
|
||||||
style: The style of the component.
|
style: The style of the component.
|
||||||
key: A unique key for the component.
|
key: A unique key for the component.
|
||||||
id: The id for the component.
|
id: The id for the component.
|
||||||
@ -449,7 +449,7 @@ class CheckboxNamespace(ComponentNamespace):
|
|||||||
required: Whether the checkbox is required
|
required: Whether the checkbox is required
|
||||||
name: The name of the checkbox control when submitting the form.
|
name: The name of the checkbox control when submitting the form.
|
||||||
value: The value of the checkbox control when submitting the form.
|
value: The value of the checkbox control when submitting the form.
|
||||||
on_change: Props to rename Fired when the checkbox is checked or unchecked.
|
on_change: Fired when the checkbox is checked or unchecked.
|
||||||
style: The style of the component.
|
style: The style of the component.
|
||||||
key: A unique key for the component.
|
key: A unique key for the component.
|
||||||
id: The id for the component.
|
id: The id for the component.
|
||||||
|
@ -148,7 +148,7 @@ class RadioGroupRoot(RadixThemesComponent):
|
|||||||
disabled: Whether the radio group is disabled
|
disabled: Whether the radio group is disabled
|
||||||
name: The name of the group. Submitted with its owning form as part of a name/value pair.
|
name: The name of the group. Submitted with its owning form as part of a name/value pair.
|
||||||
required: Whether the radio group is required
|
required: Whether the radio group is required
|
||||||
on_change: Props to rename Fired when the value of the radio group changes.
|
on_change: Fired when the value of the radio group changes.
|
||||||
style: The style of the component.
|
style: The style of the component.
|
||||||
key: A unique key for the component.
|
key: A unique key for the component.
|
||||||
id: The id for the component.
|
id: The id for the component.
|
||||||
|
@ -81,7 +81,7 @@ class SelectRoot(RadixThemesComponent):
|
|||||||
name: The name of the select control when submitting the form.
|
name: The name of the select control when submitting the form.
|
||||||
disabled: When True, prevents the user from interacting with select.
|
disabled: When True, prevents the user from interacting with select.
|
||||||
required: When True, indicates that the user must select a value before the owning form can be submitted.
|
required: When True, indicates that the user must select a value before the owning form can be submitted.
|
||||||
on_change: Props to rename Fired when the value of the select changes.
|
on_change: Fired when the value of the select changes.
|
||||||
on_open_change: Fired when the select is opened or closed.
|
on_open_change: Fired when the select is opened or closed.
|
||||||
style: The style of the component.
|
style: The style of the component.
|
||||||
key: A unique key for the component.
|
key: A unique key for the component.
|
||||||
@ -732,7 +732,7 @@ class HighLevelSelect(SelectRoot):
|
|||||||
name: The name of the select control when submitting the form.
|
name: The name of the select control when submitting the form.
|
||||||
disabled: When True, prevents the user from interacting with select.
|
disabled: When True, prevents the user from interacting with select.
|
||||||
required: When True, indicates that the user must select a value before the owning form can be submitted.
|
required: When True, indicates that the user must select a value before the owning form can be submitted.
|
||||||
on_change: Props to rename Fired when the value of the select changes.
|
on_change: Fired when the value of the select changes.
|
||||||
on_open_change: Fired when the select is opened or closed.
|
on_open_change: Fired when the select is opened or closed.
|
||||||
style: The style of the component.
|
style: The style of the component.
|
||||||
key: A unique key for the component.
|
key: A unique key for the component.
|
||||||
@ -912,7 +912,7 @@ class Select(ComponentNamespace):
|
|||||||
name: The name of the select control when submitting the form.
|
name: The name of the select control when submitting the form.
|
||||||
disabled: When True, prevents the user from interacting with select.
|
disabled: When True, prevents the user from interacting with select.
|
||||||
required: When True, indicates that the user must select a value before the owning form can be submitted.
|
required: When True, indicates that the user must select a value before the owning form can be submitted.
|
||||||
on_change: Props to rename Fired when the value of the select changes.
|
on_change: Fired when the value of the select changes.
|
||||||
on_open_change: Fired when the select is opened or closed.
|
on_open_change: Fired when the select is opened or closed.
|
||||||
style: The style of the component.
|
style: The style of the component.
|
||||||
key: A unique key for the component.
|
key: A unique key for the component.
|
||||||
|
@ -195,7 +195,7 @@ class Slider(RadixThemesComponent):
|
|||||||
step: The step value of the slider.
|
step: The step value of the slider.
|
||||||
disabled: Whether the slider is disabled
|
disabled: Whether the slider is disabled
|
||||||
orientation: The orientation of the slider.
|
orientation: The orientation of the slider.
|
||||||
on_change: Props to rename Fired when the value of the slider changes.
|
on_change: Fired when the value of the slider changes.
|
||||||
on_value_commit: Fired when a thumb is released after being dragged.
|
on_value_commit: Fired when a thumb is released after being dragged.
|
||||||
style: The style of the component.
|
style: The style of the component.
|
||||||
key: A unique key for the component.
|
key: A unique key for the component.
|
||||||
|
@ -157,7 +157,7 @@ class Switch(RadixThemesComponent):
|
|||||||
color_scheme: Override theme color for switch
|
color_scheme: Override theme color for switch
|
||||||
high_contrast: Whether to render the switch with higher contrast color against background
|
high_contrast: Whether to render the switch with higher contrast color against background
|
||||||
radius: Override theme radius for switch: "none" | "small" | "full"
|
radius: Override theme radius for switch: "none" | "small" | "full"
|
||||||
on_change: Props to rename Fired when the value of the switch changes
|
on_change: Fired when the value of the switch changes
|
||||||
style: The style of the component.
|
style: The style of the component.
|
||||||
key: A unique key for the component.
|
key: A unique key for the component.
|
||||||
id: The id for the component.
|
id: The id for the component.
|
||||||
|
@ -72,7 +72,7 @@ class TabsRoot(RadixThemesComponent):
|
|||||||
orientation: The orientation of the tabs.
|
orientation: The orientation of the tabs.
|
||||||
dir: Reading direction of the tabs.
|
dir: Reading direction of the tabs.
|
||||||
activation_mode: The mode of activation for the tabs. "automatic" will activate the tab when focused. "manual" will activate the tab when clicked.
|
activation_mode: The mode of activation for the tabs. "automatic" will activate the tab when focused. "manual" will activate the tab when clicked.
|
||||||
on_change: Props to rename Fired when the value of the tabs changes.
|
on_change: Fired when the value of the tabs changes.
|
||||||
style: The style of the component.
|
style: The style of the component.
|
||||||
key: A unique key for the component.
|
key: A unique key for the component.
|
||||||
id: The id for the component.
|
id: The id for the component.
|
||||||
@ -374,7 +374,7 @@ class Tabs(ComponentNamespace):
|
|||||||
orientation: The orientation of the tabs.
|
orientation: The orientation of the tabs.
|
||||||
dir: Reading direction of the tabs.
|
dir: Reading direction of the tabs.
|
||||||
activation_mode: The mode of activation for the tabs. "automatic" will activate the tab when focused. "manual" will activate the tab when clicked.
|
activation_mode: The mode of activation for the tabs. "automatic" will activate the tab when focused. "manual" will activate the tab when clicked.
|
||||||
on_change: Props to rename Fired when the value of the tabs changes.
|
on_change: Fired when the value of the tabs changes.
|
||||||
style: The style of the component.
|
style: The style of the component.
|
||||||
key: A unique key for the component.
|
key: A unique key for the component.
|
||||||
id: The id for the component.
|
id: The id for the component.
|
||||||
|
@ -9,7 +9,9 @@ from reflex.components.core.breakpoints import Responsive
|
|||||||
from reflex.components.core.debounce import DebounceInput
|
from reflex.components.core.debounce import DebounceInput
|
||||||
from reflex.components.el import elements
|
from reflex.components.el import elements
|
||||||
from reflex.event import EventHandler, input_event, key_event
|
from reflex.event import EventHandler, input_event, key_event
|
||||||
|
from reflex.utils.types import is_optional
|
||||||
from reflex.vars.base import Var
|
from reflex.vars.base import Var
|
||||||
|
from reflex.vars.number import ternary_operation
|
||||||
|
|
||||||
from ..base import LiteralAccentColor, LiteralRadius, RadixThemesComponent
|
from ..base import LiteralAccentColor, LiteralRadius, RadixThemesComponent
|
||||||
|
|
||||||
@ -96,6 +98,19 @@ class TextFieldRoot(elements.Div, RadixThemesComponent):
|
|||||||
Returns:
|
Returns:
|
||||||
The component.
|
The component.
|
||||||
"""
|
"""
|
||||||
|
value = props.get("value")
|
||||||
|
|
||||||
|
# React expects an empty string(instead of null) for controlled inputs.
|
||||||
|
if value is not None and is_optional(
|
||||||
|
(value_var := Var.create(value))._var_type
|
||||||
|
):
|
||||||
|
props["value"] = ternary_operation(
|
||||||
|
(value_var != Var.create(None)) # pyright: ignore [reportGeneralTypeIssues]
|
||||||
|
& (value_var != Var(_js_expr="undefined")),
|
||||||
|
value,
|
||||||
|
Var.create(""),
|
||||||
|
)
|
||||||
|
|
||||||
component = super().create(*children, **props)
|
component = super().create(*children, **props)
|
||||||
if props.get("value") is not None and props.get("on_change") is not None:
|
if props.get("value") is not None and props.get("on_change") is not None:
|
||||||
# create a debounced input if the user requests full control to avoid typing jank
|
# create a debounced input if the user requests full control to avoid typing jank
|
||||||
|
@ -64,7 +64,6 @@ class BaseList(Component, MarkdownComponentMap):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The list component.
|
The list component.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
items = props.pop("items", None)
|
items = props.pop("items", None)
|
||||||
list_style_type = props.pop("list_style_type", "none")
|
list_style_type = props.pop("list_style_type", "none")
|
||||||
@ -114,7 +113,6 @@ class UnorderedList(BaseList, Ul):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The list component.
|
The list component.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
items = props.pop("items", None)
|
items = props.pop("items", None)
|
||||||
list_style_type = props.pop("list_style_type", "disc")
|
list_style_type = props.pop("list_style_type", "disc")
|
||||||
@ -144,7 +142,6 @@ class OrderedList(BaseList, Ol):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The list component.
|
The list component.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
items = props.pop("items", None)
|
items = props.pop("items", None)
|
||||||
list_style_type = props.pop("list_style_type", "decimal")
|
list_style_type = props.pop("list_style_type", "decimal")
|
||||||
@ -168,7 +165,6 @@ class ListItem(Li, MarkdownComponentMap):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The list item component.
|
The list item component.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
for child in children:
|
for child in children:
|
||||||
if isinstance(child, Text):
|
if isinstance(child, Text):
|
||||||
|
@ -118,7 +118,6 @@ class BaseList(Component, MarkdownComponentMap):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The list component.
|
The list component.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
@ -252,7 +251,6 @@ class UnorderedList(BaseList, Ul):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The list component.
|
The list component.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
@ -390,7 +388,6 @@ class OrderedList(BaseList, Ol):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The list component.
|
The list component.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
@ -477,7 +474,6 @@ class ListItem(Li, MarkdownComponentMap):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The list item component.
|
The list item component.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
@ -571,7 +567,6 @@ class List(ComponentNamespace):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The list component.
|
The list component.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@ -416,7 +416,7 @@ class Bar(Cartesian):
|
|||||||
radius: Var[Union[int, List[int]]]
|
radius: Var[Union[int, List[int]]]
|
||||||
|
|
||||||
# The active bar is shown when a user enters a bar chart and this chart has tooltip. If set to false, no active bar will be drawn. If set to true, active bar will be drawn with the props calculated internally. If passed an object, active bar will be drawn, and the internally calculated props will be merged with the key value pairs of the passed object.
|
# The active bar is shown when a user enters a bar chart and this chart has tooltip. If set to false, no active bar will be drawn. If set to true, active bar will be drawn with the props calculated internally. If passed an object, active bar will be drawn, and the internally calculated props will be merged with the key value pairs of the passed object.
|
||||||
# active_bar: Var[Union[bool, Dict[str, Any]]]
|
# active_bar: Var[Union[bool, Dict[str, Any]]] #noqa: ERA001
|
||||||
|
|
||||||
# Valid children components
|
# Valid children components
|
||||||
_valid_children: List[str] = ["Cell", "LabelList", "ErrorBar"]
|
_valid_children: List[str] = ["Cell", "LabelList", "ErrorBar"]
|
||||||
|
@ -136,7 +136,7 @@ class Radar(Recharts):
|
|||||||
# Fill color. Default: rx.color("accent", 3)
|
# Fill color. Default: rx.color("accent", 3)
|
||||||
fill: Var[str] = LiteralVar.create(Color("accent", 3))
|
fill: Var[str] = LiteralVar.create(Color("accent", 3))
|
||||||
|
|
||||||
# opacity. Default: 0.6
|
# The opacity to fill the chart. Default: 0.6
|
||||||
fill_opacity: Var[float] = LiteralVar.create(0.6)
|
fill_opacity: Var[float] = LiteralVar.create(0.6)
|
||||||
|
|
||||||
# The type of icon in legend. If set to 'none', no legend item will be rendered. Default: "rect"
|
# The type of icon in legend. If set to 'none', no legend item will be rendered. Default: "rect"
|
||||||
|
@ -204,7 +204,7 @@ class Radar(Recharts):
|
|||||||
dot: If false set, dots will not be drawn. Default: True
|
dot: If false set, dots will not be drawn. Default: True
|
||||||
stroke: Stoke color. Default: rx.color("accent", 9)
|
stroke: Stoke color. Default: rx.color("accent", 9)
|
||||||
fill: Fill color. Default: rx.color("accent", 3)
|
fill: Fill color. Default: rx.color("accent", 3)
|
||||||
fill_opacity: opacity. Default: 0.6
|
fill_opacity: The opacity to fill the chart. Default: 0.6
|
||||||
legend_type: The type of icon in legend. If set to 'none', no legend item will be rendered. Default: "rect"
|
legend_type: The type of icon in legend. If set to 'none', no legend item will be rendered. Default: "rect"
|
||||||
label: If false set, labels will not be drawn. Default: True
|
label: If false set, labels will not be drawn. Default: True
|
||||||
is_animation_active: If set false, animation of polygon will be disabled. Default: True in CSR, and False in SSR
|
is_animation_active: If set false, animation of polygon will be disabled. Default: True in CSR, and False in SSR
|
||||||
|
@ -98,7 +98,7 @@ class ToastProps(PropsBase, NoExtrasAllowedProps):
|
|||||||
|
|
||||||
# TODO: fix serialization of icons for toast? (might not be possible yet)
|
# TODO: fix serialization of icons for toast? (might not be possible yet)
|
||||||
# Icon displayed in front of toast's text, aligned vertically.
|
# Icon displayed in front of toast's text, aligned vertically.
|
||||||
# icon: Optional[Icon] = None
|
# icon: Optional[Icon] = None # noqa: ERA001
|
||||||
|
|
||||||
# TODO: fix implementation for action / cancel buttons
|
# TODO: fix implementation for action / cancel buttons
|
||||||
# Renders a primary button, clicking it will close the toast.
|
# Renders a primary button, clicking it will close the toast.
|
||||||
@ -364,9 +364,7 @@ class Toaster(Component):
|
|||||||
return super().create(*children, **props)
|
return super().create(*children, **props)
|
||||||
|
|
||||||
|
|
||||||
# TODO: figure out why loading toast stay open forever
|
# TODO: figure out why loading toast stay open forever when using level="loading" in toast()
|
||||||
# def toast_loading(message: str, **kwargs):
|
|
||||||
# return _toast(message, level="loading", **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class ToastNamespace(ComponentNamespace):
|
class ToastNamespace(ComponentNamespace):
|
||||||
@ -379,7 +377,6 @@ class ToastNamespace(ComponentNamespace):
|
|||||||
error = staticmethod(Toaster.toast_error)
|
error = staticmethod(Toaster.toast_error)
|
||||||
success = staticmethod(Toaster.toast_success)
|
success = staticmethod(Toaster.toast_success)
|
||||||
dismiss = staticmethod(Toaster.toast_dismiss)
|
dismiss = staticmethod(Toaster.toast_dismiss)
|
||||||
# loading = staticmethod(toast_loading)
|
|
||||||
__call__ = staticmethod(Toaster.send_toast)
|
__call__ = staticmethod(Toaster.send_toast)
|
||||||
|
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ class Editor(NoSSRComponent):
|
|||||||
# Please refer to the library docs for this.
|
# Please refer to the library docs for this.
|
||||||
# options: "en" | "da" | "de" | "es" | "fr" | "ja" | "ko" | "pt_br" |
|
# options: "en" | "da" | "de" | "es" | "fr" | "ja" | "ko" | "pt_br" |
|
||||||
# "ru" | "zh_cn" | "ro" | "pl" | "ckb" | "lv" | "se" | "ua" | "he" | "it"
|
# "ru" | "zh_cn" | "ro" | "pl" | "ckb" | "lv" | "se" | "ua" | "he" | "it"
|
||||||
# default : "en"
|
# default: "en".
|
||||||
lang: Var[
|
lang: Var[
|
||||||
Union[
|
Union[
|
||||||
Literal[
|
Literal[
|
||||||
@ -172,7 +172,7 @@ class Editor(NoSSRComponent):
|
|||||||
set_options: Var[Dict]
|
set_options: Var[Dict]
|
||||||
|
|
||||||
# Whether all SunEditor plugins should be loaded.
|
# Whether all SunEditor plugins should be loaded.
|
||||||
# default: True
|
# default: True.
|
||||||
set_all_plugins: Var[bool]
|
set_all_plugins: Var[bool]
|
||||||
|
|
||||||
# Set the content of the editor.
|
# Set the content of the editor.
|
||||||
@ -191,19 +191,19 @@ class Editor(NoSSRComponent):
|
|||||||
set_default_style: Var[str]
|
set_default_style: Var[str]
|
||||||
|
|
||||||
# Disable the editor
|
# Disable the editor
|
||||||
# default: False
|
# default: False.
|
||||||
disable: Var[bool]
|
disable: Var[bool]
|
||||||
|
|
||||||
# Hide the editor
|
# Hide the editor
|
||||||
# default: False
|
# default: False.
|
||||||
hide: Var[bool]
|
hide: Var[bool]
|
||||||
|
|
||||||
# Hide the editor toolbar
|
# Hide the editor toolbar
|
||||||
# default: False
|
# default: False.
|
||||||
hide_toolbar: Var[bool]
|
hide_toolbar: Var[bool]
|
||||||
|
|
||||||
# Disable the editor toolbar
|
# Disable the editor toolbar
|
||||||
# default: False
|
# default: False.
|
||||||
disable_toolbar: Var[bool]
|
disable_toolbar: Var[bool]
|
||||||
|
|
||||||
# Fired when the editor content changes.
|
# Fired when the editor content changes.
|
||||||
|
@ -172,7 +172,7 @@ class Editor(NoSSRComponent):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
set_options(Optional[EditorOptions]): Configuration object to further configure the instance.
|
set_options(Optional[EditorOptions]): Configuration object to further configure the instance.
|
||||||
lang: Language of the editor. Alternatively to a string, a dict of your language can be passed to this prop. Please refer to the library docs for this. options: "en" | "da" | "de" | "es" | "fr" | "ja" | "ko" | "pt_br" | "ru" | "zh_cn" | "ro" | "pl" | "ckb" | "lv" | "se" | "ua" | "he" | "it" default : "en"
|
lang: Language of the editor. Alternatively to a string, a dict of your language can be passed to this prop. Please refer to the library docs for this. options: "en" | "da" | "de" | "es" | "fr" | "ja" | "ko" | "pt_br" | "ru" | "zh_cn" | "ro" | "pl" | "ckb" | "lv" | "se" | "ua" | "he" | "it" default: "en".
|
||||||
name: This is used to set the HTML form name of the editor. This means on HTML form submission, it will be submitted together with contents of the editor by the name provided.
|
name: This is used to set the HTML form name of the editor. This means on HTML form submission, it will be submitted together with contents of the editor by the name provided.
|
||||||
default_value: Sets the default value of the editor. This is useful if you don't want the on_change method to be called on render. If you want the on_change method to be called on render please use the set_contents prop
|
default_value: Sets the default value of the editor. This is useful if you don't want the on_change method to be called on render. If you want the on_change method to be called on render please use the set_contents prop
|
||||||
width: Sets the width of the editor. px and percentage values are accepted, eg width="100%" or width="500px" default: 100%
|
width: Sets the width of the editor. px and percentage values are accepted, eg width="100%" or width="500px" default: 100%
|
||||||
@ -180,14 +180,14 @@ class Editor(NoSSRComponent):
|
|||||||
placeholder: Sets the placeholder of the editor.
|
placeholder: Sets the placeholder of the editor.
|
||||||
auto_focus: Should the editor receive focus when initialized?
|
auto_focus: Should the editor receive focus when initialized?
|
||||||
set_options: Pass an EditorOptions instance to modify the behaviour of Editor even more.
|
set_options: Pass an EditorOptions instance to modify the behaviour of Editor even more.
|
||||||
set_all_plugins: Whether all SunEditor plugins should be loaded. default: True
|
set_all_plugins: Whether all SunEditor plugins should be loaded. default: True.
|
||||||
set_contents: Set the content of the editor. Note: To set the initial contents of the editor without calling the on_change event, please use the default_value prop. set_contents is used to set the contents of the editor programmatically. You must be aware that, when the set_contents's prop changes, the on_change event is triggered.
|
set_contents: Set the content of the editor. Note: To set the initial contents of the editor without calling the on_change event, please use the default_value prop. set_contents is used to set the contents of the editor programmatically. You must be aware that, when the set_contents's prop changes, the on_change event is triggered.
|
||||||
append_contents: Append editor content
|
append_contents: Append editor content
|
||||||
set_default_style: Sets the default style of the editor's edit area
|
set_default_style: Sets the default style of the editor's edit area
|
||||||
disable: Disable the editor default: False
|
disable: Disable the editor default: False.
|
||||||
hide: Hide the editor default: False
|
hide: Hide the editor default: False.
|
||||||
hide_toolbar: Hide the editor toolbar default: False
|
hide_toolbar: Hide the editor toolbar default: False.
|
||||||
disable_toolbar: Disable the editor toolbar default: False
|
disable_toolbar: Disable the editor toolbar default: False.
|
||||||
on_change: Fired when the editor content changes.
|
on_change: Fired when the editor content changes.
|
||||||
on_input: Fired when the something is inputted in the editor.
|
on_input: Fired when the something is inputted in the editor.
|
||||||
on_blur: Fired when the editor loses focus.
|
on_blur: Fired when the editor loses focus.
|
||||||
|
@ -82,7 +82,7 @@ class DBConfig(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def postgresql_psycopg2(
|
def postgresql_psycopg(
|
||||||
cls,
|
cls,
|
||||||
database: str,
|
database: str,
|
||||||
username: str,
|
username: str,
|
||||||
@ -90,7 +90,7 @@ class DBConfig(Base):
|
|||||||
host: str | None = None,
|
host: str | None = None,
|
||||||
port: int | None = 5432,
|
port: int | None = 5432,
|
||||||
) -> DBConfig:
|
) -> DBConfig:
|
||||||
"""Create an instance with postgresql+psycopg2 engine.
|
"""Create an instance with postgresql+psycopg engine.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
database: Database name.
|
database: Database name.
|
||||||
@ -103,7 +103,7 @@ class DBConfig(Base):
|
|||||||
DBConfig instance.
|
DBConfig instance.
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
engine="postgresql+psycopg2",
|
engine="postgresql+psycopg",
|
||||||
username=username,
|
username=username,
|
||||||
password=password,
|
password=password,
|
||||||
host=host,
|
host=host,
|
||||||
@ -512,6 +512,9 @@ class EnvironmentVariables:
|
|||||||
# Whether to print the SQL queries if the log level is INFO or lower.
|
# Whether to print the SQL queries if the log level is INFO or lower.
|
||||||
SQLALCHEMY_ECHO: EnvVar[bool] = env_var(False)
|
SQLALCHEMY_ECHO: EnvVar[bool] = env_var(False)
|
||||||
|
|
||||||
|
# Whether to check db connections before using them.
|
||||||
|
SQLALCHEMY_POOL_PRE_PING: EnvVar[bool] = env_var(True)
|
||||||
|
|
||||||
# Whether to ignore the redis config error. Some redis servers only allow out-of-band configuration.
|
# Whether to ignore the redis config error. Some redis servers only allow out-of-band configuration.
|
||||||
REFLEX_IGNORE_REDIS_CONFIG_ERROR: EnvVar[bool] = env_var(False)
|
REFLEX_IGNORE_REDIS_CONFIG_ERROR: EnvVar[bool] = env_var(False)
|
||||||
|
|
||||||
@ -568,6 +571,10 @@ class EnvironmentVariables:
|
|||||||
environment = EnvironmentVariables()
|
environment = EnvironmentVariables()
|
||||||
|
|
||||||
|
|
||||||
|
# These vars are not logged because they may contain sensitive information.
|
||||||
|
_sensitive_env_vars = {"DB_URL", "ASYNC_DB_URL", "REDIS_URL"}
|
||||||
|
|
||||||
|
|
||||||
class Config(Base):
|
class Config(Base):
|
||||||
"""The config defines runtime settings for the app.
|
"""The config defines runtime settings for the app.
|
||||||
|
|
||||||
@ -621,6 +628,9 @@ class Config(Base):
|
|||||||
# The database url used by rx.Model.
|
# The database url used by rx.Model.
|
||||||
db_url: Optional[str] = "sqlite:///reflex.db"
|
db_url: Optional[str] = "sqlite:///reflex.db"
|
||||||
|
|
||||||
|
# The async database url used by rx.Model.
|
||||||
|
async_db_url: Optional[str] = None
|
||||||
|
|
||||||
# The redis url
|
# The redis url
|
||||||
redis_url: Optional[str] = None
|
redis_url: Optional[str] = None
|
||||||
|
|
||||||
@ -674,6 +684,9 @@ class Config(Base):
|
|||||||
# Maximum expiration lock time for redis state manager
|
# Maximum expiration lock time for redis state manager
|
||||||
redis_lock_expiration: int = constants.Expiration.LOCK
|
redis_lock_expiration: int = constants.Expiration.LOCK
|
||||||
|
|
||||||
|
# Maximum lock time before warning for redis state manager.
|
||||||
|
redis_lock_warning_threshold: int = constants.Expiration.LOCK_WARNING_THRESHOLD
|
||||||
|
|
||||||
# Token expiration time for redis state manager
|
# Token expiration time for redis state manager
|
||||||
redis_token_expiration: int = constants.Expiration.TOKEN
|
redis_token_expiration: int = constants.Expiration.TOKEN
|
||||||
|
|
||||||
@ -748,18 +761,20 @@ class Config(Base):
|
|||||||
|
|
||||||
# If the env var is set, override the config value.
|
# If the env var is set, override the config value.
|
||||||
if env_var is not None:
|
if env_var is not None:
|
||||||
if key.upper() != "DB_URL":
|
|
||||||
console.info(
|
|
||||||
f"Overriding config value {key} with env var {key.upper()}={env_var}",
|
|
||||||
dedupe=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Interpret the value.
|
# Interpret the value.
|
||||||
value = interpret_env_var_value(env_var, field.outer_type_, field.name)
|
value = interpret_env_var_value(env_var, field.outer_type_, field.name)
|
||||||
|
|
||||||
# Set the value.
|
# Set the value.
|
||||||
updated_values[key] = value
|
updated_values[key] = value
|
||||||
|
|
||||||
|
if key.upper() in _sensitive_env_vars:
|
||||||
|
env_var = "***"
|
||||||
|
|
||||||
|
console.info(
|
||||||
|
f"Overriding config value {key} with env var {key.upper()}={env_var}",
|
||||||
|
dedupe=True,
|
||||||
|
)
|
||||||
|
|
||||||
return updated_values
|
return updated_values
|
||||||
|
|
||||||
def get_event_namespace(self) -> str:
|
def get_event_namespace(self) -> str:
|
||||||
|
@ -26,9 +26,11 @@ class Expiration(SimpleNamespace):
|
|||||||
# Token expiration time in seconds
|
# Token expiration time in seconds
|
||||||
TOKEN = 60 * 60
|
TOKEN = 60 * 60
|
||||||
# Maximum time in milliseconds that a state can be locked for exclusive access.
|
# Maximum time in milliseconds that a state can be locked for exclusive access.
|
||||||
LOCK = 1000
|
LOCK = 10000
|
||||||
# The PING timeout
|
# The PING timeout
|
||||||
PING = 120
|
PING = 120
|
||||||
|
# The maximum time in milliseconds to hold a lock before throwing a warning.
|
||||||
|
LOCK_WARNING_THRESHOLD = 1000
|
||||||
|
|
||||||
|
|
||||||
class GitIgnore(SimpleNamespace):
|
class GitIgnore(SimpleNamespace):
|
||||||
|
@ -1222,7 +1222,7 @@ def call_event_handler(
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
# TODO: In 0.7.0, remove this block and raise the exception
|
# TODO: In 0.7.0, remove this block and raise the exception
|
||||||
# raise TypeError(
|
# raise TypeError(
|
||||||
# f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_handler.fn.__qualname__} provided for {key}."
|
# f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_handler.fn.__qualname__} provided for {key}." # noqa: ERA001
|
||||||
# ) from e
|
# ) from e
|
||||||
console.warn(
|
console.warn(
|
||||||
f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_callback.fn.__qualname__} provided for {key}."
|
f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_callback.fn.__qualname__} provided for {key}."
|
||||||
@ -1556,7 +1556,7 @@ class LiteralEventVar(VarOperationCall, LiteralVar, EventVar):
|
|||||||
Returns:
|
Returns:
|
||||||
The hash of the var.
|
The hash of the var.
|
||||||
"""
|
"""
|
||||||
return hash((self.__class__.__name__, self._js_expr))
|
return hash((type(self).__name__, self._js_expr))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(
|
def create(
|
||||||
@ -1620,7 +1620,7 @@ class LiteralEventChainVar(ArgsFunctionOperationBuilder, LiteralVar, EventChainV
|
|||||||
Returns:
|
Returns:
|
||||||
The hash of the var.
|
The hash of the var.
|
||||||
"""
|
"""
|
||||||
return hash((self.__class__.__name__, self._js_expr))
|
return hash((type(self).__name__, self._js_expr))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(
|
def create(
|
||||||
|
@ -242,4 +242,5 @@ class ClientStateVar(Var):
|
|||||||
"""
|
"""
|
||||||
if not self._global_ref:
|
if not self._global_ref:
|
||||||
raise ValueError("ClientStateVar must be global to push the value.")
|
raise ValueError("ClientStateVar must be global to push the value.")
|
||||||
|
value = Var.create(value)
|
||||||
return run_script(f"{_client_state_ref(self._setter_name)}({value})")
|
return run_script(f"{_client_state_ref(self._setter_name)}({value})")
|
||||||
|
@ -33,12 +33,6 @@ class Sidebar(Box, MemoizationLeaf):
|
|||||||
Returns:
|
Returns:
|
||||||
The sidebar component.
|
The sidebar component.
|
||||||
"""
|
"""
|
||||||
# props.setdefault("border_right", f"1px solid {color('accent', 12)}")
|
|
||||||
# props.setdefault("background_color", color("accent", 1))
|
|
||||||
# props.setdefault("width", "20vw")
|
|
||||||
# props.setdefault("height", "100vh")
|
|
||||||
# props.setdefault("position", "fixed")
|
|
||||||
|
|
||||||
return super().create(
|
return super().create(
|
||||||
Box.create(*children, **props), # sidebar for content
|
Box.create(*children, **props), # sidebar for content
|
||||||
Box.create(width=props.get("width")), # spacer for layout
|
Box.create(width=props.get("width")), # spacer for layout
|
||||||
|
126
reflex/model.py
126
reflex/model.py
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Any, ClassVar, Optional, Type, Union
|
from typing import Any, ClassVar, Optional, Type, Union
|
||||||
|
|
||||||
@ -14,6 +15,7 @@ import alembic.script
|
|||||||
import alembic.util
|
import alembic.util
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
import sqlalchemy.exc
|
import sqlalchemy.exc
|
||||||
|
import sqlalchemy.ext.asyncio
|
||||||
import sqlalchemy.orm
|
import sqlalchemy.orm
|
||||||
|
|
||||||
from reflex.base import Base
|
from reflex.base import Base
|
||||||
@ -21,6 +23,48 @@ from reflex.config import environment, get_config
|
|||||||
from reflex.utils import console
|
from reflex.utils import console
|
||||||
from reflex.utils.compat import sqlmodel, sqlmodel_field_has_primary_key
|
from reflex.utils.compat import sqlmodel, sqlmodel_field_has_primary_key
|
||||||
|
|
||||||
|
_ENGINE: dict[str, sqlalchemy.engine.Engine] = {}
|
||||||
|
_ASYNC_ENGINE: dict[str, sqlalchemy.ext.asyncio.AsyncEngine] = {}
|
||||||
|
_AsyncSessionLocal: dict[str | None, sqlalchemy.ext.asyncio.async_sessionmaker] = {}
|
||||||
|
|
||||||
|
# Import AsyncSession _after_ reflex.utils.compat
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_db_url_for_logging(url: str) -> str:
|
||||||
|
"""Remove username and password from the database URL for logging.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: The database URL.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The database URL with the username and password removed.
|
||||||
|
"""
|
||||||
|
return re.sub(r"://[^@]+@", "://<username>:<password>@", url)
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine_args(url: str | None = None) -> dict[str, Any]:
|
||||||
|
"""Get the database engine arguments.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: The database url.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The database engine arguments as a dict.
|
||||||
|
"""
|
||||||
|
kwargs: dict[str, Any] = dict(
|
||||||
|
# Print the SQL queries if the log level is INFO or lower.
|
||||||
|
echo=environment.SQLALCHEMY_ECHO.get(),
|
||||||
|
# Check connections before returning them.
|
||||||
|
pool_pre_ping=environment.SQLALCHEMY_POOL_PRE_PING.get(),
|
||||||
|
)
|
||||||
|
conf = get_config()
|
||||||
|
url = url or conf.db_url
|
||||||
|
if url is not None and url.startswith("sqlite"):
|
||||||
|
# Needed for the admin dash on sqlite.
|
||||||
|
kwargs["connect_args"] = {"check_same_thread": False}
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
def get_engine(url: str | None = None) -> sqlalchemy.engine.Engine:
|
def get_engine(url: str | None = None) -> sqlalchemy.engine.Engine:
|
||||||
"""Get the database engine.
|
"""Get the database engine.
|
||||||
@ -38,15 +82,62 @@ def get_engine(url: str | None = None) -> sqlalchemy.engine.Engine:
|
|||||||
url = url or conf.db_url
|
url = url or conf.db_url
|
||||||
if url is None:
|
if url is None:
|
||||||
raise ValueError("No database url configured")
|
raise ValueError("No database url configured")
|
||||||
|
|
||||||
|
global _ENGINE
|
||||||
|
if url in _ENGINE:
|
||||||
|
return _ENGINE[url]
|
||||||
|
|
||||||
if not environment.ALEMBIC_CONFIG.get().exists():
|
if not environment.ALEMBIC_CONFIG.get().exists():
|
||||||
console.warn(
|
console.warn(
|
||||||
"Database is not initialized, run [bold]reflex db init[/bold] first."
|
"Database is not initialized, run [bold]reflex db init[/bold] first."
|
||||||
)
|
)
|
||||||
# Print the SQL queries if the log level is INFO or lower.
|
_ENGINE[url] = sqlmodel.create_engine(
|
||||||
echo_db_query = environment.SQLALCHEMY_ECHO.get()
|
url,
|
||||||
# Needed for the admin dash on sqlite.
|
**get_engine_args(url),
|
||||||
connect_args = {"check_same_thread": False} if url.startswith("sqlite") else {}
|
)
|
||||||
return sqlmodel.create_engine(url, echo=echo_db_query, connect_args=connect_args)
|
return _ENGINE[url]
|
||||||
|
|
||||||
|
|
||||||
|
def get_async_engine(url: str | None) -> sqlalchemy.ext.asyncio.AsyncEngine:
|
||||||
|
"""Get the async database engine.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: The database url.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The async database engine.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the async database url is None.
|
||||||
|
"""
|
||||||
|
if url is None:
|
||||||
|
conf = get_config()
|
||||||
|
url = conf.async_db_url
|
||||||
|
if url is not None and conf.db_url is not None:
|
||||||
|
async_db_url_tail = url.partition("://")[2]
|
||||||
|
db_url_tail = conf.db_url.partition("://")[2]
|
||||||
|
if async_db_url_tail != db_url_tail:
|
||||||
|
console.warn(
|
||||||
|
f"async_db_url `{_safe_db_url_for_logging(url)}` "
|
||||||
|
"should reference the same database as "
|
||||||
|
f"db_url `{_safe_db_url_for_logging(conf.db_url)}`."
|
||||||
|
)
|
||||||
|
if url is None:
|
||||||
|
raise ValueError("No async database url configured")
|
||||||
|
|
||||||
|
global _ASYNC_ENGINE
|
||||||
|
if url in _ASYNC_ENGINE:
|
||||||
|
return _ASYNC_ENGINE[url]
|
||||||
|
|
||||||
|
if not environment.ALEMBIC_CONFIG.get().exists():
|
||||||
|
console.warn(
|
||||||
|
"Database is not initialized, run [bold]reflex db init[/bold] first."
|
||||||
|
)
|
||||||
|
_ASYNC_ENGINE[url] = sqlalchemy.ext.asyncio.create_async_engine(
|
||||||
|
url,
|
||||||
|
**get_engine_args(url),
|
||||||
|
)
|
||||||
|
return _ASYNC_ENGINE[url]
|
||||||
|
|
||||||
|
|
||||||
async def get_db_status() -> bool:
|
async def get_db_status() -> bool:
|
||||||
@ -425,6 +516,31 @@ def session(url: str | None = None) -> sqlmodel.Session:
|
|||||||
return sqlmodel.Session(get_engine(url))
|
return sqlmodel.Session(get_engine(url))
|
||||||
|
|
||||||
|
|
||||||
|
def asession(url: str | None = None) -> AsyncSession:
|
||||||
|
"""Get an async sqlmodel session to interact with the database.
|
||||||
|
|
||||||
|
async with rx.asession() as asession:
|
||||||
|
...
|
||||||
|
|
||||||
|
Most operations against the `asession` must be awaited.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: The database url.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An async database session.
|
||||||
|
"""
|
||||||
|
global _AsyncSessionLocal
|
||||||
|
if url not in _AsyncSessionLocal:
|
||||||
|
_AsyncSessionLocal[url] = sqlalchemy.ext.asyncio.async_sessionmaker(
|
||||||
|
bind=get_async_engine(url),
|
||||||
|
class_=AsyncSession,
|
||||||
|
autocommit=False,
|
||||||
|
autoflush=False,
|
||||||
|
)
|
||||||
|
return _AsyncSessionLocal[url]()
|
||||||
|
|
||||||
|
|
||||||
def sqla_session(url: str | None = None) -> sqlalchemy.orm.Session:
|
def sqla_session(url: str | None = None) -> sqlalchemy.orm.Session:
|
||||||
"""Get a bare sqlalchemy session to interact with the database.
|
"""Get a bare sqlalchemy session to interact with the database.
|
||||||
|
|
||||||
|
133
reflex/state.py
133
reflex/state.py
@ -11,6 +11,7 @@ import inspect
|
|||||||
import json
|
import json
|
||||||
import pickle
|
import pickle
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
import typing
|
import typing
|
||||||
import uuid
|
import uuid
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
@ -39,6 +40,7 @@ from typing import (
|
|||||||
get_type_hints,
|
get_type_hints,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from redis.asyncio.client import PubSub
|
||||||
from sqlalchemy.orm import DeclarativeBase
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
@ -69,6 +71,11 @@ try:
|
|||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
BaseModelV1 = BaseModelV2
|
BaseModelV1 = BaseModelV2
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pydantic.v1 import validator
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
from pydantic import validator
|
||||||
|
|
||||||
import wrapt
|
import wrapt
|
||||||
from redis.asyncio import Redis
|
from redis.asyncio import Redis
|
||||||
from redis.exceptions import ResponseError
|
from redis.exceptions import ResponseError
|
||||||
@ -92,11 +99,13 @@ from reflex.utils.exceptions import (
|
|||||||
DynamicRouteArgShadowsStateVar,
|
DynamicRouteArgShadowsStateVar,
|
||||||
EventHandlerShadowsBuiltInStateMethod,
|
EventHandlerShadowsBuiltInStateMethod,
|
||||||
ImmutableStateError,
|
ImmutableStateError,
|
||||||
|
InvalidLockWarningThresholdError,
|
||||||
InvalidStateManagerMode,
|
InvalidStateManagerMode,
|
||||||
LockExpiredError,
|
LockExpiredError,
|
||||||
ReflexRuntimeError,
|
ReflexRuntimeError,
|
||||||
SetUndefinedStateVarError,
|
SetUndefinedStateVarError,
|
||||||
StateSchemaMismatchError,
|
StateSchemaMismatchError,
|
||||||
|
StateSerializationError,
|
||||||
StateTooLargeError,
|
StateTooLargeError,
|
||||||
)
|
)
|
||||||
from reflex.utils.exec import is_testing_env
|
from reflex.utils.exec import is_testing_env
|
||||||
@ -438,7 +447,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|||||||
Returns:
|
Returns:
|
||||||
The string representation of the state.
|
The string representation of the state.
|
||||||
"""
|
"""
|
||||||
return f"{self.__class__.__name__}({self.dict()})"
|
return f"{type(self).__name__}({self.dict()})"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_computed_vars(cls) -> list[ComputedVar]:
|
def _get_computed_vars(cls) -> list[ComputedVar]:
|
||||||
@ -1094,6 +1103,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|||||||
if (
|
if (
|
||||||
not field.required
|
not field.required
|
||||||
and field.default is None
|
and field.default is None
|
||||||
|
and field.default_factory is None
|
||||||
and not types.is_optional(prop._var_type)
|
and not types.is_optional(prop._var_type)
|
||||||
):
|
):
|
||||||
# Ensure frontend uses null coalescing when accessing.
|
# Ensure frontend uses null coalescing when accessing.
|
||||||
@ -2121,14 +2131,26 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|||||||
state["__dict__"].pop("router", None)
|
state["__dict__"].pop("router", None)
|
||||||
state["__dict__"].pop("router_data", None)
|
state["__dict__"].pop("router_data", None)
|
||||||
# Never serialize parent_state or substates.
|
# Never serialize parent_state or substates.
|
||||||
state["__dict__"]["parent_state"] = None
|
state["__dict__"].pop("parent_state", None)
|
||||||
state["__dict__"]["substates"] = {}
|
state["__dict__"].pop("substates", None)
|
||||||
state["__dict__"].pop("_was_touched", None)
|
state["__dict__"].pop("_was_touched", None)
|
||||||
# Remove all inherited vars.
|
# Remove all inherited vars.
|
||||||
for inherited_var_name in self.inherited_vars:
|
for inherited_var_name in self.inherited_vars:
|
||||||
state["__dict__"].pop(inherited_var_name, None)
|
state["__dict__"].pop(inherited_var_name, None)
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
def __setstate__(self, state: dict[str, Any]):
|
||||||
|
"""Set the state from redis deserialization.
|
||||||
|
|
||||||
|
This method is called by pickle to deserialize the object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state: The state dict for deserialization.
|
||||||
|
"""
|
||||||
|
state["__dict__"]["parent_state"] = None
|
||||||
|
state["__dict__"]["substates"] = {}
|
||||||
|
super().__setstate__(state)
|
||||||
|
|
||||||
def _check_state_size(
|
def _check_state_size(
|
||||||
self,
|
self,
|
||||||
pickle_state_size: int,
|
pickle_state_size: int,
|
||||||
@ -2193,8 +2215,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The serialized state.
|
The serialized state.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
StateSerializationError: If the state cannot be serialized.
|
||||||
"""
|
"""
|
||||||
payload = b""
|
payload = b""
|
||||||
|
error = ""
|
||||||
try:
|
try:
|
||||||
payload = pickle.dumps((self._to_schema(), self))
|
payload = pickle.dumps((self._to_schema(), self))
|
||||||
except HANDLED_PICKLE_ERRORS as og_pickle_error:
|
except HANDLED_PICKLE_ERRORS as og_pickle_error:
|
||||||
@ -2214,8 +2240,13 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|||||||
except HANDLED_PICKLE_ERRORS as ex:
|
except HANDLED_PICKLE_ERRORS as ex:
|
||||||
error += f"Dill was also unable to pickle the state: {ex}"
|
error += f"Dill was also unable to pickle the state: {ex}"
|
||||||
console.warn(error)
|
console.warn(error)
|
||||||
|
|
||||||
if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF:
|
if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF:
|
||||||
self._check_state_size(len(payload))
|
self._check_state_size(len(payload))
|
||||||
|
|
||||||
|
if not payload:
|
||||||
|
raise StateSerializationError(error)
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -2809,6 +2840,7 @@ class StateManager(Base, ABC):
|
|||||||
redis=redis,
|
redis=redis,
|
||||||
token_expiration=config.redis_token_expiration,
|
token_expiration=config.redis_token_expiration,
|
||||||
lock_expiration=config.redis_lock_expiration,
|
lock_expiration=config.redis_lock_expiration,
|
||||||
|
lock_warning_threshold=config.redis_lock_warning_threshold,
|
||||||
)
|
)
|
||||||
raise InvalidStateManagerMode(
|
raise InvalidStateManagerMode(
|
||||||
f"Expected one of: DISK, MEMORY, REDIS, got {config.state_manager_mode}"
|
f"Expected one of: DISK, MEMORY, REDIS, got {config.state_manager_mode}"
|
||||||
@ -3178,6 +3210,15 @@ def _default_lock_expiration() -> int:
|
|||||||
return get_config().redis_lock_expiration
|
return get_config().redis_lock_expiration
|
||||||
|
|
||||||
|
|
||||||
|
def _default_lock_warning_threshold() -> int:
|
||||||
|
"""Get the default lock warning threshold.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The default lock warning threshold.
|
||||||
|
"""
|
||||||
|
return get_config().redis_lock_warning_threshold
|
||||||
|
|
||||||
|
|
||||||
class StateManagerRedis(StateManager):
|
class StateManagerRedis(StateManager):
|
||||||
"""A state manager that stores states in redis."""
|
"""A state manager that stores states in redis."""
|
||||||
|
|
||||||
@ -3190,6 +3231,11 @@ class StateManagerRedis(StateManager):
|
|||||||
# The maximum time to hold a lock (ms).
|
# The maximum time to hold a lock (ms).
|
||||||
lock_expiration: int = pydantic.Field(default_factory=_default_lock_expiration)
|
lock_expiration: int = pydantic.Field(default_factory=_default_lock_expiration)
|
||||||
|
|
||||||
|
# The maximum time to hold a lock (ms) before warning.
|
||||||
|
lock_warning_threshold: int = pydantic.Field(
|
||||||
|
default_factory=_default_lock_warning_threshold
|
||||||
|
)
|
||||||
|
|
||||||
# The keyspace subscription string when redis is waiting for lock to be released
|
# The keyspace subscription string when redis is waiting for lock to be released
|
||||||
_redis_notify_keyspace_events: str = (
|
_redis_notify_keyspace_events: str = (
|
||||||
"K" # Enable keyspace notifications (target a particular key)
|
"K" # Enable keyspace notifications (target a particular key)
|
||||||
@ -3377,6 +3423,17 @@ class StateManagerRedis(StateManager):
|
|||||||
f"`app.state_manager.lock_expiration` (currently {self.lock_expiration}) "
|
f"`app.state_manager.lock_expiration` (currently {self.lock_expiration}) "
|
||||||
"or use `@rx.event(background=True)` decorator for long-running tasks."
|
"or use `@rx.event(background=True)` decorator for long-running tasks."
|
||||||
)
|
)
|
||||||
|
elif lock_id is not None:
|
||||||
|
time_taken = self.lock_expiration / 1000 - (
|
||||||
|
await self.redis.ttl(self._lock_key(token))
|
||||||
|
)
|
||||||
|
if time_taken > self.lock_warning_threshold / 1000:
|
||||||
|
console.warn(
|
||||||
|
f"Lock for token {token} was held too long {time_taken=}s, "
|
||||||
|
f"use `@rx.event(background=True)` decorator for long-running tasks.",
|
||||||
|
dedupe=True,
|
||||||
|
)
|
||||||
|
|
||||||
client_token, substate_name = _split_substate_key(token)
|
client_token, substate_name = _split_substate_key(token)
|
||||||
# If the substate name on the token doesn't match the instance name, it cannot have a parent.
|
# If the substate name on the token doesn't match the instance name, it cannot have a parent.
|
||||||
if state.parent_state is not None and state.get_full_name() != substate_name:
|
if state.parent_state is not None and state.get_full_name() != substate_name:
|
||||||
@ -3426,6 +3483,27 @@ class StateManagerRedis(StateManager):
|
|||||||
yield state
|
yield state
|
||||||
await self.set_state(token, state, lock_id)
|
await self.set_state(token, state, lock_id)
|
||||||
|
|
||||||
|
@validator("lock_warning_threshold")
|
||||||
|
@classmethod
|
||||||
|
def validate_lock_warning_threshold(cls, lock_warning_threshold: int, values):
|
||||||
|
"""Validate the lock warning threshold.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lock_warning_threshold: The lock warning threshold.
|
||||||
|
values: The validated attributes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The lock warning threshold.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
InvalidLockWarningThresholdError: If the lock warning threshold is invalid.
|
||||||
|
"""
|
||||||
|
if lock_warning_threshold >= (lock_expiration := values["lock_expiration"]):
|
||||||
|
raise InvalidLockWarningThresholdError(
|
||||||
|
f"The lock warning threshold({lock_warning_threshold}) must be less than the lock expiration time({lock_expiration})."
|
||||||
|
)
|
||||||
|
return lock_warning_threshold
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _lock_key(token: str) -> bytes:
|
def _lock_key(token: str) -> bytes:
|
||||||
"""Get the redis key for a token's lock.
|
"""Get the redis key for a token's lock.
|
||||||
@ -3457,6 +3535,35 @@ class StateManagerRedis(StateManager):
|
|||||||
nx=True, # only set if it doesn't exist
|
nx=True, # only set if it doesn't exist
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _get_pubsub_message(
|
||||||
|
self, pubsub: PubSub, timeout: float | None = None
|
||||||
|
) -> None:
|
||||||
|
"""Get lock release events from the pubsub.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pubsub: The pubsub to get a message from.
|
||||||
|
timeout: Remaining time to wait for a message.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The message.
|
||||||
|
"""
|
||||||
|
if timeout is None:
|
||||||
|
timeout = self.lock_expiration / 1000.0
|
||||||
|
|
||||||
|
started = time.time()
|
||||||
|
message = await pubsub.get_message(
|
||||||
|
ignore_subscribe_messages=True,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
message is None
|
||||||
|
or message["data"] not in self._redis_keyspace_lock_release_events
|
||||||
|
):
|
||||||
|
remaining = timeout - (time.time() - started)
|
||||||
|
if remaining <= 0:
|
||||||
|
return
|
||||||
|
await self._get_pubsub_message(pubsub, timeout=remaining)
|
||||||
|
|
||||||
async def _wait_lock(self, lock_key: bytes, lock_id: bytes) -> None:
|
async def _wait_lock(self, lock_key: bytes, lock_id: bytes) -> None:
|
||||||
"""Wait for a redis lock to be released via pubsub.
|
"""Wait for a redis lock to be released via pubsub.
|
||||||
|
|
||||||
@ -3469,7 +3576,6 @@ class StateManagerRedis(StateManager):
|
|||||||
Raises:
|
Raises:
|
||||||
ResponseError: when the keyspace config cannot be set.
|
ResponseError: when the keyspace config cannot be set.
|
||||||
"""
|
"""
|
||||||
state_is_locked = False
|
|
||||||
lock_key_channel = f"__keyspace@0__:{lock_key.decode()}"
|
lock_key_channel = f"__keyspace@0__:{lock_key.decode()}"
|
||||||
# Enable keyspace notifications for the lock key, so we know when it is available.
|
# Enable keyspace notifications for the lock key, so we know when it is available.
|
||||||
try:
|
try:
|
||||||
@ -3483,20 +3589,13 @@ class StateManagerRedis(StateManager):
|
|||||||
raise
|
raise
|
||||||
async with self.redis.pubsub() as pubsub:
|
async with self.redis.pubsub() as pubsub:
|
||||||
await pubsub.psubscribe(lock_key_channel)
|
await pubsub.psubscribe(lock_key_channel)
|
||||||
while not state_is_locked:
|
|
||||||
# wait for the lock to be released
|
# wait for the lock to be released
|
||||||
while True:
|
while True:
|
||||||
if not await self.redis.exists(lock_key):
|
# fast path
|
||||||
break # key was removed, try to get the lock again
|
if await self._try_get_lock(lock_key, lock_id):
|
||||||
message = await pubsub.get_message(
|
return
|
||||||
ignore_subscribe_messages=True,
|
# wait for lock events
|
||||||
timeout=self.lock_expiration / 1000.0,
|
await self._get_pubsub_message(pubsub)
|
||||||
)
|
|
||||||
if message is None:
|
|
||||||
continue
|
|
||||||
if message["data"] in self._redis_keyspace_lock_release_events:
|
|
||||||
break
|
|
||||||
state_is_locked = await self._try_get_lock(lock_key, lock_id)
|
|
||||||
|
|
||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
async def _lock(self, token: str):
|
async def _lock(self, token: str):
|
||||||
@ -3618,7 +3717,7 @@ class MutableProxy(wrapt.ObjectProxy):
|
|||||||
Returns:
|
Returns:
|
||||||
The representation of the wrapped object.
|
The representation of the wrapped object.
|
||||||
"""
|
"""
|
||||||
return f"{self.__class__.__name__}({self.__wrapped__})"
|
return f"{type(self).__name__}({self.__wrapped__})"
|
||||||
|
|
||||||
def _mark_dirty(
|
def _mark_dirty(
|
||||||
self,
|
self,
|
||||||
|
@ -138,9 +138,6 @@ def convert_item(
|
|||||||
if isinstance(style_item, Var):
|
if isinstance(style_item, Var):
|
||||||
return style_item, style_item._get_all_var_data()
|
return style_item, style_item._get_all_var_data()
|
||||||
|
|
||||||
# if isinstance(style_item, str) and REFLEX_VAR_OPENING_TAG not in style_item:
|
|
||||||
# return style_item, None
|
|
||||||
|
|
||||||
# Otherwise, convert to Var to collapse VarData encoded in f-string.
|
# Otherwise, convert to Var to collapse VarData encoded in f-string.
|
||||||
new_var = LiteralVar.create(style_item)
|
new_var = LiteralVar.create(style_item)
|
||||||
var_data = new_var._get_all_var_data() if new_var is not None else None
|
var_data = new_var._get_all_var_data() if new_var is not None else None
|
||||||
|
@ -206,7 +206,7 @@ class AppHarness:
|
|||||||
The full state name
|
The full state name
|
||||||
"""
|
"""
|
||||||
# NOTE: using State.get_name() somehow causes trouble here
|
# NOTE: using State.get_name() somehow causes trouble here
|
||||||
# path = [State.get_name()] + [self.get_state_name(p) for p in path]
|
# path = [State.get_name()] + [self.get_state_name(p) for p in path] # noqa: ERA001
|
||||||
path = ["reflex___state____state"] + [self.get_state_name(p) for p in path]
|
path = ["reflex___state____state"] + [self.get_state_name(p) for p in path]
|
||||||
return ".".join(path)
|
return ".".join(path)
|
||||||
|
|
||||||
@ -436,7 +436,6 @@ class AppHarness:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The rendered app global code.
|
The rendered app global code.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not inspect.isclass(value) and not inspect.isfunction(value):
|
if not inspect.isclass(value) and not inspect.isfunction(value):
|
||||||
return f"{key} = {value!r}"
|
return f"{key} = {value!r}"
|
||||||
|
@ -20,6 +20,24 @@ _EMITTED_DEPRECATION_WARNINGS = set()
|
|||||||
# Info messages which have been printed.
|
# Info messages which have been printed.
|
||||||
_EMITTED_INFO = set()
|
_EMITTED_INFO = set()
|
||||||
|
|
||||||
|
# Warnings which have been printed.
|
||||||
|
_EMIITED_WARNINGS = set()
|
||||||
|
|
||||||
|
# Errors which have been printed.
|
||||||
|
_EMITTED_ERRORS = set()
|
||||||
|
|
||||||
|
# Success messages which have been printed.
|
||||||
|
_EMITTED_SUCCESS = set()
|
||||||
|
|
||||||
|
# Debug messages which have been printed.
|
||||||
|
_EMITTED_DEBUG = set()
|
||||||
|
|
||||||
|
# Logs which have been printed.
|
||||||
|
_EMITTED_LOGS = set()
|
||||||
|
|
||||||
|
# Prints which have been printed.
|
||||||
|
_EMITTED_PRINTS = set()
|
||||||
|
|
||||||
|
|
||||||
def set_log_level(log_level: LogLevel):
|
def set_log_level(log_level: LogLevel):
|
||||||
"""Set the log level.
|
"""Set the log level.
|
||||||
@ -55,25 +73,37 @@ def is_debug() -> bool:
|
|||||||
return _LOG_LEVEL <= LogLevel.DEBUG
|
return _LOG_LEVEL <= LogLevel.DEBUG
|
||||||
|
|
||||||
|
|
||||||
def print(msg: str, **kwargs):
|
def print(msg: str, dedupe: bool = False, **kwargs):
|
||||||
"""Print a message.
|
"""Print a message.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg: The message to print.
|
msg: The message to print.
|
||||||
|
dedupe: If True, suppress multiple console logs of print message.
|
||||||
kwargs: Keyword arguments to pass to the print function.
|
kwargs: Keyword arguments to pass to the print function.
|
||||||
"""
|
"""
|
||||||
|
if dedupe:
|
||||||
|
if msg in _EMITTED_PRINTS:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
_EMITTED_PRINTS.add(msg)
|
||||||
_console.print(msg, **kwargs)
|
_console.print(msg, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def debug(msg: str, **kwargs):
|
def debug(msg: str, dedupe: bool = False, **kwargs):
|
||||||
"""Print a debug message.
|
"""Print a debug message.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg: The debug message.
|
msg: The debug message.
|
||||||
|
dedupe: If True, suppress multiple console logs of debug message.
|
||||||
kwargs: Keyword arguments to pass to the print function.
|
kwargs: Keyword arguments to pass to the print function.
|
||||||
"""
|
"""
|
||||||
if is_debug():
|
if is_debug():
|
||||||
msg_ = f"[purple]Debug: {msg}[/purple]"
|
msg_ = f"[purple]Debug: {msg}[/purple]"
|
||||||
|
if dedupe:
|
||||||
|
if msg_ in _EMITTED_DEBUG:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
_EMITTED_DEBUG.add(msg_)
|
||||||
if progress := kwargs.pop("progress", None):
|
if progress := kwargs.pop("progress", None):
|
||||||
progress.console.print(msg_, **kwargs)
|
progress.console.print(msg_, **kwargs)
|
||||||
else:
|
else:
|
||||||
@ -97,25 +127,37 @@ def info(msg: str, dedupe: bool = False, **kwargs):
|
|||||||
print(f"[cyan]Info: {msg}[/cyan]", **kwargs)
|
print(f"[cyan]Info: {msg}[/cyan]", **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def success(msg: str, **kwargs):
|
def success(msg: str, dedupe: bool = False, **kwargs):
|
||||||
"""Print a success message.
|
"""Print a success message.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg: The success message.
|
msg: The success message.
|
||||||
|
dedupe: If True, suppress multiple console logs of success message.
|
||||||
kwargs: Keyword arguments to pass to the print function.
|
kwargs: Keyword arguments to pass to the print function.
|
||||||
"""
|
"""
|
||||||
if _LOG_LEVEL <= LogLevel.INFO:
|
if _LOG_LEVEL <= LogLevel.INFO:
|
||||||
|
if dedupe:
|
||||||
|
if msg in _EMITTED_SUCCESS:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
_EMITTED_SUCCESS.add(msg)
|
||||||
print(f"[green]Success: {msg}[/green]", **kwargs)
|
print(f"[green]Success: {msg}[/green]", **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def log(msg: str, **kwargs):
|
def log(msg: str, dedupe: bool = False, **kwargs):
|
||||||
"""Takes a string and logs it to the console.
|
"""Takes a string and logs it to the console.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg: The message to log.
|
msg: The message to log.
|
||||||
|
dedupe: If True, suppress multiple console logs of log message.
|
||||||
kwargs: Keyword arguments to pass to the print function.
|
kwargs: Keyword arguments to pass to the print function.
|
||||||
"""
|
"""
|
||||||
if _LOG_LEVEL <= LogLevel.INFO:
|
if _LOG_LEVEL <= LogLevel.INFO:
|
||||||
|
if dedupe:
|
||||||
|
if msg in _EMITTED_LOGS:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
_EMITTED_LOGS.add(msg)
|
||||||
_console.log(msg, **kwargs)
|
_console.log(msg, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@ -129,14 +171,20 @@ def rule(title: str, **kwargs):
|
|||||||
_console.rule(title, **kwargs)
|
_console.rule(title, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def warn(msg: str, **kwargs):
|
def warn(msg: str, dedupe: bool = False, **kwargs):
|
||||||
"""Print a warning message.
|
"""Print a warning message.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg: The warning message.
|
msg: The warning message.
|
||||||
|
dedupe: If True, suppress multiple console logs of warning message.
|
||||||
kwargs: Keyword arguments to pass to the print function.
|
kwargs: Keyword arguments to pass to the print function.
|
||||||
"""
|
"""
|
||||||
if _LOG_LEVEL <= LogLevel.WARNING:
|
if _LOG_LEVEL <= LogLevel.WARNING:
|
||||||
|
if dedupe:
|
||||||
|
if msg in _EMIITED_WARNINGS:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
_EMIITED_WARNINGS.add(msg)
|
||||||
print(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
|
print(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@ -169,14 +217,20 @@ def deprecate(
|
|||||||
_EMITTED_DEPRECATION_WARNINGS.add(feature_name)
|
_EMITTED_DEPRECATION_WARNINGS.add(feature_name)
|
||||||
|
|
||||||
|
|
||||||
def error(msg: str, **kwargs):
|
def error(msg: str, dedupe: bool = False, **kwargs):
|
||||||
"""Print an error message.
|
"""Print an error message.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg: The error message.
|
msg: The error message.
|
||||||
|
dedupe: If True, suppress multiple console logs of error message.
|
||||||
kwargs: Keyword arguments to pass to the print function.
|
kwargs: Keyword arguments to pass to the print function.
|
||||||
"""
|
"""
|
||||||
if _LOG_LEVEL <= LogLevel.ERROR:
|
if _LOG_LEVEL <= LogLevel.ERROR:
|
||||||
|
if dedupe:
|
||||||
|
if msg in _EMITTED_ERRORS:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
_EMITTED_ERRORS.add(msg)
|
||||||
print(f"[red]{msg}[/red]", **kwargs)
|
print(f"[red]{msg}[/red]", **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,6 +63,10 @@ class UploadValueError(ReflexError, ValueError):
|
|||||||
"""Custom ValueError for upload related errors."""
|
"""Custom ValueError for upload related errors."""
|
||||||
|
|
||||||
|
|
||||||
|
class PageValueError(ReflexError, ValueError):
|
||||||
|
"""Custom ValueError for page related errors."""
|
||||||
|
|
||||||
|
|
||||||
class RouteValueError(ReflexError, ValueError):
|
class RouteValueError(ReflexError, ValueError):
|
||||||
"""Custom ValueError for route related errors."""
|
"""Custom ValueError for route related errors."""
|
||||||
|
|
||||||
@ -155,6 +159,10 @@ class StateTooLargeError(ReflexError):
|
|||||||
"""Raised when the state is too large to be serialized."""
|
"""Raised when the state is too large to be serialized."""
|
||||||
|
|
||||||
|
|
||||||
|
class StateSerializationError(ReflexError):
|
||||||
|
"""Raised when the state cannot be serialized."""
|
||||||
|
|
||||||
|
|
||||||
class SystemPackageMissingError(ReflexError):
|
class SystemPackageMissingError(ReflexError):
|
||||||
"""Raised when a system package is missing."""
|
"""Raised when a system package is missing."""
|
||||||
|
|
||||||
@ -175,3 +183,7 @@ def raise_system_package_missing_error(package: str) -> NoReturn:
|
|||||||
" Please install it through your system package manager."
|
" Please install it through your system package manager."
|
||||||
+ (f" You can do so by running 'brew install {package}'." if IS_MACOS else "")
|
+ (f" You can do so by running 'brew install {package}'." if IS_MACOS else "")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidLockWarningThresholdError(ReflexError):
|
||||||
|
"""Raised when an invalid lock warning threshold is provided."""
|
||||||
|
@ -664,18 +664,22 @@ def format_library_name(library_fullname: str):
|
|||||||
return lib
|
return lib
|
||||||
|
|
||||||
|
|
||||||
def json_dumps(obj: Any) -> str:
|
def json_dumps(obj: Any, **kwargs) -> str:
|
||||||
"""Takes an object and returns a jsonified string.
|
"""Takes an object and returns a jsonified string.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: The object to be serialized.
|
obj: The object to be serialized.
|
||||||
|
kwargs: Additional keyword arguments to pass to json.dumps.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A string
|
A string
|
||||||
"""
|
"""
|
||||||
from reflex.utils import serializers
|
from reflex.utils import serializers
|
||||||
|
|
||||||
return json.dumps(obj, ensure_ascii=False, default=serializers.serialize)
|
kwargs.setdefault("ensure_ascii", False)
|
||||||
|
kwargs.setdefault("default", serializers.serialize)
|
||||||
|
|
||||||
|
return json.dumps(obj, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def collect_form_dict_names(form_dict: dict[str, Any]) -> dict[str, Any]:
|
def collect_form_dict_names(form_dict: dict[str, Any]) -> dict[str, Any]:
|
||||||
@ -712,7 +716,6 @@ def format_array_ref(refs: str, idx: Var | None) -> str:
|
|||||||
"""
|
"""
|
||||||
clean_ref = re.sub(r"[^\w]+", "_", refs)
|
clean_ref = re.sub(r"[^\w]+", "_", refs)
|
||||||
if idx is not None:
|
if idx is not None:
|
||||||
# idx._var_is_local = True
|
|
||||||
return f"refs_{clean_ref}[{idx!s}]"
|
return f"refs_{clean_ref}[{idx!s}]"
|
||||||
return f"refs_{clean_ref}"
|
return f"refs_{clean_ref}"
|
||||||
|
|
||||||
|
@ -196,12 +196,7 @@ def _get_type_hint(value, type_hint_globals, is_optional=True) -> str:
|
|||||||
elif isinstance(value, str):
|
elif isinstance(value, str):
|
||||||
ev = eval(value, type_hint_globals)
|
ev = eval(value, type_hint_globals)
|
||||||
if rx_types.is_optional(ev):
|
if rx_types.is_optional(ev):
|
||||||
# hints = {
|
|
||||||
# _get_type_hint(arg, type_hint_globals, is_optional=False)
|
|
||||||
# for arg in ev.__args__
|
|
||||||
# }
|
|
||||||
return _get_type_hint(ev, type_hint_globals, is_optional=False)
|
return _get_type_hint(ev, type_hint_globals, is_optional=False)
|
||||||
# return f"Optional[{', '.join(hints)}]"
|
|
||||||
|
|
||||||
if rx_types.is_union(ev):
|
if rx_types.is_union(ev):
|
||||||
res = [
|
res = [
|
||||||
@ -260,8 +255,15 @@ def _generate_docstrings(clzs: list[Type[Component]], props: list[str]) -> str:
|
|||||||
# We've reached the functions, so stop.
|
# We've reached the functions, so stop.
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if line == "":
|
||||||
|
# We hit a blank line, so clear comments to avoid commented out prop appearing in next prop docs.
|
||||||
|
comments.clear()
|
||||||
|
continue
|
||||||
|
|
||||||
# Get comments for prop
|
# Get comments for prop
|
||||||
if line.strip().startswith("#"):
|
if line.strip().startswith("#"):
|
||||||
|
# Remove noqa from the comments.
|
||||||
|
line = line.partition(" # noqa")[0]
|
||||||
comments.append(line)
|
comments.append(line)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -97,7 +97,6 @@ StateIterVar = Union[list, set, tuple]
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from reflex.vars.base import Var
|
from reflex.vars.base import Var
|
||||||
|
|
||||||
# ArgsSpec = Callable[[Var], list[Var]]
|
|
||||||
ArgsSpec = (
|
ArgsSpec = (
|
||||||
Callable[[], Sequence[Var]]
|
Callable[[], Sequence[Var]]
|
||||||
| Callable[[Var], Sequence[Var]]
|
| Callable[[Var], Sequence[Var]]
|
||||||
@ -331,7 +330,11 @@ def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None
|
|||||||
type_ = field.outer_type_
|
type_ = field.outer_type_
|
||||||
if isinstance(type_, ModelField):
|
if isinstance(type_, ModelField):
|
||||||
type_ = type_.type_
|
type_ = type_.type_
|
||||||
if not field.required and field.default is None:
|
if (
|
||||||
|
not field.required
|
||||||
|
and field.default is None
|
||||||
|
and field.default_factory is None
|
||||||
|
):
|
||||||
# Ensure frontend uses null coalescing when accessing.
|
# Ensure frontend uses null coalescing when accessing.
|
||||||
type_ = Optional[type_]
|
type_ = Optional[type_]
|
||||||
return type_
|
return type_
|
||||||
|
@ -1569,7 +1569,7 @@ class CachedVarOperation:
|
|||||||
if name == "_js_expr":
|
if name == "_js_expr":
|
||||||
return self._cached_var_name
|
return self._cached_var_name
|
||||||
|
|
||||||
parent_classes = inspect.getmro(self.__class__)
|
parent_classes = inspect.getmro(type(self))
|
||||||
|
|
||||||
next_class = parent_classes[parent_classes.index(CachedVarOperation) + 1]
|
next_class = parent_classes[parent_classes.index(CachedVarOperation) + 1]
|
||||||
|
|
||||||
@ -1611,7 +1611,7 @@ class CachedVarOperation:
|
|||||||
"""
|
"""
|
||||||
return hash(
|
return hash(
|
||||||
(
|
(
|
||||||
self.__class__.__name__,
|
type(self).__name__,
|
||||||
*[
|
*[
|
||||||
getattr(self, field.name)
|
getattr(self, field.name)
|
||||||
for field in dataclasses.fields(self) # type: ignore
|
for field in dataclasses.fields(self) # type: ignore
|
||||||
@ -1733,7 +1733,7 @@ class CallableVar(Var):
|
|||||||
Returns:
|
Returns:
|
||||||
The hash of the object.
|
The hash of the object.
|
||||||
"""
|
"""
|
||||||
return hash((self.__class__.__name__, self.original_var))
|
return hash((type(self).__name__, self.original_var))
|
||||||
|
|
||||||
|
|
||||||
RETURN_TYPE = TypeVar("RETURN_TYPE")
|
RETURN_TYPE = TypeVar("RETURN_TYPE")
|
||||||
|
@ -1012,7 +1012,7 @@ class LiteralNumberVar(LiteralVar, NumberVar):
|
|||||||
Returns:
|
Returns:
|
||||||
int: The hash value of the object.
|
int: The hash value of the object.
|
||||||
"""
|
"""
|
||||||
return hash((self.__class__.__name__, self._var_value))
|
return hash((type(self).__name__, self._var_value))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, value: float | int, _var_data: VarData | None = None):
|
def create(cls, value: float | int, _var_data: VarData | None = None):
|
||||||
@ -1064,7 +1064,7 @@ class LiteralBooleanVar(LiteralVar, BooleanVar):
|
|||||||
Returns:
|
Returns:
|
||||||
int: The hash value of the object.
|
int: The hash value of the object.
|
||||||
"""
|
"""
|
||||||
return hash((self.__class__.__name__, self._var_value))
|
return hash((type(self).__name__, self._var_value))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, value: bool, _var_data: VarData | None = None):
|
def create(cls, value: bool, _var_data: VarData | None = None):
|
||||||
|
@ -362,7 +362,7 @@ class LiteralObjectVar(CachedVarOperation, ObjectVar[OBJECT_TYPE], LiteralVar):
|
|||||||
Returns:
|
Returns:
|
||||||
The hash of the var.
|
The hash of the var.
|
||||||
"""
|
"""
|
||||||
return hash((self.__class__.__name__, self._js_expr))
|
return hash((type(self).__name__, self._js_expr))
|
||||||
|
|
||||||
@cached_property_no_lock
|
@cached_property_no_lock
|
||||||
def _cached_get_all_var_data(self) -> VarData | None:
|
def _cached_get_all_var_data(self) -> VarData | None:
|
||||||
|
@ -667,7 +667,7 @@ class LiteralStringVar(LiteralVar, StringVar[str]):
|
|||||||
Returns:
|
Returns:
|
||||||
The hash of the var.
|
The hash of the var.
|
||||||
"""
|
"""
|
||||||
return hash((self.__class__.__name__, self._var_value))
|
return hash((type(self).__name__, self._var_value))
|
||||||
|
|
||||||
def json(self) -> str:
|
def json(self) -> str:
|
||||||
"""Get the JSON representation of the var.
|
"""Get the JSON representation of the var.
|
||||||
|
@ -6,6 +6,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import reflex.app
|
||||||
from reflex.config import environment
|
from reflex.config import environment
|
||||||
from reflex.testing import AppHarness, AppHarnessProd
|
from reflex.testing import AppHarness, AppHarnessProd
|
||||||
|
|
||||||
@ -76,3 +77,25 @@ def app_harness_env(request):
|
|||||||
The AppHarness class to use for the test.
|
The AppHarness class to use for the test.
|
||||||
"""
|
"""
|
||||||
return request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def raise_console_error(request, mocker):
|
||||||
|
"""Spy on calls to `console.error` used by the framework.
|
||||||
|
|
||||||
|
Help catch spurious error conditions that might otherwise go unnoticed.
|
||||||
|
|
||||||
|
If a test is marked with `ignore_console_error`, the spy will be ignored
|
||||||
|
after the test.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The pytest request object.
|
||||||
|
mocker: The pytest mocker object.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
control to the test function.
|
||||||
|
"""
|
||||||
|
spy = mocker.spy(reflex.app.console, "error")
|
||||||
|
yield
|
||||||
|
if "ignore_console_error" not in request.keywords:
|
||||||
|
spy.assert_not_called()
|
||||||
|
@ -637,8 +637,7 @@ async def test_client_side_state(
|
|||||||
assert await AppHarness._poll_for_async(poll_for_not_hydrated)
|
assert await AppHarness._poll_for_async(poll_for_not_hydrated)
|
||||||
|
|
||||||
# Trigger event to get a new instance of the state since the old was expired.
|
# Trigger event to get a new instance of the state since the old was expired.
|
||||||
state_var_input = driver.find_element(By.ID, "state_var")
|
set_sub("c1", "c1 post expire")
|
||||||
state_var_input.send_keys("re-triggering")
|
|
||||||
|
|
||||||
# get new references to all cookie and local storage elements (again)
|
# get new references to all cookie and local storage elements (again)
|
||||||
c1 = driver.find_element(By.ID, "c1")
|
c1 = driver.find_element(By.ID, "c1")
|
||||||
@ -659,7 +658,7 @@ async def test_client_side_state(
|
|||||||
l1s = driver.find_element(By.ID, "l1s")
|
l1s = driver.find_element(By.ID, "l1s")
|
||||||
s1s = driver.find_element(By.ID, "s1s")
|
s1s = driver.find_element(By.ID, "s1s")
|
||||||
|
|
||||||
assert c1.text == "c1 value"
|
assert c1.text == "c1 post expire"
|
||||||
assert c2.text == "c2 value"
|
assert c2.text == "c2 value"
|
||||||
assert c3.text == "" # temporary cookie expired after reset state!
|
assert c3.text == "" # temporary cookie expired after reset state!
|
||||||
assert c4.text == "c4 value"
|
assert c4.text == "c4 value"
|
||||||
@ -690,11 +689,11 @@ async def test_client_side_state(
|
|||||||
|
|
||||||
async def poll_for_c1_set():
|
async def poll_for_c1_set():
|
||||||
sub_state = await get_sub_state()
|
sub_state = await get_sub_state()
|
||||||
return sub_state.c1 == "c1 value"
|
return sub_state.c1 == "c1 post expire"
|
||||||
|
|
||||||
assert await AppHarness._poll_for_async(poll_for_c1_set)
|
assert await AppHarness._poll_for_async(poll_for_c1_set)
|
||||||
sub_state = await get_sub_state()
|
sub_state = await get_sub_state()
|
||||||
assert sub_state.c1 == "c1 value"
|
assert sub_state.c1 == "c1 post expire"
|
||||||
assert sub_state.c2 == "c2 value"
|
assert sub_state.c2 == "c2 value"
|
||||||
assert sub_state.c3 == ""
|
assert sub_state.c3 == ""
|
||||||
assert sub_state.c4 == "c4 value"
|
assert sub_state.c4 == "c4 value"
|
||||||
|
@ -106,7 +106,6 @@ def ComputedVars():
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# raise Exception(State.count3._deps(objclass=State))
|
|
||||||
app = rx.App()
|
app = rx.App()
|
||||||
app.add_page(index)
|
app.add_page(index)
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@ from selenium.webdriver.support.ui import WebDriverWait
|
|||||||
|
|
||||||
from reflex.testing import AppHarness, AppHarnessProd
|
from reflex.testing import AppHarness, AppHarnessProd
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.ignore_console_error]
|
||||||
|
|
||||||
|
|
||||||
def TestApp():
|
def TestApp():
|
||||||
"""A test app for event exception handler integration."""
|
"""A test app for event exception handler integration."""
|
||||||
|
@ -183,6 +183,6 @@ async def test_fully_controlled_input(fully_controlled_input: AppHarness):
|
|||||||
clear_button.click()
|
clear_button.click()
|
||||||
assert AppHarness._poll_for(lambda: on_change_input.get_attribute("value") == "")
|
assert AppHarness._poll_for(lambda: on_change_input.get_attribute("value") == "")
|
||||||
# potential bug: clearing the on_change field doesn't itself trigger on_change
|
# potential bug: clearing the on_change field doesn't itself trigger on_change
|
||||||
# assert backend_state.text == ""
|
# assert backend_state.text == "" #noqa: ERA001
|
||||||
# assert debounce_input.get_attribute("value") == ""
|
# assert debounce_input.get_attribute("value") == "" #noqa: ERA001
|
||||||
# assert value_input.get_attribute("value") == ""
|
# assert value_input.get_attribute("value") == "" #noqa: ERA001
|
||||||
|
@ -73,7 +73,7 @@ def StateInheritance():
|
|||||||
def on_click_other_mixin(self):
|
def on_click_other_mixin(self):
|
||||||
self.other_mixin_clicks += 1
|
self.other_mixin_clicks += 1
|
||||||
self.other_mixin = (
|
self.other_mixin = (
|
||||||
f"{self.__class__.__name__}.clicked.{self.other_mixin_clicks}"
|
f"{type(self).__name__}.clicked.{self.other_mixin_clicks}"
|
||||||
)
|
)
|
||||||
|
|
||||||
class Base1(Mixin, rx.State):
|
class Base1(Mixin, rx.State):
|
||||||
|
@ -381,9 +381,22 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive
|
|||||||
await asyncio.sleep(0.3)
|
await asyncio.sleep(0.3)
|
||||||
cancel_button.click()
|
cancel_button.click()
|
||||||
|
|
||||||
# look up the backend state and assert on progress
|
# Wait a bit for the upload to get cancelled.
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
# Get interim progress dicts saved in the on_upload_progress handler.
|
||||||
|
async def _progress_dicts():
|
||||||
|
state = await upload_file.get_state(substate_token)
|
||||||
|
return state.substates[state_name].progress_dicts
|
||||||
|
|
||||||
|
# We should have _some_ progress
|
||||||
|
assert await AppHarness._poll_for_async(_progress_dicts)
|
||||||
|
|
||||||
|
# But there should never be a final progress record for a cancelled upload.
|
||||||
|
for p in await _progress_dicts():
|
||||||
|
assert p["progress"] != 1
|
||||||
|
|
||||||
state = await upload_file.get_state(substate_token)
|
state = await upload_file.get_state(substate_token)
|
||||||
assert state.substates[state_name].progress_dicts
|
|
||||||
file_data = state.substates[state_name]._file_data
|
file_data = state.substates[state_name]._file_data
|
||||||
assert isinstance(file_data, dict)
|
assert isinstance(file_data, dict)
|
||||||
normalized_file_data = {Path(k).name: v for k, v in file_data.items()}
|
normalized_file_data = {Path(k).name: v for k, v in file_data.items()}
|
||||||
|
218
tests/integration/tests_playwright/test_appearance.py
Normal file
218
tests/integration/tests_playwright/test_appearance.py
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
from typing import Generator
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from playwright.sync_api import Page, expect
|
||||||
|
|
||||||
|
from reflex.testing import AppHarness
|
||||||
|
|
||||||
|
|
||||||
|
def DefaultLightModeApp():
|
||||||
|
import reflex as rx
|
||||||
|
from reflex.style import color_mode
|
||||||
|
|
||||||
|
app = rx.App(theme=rx.theme(appearance="light"))
|
||||||
|
|
||||||
|
@app.add_page
|
||||||
|
def index():
|
||||||
|
return rx.text(color_mode)
|
||||||
|
|
||||||
|
|
||||||
|
def DefaultDarkModeApp():
|
||||||
|
import reflex as rx
|
||||||
|
from reflex.style import color_mode
|
||||||
|
|
||||||
|
app = rx.App(theme=rx.theme(appearance="dark"))
|
||||||
|
|
||||||
|
@app.add_page
|
||||||
|
def index():
|
||||||
|
return rx.text(color_mode)
|
||||||
|
|
||||||
|
|
||||||
|
def DefaultSystemModeApp():
|
||||||
|
import reflex as rx
|
||||||
|
from reflex.style import color_mode
|
||||||
|
|
||||||
|
app = rx.App()
|
||||||
|
|
||||||
|
@app.add_page
|
||||||
|
def index():
|
||||||
|
return rx.text(color_mode)
|
||||||
|
|
||||||
|
|
||||||
|
def ColorToggleApp():
|
||||||
|
import reflex as rx
|
||||||
|
from reflex.style import color_mode, resolved_color_mode, set_color_mode
|
||||||
|
|
||||||
|
app = rx.App(theme=rx.theme(appearance="light"))
|
||||||
|
|
||||||
|
@app.add_page
|
||||||
|
def index():
|
||||||
|
return rx.box(
|
||||||
|
rx.segmented_control.root(
|
||||||
|
rx.segmented_control.item(
|
||||||
|
rx.icon(tag="monitor", size=20),
|
||||||
|
value="system",
|
||||||
|
),
|
||||||
|
rx.segmented_control.item(
|
||||||
|
rx.icon(tag="sun", size=20),
|
||||||
|
value="light",
|
||||||
|
),
|
||||||
|
rx.segmented_control.item(
|
||||||
|
rx.icon(tag="moon", size=20),
|
||||||
|
value="dark",
|
||||||
|
),
|
||||||
|
on_change=set_color_mode,
|
||||||
|
variant="classic",
|
||||||
|
radius="large",
|
||||||
|
value=color_mode,
|
||||||
|
),
|
||||||
|
rx.text(color_mode, id="current_color_mode"),
|
||||||
|
rx.text(resolved_color_mode, id="resolved_color_mode"),
|
||||||
|
rx.text(rx.color_mode_cond("LightMode", "DarkMode"), id="color_mode_cond"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def light_mode_app(tmp_path_factory) -> Generator[AppHarness, None, None]:
|
||||||
|
"""Start DefaultLightMode app at tmp_path via AppHarness.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tmp_path_factory: pytest tmp_path_factory fixture
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
running AppHarness instance
|
||||||
|
|
||||||
|
"""
|
||||||
|
with AppHarness.create(
|
||||||
|
root=tmp_path_factory.mktemp("appearance_app"),
|
||||||
|
app_source=DefaultLightModeApp, # type: ignore
|
||||||
|
) as harness:
|
||||||
|
assert harness.app_instance is not None, "app is not running"
|
||||||
|
yield harness
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def dark_mode_app(tmp_path_factory) -> Generator[AppHarness, None, None]:
|
||||||
|
"""Start DefaultDarkMode app at tmp_path via AppHarness.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tmp_path_factory: pytest tmp_path_factory fixture
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
running AppHarness instance
|
||||||
|
|
||||||
|
"""
|
||||||
|
with AppHarness.create(
|
||||||
|
root=tmp_path_factory.mktemp("appearance_app"),
|
||||||
|
app_source=DefaultDarkModeApp, # type: ignore
|
||||||
|
) as harness:
|
||||||
|
assert harness.app_instance is not None, "app is not running"
|
||||||
|
yield harness
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def system_mode_app(tmp_path_factory) -> Generator[AppHarness, None, None]:
|
||||||
|
"""Start DefaultSystemMode app at tmp_path via AppHarness.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tmp_path_factory: pytest tmp_path_factory fixture
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
running AppHarness instance
|
||||||
|
|
||||||
|
"""
|
||||||
|
with AppHarness.create(
|
||||||
|
root=tmp_path_factory.mktemp("appearance_app"),
|
||||||
|
app_source=DefaultSystemModeApp, # type: ignore
|
||||||
|
) as harness:
|
||||||
|
assert harness.app_instance is not None, "app is not running"
|
||||||
|
yield harness
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def color_toggle_app(tmp_path_factory) -> Generator[AppHarness, None, None]:
|
||||||
|
"""Start ColorToggle app at tmp_path via AppHarness.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tmp_path_factory: pytest tmp_path_factory fixture
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
running AppHarness instance
|
||||||
|
|
||||||
|
"""
|
||||||
|
with AppHarness.create(
|
||||||
|
root=tmp_path_factory.mktemp("appearance_app"),
|
||||||
|
app_source=ColorToggleApp, # type: ignore
|
||||||
|
) as harness:
|
||||||
|
assert harness.app_instance is not None, "app is not running"
|
||||||
|
yield harness
|
||||||
|
|
||||||
|
|
||||||
|
def test_appearance_light_mode(light_mode_app: AppHarness, page: Page):
|
||||||
|
assert light_mode_app.frontend_url is not None
|
||||||
|
page.goto(light_mode_app.frontend_url)
|
||||||
|
|
||||||
|
expect(page.get_by_text("light")).to_be_visible()
|
||||||
|
|
||||||
|
|
||||||
|
def test_appearance_dark_mode(dark_mode_app: AppHarness, page: Page):
|
||||||
|
assert dark_mode_app.frontend_url is not None
|
||||||
|
page.goto(dark_mode_app.frontend_url)
|
||||||
|
|
||||||
|
expect(page.get_by_text("dark")).to_be_visible()
|
||||||
|
|
||||||
|
|
||||||
|
def test_appearance_system_mode(system_mode_app: AppHarness, page: Page):
|
||||||
|
assert system_mode_app.frontend_url is not None
|
||||||
|
page.goto(system_mode_app.frontend_url)
|
||||||
|
|
||||||
|
expect(page.get_by_text("system")).to_be_visible()
|
||||||
|
|
||||||
|
|
||||||
|
def test_appearance_color_toggle(color_toggle_app: AppHarness, page: Page):
|
||||||
|
assert color_toggle_app.frontend_url is not None
|
||||||
|
page.goto(color_toggle_app.frontend_url)
|
||||||
|
|
||||||
|
# Radio buttons locators.
|
||||||
|
radio_system = page.get_by_role("radio").nth(0)
|
||||||
|
radio_light = page.get_by_role("radio").nth(1)
|
||||||
|
radio_dark = page.get_by_role("radio").nth(2)
|
||||||
|
|
||||||
|
# Text locators to check.
|
||||||
|
current_color_mode = page.locator("id=current_color_mode")
|
||||||
|
resolved_color_mode = page.locator("id=resolved_color_mode")
|
||||||
|
color_mode_cond = page.locator("id=color_mode_cond")
|
||||||
|
root_body = page.locator('div[data-is-root-theme="true"]')
|
||||||
|
|
||||||
|
# Background colors.
|
||||||
|
dark_background = "rgb(17, 17, 19)" # value based on dark native appearance, can change depending on the browser
|
||||||
|
light_background = "rgb(255, 255, 255)"
|
||||||
|
|
||||||
|
# check initial state
|
||||||
|
expect(current_color_mode).to_have_text("light")
|
||||||
|
expect(resolved_color_mode).to_have_text("light")
|
||||||
|
expect(color_mode_cond).to_have_text("LightMode")
|
||||||
|
expect(root_body).to_have_css("background-color", light_background)
|
||||||
|
|
||||||
|
# click dark mode
|
||||||
|
radio_dark.click()
|
||||||
|
expect(current_color_mode).to_have_text("dark")
|
||||||
|
expect(resolved_color_mode).to_have_text("dark")
|
||||||
|
expect(color_mode_cond).to_have_text("DarkMode")
|
||||||
|
expect(root_body).to_have_css("background-color", dark_background)
|
||||||
|
|
||||||
|
# click light mode
|
||||||
|
radio_light.click()
|
||||||
|
expect(current_color_mode).to_have_text("light")
|
||||||
|
expect(resolved_color_mode).to_have_text("light")
|
||||||
|
expect(color_mode_cond).to_have_text("LightMode")
|
||||||
|
expect(root_body).to_have_css("background-color", light_background)
|
||||||
|
page.reload()
|
||||||
|
expect(root_body).to_have_css("background-color", light_background)
|
||||||
|
|
||||||
|
# click system mode
|
||||||
|
radio_system.click()
|
||||||
|
expect(current_color_mode).to_have_text("system")
|
||||||
|
expect(resolved_color_mode).to_have_text("light")
|
||||||
|
expect(color_mode_cond).to_have_text("LightMode")
|
||||||
|
expect(root_body).to_have_css("background-color", light_background)
|
@ -13,9 +13,6 @@ STATE_VAR = Var(_js_expr="default_state.name")
|
|||||||
("{}", '{"{}"}'),
|
("{}", '{"{}"}'),
|
||||||
(None, '{""}'),
|
(None, '{""}'),
|
||||||
(STATE_VAR, "{default_state.name}"),
|
(STATE_VAR, "{default_state.name}"),
|
||||||
# This behavior is now unsupported.
|
|
||||||
# ("${default_state.name}", "${default_state.name}"),
|
|
||||||
# ("{state.name}", "{state.name}"),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_fstrings(contents, expected):
|
def test_fstrings(contents, expected):
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
from typing import Dict, List, Set, Tuple, Union
|
from typing import Dict, List, Set, Tuple, Union
|
||||||
|
|
||||||
|
import pydantic.v1
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from reflex import el
|
from reflex import el
|
||||||
|
from reflex.base import Base
|
||||||
from reflex.components.component import Component
|
from reflex.components.component import Component
|
||||||
from reflex.components.core.foreach import (
|
from reflex.components.core.foreach import (
|
||||||
Foreach,
|
Foreach,
|
||||||
@ -18,6 +20,12 @@ from reflex.vars.number import NumberVar
|
|||||||
from reflex.vars.sequence import ArrayVar
|
from reflex.vars.sequence import ArrayVar
|
||||||
|
|
||||||
|
|
||||||
|
class ForEachTag(Base):
|
||||||
|
"""A tag for testing the ForEach component."""
|
||||||
|
|
||||||
|
name: str = ""
|
||||||
|
|
||||||
|
|
||||||
class ForEachState(BaseState):
|
class ForEachState(BaseState):
|
||||||
"""A state for testing the ForEach component."""
|
"""A state for testing the ForEach component."""
|
||||||
|
|
||||||
@ -46,6 +54,8 @@ class ForEachState(BaseState):
|
|||||||
bad_annotation_list: list = [["red", "orange"], ["yellow", "blue"]]
|
bad_annotation_list: list = [["red", "orange"], ["yellow", "blue"]]
|
||||||
color_index_tuple: Tuple[int, str] = (0, "red")
|
color_index_tuple: Tuple[int, str] = (0, "red")
|
||||||
|
|
||||||
|
default_factory_list: list[ForEachTag] = pydantic.v1.Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class ComponentStateTest(ComponentState):
|
class ComponentStateTest(ComponentState):
|
||||||
"""A test component state."""
|
"""A test component state."""
|
||||||
@ -290,3 +300,11 @@ def test_foreach_component_state():
|
|||||||
ForEachState.colors_list,
|
ForEachState.colors_list,
|
||||||
ComponentStateTest.create,
|
ComponentStateTest.create,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_foreach_default_factory():
|
||||||
|
"""Test that the default factory is called."""
|
||||||
|
_ = Foreach.create(
|
||||||
|
ForEachState.default_factory_list,
|
||||||
|
lambda tag: text(tag.name),
|
||||||
|
)
|
||||||
|
@ -42,7 +42,7 @@ def test_set_src_str():
|
|||||||
"`pic2.jpeg`",
|
"`pic2.jpeg`",
|
||||||
)
|
)
|
||||||
# For plain rx.el.img, an explicit var is not created, so the quoting happens later
|
# For plain rx.el.img, an explicit var is not created, so the quoting happens later
|
||||||
# assert str(image.src) == "pic2.jpeg" # type: ignore
|
# assert str(image.src) == "pic2.jpeg" # type: ignore #noqa: ERA001
|
||||||
|
|
||||||
|
|
||||||
def test_set_src_img(pil_image: Img):
|
def test_set_src_img(pil_image: Img):
|
||||||
|
@ -918,17 +918,17 @@ def test_invalid_event_handler_args(component2, test_state):
|
|||||||
# # Event Handler types must match
|
# # Event Handler types must match
|
||||||
# with pytest.raises(EventHandlerArgTypeMismatch):
|
# with pytest.raises(EventHandlerArgTypeMismatch):
|
||||||
# component2.create(
|
# component2.create(
|
||||||
# on_user_visited_count_changed=test_state.do_something_with_bool
|
# on_user_visited_count_changed=test_state.do_something_with_bool # noqa: ERA001 RUF100
|
||||||
# )
|
# ) # noqa: ERA001 RUF100
|
||||||
# with pytest.raises(EventHandlerArgTypeMismatch):
|
# with pytest.raises(EventHandlerArgTypeMismatch):
|
||||||
# component2.create(on_user_list_changed=test_state.do_something_with_int)
|
# component2.create(on_user_list_changed=test_state.do_something_with_int) #noqa: ERA001
|
||||||
# with pytest.raises(EventHandlerArgTypeMismatch):
|
# with pytest.raises(EventHandlerArgTypeMismatch):
|
||||||
# component2.create(on_user_list_changed=test_state.do_something_with_list_int)
|
# component2.create(on_user_list_changed=test_state.do_something_with_list_int) #noqa: ERA001
|
||||||
|
|
||||||
# component2.create(on_open=test_state.do_something_with_int)
|
# component2.create(on_open=test_state.do_something_with_int) #noqa: ERA001
|
||||||
# component2.create(on_open=test_state.do_something_with_bool)
|
# component2.create(on_open=test_state.do_something_with_bool) #noqa: ERA001
|
||||||
# component2.create(on_user_visited_count_changed=test_state.do_something_with_int)
|
# component2.create(on_user_visited_count_changed=test_state.do_something_with_int) #noqa: ERA001
|
||||||
# component2.create(on_user_list_changed=test_state.do_something_with_list_str)
|
# component2.create(on_user_list_changed=test_state.do_something_with_list_str) #noqa: ERA001
|
||||||
|
|
||||||
# lambda cannot return weird values.
|
# lambda cannot return weird values.
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
@ -1437,8 +1437,6 @@ def test_get_vars(component, exp_vars):
|
|||||||
comp_vars,
|
comp_vars,
|
||||||
sorted(exp_vars, key=lambda v: v._js_expr),
|
sorted(exp_vars, key=lambda v: v._js_expr),
|
||||||
):
|
):
|
||||||
# print(str(comp_var), str(exp_var))
|
|
||||||
# print(comp_var._get_all_var_data(), exp_var._get_all_var_data())
|
|
||||||
assert comp_var.equals(exp_var)
|
assert comp_var.equals(exp_var)
|
||||||
|
|
||||||
|
|
||||||
|
@ -899,8 +899,6 @@ class DynamicState(BaseState):
|
|||||||
loaded: int = 0
|
loaded: int = 0
|
||||||
counter: int = 0
|
counter: int = 0
|
||||||
|
|
||||||
# side_effect_counter: int = 0
|
|
||||||
|
|
||||||
def on_load(self):
|
def on_load(self):
|
||||||
"""Event handler for page on_load, should trigger for all navigation events."""
|
"""Event handler for page on_load, should trigger for all navigation events."""
|
||||||
self.loaded = self.loaded + 1
|
self.loaded = self.loaded + 1
|
||||||
@ -917,7 +915,6 @@ class DynamicState(BaseState):
|
|||||||
Returns:
|
Returns:
|
||||||
same as self.dynamic
|
same as self.dynamic
|
||||||
"""
|
"""
|
||||||
# self.side_effect_counter = self.side_effect_counter + 1
|
|
||||||
return self.dynamic
|
return self.dynamic
|
||||||
|
|
||||||
on_load_internal = OnLoadInternalState.on_load_internal.fn
|
on_load_internal = OnLoadInternalState.on_load_internal.fn
|
||||||
@ -1059,7 +1056,6 @@ async def test_dynamic_route_var_route_change_completed_on_load(
|
|||||||
arg_name: exp_val,
|
arg_name: exp_val,
|
||||||
f"comp_{arg_name}": exp_val,
|
f"comp_{arg_name}": exp_val,
|
||||||
constants.CompileVars.IS_HYDRATED: False,
|
constants.CompileVars.IS_HYDRATED: False,
|
||||||
# "side_effect_counter": exp_index,
|
|
||||||
"router": exp_router,
|
"router": exp_router,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1155,8 +1151,6 @@ async def test_dynamic_route_var_route_change_completed_on_load(
|
|||||||
state = await app.state_manager.get_state(substate_token)
|
state = await app.state_manager.get_state(substate_token)
|
||||||
assert state.loaded == len(exp_vals)
|
assert state.loaded == len(exp_vals)
|
||||||
assert state.counter == len(exp_vals)
|
assert state.counter == len(exp_vals)
|
||||||
# print(f"Expected {exp_vals} rendering side effects, got {state.side_effect_counter}")
|
|
||||||
# assert state.side_effect_counter == len(exp_vals)
|
|
||||||
|
|
||||||
if isinstance(app.state_manager, StateManagerRedis):
|
if isinstance(app.state_manager, StateManagerRedis):
|
||||||
await app.state_manager.close()
|
await app.state_manager.close()
|
||||||
|
@ -3,11 +3,19 @@ from __future__ import annotations
|
|||||||
from typing import Dict, List, Optional, Type, Union
|
from typing import Dict, List, Optional, Type, Union
|
||||||
|
|
||||||
import attrs
|
import attrs
|
||||||
|
import pydantic.v1
|
||||||
import pytest
|
import pytest
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
import sqlmodel
|
||||||
from sqlalchemy import JSON, TypeDecorator
|
from sqlalchemy import JSON, TypeDecorator
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
from sqlalchemy.orm import (
|
||||||
|
DeclarativeBase,
|
||||||
|
Mapped,
|
||||||
|
MappedAsDataclass,
|
||||||
|
mapped_column,
|
||||||
|
relationship,
|
||||||
|
)
|
||||||
|
|
||||||
import reflex as rx
|
import reflex as rx
|
||||||
from reflex.utils.types import GenericType, get_attribute_access_type
|
from reflex.utils.types import GenericType, get_attribute_access_type
|
||||||
@ -53,6 +61,10 @@ class SQLALabel(SQLABase):
|
|||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
test_id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey("test.id"))
|
test_id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey("test.id"))
|
||||||
test: Mapped[SQLAClass] = relationship(back_populates="labels")
|
test: Mapped[SQLAClass] = relationship(back_populates="labels")
|
||||||
|
test_dataclass_id: Mapped[int] = mapped_column(
|
||||||
|
sqlalchemy.ForeignKey("test_dataclass.id")
|
||||||
|
)
|
||||||
|
test_dataclass: Mapped[SQLAClassDataclass] = relationship(back_populates="labels")
|
||||||
|
|
||||||
|
|
||||||
class SQLAClass(SQLABase):
|
class SQLAClass(SQLABase):
|
||||||
@ -104,9 +116,64 @@ class SQLAClass(SQLABase):
|
|||||||
return self.labels[0] if self.labels else None
|
return self.labels[0] if self.labels else None
|
||||||
|
|
||||||
|
|
||||||
|
class SQLAClassDataclass(MappedAsDataclass, SQLABase):
|
||||||
|
"""Test sqlalchemy model."""
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
no_default: Mapped[int] = mapped_column(nullable=True)
|
||||||
|
count: Mapped[int] = mapped_column()
|
||||||
|
name: Mapped[str] = mapped_column()
|
||||||
|
int_list: Mapped[List[int]] = mapped_column(
|
||||||
|
sqlalchemy.types.ARRAY(item_type=sqlalchemy.INTEGER)
|
||||||
|
)
|
||||||
|
str_list: Mapped[List[str]] = mapped_column(
|
||||||
|
sqlalchemy.types.ARRAY(item_type=sqlalchemy.String)
|
||||||
|
)
|
||||||
|
optional_int: Mapped[Optional[int]] = mapped_column(nullable=True)
|
||||||
|
sqla_tag_id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey(SQLATag.id))
|
||||||
|
sqla_tag: Mapped[Optional[SQLATag]] = relationship()
|
||||||
|
labels: Mapped[List[SQLALabel]] = relationship(back_populates="test_dataclass")
|
||||||
|
# do not use lower case dict here!
|
||||||
|
# https://github.com/sqlalchemy/sqlalchemy/issues/9902
|
||||||
|
dict_str_str: Mapped[Dict[str, str]] = mapped_column()
|
||||||
|
default_factory: Mapped[List[int]] = mapped_column(
|
||||||
|
sqlalchemy.types.ARRAY(item_type=sqlalchemy.INTEGER),
|
||||||
|
default_factory=list,
|
||||||
|
)
|
||||||
|
__tablename__: str = "test_dataclass"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def str_property(self) -> str:
|
||||||
|
"""String property.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Name attribute
|
||||||
|
"""
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def str_or_int_property(self) -> Union[str, int]:
|
||||||
|
"""String or int property.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Name attribute
|
||||||
|
"""
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def first_label(self) -> Optional[SQLALabel]:
|
||||||
|
"""First label property.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
First label
|
||||||
|
"""
|
||||||
|
return self.labels[0] if self.labels else None
|
||||||
|
|
||||||
|
|
||||||
class ModelClass(rx.Model):
|
class ModelClass(rx.Model):
|
||||||
"""Test reflex model."""
|
"""Test reflex model."""
|
||||||
|
|
||||||
|
no_default: Optional[int] = sqlmodel.Field(nullable=True)
|
||||||
count: int = 0
|
count: int = 0
|
||||||
name: str = "test"
|
name: str = "test"
|
||||||
int_list: List[int] = []
|
int_list: List[int] = []
|
||||||
@ -115,6 +182,7 @@ class ModelClass(rx.Model):
|
|||||||
sqla_tag: Optional[SQLATag] = None
|
sqla_tag: Optional[SQLATag] = None
|
||||||
labels: List[SQLALabel] = []
|
labels: List[SQLALabel] = []
|
||||||
dict_str_str: Dict[str, str] = {}
|
dict_str_str: Dict[str, str] = {}
|
||||||
|
default_factory: List[int] = sqlmodel.Field(default_factory=list)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def str_property(self) -> str:
|
def str_property(self) -> str:
|
||||||
@ -147,6 +215,7 @@ class ModelClass(rx.Model):
|
|||||||
class BaseClass(rx.Base):
|
class BaseClass(rx.Base):
|
||||||
"""Test rx.Base class."""
|
"""Test rx.Base class."""
|
||||||
|
|
||||||
|
no_default: Optional[int] = pydantic.v1.Field(required=False)
|
||||||
count: int = 0
|
count: int = 0
|
||||||
name: str = "test"
|
name: str = "test"
|
||||||
int_list: List[int] = []
|
int_list: List[int] = []
|
||||||
@ -155,6 +224,7 @@ class BaseClass(rx.Base):
|
|||||||
sqla_tag: Optional[SQLATag] = None
|
sqla_tag: Optional[SQLATag] = None
|
||||||
labels: List[SQLALabel] = []
|
labels: List[SQLALabel] = []
|
||||||
dict_str_str: Dict[str, str] = {}
|
dict_str_str: Dict[str, str] = {}
|
||||||
|
default_factory: List[int] = pydantic.v1.Field(default_factory=list)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def str_property(self) -> str:
|
def str_property(self) -> str:
|
||||||
@ -236,6 +306,7 @@ class AttrClass:
|
|||||||
sqla_tag: Optional[SQLATag] = None
|
sqla_tag: Optional[SQLATag] = None
|
||||||
labels: List[SQLALabel] = []
|
labels: List[SQLALabel] = []
|
||||||
dict_str_str: Dict[str, str] = {}
|
dict_str_str: Dict[str, str] = {}
|
||||||
|
default_factory: List[int] = attrs.field(factory=list)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def str_property(self) -> str:
|
def str_property(self) -> str:
|
||||||
@ -265,27 +336,17 @@ class AttrClass:
|
|||||||
return self.labels[0] if self.labels else None
|
return self.labels[0] if self.labels else None
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(
|
@pytest.mark.parametrize(
|
||||||
params=[
|
"cls",
|
||||||
|
[
|
||||||
SQLAClass,
|
SQLAClass,
|
||||||
|
SQLAClassDataclass,
|
||||||
BaseClass,
|
BaseClass,
|
||||||
BareClass,
|
BareClass,
|
||||||
ModelClass,
|
ModelClass,
|
||||||
AttrClass,
|
AttrClass,
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
def cls(request: pytest.FixtureRequest) -> type:
|
|
||||||
"""Fixture for the class to test.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request: pytest request object.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Class to test.
|
|
||||||
"""
|
|
||||||
return request.param
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"attr, expected",
|
"attr, expected",
|
||||||
[
|
[
|
||||||
@ -311,3 +372,38 @@ def test_get_attribute_access_type(cls: type, attr: str, expected: GenericType)
|
|||||||
expected: Expected type.
|
expected: Expected type.
|
||||||
"""
|
"""
|
||||||
assert get_attribute_access_type(cls, attr) == expected
|
assert get_attribute_access_type(cls, attr) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cls",
|
||||||
|
[
|
||||||
|
SQLAClassDataclass,
|
||||||
|
BaseClass,
|
||||||
|
ModelClass,
|
||||||
|
AttrClass,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_attribute_access_type_default_factory(cls: type) -> None:
|
||||||
|
"""Test get_attribute_access_type returns the correct type for default factory fields.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cls: Class to test.
|
||||||
|
"""
|
||||||
|
assert get_attribute_access_type(cls, "default_factory") == List[int]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cls",
|
||||||
|
[
|
||||||
|
SQLAClassDataclass,
|
||||||
|
BaseClass,
|
||||||
|
ModelClass,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_attribute_access_type_no_default(cls: type) -> None:
|
||||||
|
"""Test get_attribute_access_type returns the correct type for fields with no default which are not required.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cls: Class to test.
|
||||||
|
"""
|
||||||
|
assert get_attribute_access_type(cls, "no_default") == Optional[int]
|
||||||
|
@ -164,7 +164,7 @@ def test_constructor_postgresql(username, password, host, port, database, expect
|
|||||||
"localhost",
|
"localhost",
|
||||||
5432,
|
5432,
|
||||||
"db",
|
"db",
|
||||||
"postgresql+psycopg2://user:pass@localhost:5432/db",
|
"postgresql+psycopg://user:pass@localhost:5432/db",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user",
|
"user",
|
||||||
@ -172,17 +172,17 @@ def test_constructor_postgresql(username, password, host, port, database, expect
|
|||||||
"localhost",
|
"localhost",
|
||||||
None,
|
None,
|
||||||
"db",
|
"db",
|
||||||
"postgresql+psycopg2://user@localhost/db",
|
"postgresql+psycopg://user@localhost/db",
|
||||||
),
|
),
|
||||||
("user", "", "", None, "db", "postgresql+psycopg2://user@/db"),
|
("user", "", "", None, "db", "postgresql+psycopg://user@/db"),
|
||||||
("", "", "localhost", 5432, "db", "postgresql+psycopg2://localhost:5432/db"),
|
("", "", "localhost", 5432, "db", "postgresql+psycopg://localhost:5432/db"),
|
||||||
("", "", "", None, "db", "postgresql+psycopg2:///db"),
|
("", "", "", None, "db", "postgresql+psycopg:///db"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_constructor_postgresql_psycopg2(
|
def test_constructor_postgresql_psycopg(
|
||||||
username, password, host, port, database, expected_url
|
username, password, host, port, database, expected_url
|
||||||
):
|
):
|
||||||
"""Test DBConfig.postgresql_psycopg2 constructor creates the instance correctly.
|
"""Test DBConfig.postgresql_psycopg constructor creates the instance correctly.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
username: Database username.
|
username: Database username.
|
||||||
@ -192,10 +192,10 @@ def test_constructor_postgresql_psycopg2(
|
|||||||
database: Database name.
|
database: Database name.
|
||||||
expected_url: Expected database URL generated.
|
expected_url: Expected database URL generated.
|
||||||
"""
|
"""
|
||||||
db_config = DBConfig.postgresql_psycopg2(
|
db_config = DBConfig.postgresql_psycopg(
|
||||||
username=username, password=password, host=host, port=port, database=database
|
username=username, password=password, host=host, port=port, database=database
|
||||||
)
|
)
|
||||||
assert db_config.engine == "postgresql+psycopg2"
|
assert db_config.engine == "postgresql+psycopg"
|
||||||
assert db_config.username == username
|
assert db_config.username == username
|
||||||
assert db_config.password == password
|
assert db_config.password == password
|
||||||
assert db_config.host == host
|
assert db_config.host == host
|
||||||
|
@ -209,10 +209,6 @@ def test_event_redirect(input, output):
|
|||||||
assert isinstance(spec, EventSpec)
|
assert isinstance(spec, EventSpec)
|
||||||
assert spec.handler.fn.__qualname__ == "_redirect"
|
assert spec.handler.fn.__qualname__ == "_redirect"
|
||||||
|
|
||||||
# this asserts need comment about what it's testing (they fail with Var as input)
|
|
||||||
# assert spec.args[0][0].equals(Var(_js_expr="path"))
|
|
||||||
# assert spec.args[0][1].equals(Var(_js_expr="/path"))
|
|
||||||
|
|
||||||
assert format.format_event(spec) == output
|
assert format.format_event(spec) == output
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ def test_default_primary_key(model_default_primary: Model):
|
|||||||
Args:
|
Args:
|
||||||
model_default_primary: Fixture.
|
model_default_primary: Fixture.
|
||||||
"""
|
"""
|
||||||
assert "id" in model_default_primary.__class__.__fields__
|
assert "id" in type(model_default_primary).__fields__
|
||||||
|
|
||||||
|
|
||||||
def test_custom_primary_key(model_custom_primary: Model):
|
def test_custom_primary_key(model_custom_primary: Model):
|
||||||
@ -55,7 +55,7 @@ def test_custom_primary_key(model_custom_primary: Model):
|
|||||||
Args:
|
Args:
|
||||||
model_custom_primary: Fixture.
|
model_custom_primary: Fixture.
|
||||||
"""
|
"""
|
||||||
assert "id" not in model_custom_primary.__class__.__fields__
|
assert "id" not in type(model_custom_primary).__fields__
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings(
|
@pytest.mark.filterwarnings(
|
||||||
|
@ -127,8 +127,8 @@ def test_automigration(
|
|||||||
assert result[0].b == 4.2
|
assert result[0].b == 4.2
|
||||||
|
|
||||||
# No-op
|
# No-op
|
||||||
# assert Model.migrate(autogenerate=True)
|
# assert Model.migrate(autogenerate=True) #noqa: ERA001
|
||||||
# assert len(list(versions.glob("*.py"))) == 4
|
# assert len(list(versions.glob("*.py"))) == 4 #noqa: ERA001
|
||||||
|
|
||||||
# drop table (AlembicSecond)
|
# drop table (AlembicSecond)
|
||||||
model_registry.get_metadata().clear()
|
model_registry.get_metadata().clear()
|
||||||
|
@ -55,7 +55,12 @@ from reflex.state import (
|
|||||||
)
|
)
|
||||||
from reflex.testing import chdir
|
from reflex.testing import chdir
|
||||||
from reflex.utils import format, prerequisites, types
|
from reflex.utils import format, prerequisites, types
|
||||||
from reflex.utils.exceptions import ReflexRuntimeError, SetUndefinedStateVarError
|
from reflex.utils.exceptions import (
|
||||||
|
InvalidLockWarningThresholdError,
|
||||||
|
ReflexRuntimeError,
|
||||||
|
SetUndefinedStateVarError,
|
||||||
|
StateSerializationError,
|
||||||
|
)
|
||||||
from reflex.utils.format import json_dumps
|
from reflex.utils.format import json_dumps
|
||||||
from reflex.vars.base import Var, computed_var
|
from reflex.vars.base import Var, computed_var
|
||||||
from tests.units.states.mutation import MutableSQLAModel, MutableTestState
|
from tests.units.states.mutation import MutableSQLAModel, MutableTestState
|
||||||
@ -63,7 +68,9 @@ from tests.units.states.mutation import MutableSQLAModel, MutableTestState
|
|||||||
from .states import GenState
|
from .states import GenState
|
||||||
|
|
||||||
CI = bool(os.environ.get("CI", False))
|
CI = bool(os.environ.get("CI", False))
|
||||||
LOCK_EXPIRATION = 2000 if CI else 300
|
LOCK_EXPIRATION = 2500 if CI else 300
|
||||||
|
LOCK_WARNING_THRESHOLD = 1000 if CI else 100
|
||||||
|
LOCK_WARN_SLEEP = 1.5 if CI else 0.15
|
||||||
LOCK_EXPIRE_SLEEP = 2.5 if CI else 0.4
|
LOCK_EXPIRE_SLEEP = 2.5 if CI else 0.4
|
||||||
|
|
||||||
|
|
||||||
@ -787,7 +794,6 @@ async def test_process_event_simple(test_state):
|
|||||||
assert test_state.num1 == 69
|
assert test_state.num1 == 69
|
||||||
|
|
||||||
# The delta should contain the changes, including computed vars.
|
# The delta should contain the changes, including computed vars.
|
||||||
# assert update.delta == {"test_state": {"num1": 69, "sum": 72.14}}
|
|
||||||
assert update.delta == {
|
assert update.delta == {
|
||||||
TestState.get_full_name(): {"num1": 69, "sum": 72.14, "upper": ""},
|
TestState.get_full_name(): {"num1": 69, "sum": 72.14, "upper": ""},
|
||||||
GrandchildState3.get_full_name(): {"computed": ""},
|
GrandchildState3.get_full_name(): {"computed": ""},
|
||||||
@ -1698,7 +1704,7 @@ async def test_state_manager_modify_state(
|
|||||||
assert not state_manager._states_locks[token].locked()
|
assert not state_manager._states_locks[token].locked()
|
||||||
|
|
||||||
# separate instances should NOT share locks
|
# separate instances should NOT share locks
|
||||||
sm2 = state_manager.__class__(state=TestState)
|
sm2 = type(state_manager)(state=TestState)
|
||||||
assert sm2._state_manager_lock is state_manager._state_manager_lock
|
assert sm2._state_manager_lock is state_manager._state_manager_lock
|
||||||
assert not sm2._states_locks
|
assert not sm2._states_locks
|
||||||
if state_manager._states_locks:
|
if state_manager._states_locks:
|
||||||
@ -1784,6 +1790,7 @@ async def test_state_manager_lock_expire(
|
|||||||
substate_token_redis: A token + substate name for looking up in state manager.
|
substate_token_redis: A token + substate name for looking up in state manager.
|
||||||
"""
|
"""
|
||||||
state_manager_redis.lock_expiration = LOCK_EXPIRATION
|
state_manager_redis.lock_expiration = LOCK_EXPIRATION
|
||||||
|
state_manager_redis.lock_warning_threshold = LOCK_WARNING_THRESHOLD
|
||||||
|
|
||||||
async with state_manager_redis.modify_state(substate_token_redis):
|
async with state_manager_redis.modify_state(substate_token_redis):
|
||||||
await asyncio.sleep(0.01)
|
await asyncio.sleep(0.01)
|
||||||
@ -1808,6 +1815,7 @@ async def test_state_manager_lock_expire_contend(
|
|||||||
unexp_num1 = 666
|
unexp_num1 = 666
|
||||||
|
|
||||||
state_manager_redis.lock_expiration = LOCK_EXPIRATION
|
state_manager_redis.lock_expiration = LOCK_EXPIRATION
|
||||||
|
state_manager_redis.lock_warning_threshold = LOCK_WARNING_THRESHOLD
|
||||||
|
|
||||||
order = []
|
order = []
|
||||||
|
|
||||||
@ -1837,6 +1845,57 @@ async def test_state_manager_lock_expire_contend(
|
|||||||
assert (await state_manager_redis.get_state(substate_token_redis)).num1 == exp_num1
|
assert (await state_manager_redis.get_state(substate_token_redis)).num1 == exp_num1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_state_manager_lock_warning_threshold_contend(
|
||||||
|
state_manager_redis: StateManager, token: str, substate_token_redis: str, mocker
|
||||||
|
):
|
||||||
|
"""Test that the state manager triggers a warning when lock contention exceeds the warning threshold.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state_manager_redis: A state manager instance.
|
||||||
|
token: A token.
|
||||||
|
substate_token_redis: A token + substate name for looking up in state manager.
|
||||||
|
mocker: Pytest mocker object.
|
||||||
|
"""
|
||||||
|
console_warn = mocker.patch("reflex.utils.console.warn")
|
||||||
|
|
||||||
|
state_manager_redis.lock_expiration = LOCK_EXPIRATION
|
||||||
|
state_manager_redis.lock_warning_threshold = LOCK_WARNING_THRESHOLD
|
||||||
|
|
||||||
|
order = []
|
||||||
|
|
||||||
|
async def _coro_blocker():
|
||||||
|
async with state_manager_redis.modify_state(substate_token_redis):
|
||||||
|
order.append("blocker")
|
||||||
|
await asyncio.sleep(LOCK_WARN_SLEEP)
|
||||||
|
|
||||||
|
tasks = [
|
||||||
|
asyncio.create_task(_coro_blocker()),
|
||||||
|
]
|
||||||
|
|
||||||
|
await tasks[0]
|
||||||
|
console_warn.assert_called()
|
||||||
|
assert console_warn.call_count == 7
|
||||||
|
|
||||||
|
|
||||||
|
class CopyingAsyncMock(AsyncMock):
|
||||||
|
"""An AsyncMock, but deepcopy the args and kwargs first."""
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
"""Call the mock.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: the arguments passed to the mock
|
||||||
|
kwargs: the keyword arguments passed to the mock
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The result of the mock call
|
||||||
|
"""
|
||||||
|
args = copy.deepcopy(args)
|
||||||
|
kwargs = copy.deepcopy(kwargs)
|
||||||
|
return super().__call__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def mock_app_simple(monkeypatch) -> rx.App:
|
def mock_app_simple(monkeypatch) -> rx.App:
|
||||||
"""Simple Mock app fixture.
|
"""Simple Mock app fixture.
|
||||||
@ -1853,7 +1912,7 @@ def mock_app_simple(monkeypatch) -> rx.App:
|
|||||||
|
|
||||||
setattr(app_module, CompileVars.APP, app)
|
setattr(app_module, CompileVars.APP, app)
|
||||||
app.state = TestState
|
app.state = TestState
|
||||||
app.event_namespace.emit = AsyncMock() # type: ignore
|
app.event_namespace.emit = CopyingAsyncMock() # type: ignore
|
||||||
|
|
||||||
def _mock_get_app(*args, **kwargs):
|
def _mock_get_app(*args, **kwargs):
|
||||||
return app_module
|
return app_module
|
||||||
@ -1957,8 +2016,7 @@ async def test_state_proxy(grandchild_state: GrandchildState, mock_app: rx.App):
|
|||||||
mock_app.event_namespace.emit.assert_called_once()
|
mock_app.event_namespace.emit.assert_called_once()
|
||||||
mcall = mock_app.event_namespace.emit.mock_calls[0]
|
mcall = mock_app.event_namespace.emit.mock_calls[0]
|
||||||
assert mcall.args[0] == str(SocketEvent.EVENT)
|
assert mcall.args[0] == str(SocketEvent.EVENT)
|
||||||
assert json.loads(mcall.args[1]) == dataclasses.asdict(
|
assert mcall.args[1] == StateUpdate(
|
||||||
StateUpdate(
|
|
||||||
delta={
|
delta={
|
||||||
parent_state.get_full_name(): {
|
parent_state.get_full_name(): {
|
||||||
"upper": "",
|
"upper": "",
|
||||||
@ -1972,7 +2030,6 @@ async def test_state_proxy(grandchild_state: GrandchildState, mock_app: rx.App):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
|
||||||
assert mcall.kwargs["to"] == grandchild_state.router.session.session_id
|
assert mcall.kwargs["to"] == grandchild_state.router.session.session_id
|
||||||
|
|
||||||
|
|
||||||
@ -2153,51 +2210,51 @@ async def test_background_task_no_block(mock_app: rx.App, token: str):
|
|||||||
assert mock_app.event_namespace is not None
|
assert mock_app.event_namespace is not None
|
||||||
emit_mock = mock_app.event_namespace.emit
|
emit_mock = mock_app.event_namespace.emit
|
||||||
|
|
||||||
first_ws_message = json.loads(emit_mock.mock_calls[0].args[1])
|
first_ws_message = emit_mock.mock_calls[0].args[1]
|
||||||
assert (
|
assert (
|
||||||
first_ws_message["delta"][BackgroundTaskState.get_full_name()].pop("router")
|
first_ws_message.delta[BackgroundTaskState.get_full_name()].pop("router")
|
||||||
is not None
|
is not None
|
||||||
)
|
)
|
||||||
assert first_ws_message == {
|
assert first_ws_message == StateUpdate(
|
||||||
"delta": {
|
delta={
|
||||||
BackgroundTaskState.get_full_name(): {
|
BackgroundTaskState.get_full_name(): {
|
||||||
"order": ["background_task:start"],
|
"order": ["background_task:start"],
|
||||||
"computed_order": ["background_task:start"],
|
"computed_order": ["background_task:start"],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"events": [],
|
events=[],
|
||||||
"final": True,
|
final=True,
|
||||||
}
|
)
|
||||||
for call in emit_mock.mock_calls[1:5]:
|
for call in emit_mock.mock_calls[1:5]:
|
||||||
assert json.loads(call.args[1]) == {
|
assert call.args[1] == StateUpdate(
|
||||||
"delta": {
|
delta={
|
||||||
BackgroundTaskState.get_full_name(): {
|
BackgroundTaskState.get_full_name(): {
|
||||||
"computed_order": ["background_task:start"],
|
"computed_order": ["background_task:start"],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"events": [],
|
events=[],
|
||||||
"final": True,
|
final=True,
|
||||||
}
|
)
|
||||||
assert json.loads(emit_mock.mock_calls[-2].args[1]) == {
|
assert emit_mock.mock_calls[-2].args[1] == StateUpdate(
|
||||||
"delta": {
|
delta={
|
||||||
BackgroundTaskState.get_full_name(): {
|
BackgroundTaskState.get_full_name(): {
|
||||||
"order": exp_order,
|
"order": exp_order,
|
||||||
"computed_order": exp_order,
|
"computed_order": exp_order,
|
||||||
"dict_list": {},
|
"dict_list": {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"events": [],
|
events=[],
|
||||||
"final": True,
|
final=True,
|
||||||
}
|
)
|
||||||
assert json.loads(emit_mock.mock_calls[-1].args[1]) == {
|
assert emit_mock.mock_calls[-1].args[1] == StateUpdate(
|
||||||
"delta": {
|
delta={
|
||||||
BackgroundTaskState.get_full_name(): {
|
BackgroundTaskState.get_full_name(): {
|
||||||
"computed_order": exp_order,
|
"computed_order": exp_order,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"events": [],
|
events=[],
|
||||||
"final": True,
|
final=True,
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -3234,12 +3291,42 @@ async def test_setvar_async_setter():
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"expiration_kwargs, expected_values",
|
"expiration_kwargs, expected_values",
|
||||||
[
|
[
|
||||||
({"redis_lock_expiration": 20000}, (20000, constants.Expiration.TOKEN)),
|
(
|
||||||
|
{"redis_lock_expiration": 20000},
|
||||||
|
(
|
||||||
|
20000,
|
||||||
|
constants.Expiration.TOKEN,
|
||||||
|
constants.Expiration.LOCK_WARNING_THRESHOLD,
|
||||||
|
),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
{"redis_lock_expiration": 50000, "redis_token_expiration": 5600},
|
{"redis_lock_expiration": 50000, "redis_token_expiration": 5600},
|
||||||
(50000, 5600),
|
(50000, 5600, constants.Expiration.LOCK_WARNING_THRESHOLD),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"redis_token_expiration": 7600},
|
||||||
|
(
|
||||||
|
constants.Expiration.LOCK,
|
||||||
|
7600,
|
||||||
|
constants.Expiration.LOCK_WARNING_THRESHOLD,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"redis_lock_expiration": 50000, "redis_lock_warning_threshold": 1500},
|
||||||
|
(50000, constants.Expiration.TOKEN, 1500),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"redis_token_expiration": 5600, "redis_lock_warning_threshold": 3000},
|
||||||
|
(constants.Expiration.LOCK, 5600, 3000),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"redis_lock_expiration": 50000,
|
||||||
|
"redis_token_expiration": 5600,
|
||||||
|
"redis_lock_warning_threshold": 2000,
|
||||||
|
},
|
||||||
|
(50000, 5600, 2000),
|
||||||
),
|
),
|
||||||
({"redis_token_expiration": 7600}, (constants.Expiration.LOCK, 7600)),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_redis_state_manager_config_knobs(tmp_path, expiration_kwargs, expected_values):
|
def test_redis_state_manager_config_knobs(tmp_path, expiration_kwargs, expected_values):
|
||||||
@ -3269,6 +3356,44 @@ config = rx.Config(
|
|||||||
state_manager = StateManager.create(state=State)
|
state_manager = StateManager.create(state=State)
|
||||||
assert state_manager.lock_expiration == expected_values[0] # type: ignore
|
assert state_manager.lock_expiration == expected_values[0] # type: ignore
|
||||||
assert state_manager.token_expiration == expected_values[1] # type: ignore
|
assert state_manager.token_expiration == expected_values[1] # type: ignore
|
||||||
|
assert state_manager.lock_warning_threshold == expected_values[2] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif("REDIS_URL" not in os.environ, reason="Test requires redis")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"redis_lock_expiration, redis_lock_warning_threshold",
|
||||||
|
[
|
||||||
|
(10000, 10000),
|
||||||
|
(20000, 30000),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_redis_state_manager_config_knobs_invalid_lock_warning_threshold(
|
||||||
|
tmp_path, redis_lock_expiration, redis_lock_warning_threshold
|
||||||
|
):
|
||||||
|
proj_root = tmp_path / "project1"
|
||||||
|
proj_root.mkdir()
|
||||||
|
|
||||||
|
config_string = f"""
|
||||||
|
import reflex as rx
|
||||||
|
config = rx.Config(
|
||||||
|
app_name="project1",
|
||||||
|
redis_url="redis://localhost:6379",
|
||||||
|
state_manager_mode="redis",
|
||||||
|
redis_lock_expiration = {redis_lock_expiration},
|
||||||
|
redis_lock_warning_threshold = {redis_lock_warning_threshold},
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
(proj_root / "rxconfig.py").write_text(dedent(config_string))
|
||||||
|
|
||||||
|
with chdir(proj_root):
|
||||||
|
# reload config for each parameter to avoid stale values
|
||||||
|
reflex.config.get_config(reload=True)
|
||||||
|
from reflex.state import State, StateManager
|
||||||
|
|
||||||
|
with pytest.raises(InvalidLockWarningThresholdError):
|
||||||
|
StateManager.create(state=State)
|
||||||
|
del sys.modules[constants.Config.MODULE]
|
||||||
|
|
||||||
|
|
||||||
class MixinState(State, mixin=True):
|
class MixinState(State, mixin=True):
|
||||||
@ -3433,8 +3558,9 @@ def test_fallback_pickle():
|
|||||||
# Some object, like generator, are still unpicklable with dill.
|
# Some object, like generator, are still unpicklable with dill.
|
||||||
state3 = DillState(_reflex_internal_init=True) # type: ignore
|
state3 = DillState(_reflex_internal_init=True) # type: ignore
|
||||||
state3._g = (i for i in range(10))
|
state3._g = (i for i in range(10))
|
||||||
pk3 = state3._serialize()
|
|
||||||
assert len(pk3) == 0
|
with pytest.raises(StateSerializationError):
|
||||||
|
_ = state3._serialize()
|
||||||
|
|
||||||
|
|
||||||
def test_typed_state() -> None:
|
def test_typed_state() -> None:
|
||||||
@ -3485,10 +3611,10 @@ def test_mutable_models():
|
|||||||
state.dirty_vars.clear()
|
state.dirty_vars.clear()
|
||||||
|
|
||||||
# Not yet supported ENG-4083
|
# Not yet supported ENG-4083
|
||||||
# assert isinstance(state.dc, MutableProxy)
|
# assert isinstance(state.dc, MutableProxy) #noqa: ERA001
|
||||||
# state.dc.foo = "baz"
|
# state.dc.foo = "baz" #noqa: ERA001
|
||||||
# assert state.dirty_vars == {"dc"}
|
# assert state.dirty_vars == {"dc"} #noqa: ERA001
|
||||||
# state.dirty_vars.clear()
|
# state.dirty_vars.clear() #noqa: ERA001
|
||||||
|
|
||||||
|
|
||||||
def test_get_value():
|
def test_get_value():
|
||||||
|
@ -34,12 +34,6 @@ def test_disable():
|
|||||||
@pytest.mark.parametrize("event", ["init", "reinit", "run-dev", "run-prod", "export"])
|
@pytest.mark.parametrize("event", ["init", "reinit", "run-dev", "run-prod", "export"])
|
||||||
def test_send(mocker, event):
|
def test_send(mocker, event):
|
||||||
httpx_post_mock = mocker.patch("httpx.post")
|
httpx_post_mock = mocker.patch("httpx.post")
|
||||||
# mocker.patch(
|
|
||||||
# "builtins.open",
|
|
||||||
# mocker.mock_open(
|
|
||||||
# read_data='{"project_hash": "78285505863498957834586115958872998605"}'
|
|
||||||
# ),
|
|
||||||
# )
|
|
||||||
|
|
||||||
# Mock the read_text method of Path
|
# Mock the read_text method of Path
|
||||||
pathlib_path_read_text_mock = mocker.patch(
|
pathlib_path_read_text_mock = mocker.patch(
|
||||||
|
@ -1495,8 +1495,6 @@ def test_valid_var_operations(operand1_var: Var, operand2_var, operators: List[s
|
|||||||
)
|
)
|
||||||
eval(f"operand1_var {operator} operand2_var")
|
eval(f"operand1_var {operator} operand2_var")
|
||||||
eval(f"operand2_var {operator} operand1_var")
|
eval(f"operand2_var {operator} operand1_var")
|
||||||
# operand1_var.operation(op=operator, other=operand2_var)
|
|
||||||
# operand1_var.operation(op=operator, other=operand2_var, flip=True)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@ -1773,11 +1771,9 @@ def test_invalid_var_operations(operand1_var: Var, operand2_var, operators: List
|
|||||||
print(f"testing {operator} on {operand1_var!s} and {operand2_var!s}")
|
print(f"testing {operator} on {operand1_var!s} and {operand2_var!s}")
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
print(eval(f"operand1_var {operator} operand2_var"))
|
print(eval(f"operand1_var {operator} operand2_var"))
|
||||||
# operand1_var.operation(op=operator, other=operand2_var)
|
|
||||||
|
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
print(eval(f"operand2_var {operator} operand1_var"))
|
print(eval(f"operand2_var {operator} operand1_var"))
|
||||||
# operand1_var.operation(op=operator, other=operand2_var, flip=True)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
Loading…
Reference in New Issue
Block a user