Merge branch 'main' into add-validation-to-function-vars

This commit is contained in:
Khaleel Al-Adhami 2024-12-12 07:33:24 +03:00
commit 06eb04f005
181 changed files with 2975 additions and 1494 deletions

View File

@ -2,7 +2,6 @@
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: bug
assignees: '' assignees: ''
--- ---

View File

@ -0,0 +1,19 @@
---
name: Enhancement Request
about: Suggest an enhancement for an existing Reflex feature.
title: ''
labels: 'enhancement'
assignees: ''
---
**Describe the Enhancement you want**
A clear and concise description of what the improvement does.
- Which feature do you want to improve? (and what problem does it have)
- What is the benefit of the enhancement?
- Show an example/usecase were the improvement are needed.
**Additional context**
Add any other context here.

View File

@ -0,0 +1,18 @@
---
name: Feature Request
about: Suggest a new feature for Reflex
title: ''
labels: 'feature request'
assignees: ''
---
**Describe the Features**
A clear and concise description of what the features does.
- What is the purpose of the feature?
- Show an example / use cases for the new feature.
**Additional context**
Add any other context here.

View File

@ -50,7 +50,7 @@ jobs:
- run: poetry run uv pip install pyvirtualdisplay pillow pytest-split - run: poetry run uv pip install pyvirtualdisplay pillow pytest-split
- name: Run app harness tests - name: Run app harness tests
env: env:
SCREENSHOT_DIR: /tmp/screenshots SCREENSHOT_DIR: /tmp/screenshots/${{ matrix.state_manager }}/${{ matrix.python-version }}/${{ matrix.split_index }}
REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }} REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }}
run: | run: |
poetry run playwright install --with-deps poetry run playwright install --with-deps

View File

@ -163,6 +163,35 @@ jobs:
--pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}" --pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}"
--app-name "reflex-web" --path ./reflex-web/.web --app-name "reflex-web" --path ./reflex-web/.web
rx-shout-from-template:
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup_build_env
with:
python-version: '3.11.4'
run-poetry-install: true
create-venv-at-path: .venv
- name: Create app directory
run: mkdir rx-shout-from-template
- name: Init reflex-web from template
run: poetry run reflex init --template https://github.com/masenf/rx_shout
working-directory: ./rx-shout-from-template
- name: ignore reflex pin in requirements
run: sed -i -e '/reflex==/d' requirements.txt
working-directory: ./rx-shout-from-template
- name: Install additional dependencies
run: poetry run uv pip install -r requirements.txt
working-directory: ./rx-shout-from-template
- name: Run Website and Check for errors
run: |
# Check that npm is home
npm -v
poetry run bash scripts/integration.sh ./rx-shout-from-template prod
reflex-web-macos: reflex-web-macos:
if: github.event_name == 'push' && github.ref == 'refs/heads/main' if: github.event_name == 'push' && github.ref == 'refs/heads/main'
strategy: strategy:

View File

@ -3,7 +3,7 @@ fail_fast: true
repos: repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.7.2 rev: v0.8.2
hooks: hooks:
- id: ruff-format - id: ruff-format
args: [reflex, tests] args: [reflex, tests]

View File

@ -23,7 +23,7 @@
# for example, pass `docker build --platform=linux/amd64 ...` # for example, pass `docker build --platform=linux/amd64 ...`
# Stage 1: init # Stage 1: init
FROM python:3.11 as init FROM python:3.13 as init
ARG uv=/root/.local/bin/uv ARG uv=/root/.local/bin/uv
@ -48,7 +48,7 @@ RUN $uv pip install -r requirements.txt
RUN reflex init RUN reflex init
# Stage 2: copy artifacts into slim image # Stage 2: copy artifacts into slim image
FROM python:3.11-slim 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

View File

@ -2,7 +2,7 @@
# instance of a Reflex app. # instance of a Reflex app.
# Stage 1: init # Stage 1: init
FROM python:3.11 as init FROM python:3.13 as init
ARG uv=/root/.local/bin/uv ARG uv=/root/.local/bin/uv
@ -35,7 +35,7 @@ RUN rm -rf .web && mkdir .web
RUN mv /tmp/_static .web/_static RUN mv /tmp/_static .web/_static
# Stage 2: copy artifacts into slim image # Stage 2: copy artifacts into slim image
FROM python:3.11-slim 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

View File

@ -0,0 +1,3 @@
.web
!.web/bun.lockb
!.web/package.json

View File

@ -0,0 +1,14 @@
:{$PORT}
encode gzip
@backend_routes path /_event/* /ping /_upload /_upload/*
handle @backend_routes {
reverse_proxy localhost:8000
}
root * /srv
route {
try_files {path} {path}/ /404.html
file_server
}

View File

@ -0,0 +1,62 @@
# This Dockerfile is used to deploy a single-container Reflex app instance
# to services like Render, Railway, Heroku, GCP, and others.
# If the service expects a different port, provide it here (f.e Render expects port 10000)
ARG PORT=8080
# Only set for local/direct access. When TLS is used, the API_URL is assumed to be the same as the frontend.
ARG API_URL
# It uses a reverse proxy to serve the frontend statically and proxy to backend
# from a single exposed port, expecting TLS termination to be handled at the
# edge by the given platform.
FROM python:3.13 as builder
RUN mkdir -p /app/.web
RUN python -m venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"
WORKDIR /app
# Install python app requirements and reflex in the container
COPY requirements.txt .
RUN pip install -r requirements.txt
# Install reflex helper utilities like bun/fnm/node
COPY rxconfig.py ./
RUN reflex init
# Install pre-cached frontend dependencies (if exist)
COPY *.web/bun.lockb *.web/package.json .web/
RUN if [ -f .web/bun.lockb ]; then cd .web && ~/.local/share/reflex/bun/bin/bun install --frozen-lockfile; fi
# Copy local context to `/app` inside container (see .dockerignore)
COPY . .
ARG PORT API_URL
# Download other npm dependencies and compile frontend
RUN API_URL=${API_URL:-http://localhost:$PORT} reflex export --loglevel debug --frontend-only --no-zip && mv .web/_static/* /srv/ && rm -rf .web
# Final image with only necessary files
FROM python:3.13-slim
# Install Caddy and redis server inside image
RUN apt-get update -y && apt-get install -y caddy redis-server && rm -rf /var/lib/apt/lists/*
ARG PORT API_URL
ENV PATH="/app/.venv/bin:$PATH" PORT=$PORT API_URL=${API_URL:-http://localhost:$PORT} REDIS_URL=redis://localhost PYTHONUNBUFFERED=1
WORKDIR /app
COPY --from=builder /app /app
COPY --from=builder /srv /srv
# Needed until Reflex properly passes SIGTERM on backend.
STOPSIGNAL SIGKILL
EXPOSE $PORT
# Apply migrations before starting the backend.
CMD [ -d alembic ] && reflex db migrate; \
caddy start && \
redis-server --daemonize yes && \
exec reflex run --env prod --backend-only

View File

@ -0,0 +1,37 @@
# production-one-port
This docker deployment runs Reflex in prod mode, exposing a single HTTP port:
* `8080` (`$PORT`) - Caddy server hosting the frontend statically and proxying requests to the backend.
The deployment also runs a local Redis server to store state for each user.
Conceptually it is similar to the `simple-one-port` example except it:
* has layer caching for python, reflex, and node dependencies
* uses multi-stage build to reduce the size of the final image
Using this method may be preferable for deploying in memory constrained
environments, because it serves a static frontend export, rather than running
the NextJS server via node.
## Build
```console
docker build -t reflex-production-one-port .
```
## Run
```console
docker run -p 8080:8080 reflex-production-one-port
```
Note that this container has _no persistence_ and will lose all data when
stopped. You can use bind mounts or named volumes to persist the database and
uploaded_files directories as needed.
## Usage
This container should be used with an existing load balancer or reverse proxy to
terminate TLS.
It is also useful for deploying to simple app platforms, such as Render or Heroku.

View File

@ -4,7 +4,7 @@
# It uses a reverse proxy to serve the frontend statically and proxy to backend # It uses a reverse proxy to serve the frontend statically and proxy to backend
# from a single exposed port, expecting TLS termination to be handled at the # from a single exposed port, expecting TLS termination to be handled at the
# edge by the given platform. # edge by the given platform.
FROM python:3.11 FROM python:3.13
# If the service expects a different port, provide it here (f.e Render expects port 10000) # If the service expects a different port, provide it here (f.e Render expects port 10000)
ARG PORT=8080 ARG PORT=8080

View File

@ -1,5 +1,5 @@
# This Dockerfile is used to deploy a simple single-container Reflex app instance. # This Dockerfile is used to deploy a simple single-container Reflex app instance.
FROM python:3.12 FROM python:3.13
RUN apt-get update && apt-get install -y redis-server && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y redis-server && rm -rf /var/lib/apt/lists/*
ENV REDIS_URL=redis://localhost PYTHONUNBUFFERED=1 ENV REDIS_URL=redis://localhost PYTHONUNBUFFERED=1

873
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "reflex" name = "reflex"
version = "0.6.6dev1" version = "0.6.7dev1"
description = "Web apps in pure Python." description = "Web apps in pure Python."
license = "Apache-2.0" license = "Apache-2.0"
authors = [ authors = [
@ -49,16 +49,17 @@ wrapt = [
{version = ">=1.11.0,<2.0", python = "<3.11"}, {version = ">=1.11.0,<2.0", python = "<3.11"},
] ]
packaging = ">=23.1,<25.0" packaging = ">=23.1,<25.0"
reflex-hosting-cli = ">=0.1.15,<2.0" reflex-hosting-cli = ">=0.1.29,<2.0"
charset-normalizer = ">=3.3.2,<4.0" charset-normalizer = ">=3.3.2,<4.0"
wheel = ">=0.42.0,<1.0" wheel = ">=0.42.0,<1.0"
build = ">=1.0.3,<2.0" build = ">=1.0.3,<2.0"
setuptools = ">=75.0" setuptools = ">=75.0"
httpx = ">=0.25.1,<1.0" httpx = ">=0.25.1,<1.0"
twine = ">=4.0.0,<6.0" twine = ">=4.0.0,<7.0"
tomlkit = ">=0.12.4,<1.0" tomlkit = ">=0.12.4,<1.0"
lazy_loader = ">=0.4" lazy_loader = ">=0.4"
reflex-chakra = ">=0.6.0" reflex-chakra = ">=0.6.0"
typing_extensions = ">=4.6.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest = ">=7.1.2,<9.0" pytest = ">=7.1.2,<9.0"
@ -69,7 +70,7 @@ dill = ">=0.3.8"
toml = ">=0.10.2,<1.0" toml = ">=0.10.2,<1.0"
pytest-asyncio = ">=0.24.0" pytest-asyncio = ">=0.24.0"
pytest-cov = ">=4.0.0,<7.0" pytest-cov = ">=4.0.0,<7.0"
ruff = "0.7.2" ruff = "0.8.2"
pandas = ">=2.1.1,<3.0" pandas = ">=2.1.1,<3.0"
pillow = ">=10.0.0,<12.0" pillow = ">=10.0.0,<12.0"
plotly = ">=5.13.0,<6.0" plotly = ">=5.13.0,<6.0"
@ -93,8 +94,8 @@ reportIncompatibleMethodOverride = false
[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"] lint.select = ["B", "D", "E", "F", "I", "SIM", "W", "RUF", "FURB", "ERA"]
lint.ignore = ["B008", "D205", "E501", "F403", "SIM115"] lint.ignore = ["B008", "D205", "E501", "F403", "SIM115", "RUF006", "RUF012"]
lint.pydocstyle.convention = "google" lint.pydocstyle.convention = "google"
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]

View File

@ -454,6 +454,10 @@ export const connect = async (
queueEvents(update.events, socket); queueEvents(update.events, socket);
} }
}); });
socket.current.on("reload", async (event) => {
event_processing = false;
queueEvents([...initialEvents(), JSON5.parse(event)], socket);
});
document.addEventListener("visibilitychange", checkVisibility); document.addEventListener("visibilitychange", checkVisibility);
}; };
@ -486,23 +490,30 @@ export const uploadFiles = async (
return false; return false;
} }
// Track how many partial updates have been processed for this upload.
let resp_idx = 0; let resp_idx = 0;
const eventHandler = (progressEvent) => { const eventHandler = (progressEvent) => {
// handle any delta / event streamed from the upload event handler const event_callbacks = socket._callbacks.$event;
// 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.
chunks.slice(resp_idx).map((chunk) => { chunks.slice(resp_idx).map((chunk) => {
try { event_callbacks.map((f, ix) => {
socket._callbacks.$event.map((f) => { f(chunk)
f(chunk); .then(() => {
}); if (ix === event_callbacks.length - 1) {
// Mark this chunk as processed.
resp_idx += 1; resp_idx += 1;
} 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 parsing chunk", chunk, e);
} }
return; return;
} });
});
}); });
}; };
@ -705,6 +716,11 @@ export const useEventLoop = (
_e.stopPropagation(); _e.stopPropagation();
} }
const combined_name = events.map((e) => e.name).join("+++"); const combined_name = events.map((e) => e.name).join("+++");
if (event_actions?.temporal) {
if (!socket.current || !socket.current.connected) {
return; // don't queue when the backend is not connected
}
}
if (event_actions?.throttle) { if (event_actions?.throttle) {
// If throttle returns false, the events are not added to the queue. // If throttle returns false, the events are not added to the queue.
if (!throttle(combined_name, event_actions.throttle)) { if (!throttle(combined_name, event_actions.throttle)) {
@ -762,7 +778,7 @@ export const useEventLoop = (
window.onunhandledrejection = function (event) { window.onunhandledrejection = function (event) {
addEvents([ addEvents([
Event(`${exception_state_name}.handle_frontend_exception`, { Event(`${exception_state_name}.handle_frontend_exception`, {
stack: event.reason.stack, stack: event.reason?.stack,
component_stack: "", component_stack: "",
}), }),
]); ]);
@ -783,7 +799,7 @@ export const useEventLoop = (
connect( connect(
socket, socket,
dispatch, dispatch,
["websocket", "polling"], ["websocket"],
setConnectErrors, setConnectErrors,
client_storage client_storage
); );
@ -837,11 +853,20 @@ export const useEventLoop = (
} }
}; };
const change_complete = () => addEvents(onLoadInternalEvent()); const change_complete = () => addEvents(onLoadInternalEvent());
const change_error = () => {
// Remove cached error state from router for this page, otherwise the
// page will never send on_load events again.
if (router.components[router.pathname].error) {
delete router.components[router.pathname].error;
}
};
router.events.on("routeChangeStart", change_start); router.events.on("routeChangeStart", change_start);
router.events.on("routeChangeComplete", change_complete); router.events.on("routeChangeComplete", change_complete);
router.events.on("routeChangeError", change_error);
return () => { return () => {
router.events.off("routeChangeStart", change_start); router.events.off("routeChangeStart", change_start);
router.events.off("routeChangeComplete", change_complete); router.events.off("routeChangeComplete", change_complete);
router.events.off("routeChangeError", change_error);
}; };
}, [router]); }, [router]);

View File

@ -264,6 +264,7 @@ _MAPPING: dict = {
"experimental": ["_x"], "experimental": ["_x"],
"admin": ["AdminDash"], "admin": ["AdminDash"],
"app": ["App", "UploadFile"], "app": ["App", "UploadFile"],
"assets": ["asset"],
"base": ["Base"], "base": ["Base"],
"components.component": [ "components.component": [
"Component", "Component",
@ -330,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",

View File

@ -19,6 +19,7 @@ from . import vars as vars
from .admin import AdminDash as AdminDash from .admin import AdminDash as AdminDash
from .app import App as App from .app import App as App
from .app import UploadFile as UploadFile from .app import UploadFile as UploadFile
from .assets import asset as asset
from .base import Base as Base from .base import Base as Base
from .components import el as el from .components import el as el
from .components import lucide as lucide from .components import lucide as lucide
@ -185,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

View File

@ -73,6 +73,7 @@ from reflex.event import (
EventSpec, EventSpec,
EventType, EventType,
IndividualEventType, IndividualEventType,
get_hydrate_event,
window_alert, window_alert,
) )
from reflex.model import Model, get_db_status from reflex.model import Model, get_db_status
@ -362,6 +363,7 @@ 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,
transports=["websocket"],
) )
elif getattr(self.sio, "async_mode", "") != "asgi": elif getattr(self.sio, "async_mode", "") != "asgi":
raise RuntimeError( raise RuntimeError(
@ -466,7 +468,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,
@ -489,17 +491,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)
@ -515,7 +533,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"
) )
@ -632,10 +650,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,
@ -836,7 +858,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)
@ -850,10 +872,9 @@ class App(MiddlewareMixin, LifespanMixin):
if self.theme is not None: if self.theme is not None:
# If a theme component was provided, wrap the app with it # If a theme component was provided, wrap the app with it
app_wrappers[(20, "Theme")] = self.theme app_wrappers[(20, "Theme")] = self.theme
# Fix #2992 by removing the top-level appearance prop
self.theme.appearance = None
for route in self.unevaluated_pages: for route in self.unevaluated_pages:
console.debug(f"Evaluating page: {route}")
self._compile_page(route) self._compile_page(route)
# Add the optional endpoints (_upload) # Add the optional endpoints (_upload)
@ -947,12 +968,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 +986,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.
@ -1006,6 +1026,9 @@ class App(MiddlewareMixin, LifespanMixin):
compile_results.append( compile_results.append(
compiler.compile_contexts(self.state, self.theme), compiler.compile_contexts(self.state, self.theme),
) )
if self.theme is not None:
# Fix #2992 by removing the top-level appearance prop
self.theme.appearance = None
progress.advance(task) progress.advance(task)
# Compile the app root. # Compile the app root.
@ -1154,7 +1177,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(
@ -1257,6 +1280,21 @@ async def process(
) )
# Get the state for the session exclusively. # Get the state for the session exclusively.
async with app.state_manager.modify_state(event.substate_token) as state: async with app.state_manager.modify_state(event.substate_token) as state:
# When this is a brand new instance of the state, signal the
# frontend to reload before processing it.
if (
not state.router_data
and event.name != get_hydrate_event(state)
and app.event_namespace is not None
):
await asyncio.create_task(
app.event_namespace.emit(
"reload",
data=format.json_dumps(event),
to=sid,
)
)
return
# re-assign only when the value is different # re-assign only when the value is different
if state.router_data != router_data: if state.router_data != router_data:
# assignment will recurse into substates and force recalculation of # assignment will recurse into substates and force recalculation of
@ -1460,10 +1498,10 @@ class EventNamespace(AsyncNamespace):
app: App app: App
# Keep a mapping between socket ID and client token. # Keep a mapping between socket ID and client token.
token_to_sid: dict[str, str] = {} token_to_sid: dict[str, str]
# Keep a mapping between client token and socket ID. # Keep a mapping between client token and socket ID.
sid_to_token: dict[str, str] = {} sid_to_token: dict[str, str]
def __init__(self, namespace: str, app: App): def __init__(self, namespace: str, app: App):
"""Initialize the event namespace. """Initialize the event namespace.
@ -1473,6 +1511,8 @@ class EventNamespace(AsyncNamespace):
app: The application object. app: The application object.
""" """
super().__init__(namespace) super().__init__(namespace)
self.token_to_sid = {}
self.sid_to_token = {}
self.app = app self.app = app
def on_connect(self, sid, environ): def on_connect(self, sid, environ):

95
reflex/assets.py Normal file
View File

@ -0,0 +1,95 @@
"""Helper functions for adding assets to the app."""
import inspect
from pathlib import Path
from typing import Optional
from reflex import constants
from reflex.config import EnvironmentVariables
def asset(
path: str,
shared: bool = False,
subfolder: Optional[str] = None,
_stack_level: int = 1,
) -> str:
"""Add an asset to the app, either shared as a symlink or local.
Shared/External/Library assets:
Place the file next to your including python file.
Links the file to the app's external assets directory.
Example:
```python
# my_custom_javascript.js is a shared asset located next to the including python file.
rx.script(src=rx.asset(path="my_custom_javascript.js", shared=True))
rx.image(src=rx.asset(path="test_image.png", shared=True, subfolder="subfolder"))
```
Local/Internal assets:
Place the file in the app's assets/ directory.
Example:
```python
# local_image.png is an asset located in the app's assets/ directory. It cannot be shared when developing a library.
rx.image(src=rx.asset(path="local_image.png"))
```
Args:
path: The relative path of the asset.
subfolder: The directory to place the shared asset in.
shared: Whether to expose the asset to other apps.
_stack_level: The stack level to determine the calling file, defaults to
the immediate caller 1. When using rx.asset via a helper function,
increase this number for each helper function in the stack.
Raises:
FileNotFoundError: If the file does not exist.
ValueError: If subfolder is provided for local assets.
Returns:
The relative URL to the asset.
"""
assets = constants.Dirs.APP_ASSETS
backend_only = EnvironmentVariables.REFLEX_BACKEND_ONLY.get()
# Local asset handling
if not shared:
cwd = Path.cwd()
src_file_local = cwd / assets / path
if subfolder is not None:
raise ValueError("Subfolder is not supported for local assets.")
if not backend_only and not src_file_local.exists():
raise FileNotFoundError(f"File not found: {src_file_local}")
return f"/{path}"
# Shared asset handling
# Determine the file by which the asset is exposed.
frame = inspect.stack()[_stack_level]
calling_file = frame.filename
module = inspect.getmodule(frame[0])
assert module is not None
external = constants.Dirs.EXTERNAL_APP_ASSETS
src_file_shared = Path(calling_file).parent / path
if not src_file_shared.exists():
raise FileNotFoundError(f"File not found: {src_file_shared}")
caller_module_path = module.__name__.replace(".", "/")
subfolder = f"{caller_module_path}/{subfolder}" if subfolder else caller_module_path
# Symlink the asset to the app's external assets directory if running frontend.
if not backend_only:
# Create the asset folder in the currently compiling app.
asset_folder = Path.cwd() / assets / external / subfolder
asset_folder.mkdir(parents=True, exist_ok=True)
dst_file = asset_folder / path
if not dst_file.exists() and (
not dst_file.is_symlink() or dst_file.resolve() != src_file_shared.resolve()
):
dst_file.symlink_to(src_file_shared)
return f"/{external}/{subfolder}/{path}"

View File

@ -118,8 +118,8 @@ class Bare(Component):
def _render(self) -> Tag: def _render(self) -> Tag:
if isinstance(self.contents, Var): if isinstance(self.contents, Var):
if isinstance(self.contents, (BooleanVar, ObjectVar)): if isinstance(self.contents, (BooleanVar, ObjectVar)):
return Tagless(contents=f"{{{str(self.contents.to_string())}}}") return Tagless(contents=f"{{{self.contents.to_string()!s}}}")
return Tagless(contents=f"{{{str(self.contents)}}}") return Tagless(contents=f"{{{self.contents!s}}}")
return Tagless(contents=str(self.contents)) return Tagless(contents=str(self.contents))
def _get_vars(self, include_children: bool = False) -> Iterator[Var]: def _get_vars(self, include_children: bool = False) -> Iterator[Var]:

View File

@ -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:
@ -186,6 +186,23 @@ ComponentStyle = Dict[
ComponentChild = Union[types.PrimitiveType, Var, BaseComponent] ComponentChild = Union[types.PrimitiveType, Var, BaseComponent]
def satisfies_type_hint(obj: Any, type_hint: Any) -> bool:
"""Check if an object satisfies a type hint.
Args:
obj: The object to check.
type_hint: The type hint to check against.
Returns:
Whether the object satisfies the type hint.
"""
if isinstance(obj, LiteralVar):
return types._isinstance(obj._var_value, type_hint)
if isinstance(obj, Var):
return types._issubclass(obj._var_type, type_hint)
return types._isinstance(obj, type_hint)
class Component(BaseComponent, ABC): class Component(BaseComponent, ABC):
"""A component with style, event trigger and other props.""" """A component with style, event trigger and other props."""
@ -460,8 +477,7 @@ class Component(BaseComponent, ABC):
) )
) or ( ) or (
# Else just check if the passed var type is valid. # Else just check if the passed var type is valid.
not passed_types not passed_types and not satisfies_type_hint(value, expected_type)
and not types._issubclass(passed_type, expected_type, value)
): ):
value_name = value._js_expr if isinstance(value, Var) else value value_name = value._js_expr if isinstance(value, Var) else value
@ -567,7 +583,7 @@ class Component(BaseComponent, ABC):
return self._create_event_chain(args_spec, value.guess_type(), key=key) return self._create_event_chain(args_spec, value.guess_type(), key=key)
else: else:
raise ValueError( raise ValueError(
f"Invalid event chain: {str(value)} of type {value._var_type}" f"Invalid event chain: {value!s} of type {value._var_type}"
) )
elif isinstance(value, EventChain): elif isinstance(value, EventChain):
# Trust that the caller knows what they're doing passing an EventChain directly # Trust that the caller knows what they're doing passing an EventChain directly
@ -637,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,
@ -1095,7 +1110,7 @@ class Component(BaseComponent, ABC):
vars.append(prop_var) vars.append(prop_var)
# Style keeps track of its own VarData instance, so embed in a temp Var that is yielded. # Style keeps track of its own VarData instance, so embed in a temp Var that is yielded.
if isinstance(self.style, dict) and self.style or isinstance(self.style, Var): if (isinstance(self.style, dict) and self.style) or isinstance(self.style, Var):
vars.append( vars.append(
Var( Var(
_js_expr="style", _js_expr="style",
@ -1450,7 +1465,9 @@ class Component(BaseComponent, ABC):
""" """
ref = self.get_ref() ref = self.get_ref()
if ref is not None: if ref is not None:
return f"const {ref} = useRef(null); {str(Var(_js_expr=ref)._as_ref())} = {ref};" return (
f"const {ref} = useRef(null); {Var(_js_expr=ref)._as_ref()!s} = {ref};"
)
def _get_vars_hooks(self) -> dict[str, None]: def _get_vars_hooks(self) -> dict[str, None]:
"""Get the hooks required by vars referenced in this component. """Get the hooks required by vars referenced in this component.
@ -1904,6 +1921,11 @@ memo = custom_component
class NoSSRComponent(Component): class NoSSRComponent(Component):
"""A dynamic component that is not rendered on the server.""" """A dynamic component that is not rendered on the server."""
def _get_import_name(self) -> None | str:
if not self.library:
return None
return f"${self.library}" if self.library.startswith("/") else self.library
def _get_imports(self) -> ParsedImportDict: def _get_imports(self) -> ParsedImportDict:
"""Get the imports for the component. """Get the imports for the component.
@ -1917,8 +1939,9 @@ class NoSSRComponent(Component):
_imports = super()._get_imports() _imports = super()._get_imports()
# Do NOT import the main library/tag statically. # Do NOT import the main library/tag statically.
if self.library is not None: import_name = self._get_import_name()
_imports[self.library] = [ if import_name is not None:
_imports[import_name] = [
imports.ImportVar( imports.ImportVar(
tag=None, tag=None,
render=False, render=False,
@ -1936,10 +1959,10 @@ class NoSSRComponent(Component):
opts_fragment = ", { ssr: false });" opts_fragment = ", { ssr: false });"
# extract the correct import name from library name # extract the correct import name from library name
if self.library is None: base_import_name = self._get_import_name()
if base_import_name is None:
raise ValueError("Undefined library for NoSSRComponent") raise ValueError("Undefined library for NoSSRComponent")
import_name = format.format_library_name(base_import_name)
import_name = format.format_library_name(self.library)
library_import = f"const {self.alias if self.alias else self.tag} = dynamic(() => import('{import_name}')" library_import = f"const {self.alias if self.alias else self.tag} = dynamic(() => import('{import_name}')"
mod_import = ( mod_import = (
@ -2538,7 +2561,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(

View File

@ -109,7 +109,7 @@ class ConnectionToaster(Toaster):
) )
individual_hooks = [ individual_hooks = [
f"const toast_props = {str(LiteralVar.create(props))};", f"const toast_props = {LiteralVar.create(props)!s};",
"const [userDismissed, setUserDismissed] = useState(false);", "const [userDismissed, setUserDismissed] = useState(false);",
FunctionStringVar( FunctionStringVar(
"useEffect", "useEffect",
@ -124,7 +124,7 @@ class ConnectionToaster(Toaster):
Var( Var(
_js_expr=f""" _js_expr=f"""
() => {{ () => {{
if ({str(has_too_many_connection_errors)}) {{ if ({has_too_many_connection_errors!s}) {{
if (!userDismissed) {{ if (!userDismissed) {{
toast.error( toast.error(
`Cannot connect to server: ${{{connection_error}}}.`, `Cannot connect to server: ${{{connection_error}}}.`,

View File

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

View File

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

View File

@ -51,7 +51,7 @@ class Clipboard(Fragment):
return super().create(*children, **props) return super().create(*children, **props)
def _exclude_props(self) -> list[str]: def _exclude_props(self) -> list[str]:
return super()._exclude_props() + ["on_paste", "on_paste_event_actions"] return [*super()._exclude_props(), "on_paste", "on_paste_event_actions"]
def _render(self) -> Tag: def _render(self) -> Tag:
tag = super()._render() tag = super()._render()

View File

@ -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(
@ -94,7 +94,7 @@ class Cond(MemoizationLeaf):
).set( ).set(
props=tag.format_props(), props=tag.format_props(),
), ),
cond_state=f"isTrue({str(self.cond)})", cond_state=f"isTrue({self.cond!s})",
) )
def add_imports(self) -> ImportDict: def add_imports(self) -> ImportDict:

View File

@ -54,7 +54,7 @@ class Foreach(Component):
iterable = LiteralVar.create(iterable) iterable = LiteralVar.create(iterable)
if iterable._var_type == Any: if iterable._var_type == Any:
raise ForeachVarError( raise ForeachVarError(
f"Could not foreach over var `{str(iterable)}` of type Any. " f"Could not foreach over var `{iterable!s}` of type Any. "
"(If you are trying to foreach over a state var, add a type annotation to the var). " "(If you are trying to foreach over a state var, add a type annotation to the var). "
"See https://reflex.dev/docs/library/dynamic-rendering/foreach/" "See https://reflex.dev/docs/library/dynamic-rendering/foreach/"
) )

View File

@ -61,7 +61,7 @@ def upload_file(id_: str = DEFAULT_UPLOAD_ID) -> Var:
id_var = LiteralStringVar.create(id_) id_var = LiteralStringVar.create(id_)
var_name = f"""e => setFilesById(filesById => {{ var_name = f"""e => setFilesById(filesById => {{
const updatedFilesById = Object.assign({{}}, filesById); const updatedFilesById = Object.assign({{}}, filesById);
updatedFilesById[{str(id_var)}] = e; updatedFilesById[{id_var!s}] = e;
return updatedFilesById; return updatedFilesById;
}}) }})
""" """
@ -87,7 +87,7 @@ def selected_files(id_: str = DEFAULT_UPLOAD_ID) -> Var:
""" """
id_var = LiteralStringVar.create(id_) id_var = LiteralStringVar.create(id_)
return Var( return Var(
_js_expr=f"(filesById[{str(id_var)}] ? filesById[{str(id_var)}].map((f) => (f.path || f.name)) : [])", _js_expr=f"(filesById[{id_var!s}] ? filesById[{id_var!s}].map((f) => (f.path || f.name)) : [])",
_var_type=List[str], _var_type=List[str],
_var_data=VarData.merge( _var_data=VarData.merge(
upload_files_context_var_data, id_var._get_all_var_data() upload_files_context_var_data, id_var._get_all_var_data()
@ -120,9 +120,7 @@ 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( return run_script(f"upload_controllers[{LiteralVar.create(upload_id)!s}]?.abort()")
f"upload_controllers[{str(LiteralVar.create(upload_id))}]?.abort()"
)
def get_upload_dir() -> Path: def get_upload_dir() -> Path:
@ -293,13 +291,15 @@ class Upload(MemoizationLeaf):
format.to_camel_case(key): value for key, value in upload_props.items() format.to_camel_case(key): value for key, value in upload_props.items()
} }
use_dropzone_arguments = { use_dropzone_arguments = Var.create(
{
"onDrop": event_var, "onDrop": event_var,
**upload_props, **upload_props,
} }
)
left_side = f"const {{getRootProps: {root_props_unique_name}, getInputProps: {input_props_unique_name}}} " left_side = f"const {{getRootProps: {root_props_unique_name}, getInputProps: {input_props_unique_name}}} "
right_side = f"useDropzone({str(Var.create(use_dropzone_arguments))})" right_side = f"useDropzone({use_dropzone_arguments!s})"
var_data = VarData.merge( var_data = VarData.merge(
VarData( VarData(
@ -307,6 +307,7 @@ class Upload(MemoizationLeaf):
hooks={Hooks.EVENTS: None}, hooks={Hooks.EVENTS: None},
), ),
event_var._get_all_var_data(), event_var._get_all_var_data(),
use_dropzone_arguments._get_all_var_data(),
VarData( VarData(
hooks={ hooks={
callback_str: None, callback_str: None,

View File

@ -382,7 +382,7 @@ for theme_name in dir(Theme):
class CodeBlock(Component, MarkdownComponentMap): class CodeBlock(Component, MarkdownComponentMap):
"""A code block.""" """A code block."""
library = "react-syntax-highlighter@15.6.1" library = "react-syntax-highlighter@15.6.0"
tag = "PrismAsyncLight" tag = "PrismAsyncLight"
@ -519,13 +519,13 @@ class CodeBlock(Component, MarkdownComponentMap):
The hook to register the language. The hook to register the language.
""" """
return f""" return f"""
if ({str(_LANGUAGE)}) {{ if ({_LANGUAGE!s}) {{
(async () => {{ (async () => {{
try {{ try {{
const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{str(_LANGUAGE)}}}`); const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{_LANGUAGE!s}}}`);
SyntaxHighlighter.registerLanguage({str(_LANGUAGE)}, module.default); SyntaxHighlighter.registerLanguage({_LANGUAGE!s}, module.default);
}} catch (error) {{ }} catch (error) {{
console.error(`Error importing language module for ${{{str(_LANGUAGE)}}}:`, error); console.error(`Error importing language module for ${{{_LANGUAGE!s}}}:`, error);
}} }}
}})(); }})();
}} }}
@ -547,7 +547,7 @@ class CodeBlock(Component, MarkdownComponentMap):
The hooks for the component. The hooks for the component.
""" """
return [ return [
f"const {str(_LANGUAGE)} = {str(self.language)}", f"const {_LANGUAGE!s} = {self.language!s}",
self._get_language_registration_hook(), self._get_language_registration_hook(),
] ]

View File

@ -51,27 +51,6 @@ class GridColumnIcons(Enum):
VideoUri = "video_uri" VideoUri = "video_uri"
# @serializer
# def serialize_gridcolumn_icon(icon: GridColumnIcons) -> str:
# """Serialize grid column icon.
# Args:
# icon: the Icon to serialize.
# Returns:
# The serialized value.
# """
# return "prefix" + str(icon)
# class DataEditorColumn(Base):
# """Column."""
# title: str
# id: Optional[str] = None
# type_: str = "str"
class DataEditorTheme(Base): class DataEditorTheme(Base):
"""The theme for the DataEditor component.""" """The theme for the DataEditor component."""
@ -229,7 +208,7 @@ class DataEditor(NoSSRComponent):
header_height: Var[int] header_height: Var[int]
# Additional header icons: # Additional header icons:
# header_icons: Var[Any] # (TODO: must be a map of name: svg) # header_icons: Var[Any] # (TODO: must be a map of name: svg) #noqa: ERA001
# The maximum width a column can be automatically sized to. # The maximum width a column can be automatically sized to.
max_column_auto_width: Var[int] max_column_auto_width: Var[int]
@ -406,10 +385,8 @@ class DataEditor(NoSSRComponent):
props["rows"] = data.length() if isinstance(data, Var) else len(data) props["rows"] = data.length() if isinstance(data, Var) else len(data)
if not isinstance(columns, Var) and len(columns): if not isinstance(columns, Var) and len(columns):
if ( if types.is_dataframe(type(data)) or (
types.is_dataframe(type(data)) isinstance(data, Var) and types.is_dataframe(data._var_type)
or isinstance(data, Var)
and types.is_dataframe(data._var_type)
): ):
raise ValueError( raise ValueError(
"Cannot pass in both a pandas dataframe and columns to the data_editor component." "Cannot pass in both a pandas dataframe and columns to the data_editor component."

View File

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

View File

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

View File

@ -173,7 +173,7 @@ def load_dynamic_serializer():
f"const [{unique_var_name}, set_{unique_var_name}] = useState(null);": None, f"const [{unique_var_name}, set_{unique_var_name}] = useState(null);": None,
"useEffect(() => {" "useEffect(() => {"
"let isMounted = true;" "let isMounted = true;"
f"evalReactComponent({str(js_string)})" f"evalReactComponent({js_string!s})"
".then((component) => {" ".then((component) => {"
"if (isMounted) {" "if (isMounted) {"
f"set_{unique_var_name}(component);" f"set_{unique_var_name}(component);"
@ -183,7 +183,7 @@ def load_dynamic_serializer():
"isMounted = false;" "isMounted = false;"
"};" "};"
"}" "}"
f", [{str(js_string)}]);": None, f", [{js_string!s}]);": None,
}, },
), ),
), ),

View File

@ -5,6 +5,7 @@
from . import elements as elements from . import elements as elements
from .elements.forms import Button as Button from .elements.forms import Button as Button
from .elements.forms import Datalist as Datalist
from .elements.forms import Fieldset as Fieldset from .elements.forms import Fieldset as Fieldset
from .elements.forms import Form as Form from .elements.forms import Form as Form
from .elements.forms import Input as Input from .elements.forms import Input as Input
@ -18,6 +19,7 @@ from .elements.forms import Progress as Progress
from .elements.forms import Select as Select from .elements.forms import Select as Select
from .elements.forms import Textarea as Textarea from .elements.forms import Textarea as Textarea
from .elements.forms import button as button from .elements.forms import button as button
from .elements.forms import datalist as datalist
from .elements.forms import fieldset as fieldset from .elements.forms import fieldset as fieldset
from .elements.forms import form as form from .elements.forms import form as form
from .elements.forms import input as input from .elements.forms import input as input

View File

@ -7,6 +7,7 @@ from reflex.utils import lazy_loader
_MAPPING = { _MAPPING = {
"forms": [ "forms": [
"button", "button",
"datalist",
"fieldset", "fieldset",
"form", "form",
"input", "input",

View File

@ -4,6 +4,7 @@
# ------------------------------------------------------ # ------------------------------------------------------
from .forms import Button as Button from .forms import Button as Button
from .forms import Datalist as Datalist
from .forms import Fieldset as Fieldset from .forms import Fieldset as Fieldset
from .forms import Form as Form from .forms import Form as Form
from .forms import Input as Input from .forms import Input as Input
@ -17,6 +18,7 @@ from .forms import Progress as Progress
from .forms import Select as Select from .forms import Select as Select
from .forms import Textarea as Textarea from .forms import Textarea as Textarea
from .forms import button as button from .forms import button as button
from .forms import datalist as datalist
from .forms import fieldset as fieldset from .forms import fieldset as fieldset
from .forms import form as form from .forms import form as form
from .forms import input as input from .forms import input as input
@ -226,6 +228,7 @@ from .typography import ul as ul
_MAPPING = { _MAPPING = {
"forms": [ "forms": [
"button", "button",
"datalist",
"fieldset", "fieldset",
"form", "form",
"input", "input",

View File

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

View File

@ -1,4 +1,4 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" """Forms classes."""
from __future__ import annotations from __future__ import annotations
@ -84,7 +84,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):
@ -241,16 +240,15 @@ class Form(BaseHTML):
if ref.startswith("refs_"): if ref.startswith("refs_"):
ref_var = Var(_js_expr=ref[:-3])._as_ref() ref_var = Var(_js_expr=ref[:-3])._as_ref()
form_refs[ref[len("refs_") : -3]] = Var( form_refs[ref[len("refs_") : -3]] = Var(
_js_expr=f"getRefValues({str(ref_var)})", _js_expr=f"getRefValues({ref_var!s})",
_var_data=VarData.merge(ref_var._get_all_var_data()), _var_data=VarData.merge(ref_var._get_all_var_data()),
) )
else: else:
ref_var = Var(_js_expr=ref)._as_ref() ref_var = Var(_js_expr=ref)._as_ref()
form_refs[ref[4:]] = Var( form_refs[ref[4:]] = Var(
_js_expr=f"getRefValue({str(ref_var)})", _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]:
@ -258,7 +256,8 @@ class Form(BaseHTML):
yield from self._get_form_refs().values() yield from self._get_form_refs().values()
def _exclude_props(self) -> list[str]: def _exclude_props(self) -> list[str]:
return super()._exclude_props() + [ return [
*super()._exclude_props(),
"reset_on_submit", "reset_on_submit",
"handle_submit_unique_name", "handle_submit_unique_name",
] ]
@ -400,7 +399,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):
@ -570,6 +568,9 @@ class Textarea(BaseHTML):
# Visible width of the text control, in average character widths # Visible width of the text control, in average character widths
cols: Var[Union[str, int, bool]] cols: Var[Union[str, int, bool]]
# The default value of the textarea when initially rendered
default_value: Var[str]
# Name part of the textarea to submit in 'dir' and 'name' pair when form is submitted # Name part of the textarea to submit in 'dir' and 'name' pair when form is submitted
dirname: Var[Union[str, int, bool]] dirname: Var[Union[str, int, bool]]
@ -649,19 +650,20 @@ class Textarea(BaseHTML):
"Cannot combine `enter_key_submit` with `on_key_down`.", "Cannot combine `enter_key_submit` with `on_key_down`.",
) )
custom_attrs["on_key_down"] = Var( custom_attrs["on_key_down"] = Var(
_js_expr=f"(e) => enterKeySubmitOnKeyDown(e, {str(enter_key_submit)})", _js_expr=f"(e) => enterKeySubmitOnKeyDown(e, {enter_key_submit!s})",
_var_data=VarData.merge(enter_key_submit._get_all_var_data()), _var_data=VarData.merge(enter_key_submit._get_all_var_data()),
) )
if auto_height is not None: if auto_height is not None:
auto_height = Var.create(auto_height) auto_height = Var.create(auto_height)
custom_attrs["on_input"] = Var( custom_attrs["on_input"] = Var(
_js_expr=f"(e) => autoHeightOnInput(e, {str(auto_height)})", _js_expr=f"(e) => autoHeightOnInput(e, {auto_height!s})",
_var_data=VarData.merge(auto_height._get_all_var_data()), _var_data=VarData.merge(auto_height._get_all_var_data()),
) )
return super().create(*children, **props) return super().create(*children, **props)
def _exclude_props(self) -> list[str]: def _exclude_props(self) -> list[str]:
return super()._exclude_props() + [ return [
*super()._exclude_props(),
"auto_height", "auto_height",
"enter_key_submit", "enter_key_submit",
] ]
@ -681,6 +683,7 @@ class Textarea(BaseHTML):
button = Button.create button = Button.create
datalist = Datalist.create
fieldset = Fieldset.create fieldset = Fieldset.create
form = Form.create form = Form.create
input = Input.create input = Input.create

View File

@ -189,7 +189,7 @@ class Datalist(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element. access_key: Provides a hint for generating a keyboard shortcut for the current element.
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
content_editable: Indicates whether the element's content is editable. content_editable: Indicates whether the element's content is editable.
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
@ -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.
@ -1350,6 +1350,7 @@ class Textarea(BaseHTML):
auto_focus: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, auto_focus: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
auto_height: Optional[Union[Var[bool], bool]] = None, auto_height: Optional[Union[Var[bool], bool]] = None,
cols: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, cols: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
default_value: Optional[Union[Var[str], str]] = None,
dirname: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, dirname: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
disabled: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, disabled: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
enter_key_submit: Optional[Union[Var[bool], bool]] = None, enter_key_submit: Optional[Union[Var[bool], bool]] = None,
@ -1439,6 +1440,7 @@ class Textarea(BaseHTML):
auto_focus: Automatically focuses the textarea when the page loads auto_focus: Automatically focuses the textarea when the page loads
auto_height: Automatically fit the content height to the text (use min-height with this prop) auto_height: Automatically fit the content height to the text (use min-height with this prop)
cols: Visible width of the text control, in average character widths cols: Visible width of the text control, in average character widths
default_value: The default value of the textarea when initially rendered
dirname: Name part of the textarea to submit in 'dir' and 'name' pair when form is submitted dirname: Name part of the textarea to submit in 'dir' and 'name' pair when form is submitted
disabled: Disables the textarea disabled: Disables the textarea
enter_key_submit: Enter key submits form (shift-enter adds new line) enter_key_submit: Enter key submits form (shift-enter adds new line)
@ -1490,6 +1492,7 @@ class Textarea(BaseHTML):
... ...
button = Button.create button = Button.create
datalist = Datalist.create
fieldset = Fieldset.create fieldset = Fieldset.create
form = Form.create form = Form.create
input = Input.create input = Input.create

View File

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

View File

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

View File

@ -340,7 +340,6 @@ class Img(BaseHTML):
Returns: Returns:
The component. The component.
""" """
... ...
@ -987,7 +986,7 @@ class Picture(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element. access_key: Provides a hint for generating a keyboard shortcut for the current element.
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
content_editable: Indicates whether the element's content is editable. content_editable: Indicates whether the element's content is editable.
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
@ -1073,7 +1072,7 @@ class Portal(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element. access_key: Provides a hint for generating a keyboard shortcut for the current element.
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
content_editable: Indicates whether the element's content is editable. content_editable: Indicates whether the element's content is editable.
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.

View File

@ -1,4 +1,4 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" """Metadata classes."""
from typing import List, Union from typing import List, Union
@ -8,7 +8,7 @@ from reflex.vars.base import Var
from .base import BaseHTML from .base import BaseHTML
class Base(BaseHTML): # noqa: E742 class Base(BaseHTML):
"""Display the base element.""" """Display the base element."""
tag = "base" tag = "base"
@ -18,13 +18,13 @@ class Base(BaseHTML): # noqa: E742
target: Var[Union[str, int, bool]] target: Var[Union[str, int, bool]]
class Head(BaseHTML): # noqa: E742 class Head(BaseHTML):
"""Display the head element.""" """Display the head element."""
tag = "head" tag = "head"
class Link(BaseHTML): # noqa: E742 class Link(BaseHTML):
"""Display the link element.""" """Display the link element."""
tag = "link" tag = "link"
@ -75,14 +75,14 @@ class Meta(BaseHTML): # Inherits common attributes from BaseHTML
name: Var[Union[str, int, bool]] name: Var[Union[str, int, bool]]
class Title(Element): # noqa: E742 class Title(Element):
"""Display the title element.""" """Display the title element."""
tag = "title" tag = "title"
# Had to be named with an underscore so it doesnt conflict with reflex.style Style in pyi # Had to be named with an underscore so it doesnt conflict with reflex.style Style in pyi
class StyleEl(Element): # noqa: E742 class StyleEl(Element):
"""Display the style element.""" """Display the style element."""
tag = "style" tag = "style"

View File

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

View File

@ -244,7 +244,7 @@ class Summary(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited; used as a summary or caption for a <details> element Provides a hint for generating a keyboard shortcut for the current element. access_key: Provides a hint for generating a keyboard shortcut for the current element.
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
content_editable: Indicates whether the element's content is editable. content_editable: Indicates whether the element's content is editable.
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
@ -330,7 +330,7 @@ class Slot(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited; used as a placeholder inside a web component Provides a hint for generating a keyboard shortcut for the current element. access_key: Provides a hint for generating a keyboard shortcut for the current element.
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
content_editable: Indicates whether the element's content is editable. content_editable: Indicates whether the element's content is editable.
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
@ -416,7 +416,7 @@ class Template(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited; used for declaring fragments of HTML that can be cloned and inserted in the document Provides a hint for generating a keyboard shortcut for the current element. access_key: Provides a hint for generating a keyboard shortcut for the current element.
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
content_editable: Indicates whether the element's content is editable. content_editable: Indicates whether the element's content is editable.
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
@ -502,7 +502,7 @@ class Math(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited; used for displaying mathematical expressions Provides a hint for generating a keyboard shortcut for the current element. access_key: Provides a hint for generating a keyboard shortcut for the current element.
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
content_editable: Indicates whether the element's content is editable. content_editable: Indicates whether the element's content is editable.
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.

View File

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

View File

@ -154,7 +154,7 @@ class Noscript(BaseHTML):
Args: Args:
*children: The children of the component. *children: The children of the component.
access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element. access_key: Provides a hint for generating a keyboard shortcut for the current element.
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
content_editable: Indicates whether the element's content is editable. content_editable: Indicates whether the element's content is editable.
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu. context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.

View File

@ -1,93 +1,93 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" """Sectioning classes."""
from .base import BaseHTML from .base import BaseHTML
class Body(BaseHTML): # noqa: E742 class Body(BaseHTML):
"""Display the body element.""" """Display the body element."""
tag = "body" tag = "body"
class Address(BaseHTML): # noqa: E742 class Address(BaseHTML):
"""Display the address element.""" """Display the address element."""
tag = "address" tag = "address"
class Article(BaseHTML): # noqa: E742 class Article(BaseHTML):
"""Display the article element.""" """Display the article element."""
tag = "article" tag = "article"
class Aside(BaseHTML): # noqa: E742 class Aside(BaseHTML):
"""Display the aside element.""" """Display the aside element."""
tag = "aside" tag = "aside"
class Footer(BaseHTML): # noqa: E742 class Footer(BaseHTML):
"""Display the footer element.""" """Display the footer element."""
tag = "footer" tag = "footer"
class Header(BaseHTML): # noqa: E742 class Header(BaseHTML):
"""Display the header element.""" """Display the header element."""
tag = "header" tag = "header"
class H1(BaseHTML): # noqa: E742 class H1(BaseHTML):
"""Display the h1 element.""" """Display the h1 element."""
tag = "h1" tag = "h1"
class H2(BaseHTML): # noqa: E742 class H2(BaseHTML):
"""Display the h1 element.""" """Display the h1 element."""
tag = "h2" tag = "h2"
class H3(BaseHTML): # noqa: E742 class H3(BaseHTML):
"""Display the h1 element.""" """Display the h1 element."""
tag = "h3" tag = "h3"
class H4(BaseHTML): # noqa: E742 class H4(BaseHTML):
"""Display the h1 element.""" """Display the h1 element."""
tag = "h4" tag = "h4"
class H5(BaseHTML): # noqa: E742 class H5(BaseHTML):
"""Display the h1 element.""" """Display the h1 element."""
tag = "h5" tag = "h5"
class H6(BaseHTML): # noqa: E742 class H6(BaseHTML):
"""Display the h1 element.""" """Display the h1 element."""
tag = "h6" tag = "h6"
class Main(BaseHTML): # noqa: E742 class Main(BaseHTML):
"""Display the main element.""" """Display the main element."""
tag = "main" tag = "main"
class Nav(BaseHTML): # noqa: E742 class Nav(BaseHTML):
"""Display the nav element.""" """Display the nav element."""
tag = "nav" tag = "nav"
class Section(BaseHTML): # noqa: E742 class Section(BaseHTML):
"""Display the section element.""" """Display the section element."""
tag = "section" tag = "section"

View File

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

View File

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

View File

@ -283,7 +283,7 @@ class Markdown(Component):
# Format the code to handle inline and block code. # Format the code to handle inline and block code.
formatted_code = f""" formatted_code = f"""
const match = (className || '').match(/language-(?<lang>.*)/); const match = (className || '').match(/language-(?<lang>.*)/);
const {str(_LANGUAGE)} = match ? match[1] : ''; const {_LANGUAGE!s} = match ? match[1] : '';
{codeblock_custom_code}; {codeblock_custom_code};
return inline ? ( return inline ? (
{self.format_component("code")} {self.format_component("code")}
@ -340,7 +340,7 @@ const {str(_LANGUAGE)} = match ? match[1] : '';
# If the children are set as a prop, don't pass them as children. # If the children are set as a prop, don't pass them as children.
children_prop = props.pop("children", None) children_prop = props.pop("children", None)
if children_prop is not None: if children_prop is not None:
special_props.append(Var(_js_expr=f"children={{{str(children_prop)}}}")) special_props.append(Var(_js_expr=f"children={{{children_prop!s}}}"))
children = [] children = []
# Get the component. # Get the component.
component = self.component_map[tag](*children, **props).set( component = self.component_map[tag](*children, **props).set(
@ -429,7 +429,7 @@ const {str(_LANGUAGE)} = match ? match[1] : '';
function {self._get_component_map_name()} () {{ function {self._get_component_map_name()} () {{
{formatted_hooks} {formatted_hooks}
return ( return (
{str(LiteralVar.create(self.format_component_map()))} {LiteralVar.create(self.format_component_map())!s}
) )
}} }}
""" """

View File

@ -1,7 +1,8 @@
"""Moment component for humanized date rendering.""" """Moment component for humanized date rendering."""
import dataclasses import dataclasses
from typing import List, Optional from datetime import date, datetime, time, timedelta
from typing import List, Optional, Union
from reflex.components.component import NoSSRComponent from reflex.components.component import NoSSRComponent
from reflex.event import EventHandler, passthrough_event_spec from reflex.event import EventHandler, passthrough_event_spec
@ -19,7 +20,7 @@ class MomentDelta:
weeks: Optional[int] = dataclasses.field(default=None) weeks: Optional[int] = dataclasses.field(default=None)
days: Optional[int] = dataclasses.field(default=None) days: Optional[int] = dataclasses.field(default=None)
hours: Optional[int] = dataclasses.field(default=None) hours: Optional[int] = dataclasses.field(default=None)
minutess: Optional[int] = dataclasses.field(default=None) minutes: Optional[int] = dataclasses.field(default=None)
seconds: Optional[int] = dataclasses.field(default=None) seconds: Optional[int] = dataclasses.field(default=None)
milliseconds: Optional[int] = dataclasses.field(default=None) milliseconds: Optional[int] = dataclasses.field(default=None)
@ -78,7 +79,7 @@ class Moment(NoSSRComponent):
duration: Var[str] duration: Var[str]
# The date to display (also work if passed as children). # The date to display (also work if passed as children).
date: Var[str] date: Var[Union[str, datetime, date, time, timedelta]]
# Shows the duration (elapsed time) between now and the provided datetime. # Shows the duration (elapsed time) between now and the provided datetime.
duration_from_now: Var[bool] duration_from_now: Var[bool]

View File

@ -4,6 +4,7 @@
# This file was generated by `reflex/utils/pyi_generator.py`! # This file was generated by `reflex/utils/pyi_generator.py`!
# ------------------------------------------------------ # ------------------------------------------------------
import dataclasses import dataclasses
from datetime import date, datetime, time, timedelta
from typing import Any, Dict, Optional, Union, overload from typing import Any, Dict, Optional, Union, overload
from reflex.components.component import NoSSRComponent from reflex.components.component import NoSSRComponent
@ -20,7 +21,7 @@ class MomentDelta:
weeks: Optional[int] weeks: Optional[int]
days: Optional[int] days: Optional[int]
hours: Optional[int] hours: Optional[int]
minutess: Optional[int] minutes: Optional[int]
seconds: Optional[int] seconds: Optional[int]
milliseconds: Optional[int] milliseconds: Optional[int]
@ -46,7 +47,16 @@ class Moment(NoSSRComponent):
decimal: Optional[Union[Var[bool], bool]] = None, decimal: Optional[Union[Var[bool], bool]] = None,
unit: Optional[Union[Var[str], str]] = None, unit: Optional[Union[Var[str], str]] = None,
duration: Optional[Union[Var[str], str]] = None, duration: Optional[Union[Var[str], str]] = None,
date: Optional[Union[Var[str], str]] = None, date: Optional[
Union[
Var[Union[date, datetime, str, time, timedelta]],
date,
datetime,
str,
time,
timedelta,
]
] = None,
duration_from_now: Optional[Union[Var[bool], bool]] = None, duration_from_now: Optional[Union[Var[bool], bool]] = None,
unix: Optional[Union[Var[bool], bool]] = None, unix: Optional[Union[Var[bool], bool]] = None,
local: Optional[Union[Var[bool], bool]] = None, local: Optional[Union[Var[bool], bool]] = None,

View File

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

View File

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

View File

@ -255,7 +255,7 @@ const extractPoints = (points) => {
def _render(self): def _render(self):
tag = super()._render() tag = super()._render()
figure = self.data.to(dict) figure = self.data.to(dict) if self.data is not None else Var.create({})
merge_dicts = [] # Data will be merged and spread from these dict Vars merge_dicts = [] # Data will be merged and spread from these dict Vars
if self.layout is not None: if self.layout is not None:
# Why is this not a literal dict? Great question... it didn't work # Why is this not a literal dict? Great question... it didn't work
@ -270,11 +270,11 @@ const extractPoints = (points) => {
tag.special_props.append( tag.special_props.append(
# Merge all dictionaries and spread the result over props. # Merge all dictionaries and spread the result over props.
Var( Var(
_js_expr=f"{{...mergician({str(figure)}," _js_expr=f"{{...mergician({figure!s},"
f"{','.join(str(md) for md in merge_dicts)})}}", f"{','.join(str(md) for md in merge_dicts)})}}",
), ),
) )
else: else:
# Spread the figure dict over props, nothing to merge. # Spread the figure dict over props, nothing to merge.
tag.special_props.append(Var(_js_expr=f"{{...{str(figure)}}}")) tag.special_props.append(Var(_js_expr=f"{{...{figure!s}}}"))
return tag return tag

View File

@ -129,7 +129,8 @@ class AccordionRoot(AccordionComponent):
on_value_change: EventHandler[on_value_change] on_value_change: EventHandler[on_value_change]
def _exclude_props(self) -> list[str]: def _exclude_props(self) -> list[str]:
return super()._exclude_props() + [ return [
*super()._exclude_props(),
"radius", "radius",
"duration", "duration",
"easing", "easing",

View File

@ -11,7 +11,6 @@ from reflex.components.radix.primitives.base import RadixPrimitiveComponent
from reflex.components.radix.themes.base import Theme from reflex.components.radix.themes.base import Theme
from reflex.components.radix.themes.layout.flex import Flex from reflex.components.radix.themes.layout.flex import Flex
from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec
from reflex.utils import console
from reflex.vars.base import Var from reflex.vars.base import Var
@ -140,19 +139,19 @@ class DrawerContent(DrawerComponent):
base_style.update(style) base_style.update(style)
return {"css": base_style} return {"css": base_style}
# Fired when the drawer content is opened. Deprecated. # Fired when the drawer content is opened.
on_open_auto_focus: EventHandler[no_args_event_spec] on_open_auto_focus: EventHandler[no_args_event_spec]
# Fired when the drawer content is closed. Deprecated. # Fired when the drawer content is closed.
on_close_auto_focus: EventHandler[no_args_event_spec] on_close_auto_focus: EventHandler[no_args_event_spec]
# Fired when the escape key is pressed. Deprecated. # Fired when the escape key is pressed.
on_escape_key_down: EventHandler[no_args_event_spec] on_escape_key_down: EventHandler[no_args_event_spec]
# Fired when the pointer is down outside the drawer content. Deprecated. # Fired when the pointer is down outside the drawer content.
on_pointer_down_outside: EventHandler[no_args_event_spec] on_pointer_down_outside: EventHandler[no_args_event_spec]
# Fired when interacting outside the drawer content. Deprecated. # Fired when interacting outside the drawer content.
on_interact_outside: EventHandler[no_args_event_spec] on_interact_outside: EventHandler[no_args_event_spec]
@classmethod @classmethod
@ -170,23 +169,6 @@ class DrawerContent(DrawerComponent):
Returns: Returns:
The drawer content. The drawer content.
""" """
deprecated_properties = [
"on_open_auto_focus",
"on_close_auto_focus",
"on_escape_key_down",
"on_pointer_down_outside",
"on_interact_outside",
]
for prop in deprecated_properties:
if prop in props:
console.deprecate(
feature_name="drawer content events",
reason=f"The `{prop}` event is deprecated and will be removed in 0.7.0.",
deprecation_version="0.6.3",
removal_version="0.7.0",
)
comp = super().create(*children, **props) comp = super().create(*children, **props)
return Theme.create(comp) return Theme.create(comp)

View File

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

View File

@ -53,7 +53,7 @@ LiteralAccentColor = Literal[
class CommonMarginProps(Component): class CommonMarginProps(Component):
"""Many radix-themes elements accept shorthand margin props.""" """Many radix-themes elements accept shorthand margin props."""
# Margin: "0" - "9" # Margin: "0" - "9" # noqa: ERA001
m: Var[LiteralSpacing] m: Var[LiteralSpacing]
# Margin horizontal: "0" - "9" # Margin horizontal: "0" - "9"
@ -78,7 +78,7 @@ class CommonMarginProps(Component):
class CommonPaddingProps(Component): class CommonPaddingProps(Component):
"""Many radix-themes elements accept shorthand padding props.""" """Many radix-themes elements accept shorthand padding props."""
# Padding: "0" - "9" # Padding: "0" - "9" # noqa: ERA001
p: Var[Responsive[LiteralSpacing]] p: Var[Responsive[LiteralSpacing]]
# Padding horizontal: "0" - "9" # Padding horizontal: "0" - "9"
@ -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

View File

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

View File

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

View File

@ -140,10 +140,8 @@ class HighLevelRadioGroup(RadixThemesComponent):
color_scheme = props.pop("color_scheme", None) color_scheme = props.pop("color_scheme", None)
default_value = props.pop("default_value", "") default_value = props.pop("default_value", "")
if ( if not isinstance(items, (list, Var)) or (
not isinstance(items, (list, Var)) isinstance(items, Var) and not types._issubclass(items._var_type, list)
or isinstance(items, Var)
and not types._issubclass(items._var_type, list)
): ):
items_type = type(items) if not isinstance(items, Var) else items._var_type items_type = type(items) if not isinstance(items, Var) else items._var_type
raise TypeError( raise TypeError(

View File

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

View File

@ -12,7 +12,9 @@ from reflex.vars.base import Var
from ..base import LiteralAccentColor, RadixThemesComponent from ..base import LiteralAccentColor, RadixThemesComponent
def on_value_change(value: Var[str | List[str]]) -> Tuple[Var[str | List[str]]]: def on_value_change(
value: Var[Union[str, List[str]]],
) -> Tuple[Var[Union[str, List[str]]]]:
"""Handle the on_value_change event. """Handle the on_value_change event.
Args: Args:

View File

@ -13,7 +13,9 @@ from reflex.vars.base import Var
from ..base import RadixThemesComponent from ..base import RadixThemesComponent
def on_value_change(value: Var[str | List[str]]) -> Tuple[Var[str | List[str]]]: ... def on_value_change(
value: Var[Union[str, List[str]]],
) -> Tuple[Var[Union[str, List[str]]]]: ...
class SegmentedControlRoot(RadixThemesComponent): class SegmentedControlRoot(RadixThemesComponent):
@overload @overload
@ -118,7 +120,10 @@ class SegmentedControlRoot(RadixThemesComponent):
custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None,
on_blur: Optional[EventType[[], BASE_STATE]] = None, on_blur: Optional[EventType[[], BASE_STATE]] = None,
on_change: Optional[ on_change: Optional[
Union[EventType[[], BASE_STATE], EventType[[str | List[str]], BASE_STATE]] Union[
EventType[[], BASE_STATE],
EventType[[Union[str, List[str]]], BASE_STATE],
]
] = None, ] = None,
on_click: Optional[EventType[[], BASE_STATE]] = None, on_click: Optional[EventType[[], BASE_STATE]] = None,
on_context_menu: Optional[EventType[[], BASE_STATE]] = None, on_context_menu: Optional[EventType[[], BASE_STATE]] = None,

View File

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

View File

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

View File

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

View File

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

View File

@ -41,6 +41,9 @@ class TextArea(RadixThemesComponent, elements.Textarea):
# Automatically focuses the textarea when the page loads # Automatically focuses the textarea when the page loads
auto_focus: Var[bool] auto_focus: Var[bool]
# The default value of the textarea when initially rendered
default_value: Var[str]
# Name part of the textarea to submit in 'dir' and 'name' pair when form is submitted # Name part of the textarea to submit in 'dir' and 'name' pair when form is submitted
dirname: Var[str] dirname: Var[str]

View File

@ -123,6 +123,7 @@ class TextArea(RadixThemesComponent, elements.Textarea):
] = None, ] = None,
auto_complete: Optional[Union[Var[bool], bool]] = None, auto_complete: Optional[Union[Var[bool], bool]] = None,
auto_focus: Optional[Union[Var[bool], bool]] = None, auto_focus: Optional[Union[Var[bool], bool]] = None,
default_value: Optional[Union[Var[str], str]] = None,
dirname: Optional[Union[Var[str], str]] = None, dirname: Optional[Union[Var[str], str]] = None,
disabled: Optional[Union[Var[bool], bool]] = None, disabled: Optional[Union[Var[bool], bool]] = None,
form: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, form: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
@ -217,6 +218,7 @@ class TextArea(RadixThemesComponent, elements.Textarea):
radius: The radius of the text area: "none" | "small" | "medium" | "large" | "full" radius: The radius of the text area: "none" | "small" | "medium" | "large" | "full"
auto_complete: Whether the form control should have autocomplete enabled auto_complete: Whether the form control should have autocomplete enabled
auto_focus: Automatically focuses the textarea when the page loads auto_focus: Automatically focuses the textarea when the page loads
default_value: The default value of the textarea when initially rendered
dirname: Name part of the textarea to submit in 'dir' and 'name' pair when form is submitted dirname: Name part of the textarea to submit in 'dir' and 'name' pair when form is submitted
disabled: Disables the textarea disabled: Disables the textarea
form: Associates the textarea with a form (by id) form: Associates the textarea with a form (by id)

View File

@ -67,6 +67,9 @@ class TextFieldRoot(elements.Div, RadixThemesComponent):
# Value of the input # Value of the input
value: Var[Union[str, int, float]] value: Var[Union[str, int, float]]
# References a datalist for suggested options
list: Var[Union[str, int, bool]]
# Fired when the value of the textarea changes. # Fired when the value of the textarea changes.
on_change: EventHandler[input_event] on_change: EventHandler[input_event]

View File

@ -119,6 +119,7 @@ class TextFieldRoot(elements.Div, RadixThemesComponent):
required: Optional[Union[Var[bool], bool]] = None, required: Optional[Union[Var[bool], bool]] = None,
type: Optional[Union[Var[str], str]] = None, type: Optional[Union[Var[str], str]] = None,
value: Optional[Union[Var[Union[float, int, str]], float, int, str]] = None, value: Optional[Union[Var[Union[float, int, str]], float, int, str]] = None,
list: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
auto_capitalize: Optional[ auto_capitalize: Optional[
Union[Var[Union[bool, int, str]], bool, int, str] Union[Var[Union[bool, int, str]], bool, int, str]
@ -206,6 +207,7 @@ class TextFieldRoot(elements.Div, RadixThemesComponent):
required: Indicates that the input is required required: Indicates that the input is required
type: Specifies the type of input type: Specifies the type of input
value: Value of the input value: Value of the input
list: References a datalist for suggested options
on_change: Fired when the value of the textarea changes. on_change: Fired when the value of the textarea changes.
on_focus: Fired when the textarea is focused. on_focus: Fired when the textarea is focused.
on_blur: Fired when the textarea is blurred. on_blur: Fired when the textarea is blurred.
@ -454,6 +456,7 @@ class TextField(ComponentNamespace):
required: Optional[Union[Var[bool], bool]] = None, required: Optional[Union[Var[bool], bool]] = None,
type: Optional[Union[Var[str], str]] = None, type: Optional[Union[Var[str], str]] = None,
value: Optional[Union[Var[Union[float, int, str]], float, int, str]] = None, value: Optional[Union[Var[Union[float, int, str]], float, int, str]] = None,
list: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
auto_capitalize: Optional[ auto_capitalize: Optional[
Union[Var[Union[bool, int, str]], bool, int, str] Union[Var[Union[bool, int, str]], bool, int, str]
@ -541,6 +544,7 @@ class TextField(ComponentNamespace):
required: Indicates that the input is required required: Indicates that the input is required
type: Specifies the type of input type: Specifies the type of input
value: Value of the input value: Value of the input
list: References a datalist for suggested options
on_change: Fired when the value of the textarea changes. on_change: Fired when the value of the textarea changes.
on_focus: Fired when the textarea is focused. on_focus: Fired when the textarea is focused.
on_blur: Fired when the textarea is blurred. on_blur: Fired when the textarea is blurred.

View File

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

View File

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

View File

@ -77,13 +77,14 @@ class Link(RadixThemesComponent, A, MemoizationLeaf, MarkdownComponentMap):
Component: The link component Component: The link component
""" """
props.setdefault(":hover", {"color": color("accent", 8)}) props.setdefault(":hover", {"color": color("accent", 8)})
href = props.get("href")
is_external = props.pop("is_external", None) is_external = props.pop("is_external", None)
if is_external is not None: if is_external is not None:
props["target"] = cond(is_external, "_blank", "") props["target"] = cond(is_external, "_blank", "")
if props.get("href") is not None: if href is not None:
if not len(children): if not len(children):
raise ValueError("Link without a child will not display") raise ValueError("Link without a child will not display")
@ -101,6 +102,9 @@ class Link(RadixThemesComponent, A, MemoizationLeaf, MarkdownComponentMap):
as_child=True, as_child=True,
**props, **props,
) )
else:
props["href"] = "#"
return super().create(*children, **props) return super().create(*children, **props)

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@
from typing import Dict, Literal from typing import Dict, Literal
from reflex.components.component import Component, MemoizationLeaf, NoSSRComponent from reflex.components.component import Component, MemoizationLeaf, NoSSRComponent
from reflex.utils import console
class Recharts(Component): class Recharts(Component):
@ -11,19 +10,8 @@ class Recharts(Component):
library = "recharts@2.13.0" library = "recharts@2.13.0"
def render(self) -> Dict: def _get_style(self) -> Dict:
"""Render the tag. return {"wrapperStyle": self.style}
Returns:
The rendered tag.
"""
tag = super().render()
if any(p.startswith("css") for p in tag["props"]):
console.warn(
f"CSS props do not work for {self.__class__.__name__}. Consult docs to style it with its own prop."
)
tag["props"] = [p for p in tag["props"] if not p.startswith("css")]
return tag
class RechartsCharts(NoSSRComponent, MemoizationLeaf): class RechartsCharts(NoSSRComponent, MemoizationLeaf):

View File

@ -11,7 +11,6 @@ from reflex.style import Style
from reflex.vars.base import Var from reflex.vars.base import Var
class Recharts(Component): class Recharts(Component):
def render(self) -> Dict: ...
@overload @overload
@classmethod @classmethod
def create( # type: ignore def create( # type: ignore

View File

@ -15,6 +15,8 @@ from reflex.utils.imports import ImportVar
from reflex.utils.serializers import serializer from reflex.utils.serializers import serializer
from reflex.vars import VarData from reflex.vars import VarData
from reflex.vars.base import LiteralVar, Var from reflex.vars.base import LiteralVar, Var
from reflex.vars.function import FunctionVar
from reflex.vars.object import ObjectVar
LiteralPosition = Literal[ LiteralPosition = Literal[
"top-left", "top-left",
@ -62,7 +64,7 @@ def _toast_callback_signature(toast: Var) -> list[Var]:
""" """
return [ return [
Var( Var(
_js_expr=f"(() => {{let {{action, cancel, onDismiss, onAutoClose, ...rest}} = {str(toast)}; return rest}})()" _js_expr=f"(() => {{let {{action, cancel, onDismiss, onAutoClose, ...rest}} = {toast!s}; return rest}})()"
) )
] ]
@ -96,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.
@ -232,7 +234,9 @@ class Toaster(Component):
return [hook] return [hook]
@staticmethod @staticmethod
def send_toast(message: str = "", level: str | None = None, **props) -> EventSpec: def send_toast(
message: str | Var = "", level: str | None = None, **props
) -> EventSpec:
"""Send a toast message. """Send a toast message.
Args: Args:
@ -250,20 +254,27 @@ class Toaster(Component):
raise ValueError( raise ValueError(
"Toaster component must be created before sending a toast. (use `rx.toast.provider()`)" "Toaster component must be created before sending a toast. (use `rx.toast.provider()`)"
) )
toast_command = f"{toast_ref}.{level}" if level is not None else toast_ref
if message == "" and ("title" not in props or "description" not in props):
raise ValueError("Toast message or title or description must be provided.")
if props:
args = LiteralVar.create(ToastProps(component_name="rx.toast", **props)) # type: ignore
toast = f"{toast_command}(`{message}`, {str(args)})"
else:
toast = f"{toast_command}(`{message}`)"
toast_action = Var(_js_expr=toast) toast_command = (
return run_script(toast_action) ObjectVar.__getattr__(toast_ref.to(dict), level) if level else toast_ref
).to(FunctionVar)
if isinstance(message, Var):
props.setdefault("title", message)
message = ""
elif message == "" and "title" not in props and "description" not in props:
raise ValueError("Toast message or title or description must be provided.")
if props:
args = LiteralVar.create(ToastProps(component_name="rx.toast", **props)) # pyright: ignore [reportCallIssue, reportGeneralTypeIssues]
toast = toast_command.call(message, args)
else:
toast = toast_command.call(message)
return run_script(toast)
@staticmethod @staticmethod
def toast_info(message: str = "", **kwargs): def toast_info(message: str | Var = "", **kwargs):
"""Display an info toast message. """Display an info toast message.
Args: Args:
@ -276,7 +287,7 @@ class Toaster(Component):
return Toaster.send_toast(message, level="info", **kwargs) return Toaster.send_toast(message, level="info", **kwargs)
@staticmethod @staticmethod
def toast_warning(message: str = "", **kwargs): def toast_warning(message: str | Var = "", **kwargs):
"""Display a warning toast message. """Display a warning toast message.
Args: Args:
@ -289,7 +300,7 @@ class Toaster(Component):
return Toaster.send_toast(message, level="warning", **kwargs) return Toaster.send_toast(message, level="warning", **kwargs)
@staticmethod @staticmethod
def toast_error(message: str = "", **kwargs): def toast_error(message: str | Var = "", **kwargs):
"""Display an error toast message. """Display an error toast message.
Args: Args:
@ -302,7 +313,7 @@ class Toaster(Component):
return Toaster.send_toast(message, level="error", **kwargs) return Toaster.send_toast(message, level="error", **kwargs)
@staticmethod @staticmethod
def toast_success(message: str = "", **kwargs): def toast_success(message: str | Var = "", **kwargs):
"""Display a success toast message. """Display a success toast message.
Args: Args:
@ -327,7 +338,7 @@ class Toaster(Component):
dismiss_var_data = None dismiss_var_data = None
if isinstance(id, Var): if isinstance(id, Var):
dismiss = f"{toast_ref}.dismiss({str(id)})" dismiss = f"{toast_ref}.dismiss({id!s})"
dismiss_var_data = id._get_all_var_data() dismiss_var_data = id._get_all_var_data()
elif isinstance(id, str): elif isinstance(id, str):
dismiss = f"{toast_ref}.dismiss('{id}')" dismiss = f"{toast_ref}.dismiss('{id}')"
@ -353,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):
@ -368,7 +377,6 @@ class ToastNamespace(ComponentNamespace):
error = staticmethod(Toaster.toast_error) error = staticmethod(Toaster.toast_error)
success = staticmethod(Toaster.toast_success) success = staticmethod(Toaster.toast_success)
dismiss = staticmethod(Toaster.toast_dismiss) dismiss = staticmethod(Toaster.toast_dismiss)
# loading = staticmethod(toast_loading)
__call__ = staticmethod(Toaster.send_toast) __call__ = staticmethod(Toaster.send_toast)

View File

@ -59,16 +59,16 @@ class Toaster(Component):
def add_hooks(self) -> list[Var | str]: ... def add_hooks(self) -> list[Var | str]: ...
@staticmethod @staticmethod
def send_toast( def send_toast(
message: str = "", level: str | None = None, **props message: str | Var = "", level: str | None = None, **props
) -> EventSpec: ... ) -> EventSpec: ...
@staticmethod @staticmethod
def toast_info(message: str = "", **kwargs): ... def toast_info(message: str | Var = "", **kwargs): ...
@staticmethod @staticmethod
def toast_warning(message: str = "", **kwargs): ... def toast_warning(message: str | Var = "", **kwargs): ...
@staticmethod @staticmethod
def toast_error(message: str = "", **kwargs): ... def toast_error(message: str | Var = "", **kwargs): ...
@staticmethod @staticmethod
def toast_success(message: str = "", **kwargs): ... def toast_success(message: str | Var = "", **kwargs): ...
@staticmethod @staticmethod
def toast_dismiss(id: Var | str | None = None): ... def toast_dismiss(id: Var | str | None = None): ...
@overload @overload
@ -176,7 +176,7 @@ class ToastNamespace(ComponentNamespace):
@staticmethod @staticmethod
def __call__( def __call__(
message: str = "", level: Optional[str] = None, **props message: Union[str, Var] = "", level: Optional[str] = None, **props
) -> "Optional[EventSpec]": ) -> "Optional[EventSpec]":
"""Send a toast message. """Send a toast message.

View File

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

View File

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

View File

@ -8,7 +8,9 @@ import importlib
import inspect import inspect
import os import os
import sys import sys
import threading
import urllib.parse import urllib.parse
from importlib.util import find_spec
from pathlib import Path from pathlib import Path
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
@ -452,6 +454,14 @@ class PathExistsFlag:
ExistingPath = Annotated[Path, PathExistsFlag] ExistingPath = Annotated[Path, PathExistsFlag]
class PerformanceMode(enum.Enum):
"""Performance mode for the app."""
WARN = "warn"
RAISE = "raise"
OFF = "off"
class EnvironmentVariables: class EnvironmentVariables:
"""Environment variables class to instantiate environment variables.""" """Environment variables class to instantiate environment variables."""
@ -502,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)
@ -545,10 +558,23 @@ class EnvironmentVariables:
# Where to save screenshots when tests fail. # Where to save screenshots when tests fail.
SCREENSHOT_DIR: EnvVar[Optional[Path]] = env_var(None) SCREENSHOT_DIR: EnvVar[Optional[Path]] = env_var(None)
# Whether to check for outdated package versions.
REFLEX_CHECK_LATEST_VERSION: EnvVar[bool] = env_var(True)
# In which performance mode to run the app.
REFLEX_PERF_MODE: EnvVar[Optional[PerformanceMode]] = env_var(PerformanceMode.WARN)
# The maximum size of the reflex state in kilobytes.
REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000)
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.
@ -602,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
@ -633,9 +662,9 @@ class Config(Base):
frontend_packages: List[str] = [] frontend_packages: List[str] = []
# The hosting service backend URL. # The hosting service backend URL.
cp_backend_url: str = Hosting.CP_BACKEND_URL cp_backend_url: str = Hosting.HOSTING_SERVICE
# The hosting service frontend URL. # The hosting service frontend URL.
cp_web_url: str = Hosting.CP_WEB_URL cp_web_url: str = Hosting.HOSTING_SERVICE_UI
# The worker class used in production mode # The worker class used in production mode
gunicorn_worker_class: str = "uvicorn.workers.UvicornH11Worker" gunicorn_worker_class: str = "uvicorn.workers.UvicornH11Worker"
@ -729,18 +758,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:
@ -798,6 +829,27 @@ class Config(Base):
self._replace_defaults(**kwargs) self._replace_defaults(**kwargs)
def _get_config() -> Config:
"""Get the app config.
Returns:
The app config.
"""
# only import the module if it exists. If a module spec exists then
# the module exists.
spec = find_spec(constants.Config.MODULE)
if not spec:
# we need this condition to ensure that a ModuleNotFound error is not thrown when
# running unit/integration tests or during `reflex init`.
return Config(app_name="")
rxconfig = importlib.import_module(constants.Config.MODULE)
return rxconfig.config
# Protect sys.path from concurrent modification
_config_lock = threading.RLock()
def get_config(reload: bool = False) -> Config: def get_config(reload: bool = False) -> Config:
"""Get the app config. """Get the app config.
@ -807,15 +859,26 @@ def get_config(reload: bool = False) -> Config:
Returns: Returns:
The app config. The app config.
""" """
sys.path.insert(0, os.getcwd()) cached_rxconfig = sys.modules.get(constants.Config.MODULE, None)
# only import the module if it exists. If a module spec exists then if cached_rxconfig is not None:
# the module exists.
spec = importlib.util.find_spec(constants.Config.MODULE) # type: ignore
if not spec:
# we need this condition to ensure that a ModuleNotFound error is not thrown when
# running unit/integration tests.
return Config(app_name="")
rxconfig = importlib.import_module(constants.Config.MODULE)
if reload: if reload:
importlib.reload(rxconfig) # Remove any cached module when `reload` is requested.
return rxconfig.config del sys.modules[constants.Config.MODULE]
else:
return cached_rxconfig.config
with _config_lock:
sys_path = sys.path.copy()
sys.path.clear()
sys.path.append(os.getcwd())
try:
# Try to import the module with only the current directory in the path.
return _get_config()
except Exception:
# If the module import fails, try to import with the original sys.path.
sys.path.extend(sys_path)
return _get_config()
finally:
# Restore the original sys.path.
sys.path.clear()
sys.path.extend(sys_path)

View File

@ -97,6 +97,18 @@ class Templates(SimpleNamespace):
# The default template # The default template
DEFAULT = "blank" DEFAULT = "blank"
# The AI template
AI = "ai"
# The option for the user to choose a remote template.
CHOOSE_TEMPLATES = "choose-templates"
# The URL to find reflex templates.
REFLEX_TEMPLATES_URL = "https://reflex.dev/templates"
# Demo url for the default template.
DEFAULT_TEMPLATE_URL = "https://blank-template.reflex.run"
# The reflex.build frontend host # The reflex.build frontend host
REFLEX_BUILD_FRONTEND = "https://flexgen.reflex.run" REFLEX_BUILD_FRONTEND = "https://flexgen.reflex.run"

View File

@ -184,15 +184,15 @@ class PackageJson(SimpleNamespace):
"json5": "2.2.3", "json5": "2.2.3",
"next": "14.2.16", "next": "14.2.16",
"next-sitemap": "4.2.3", "next-sitemap": "4.2.3",
"next-themes": "0.3.0", "next-themes": "0.4.3",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-focus-lock": "2.13.2", "react-focus-lock": "2.13.2",
"socket.io-client": "4.8.1", "socket.io-client": "4.8.1",
"universal-cookie": "7.2.1", "universal-cookie": "7.2.2",
} }
DEV_DEPENDENCIES = { DEV_DEPENDENCIES = {
"autoprefixer": "10.4.20", "autoprefixer": "10.4.20",
"postcss": "8.4.47", "postcss": "8.4.49",
"postcss-import": "16.1.0", "postcss-import": "16.1.0",
} }

View File

@ -7,7 +7,7 @@ class Tailwind(SimpleNamespace):
"""Tailwind constants.""" """Tailwind constants."""
# The Tailwindcss version # The Tailwindcss version
VERSION = "tailwindcss@3.4.14" VERSION = "tailwindcss@3.4.15"
# The Tailwind config. # The Tailwind config.
CONFIG = "tailwind.config.js" CONFIG = "tailwind.config.js"
# Default Tailwind content paths # Default Tailwind content paths

View File

@ -827,11 +827,11 @@ def _collect_details_for_gallery():
Raises: Raises:
Exit: If pyproject.toml file is ill-formed or the request to the backend services fails. Exit: If pyproject.toml file is ill-formed or the request to the backend services fails.
""" """
from reflex.reflex import _login from reflex_cli.utils import hosting
console.rule("[bold]Authentication with Reflex Services") console.rule("[bold]Authentication with Reflex Services")
console.print("First let's log in to Reflex backend services.") console.print("First let's log in to Reflex backend services.")
access_token = _login() access_token, _ = hosting.authenticated_token()
console.rule("[bold]Custom Component Information") console.rule("[bold]Custom Component Information")
params = {} params = {}

View File

@ -181,6 +181,18 @@ class EventActionsMixin:
event_actions={"debounce": delay_ms, **self.event_actions}, event_actions={"debounce": delay_ms, **self.event_actions},
) )
@property
def temporal(self):
"""Do not queue the event if the backend is down.
Returns:
New EventHandler-like with temporal set to True.
"""
return dataclasses.replace(
self,
event_actions={"temporal": True, **self.event_actions},
)
@dataclasses.dataclass( @dataclasses.dataclass(
init=True, init=True,
@ -435,7 +447,7 @@ class JavascriptHTMLInputElement:
class JavascriptInputEvent: class JavascriptInputEvent:
"""Interface for a Javascript InputEvent https://developer.mozilla.org/en-US/docs/Web/API/InputEvent.""" """Interface for a Javascript InputEvent https://developer.mozilla.org/en-US/docs/Web/API/InputEvent."""
target: JavascriptHTMLInputElement = JavascriptHTMLInputElement() target: JavascriptHTMLInputElement = JavascriptHTMLInputElement() # noqa: RUF009
@dataclasses.dataclass( @dataclasses.dataclass(
@ -1210,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}."
@ -1346,6 +1358,10 @@ def check_fn_match_arg_spec(
EventFnArgMismatch: Raised if the number of mandatory arguments do not match EventFnArgMismatch: Raised if the number of mandatory arguments do not match
""" """
user_args = inspect.getfullargspec(user_func).args user_args = inspect.getfullargspec(user_func).args
# Drop the first argument if it's a bound method
if inspect.ismethod(user_func) and user_func.__self__ is not None:
user_args = user_args[1:]
user_default_args = inspect.getfullargspec(user_func).defaults user_default_args = inspect.getfullargspec(user_func).defaults
number_of_user_args = len(user_args) - number_of_bound_args number_of_user_args = len(user_args) - number_of_bound_args
number_of_user_default_args = len(user_default_args) if user_default_args else 0 number_of_user_default_args = len(user_default_args) if user_default_args else 0
@ -1540,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(
@ -1604,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(

View File

@ -1,14 +1,15 @@
"""Helper functions for adding assets to the app.""" """Helper functions for adding assets to the app."""
import inspect
from pathlib import Path
from typing import Optional from typing import Optional
from reflex import constants from reflex import assets
from reflex.utils import console
def asset(relative_filename: str, subfolder: Optional[str] = None) -> str: def asset(relative_filename: str, subfolder: Optional[str] = None) -> str:
"""Add an asset to the app. """DEPRECATED: use `rx.asset` with `shared=True` instead.
Add an asset to the app.
Place the file next to your including python file. Place the file next to your including python file.
Copies the file to the app's external assets directory. Copies the file to the app's external assets directory.
@ -22,38 +23,15 @@ def asset(relative_filename: str, subfolder: Optional[str] = None) -> str:
relative_filename: The relative filename of the asset. relative_filename: The relative filename of the asset.
subfolder: The directory to place the asset in. subfolder: The directory to place the asset in.
Raises:
FileNotFoundError: If the file does not exist.
ValueError: If the module is None.
Returns: Returns:
The relative URL to the copied asset. The relative URL to the copied asset.
""" """
# Determine the file by which the asset is exposed. console.deprecate(
calling_file = inspect.stack()[1].filename feature_name="rx._x.asset",
module = inspect.getmodule(inspect.stack()[1][0]) reason="Use `rx.asset` with `shared=True` instead of `rx._x.asset`.",
if module is None: deprecation_version="0.6.6",
raise ValueError("Module is None") removal_version="0.7.0",
caller_module_path = module.__name__.replace(".", "/") )
return assets.asset(
subfolder = f"{caller_module_path}/{subfolder}" if subfolder else caller_module_path relative_filename, shared=True, subfolder=subfolder, _stack_level=2
)
src_file = Path(calling_file).parent / relative_filename
assets = constants.Dirs.APP_ASSETS
external = constants.Dirs.EXTERNAL_APP_ASSETS
if not src_file.exists():
raise FileNotFoundError(f"File not found: {src_file}")
# Create the asset folder in the currently compiling app.
asset_folder = Path.cwd() / assets / external / subfolder
asset_folder.mkdir(parents=True, exist_ok=True)
dst_file = asset_folder / relative_filename
if not dst_file.exists():
dst_file.symlink_to(src_file)
asset_url = f"/{external}/{subfolder}/{relative_filename}"
return asset_url

View File

@ -106,7 +106,7 @@ class ClientStateVar(Var):
default_var = default default_var = default
setter_name = f"set{var_name.capitalize()}" setter_name = f"set{var_name.capitalize()}"
hooks = { hooks = {
f"const [{var_name}, {setter_name}] = useState({str(default_var)})": None, f"const [{var_name}, {setter_name}] = useState({default_var!s})": None,
} }
imports = { imports = {
"react": [ImportVar(tag="useState")], "react": [ImportVar(tag="useState")],
@ -242,4 +242,5 @@ class ClientStateVar(Var):
""" """
if not self._global_ref: if not self._global_ref:
raise ValueError("ClientStateVar must be global to push the value.") raise ValueError("ClientStateVar must be global to push the value.")
value = Var.create(value)
return run_script(f"{_client_state_ref(self._setter_name)}({value})") return run_script(f"{_client_state_ref(self._setter_name)}({value})")

View File

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

View File

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

View File

@ -9,18 +9,16 @@ from typing import List, Optional
import typer import typer
import typer.core import typer.core
from reflex_cli.deployments import deployments_cli from reflex_cli.v2.deployments import check_version, hosting_cli
from reflex_cli.utils import dependency
from reflex_cli.v2.deployments import hosting_cli
from reflex import constants from reflex import constants
from reflex.config import environment, get_config from reflex.config import environment, get_config
from reflex.custom_components.custom_components import custom_components_cli from reflex.custom_components.custom_components import custom_components_cli
from reflex.state import reset_disk_state_manager from reflex.state import reset_disk_state_manager
from reflex.utils import console, redir, telemetry from reflex.utils import console, telemetry
# Disable typer+rich integration for help panels # Disable typer+rich integration for help panels
typer.core.rich = False # type: ignore typer.core.rich = None # type: ignore
# Create the app. # Create the app.
try: try:
@ -89,30 +87,8 @@ def _init(
# Set up the web project. # Set up the web project.
prerequisites.initialize_frontend_dependencies() prerequisites.initialize_frontend_dependencies()
# Integrate with reflex.build.
generation_hash = None
if ai:
if template is None:
# If AI is requested and no template specified, redirect the user to reflex.build.
generation_hash = redir.reflex_build_redirect()
elif prerequisites.is_generation_hash(template):
# Otherwise treat the template as a generation hash.
generation_hash = template
else:
console.error(
"Cannot use `--template` option with `--ai` option. Please remove `--template` option."
)
raise typer.Exit(2)
template = constants.Templates.DEFAULT
# Initialize the app. # Initialize the app.
template = prerequisites.initialize_app(app_name, template) template = prerequisites.initialize_app(app_name, template, ai)
# If a reflex.build generation hash is available, download the code and apply it to the main module.
if generation_hash:
prerequisites.initialize_main_module_index_from_generation(
app_name, generation_hash=generation_hash
)
# Initialize the .gitignore. # Initialize the .gitignore.
prerequisites.initialize_gitignore() prerequisites.initialize_gitignore()
@ -120,7 +96,7 @@ def _init(
# Initialize the requirements.txt. # Initialize the requirements.txt.
prerequisites.initialize_requirements_txt() prerequisites.initialize_requirements_txt()
template_msg = "" if template else f" using the {template} template" template_msg = f" using the {template} template" if template else ""
# Finish initializing the app. # Finish initializing the app.
console.success(f"Initialized {app_name}{template_msg}") console.success(f"Initialized {app_name}{template_msg}")
@ -352,45 +328,16 @@ def export(
) )
def _login() -> str:
"""Helper function to authenticate with Reflex hosting service."""
from reflex_cli.utils import hosting
access_token, invitation_code = hosting.authenticated_token()
if access_token:
console.print("You already logged in.")
return access_token
# If not already logged in, open a browser window/tab to the login page.
access_token = hosting.authenticate_on_browser(invitation_code)
if not access_token:
console.error("Unable to authenticate. Please try again or contact support.")
raise typer.Exit(1)
console.print("Successfully logged in.")
return access_token
@cli.command() @cli.command()
def login( def login(loglevel: constants.LogLevel = typer.Option(config.loglevel)):
loglevel: constants.LogLevel = typer.Option(
config.loglevel, help="The log level to use."
),
):
"""Authenticate with Reflex hosting service."""
# Set the log level.
console.set_log_level(loglevel)
_login()
@cli.command()
def loginv2(loglevel: constants.LogLevel = typer.Option(config.loglevel)):
"""Authenicate with experimental Reflex hosting service.""" """Authenicate with experimental Reflex hosting service."""
from reflex_cli.v2 import cli as hosting_cli from reflex_cli.v2 import cli as hosting_cli
hosting_cli.login() check_version()
validated_info = hosting_cli.login()
if validated_info is not None:
telemetry.send("login", user_uuid=validated_info.get("user_id"))
@cli.command() @cli.command()
@ -400,29 +347,11 @@ def logout(
), ),
): ):
"""Log out of access to Reflex hosting service.""" """Log out of access to Reflex hosting service."""
from reflex_cli.utils import hosting from reflex_cli.v2.cli import logout
console.set_log_level(loglevel) check_version()
hosting.log_out_on_browser() logout(loglevel) # type: ignore
console.debug("Deleting access token from config locally")
hosting.delete_token_from_config(include_invitation_code=True)
@cli.command()
def logoutv2(
loglevel: constants.LogLevel = typer.Option(
config.loglevel, help="The log level to use."
),
):
"""Log out of access to Reflex hosting service."""
from reflex_cli.v2.utils import hosting
console.set_log_level(loglevel)
hosting.log_out_on_browser()
console.debug("Deleting access token from config locally")
hosting.delete_token_from_config(include_invitation_code=True)
db_cli = typer.Typer() db_cli = typer.Typer()
@ -507,12 +436,6 @@ def makemigrations(
@cli.command() @cli.command()
def deploy( def deploy(
key: Optional[str] = typer.Option(
None,
"-k",
"--deployment-key",
help="The name of the deployment. Domain name safe characters only.",
),
app_name: str = typer.Option( app_name: str = typer.Option(
config.app_name, config.app_name,
"--app-name", "--app-name",
@ -523,121 +446,7 @@ def deploy(
list(), list(),
"-r", "-r",
"--region", "--region",
help="The regions to deploy to.", help="The regions to deploy to. `reflex cloud regions` For multiple envs, repeat this option, e.g. --region sjc --region iad",
),
envs: List[str] = typer.Option(
list(),
"--env",
help="The environment variables to set: <key>=<value>. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.",
),
cpus: Optional[int] = typer.Option(
None, help="The number of CPUs to allocate.", hidden=True
),
memory_mb: Optional[int] = typer.Option(
None, help="The amount of memory to allocate.", hidden=True
),
auto_start: Optional[bool] = typer.Option(
None,
help="Whether to auto start the instance.",
hidden=True,
),
auto_stop: Optional[bool] = typer.Option(
None,
help="Whether to auto stop the instance.",
hidden=True,
),
frontend_hostname: Optional[str] = typer.Option(
None,
"--frontend-hostname",
help="The hostname of the frontend.",
hidden=True,
),
interactive: bool = typer.Option(
True,
help="Whether to list configuration options and ask for confirmation.",
),
with_metrics: Optional[str] = typer.Option(
None,
help="Setting for metrics scraping for the deployment. Setup required in user code.",
hidden=True,
),
with_tracing: Optional[str] = typer.Option(
None,
help="Setting to export tracing for the deployment. Setup required in user code.",
hidden=True,
),
upload_db_file: bool = typer.Option(
False,
help="Whether to include local sqlite db files when uploading to hosting service.",
hidden=True,
),
loglevel: constants.LogLevel = typer.Option(
config.loglevel, help="The log level to use."
),
):
"""Deploy the app to the Reflex hosting service."""
from reflex_cli import cli as hosting_cli
from reflex.utils import export as export_utils
from reflex.utils import prerequisites
# Set the log level.
console.set_log_level(loglevel)
# Only check requirements if interactive. There is user interaction for requirements update.
if interactive:
dependency.check_requirements()
# Check if we are set up.
if prerequisites.needs_reinit(frontend=True):
_init(name=config.app_name, loglevel=loglevel)
prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME)
hosting_cli.deploy(
app_name=app_name,
export_fn=lambda zip_dest_dir,
api_url,
deploy_url,
frontend,
backend,
zipping: export_utils.export(
zip_dest_dir=zip_dest_dir,
api_url=api_url,
deploy_url=deploy_url,
frontend=frontend,
backend=backend,
zipping=zipping,
loglevel=loglevel.subprocess_level(),
upload_db_file=upload_db_file,
),
key=key,
regions=regions,
envs=envs,
cpus=cpus,
memory_mb=memory_mb,
auto_start=auto_start,
auto_stop=auto_stop,
frontend_hostname=frontend_hostname,
interactive=interactive,
with_metrics=with_metrics,
with_tracing=with_tracing,
loglevel=loglevel.subprocess_level(),
)
@cli.command()
def deployv2(
app_name: str = typer.Option(
config.app_name,
"--app-name",
help="The name of the App to deploy under.",
hidden=True,
),
regions: List[str] = typer.Option(
list(),
"-r",
"--region",
help="The regions to deploy to. For multiple envs, repeat this option, e.g. --region sjc --region iad",
), ),
envs: List[str] = typer.Option( envs: List[str] = typer.Option(
list(), list(),
@ -647,13 +456,12 @@ def deployv2(
vmtype: Optional[str] = typer.Option( vmtype: Optional[str] = typer.Option(
None, None,
"--vmtype", "--vmtype",
help="Vm type id. Run reflex apps vmtypes list to get options.", help="Vm type id. Run `reflex cloud vmtypes` to get options.",
), ),
hostname: Optional[str] = typer.Option( hostname: Optional[str] = typer.Option(
None, None,
"--hostname", "--hostname",
help="The hostname of the frontend.", help="The hostname of the frontend.",
hidden=True,
), ),
interactive: bool = typer.Option( interactive: bool = typer.Option(
True, True,
@ -663,7 +471,6 @@ def deployv2(
None, None,
"--envfile", "--envfile",
help="The path to an env file to use. Will override any envs set manually.", help="The path to an env file to use. Will override any envs set manually.",
hidden=True,
), ),
loglevel: constants.LogLevel = typer.Option( loglevel: constants.LogLevel = typer.Option(
config.loglevel, help="The log level to use." config.loglevel, help="The log level to use."
@ -671,26 +478,33 @@ def deployv2(
project: Optional[str] = typer.Option( project: Optional[str] = typer.Option(
None, None,
"--project", "--project",
help="project to deploy to", help="project id to deploy to",
hidden=True,
), ),
token: Optional[str] = typer.Option( token: Optional[str] = typer.Option(
None, None,
"--token", "--token",
help="token to use for auth", help="token to use for auth",
hidden=True,
), ),
): ):
"""Deploy the app to the Reflex hosting service.""" """Deploy the app to the Reflex hosting service."""
from reflex_cli.utils import dependency
from reflex_cli.v2 import cli as hosting_cli from reflex_cli.v2 import cli as hosting_cli
from reflex_cli.v2.utils import dependency
from reflex.utils import export as export_utils from reflex.utils import export as export_utils
from reflex.utils import prerequisites from reflex.utils import prerequisites
check_version()
# Set the log level. # Set the log level.
console.set_log_level(loglevel) console.set_log_level(loglevel)
if not token:
# make sure user is logged in.
if interactive:
hosting_cli.login()
else:
raise SystemExit("Token is required for non-interactive mode.")
# Only check requirements if interactive. # Only check requirements if interactive.
# There is user interaction for requirements update. # There is user interaction for requirements update.
if interactive: if interactive:
@ -723,7 +537,7 @@ def deployv2(
envfile=envfile, envfile=envfile,
hostname=hostname, hostname=hostname,
interactive=interactive, interactive=interactive,
loglevel=loglevel.subprocess_level(), loglevel=type(loglevel).INFO, # type: ignore
token=token, token=token,
project=project, project=project,
) )
@ -731,15 +545,10 @@ def deployv2(
cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.") cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.") cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.")
cli.add_typer(
deployments_cli,
name="deployments",
help="Subcommands for managing the Deployments.",
)
cli.add_typer( cli.add_typer(
hosting_cli, hosting_cli,
name="apps", name="cloud",
help="Subcommands for managing the Deployments.", help="Subcommands for managing the reflex cloud.",
) )
cli.add_typer( cli.add_typer(
custom_components_cli, custom_components_cli,

View File

@ -43,7 +43,7 @@ from sqlalchemy.orm import DeclarativeBase
from typing_extensions import Self from typing_extensions import Self
from reflex import event from reflex import event
from reflex.config import get_config from reflex.config import PerformanceMode, get_config
from reflex.istate.data import RouterData from reflex.istate.data import RouterData
from reflex.istate.storage import ClientStorageBase from reflex.istate.storage import ClientStorageBase
from reflex.model import Model from reflex.model import Model
@ -62,6 +62,13 @@ try:
except ModuleNotFoundError: except ModuleNotFoundError:
import pydantic import pydantic
from pydantic import BaseModel as BaseModelV2
try:
from pydantic.v1 import BaseModel as BaseModelV1
except ModuleNotFoundError:
BaseModelV1 = BaseModelV2
import wrapt import wrapt
from redis.asyncio import Redis from redis.asyncio import Redis
from redis.exceptions import ResponseError from redis.exceptions import ResponseError
@ -87,14 +94,18 @@ from reflex.utils.exceptions import (
ImmutableStateError, ImmutableStateError,
InvalidStateManagerMode, InvalidStateManagerMode,
LockExpiredError, LockExpiredError,
ReflexRuntimeError,
SetUndefinedStateVarError, SetUndefinedStateVarError,
StateSchemaMismatchError, StateSchemaMismatchError,
StateSerializationError,
StateTooLargeError,
) )
from reflex.utils.exec import is_testing_env from reflex.utils.exec import is_testing_env
from reflex.utils.serializers import serializer from reflex.utils.serializers import serializer
from reflex.utils.types import ( from reflex.utils.types import (
_isinstance, _isinstance,
get_origin, get_origin,
is_optional,
is_union, is_union,
override, override,
value_inside_optional, value_inside_optional,
@ -109,8 +120,9 @@ Delta = Dict[str, Any]
var = computed_var var = computed_var
if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF:
# If the state is this large, it's considered a performance issue. # If the state is this large, it's considered a performance issue.
TOO_LARGE_SERIALIZED_STATE = 100 * 1024 # 100kb TOO_LARGE_SERIALIZED_STATE = environment.REFLEX_STATE_SIZE_LIMIT.get() * 1024
# Only warn about each state class size once. # Only warn about each state class size once.
_WARNED_ABOUT_STATE_SIZE: Set[str] = set() _WARNED_ABOUT_STATE_SIZE: Set[str] = set()
@ -268,6 +280,22 @@ if TYPE_CHECKING:
from pydantic.v1.fields import ModelField from pydantic.v1.fields import ModelField
def _unwrap_field_type(type_: Type) -> Type:
"""Unwrap rx.Field type annotations.
Args:
type_: The type to unwrap.
Returns:
The unwrapped type.
"""
from reflex.vars import Field
if get_origin(type_) is Field:
return get_args(type_)[0]
return type_
def get_var_for_field(cls: Type[BaseState], f: ModelField): def get_var_for_field(cls: Type[BaseState], f: ModelField):
"""Get a Var instance for a Pydantic field. """Get a Var instance for a Pydantic field.
@ -278,16 +306,12 @@ def get_var_for_field(cls: Type[BaseState], f: ModelField):
Returns: Returns:
The Var instance. The Var instance.
""" """
from reflex.vars import Field
field_name = format.format_state_name(cls.get_full_name()) + "." + f.name field_name = format.format_state_name(cls.get_full_name()) + "." + f.name
return dispatch( return dispatch(
field_name=field_name, field_name=field_name,
var_data=VarData.from_state(cls, f.name), var_data=VarData.from_state(cls, f.name),
result_var_type=f.outer_type_ result_var_type=_unwrap_field_type(f.outer_type_),
if get_origin(f.outer_type_) is not Field
else get_args(f.outer_type_)[0],
) )
@ -387,6 +411,10 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
"State classes should not be instantiated directly in a Reflex app. " "State classes should not be instantiated directly in a Reflex app. "
"See https://reflex.dev/docs/state/ for further information." "See https://reflex.dev/docs/state/ for further information."
) )
if type(self)._mixin:
raise ReflexRuntimeError(
f"{type(self).__name__} is a state mixin and cannot be instantiated directly."
)
kwargs["parent_state"] = parent_state kwargs["parent_state"] = parent_state
super().__init__() super().__init__()
for name, value in kwargs.items(): for name, value in kwargs.items():
@ -411,7 +439,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]:
@ -422,7 +450,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
""" """
return [ return [
v v
for mixin in cls._mixins() + [cls] for mixin in [*cls._mixins(), cls]
for name, v in mixin.__dict__.items() for name, v in mixin.__dict__.items()
if is_computed_var(v) and name not in cls.inherited_vars if is_computed_var(v) and name not in cls.inherited_vars
] ]
@ -1243,7 +1271,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
if parent_state is not None: if parent_state is not None:
return getattr(parent_state, name) return getattr(parent_state, name)
if isinstance(value, MutableProxy.__mutable_types__) and ( if MutableProxy._is_mutable_type(value) and (
name in super().__getattribute__("base_vars") or name in backend_vars name in super().__getattribute__("base_vars") or name in backend_vars
): ):
# track changes in mutable containers (list, dict, set, etc) # track changes in mutable containers (list, dict, set, etc)
@ -1274,6 +1302,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
return return
if name in self.backend_vars: if name in self.backend_vars:
# abort if unchanged
if self._backend_vars.get(name) == value:
return
self._backend_vars.__setitem__(name, value) self._backend_vars.__setitem__(name, value)
self.dirty_vars.add(name) self.dirty_vars.add(name)
self._mark_dirty() self._mark_dirty()
@ -1296,8 +1327,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
if name in fields: if name in fields:
field = fields[name] field = fields[name]
field_type = field.outer_type_ field_type = _unwrap_field_type(field.outer_type_)
if field.allow_none: if field.allow_none and not is_optional(field_type):
field_type = Union[field_type, None] field_type = Union[field_type, None]
if not _isinstance(value, field_type): if not _isinstance(value, field_type):
console.deprecate( console.deprecate(
@ -1734,7 +1765,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
if value is None: if value is None:
continue continue
hinted_args = value_inside_optional(hinted_args) hinted_args = value_inside_optional(hinted_args)
if isinstance(value, dict) and inspect.isclass(hinted_args): if (
isinstance(value, dict)
and inspect.isclass(hinted_args)
and not types.is_generic_alias(hinted_args) # py3.9-py3.10
):
if issubclass(hinted_args, Model): if issubclass(hinted_args, Model):
# Remove non-fields from the payload # Remove non-fields from the payload
payload[arg] = hinted_args( payload[arg] = hinted_args(
@ -1745,7 +1780,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
} }
) )
elif dataclasses.is_dataclass(hinted_args) or issubclass( elif dataclasses.is_dataclass(hinted_args) or issubclass(
hinted_args, Base hinted_args, (Base, BaseModelV1, BaseModelV2)
): ):
payload[arg] = hinted_args(**value) payload[arg] = hinted_args(**value)
if isinstance(value, list) and (hinted_args is set or hinted_args is Set): if isinstance(value, list) and (hinted_args is set or hinted_args is Set):
@ -1941,6 +1976,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
if var in self.base_vars or var in self._backend_vars: if var in self.base_vars or var in self._backend_vars:
self._was_touched = True self._was_touched = True
break break
if var == constants.ROUTER_DATA and self.parent_state is None:
self._was_touched = True
break
def _get_was_touched(self) -> bool: def _get_was_touched(self) -> bool:
"""Check current dirty_vars and flag to determine if state instance was modified. """Check current dirty_vars and flag to determine if state instance was modified.
@ -2092,7 +2130,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
state["__dict__"].pop(inherited_var_name, None) state["__dict__"].pop(inherited_var_name, None)
return state return state
def _warn_if_too_large( def _check_state_size(
self, self,
pickle_state_size: int, pickle_state_size: int,
): ):
@ -2100,6 +2138,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
Args: Args:
pickle_state_size: The size of the pickled state. pickle_state_size: The size of the pickled state.
Raises:
StateTooLargeError: If the state is too large.
""" """
state_full_name = self.get_full_name() state_full_name = self.get_full_name()
if ( if (
@ -2107,10 +2148,14 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
and pickle_state_size > TOO_LARGE_SERIALIZED_STATE and pickle_state_size > TOO_LARGE_SERIALIZED_STATE
and self.substates and self.substates
): ):
console.warn( msg = (
f"State {state_full_name} serializes to {pickle_state_size} bytes " f"State {state_full_name} serializes to {pickle_state_size} bytes "
"which may present performance issues. Consider reducing the size of this state." + "which may present performance issues. Consider reducing the size of this state."
) )
if environment.REFLEX_PERF_MODE.get() == PerformanceMode.WARN:
console.warn(msg)
elif environment.REFLEX_PERF_MODE.get() == PerformanceMode.RAISE:
raise StateTooLargeError(msg)
_WARNED_ABOUT_STATE_SIZE.add(state_full_name) _WARNED_ABOUT_STATE_SIZE.add(state_full_name)
@classmethod @classmethod
@ -2149,11 +2194,14 @@ 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""
error = ""
try: try:
pickle_state = pickle.dumps((self._to_schema(), self)) payload = pickle.dumps((self._to_schema(), self))
self._warn_if_too_large(len(pickle_state))
return pickle_state
except HANDLED_PICKLE_ERRORS as og_pickle_error: except HANDLED_PICKLE_ERRORS as og_pickle_error:
error = ( error = (
f"Failed to serialize state {self.get_full_name()} due to unpicklable object. " f"Failed to serialize state {self.get_full_name()} due to unpicklable object. "
@ -2162,7 +2210,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
try: try:
import dill import dill
return dill.dumps((self._to_schema(), self)) payload = dill.dumps((self._to_schema(), self))
except ImportError: except ImportError:
error += ( error += (
f"Pickle error: {og_pickle_error}. " f"Pickle error: {og_pickle_error}. "
@ -2171,7 +2219,14 @@ 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)
return b""
if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF:
self._check_state_size(len(payload))
if not payload:
raise StateSerializationError(error)
return payload
@classmethod @classmethod
def _deserialize( def _deserialize(
@ -2367,6 +2422,23 @@ class ComponentState(State, mixin=True):
# The number of components created from this class. # The number of components created from this class.
_per_component_state_instance_count: ClassVar[int] = 0 _per_component_state_instance_count: ClassVar[int] = 0
def __init__(self, *args, **kwargs):
"""Do not allow direct initialization of the ComponentState.
Args:
*args: The args to pass to the State init method.
**kwargs: The kwargs to pass to the State init method.
Raises:
ReflexRuntimeError: If the ComponentState is initialized directly.
"""
if type(self)._mixin:
raise ReflexRuntimeError(
f"{ComponentState.__name__} {type(self).__name__} is not meant to be initialized directly. "
+ "Use the `create` method to create a new instance and access the state via the `State` attribute."
)
super().__init__(*args, **kwargs)
@classmethod @classmethod
def __init_subclass__(cls, mixin: bool = True, **kwargs): def __init_subclass__(cls, mixin: bool = True, **kwargs):
"""Overwrite mixin default to True. """Overwrite mixin default to True.
@ -3526,7 +3598,16 @@ class MutableProxy(wrapt.ObjectProxy):
pydantic.BaseModel.__dict__ pydantic.BaseModel.__dict__
) )
__mutable_types__ = (list, dict, set, Base, DeclarativeBase) # These types will be wrapped in MutableProxy
__mutable_types__ = (
list,
dict,
set,
Base,
DeclarativeBase,
BaseModelV2,
BaseModelV1,
)
def __init__(self, wrapped: Any, state: BaseState, field_name: str): def __init__(self, wrapped: Any, state: BaseState, field_name: str):
"""Create a proxy for a mutable object that tracks changes. """Create a proxy for a mutable object that tracks changes.
@ -3541,6 +3622,14 @@ class MutableProxy(wrapt.ObjectProxy):
self._self_state = state self._self_state = state
self._self_field_name = field_name self._self_field_name = field_name
def __repr__(self) -> str:
"""Get the representation of the wrapped object.
Returns:
The representation of the wrapped object.
"""
return f"{type(self).__name__}({self.__wrapped__})"
def _mark_dirty( def _mark_dirty(
self, self,
wrapped=None, wrapped=None,
@ -3566,6 +3655,18 @@ class MutableProxy(wrapt.ObjectProxy):
if wrapped is not None: if wrapped is not None:
return wrapped(*args, **(kwargs or {})) return wrapped(*args, **(kwargs or {}))
@classmethod
def _is_mutable_type(cls, value: Any) -> bool:
"""Check if a value is of a mutable type and should be wrapped.
Args:
value: The value to check.
Returns:
Whether the value is of a mutable type.
"""
return isinstance(value, cls.__mutable_types__)
def _wrap_recursive(self, value: Any) -> Any: def _wrap_recursive(self, value: Any) -> Any:
"""Wrap a value recursively if it is mutable. """Wrap a value recursively if it is mutable.
@ -3576,9 +3677,7 @@ class MutableProxy(wrapt.ObjectProxy):
The wrapped value. The wrapped value.
""" """
# Recursively wrap mutable types, but do not re-wrap MutableProxy instances. # Recursively wrap mutable types, but do not re-wrap MutableProxy instances.
if isinstance(value, self.__mutable_types__) and not isinstance( if self._is_mutable_type(value) and not isinstance(value, MutableProxy):
value, MutableProxy
):
return type(self)( return type(self)(
wrapped=value, wrapped=value,
state=self._self_state, state=self._self_state,
@ -3636,7 +3735,7 @@ class MutableProxy(wrapt.ObjectProxy):
self._wrap_recursive_decorator, self._wrap_recursive_decorator,
) )
if isinstance(value, self.__mutable_types__) and __name not in ( if self._is_mutable_type(value) and __name not in (
"__wrapped__", "__wrapped__",
"_self_state", "_self_state",
): ):

Some files were not shown because too many files have changed in this diff Show More