Merge branch 'main' into lendemor/expose_get_state_at_top_level

This commit is contained in:
Lendemor 2024-10-23 18:17:35 +02:00
commit 414bb01e9c
28 changed files with 3604 additions and 219 deletions

View File

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

80
poetry.lock generated
View File

@ -570,18 +570,18 @@ test = ["pytest (>=6)"]
[[package]]
name = "fastapi"
version = "0.115.2"
version = "0.115.3"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
{file = "fastapi-0.115.2-py3-none-any.whl", hash = "sha256:61704c71286579cc5a598763905928f24ee98bfcc07aabe84cfefb98812bbc86"},
{file = "fastapi-0.115.2.tar.gz", hash = "sha256:3995739e0b09fa12f984bce8fa9ae197b35d433750d3d312422d846e283697ee"},
{file = "fastapi-0.115.3-py3-none-any.whl", hash = "sha256:8035e8f9a2b0aa89cea03b6c77721178ed5358e1aea4cd8570d9466895c0638c"},
{file = "fastapi-0.115.3.tar.gz", hash = "sha256:c091c6a35599c036d676fa24bd4a6e19fa30058d93d950216cdc672881f6f7db"},
]
[package.dependencies]
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
starlette = ">=0.37.2,<0.41.0"
starlette = ">=0.40.0,<0.42.0"
typing-extensions = ">=4.8.0"
[package.extras]
@ -970,13 +970,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
[[package]]
name = "mako"
version = "1.3.5"
version = "1.3.6"
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
optional = false
python-versions = ">=3.8"
files = [
{file = "Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a"},
{file = "Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"},
{file = "Mako-1.3.6-py3-none-any.whl", hash = "sha256:a91198468092a2f1a0de86ca92690fb0cfc43ca90ee17e15d93662b4c04b241a"},
{file = "mako-1.3.6.tar.gz", hash = "sha256:9ec3a1583713479fae654f83ed9fa8c9a4c16b7bb0daba0e6bbebff50c0d983d"},
]
[package.dependencies]
@ -1977,6 +1977,20 @@ files = [
[package.dependencies]
six = ">=1.5"
[[package]]
name = "python-dotenv"
version = "1.0.1"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.8"
files = [
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
]
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "python-engineio"
version = "4.10.1"
@ -2253,13 +2267,13 @@ idna2008 = ["idna"]
[[package]]
name = "rich"
version = "13.9.2"
version = "13.9.3"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"},
{file = "rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c"},
{file = "rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283"},
{file = "rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e"},
]
[package.dependencies]
@ -2272,29 +2286,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.6.9"
version = "0.7.0"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"},
{file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"},
{file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"},
{file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"},
{file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"},
{file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"},
{file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"},
{file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"},
{file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"},
{file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"},
{file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"},
{file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"},
{file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"},
{file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"},
{file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"},
{file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"},
{file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"},
{file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"},
{file = "ruff-0.7.0-py3-none-linux_armv6l.whl", hash = "sha256:0cdf20c2b6ff98e37df47b2b0bd3a34aaa155f59a11182c1303cce79be715628"},
{file = "ruff-0.7.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:496494d350c7fdeb36ca4ef1c9f21d80d182423718782222c29b3e72b3512737"},
{file = "ruff-0.7.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:214b88498684e20b6b2b8852c01d50f0651f3cc6118dfa113b4def9f14faaf06"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630fce3fefe9844e91ea5bbf7ceadab4f9981f42b704fae011bb8efcaf5d84be"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:211d877674e9373d4bb0f1c80f97a0201c61bcd1e9d045b6e9726adc42c156aa"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:194d6c46c98c73949a106425ed40a576f52291c12bc21399eb8f13a0f7073495"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:82c2579b82b9973a110fab281860403b397c08c403de92de19568f32f7178598"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9af971fe85dcd5eaed8f585ddbc6bdbe8c217fb8fcf510ea6bca5bdfff56040e"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b641c7f16939b7d24b7bfc0be4102c56562a18281f84f635604e8a6989948914"},
{file = "ruff-0.7.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d71672336e46b34e0c90a790afeac8a31954fd42872c1f6adaea1dff76fd44f9"},
{file = "ruff-0.7.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ab7d98c7eed355166f367597e513a6c82408df4181a937628dbec79abb2a1fe4"},
{file = "ruff-0.7.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1eb54986f770f49edb14f71d33312d79e00e629a57387382200b1ef12d6a4ef9"},
{file = "ruff-0.7.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dc452ba6f2bb9cf8726a84aa877061a2462afe9ae0ea1d411c53d226661c601d"},
{file = "ruff-0.7.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4b406c2dce5be9bad59f2de26139a86017a517e6bcd2688da515481c05a2cb11"},
{file = "ruff-0.7.0-py3-none-win32.whl", hash = "sha256:f6c968509f767776f524a8430426539587d5ec5c662f6addb6aa25bc2e8195ec"},
{file = "ruff-0.7.0-py3-none-win_amd64.whl", hash = "sha256:ff4aabfbaaba880e85d394603b9e75d32b0693152e16fa659a3064a85df7fce2"},
{file = "ruff-0.7.0-py3-none-win_arm64.whl", hash = "sha256:10842f69c245e78d6adec7e1db0a7d9ddc2fff0621d730e61657b64fa36f207e"},
{file = "ruff-0.7.0.tar.gz", hash = "sha256:47a86360cf62d9cd53ebfb0b5eb0e882193fc191c6d717e8bef4462bc3b9ea2b"},
]
[[package]]
@ -2525,13 +2539,13 @@ SQLAlchemy = ">=2.0.14,<2.1.0"
[[package]]
name = "starlette"
version = "0.40.0"
version = "0.41.0"
description = "The little ASGI library that shines."
optional = false
python-versions = ">=3.8"
files = [
{file = "starlette-0.40.0-py3-none-any.whl", hash = "sha256:c494a22fae73805376ea6bf88439783ecfba9aac88a43911b48c653437e784c4"},
{file = "starlette-0.40.0.tar.gz", hash = "sha256:1a3139688fb298ce5e2d661d37046a66ad996ce94be4d4983be019a23a04ea35"},
{file = "starlette-0.41.0-py3-none-any.whl", hash = "sha256:a0193a3c413ebc9c78bff1c3546a45bb8c8bcb4a84cae8747d650a65bd37210a"},
{file = "starlette-0.41.0.tar.gz", hash = "sha256:39cbd8768b107d68bfe1ff1672b38a2c38b49777de46d2a592841d58e3bf7c2a"},
]
[package.dependencies]
@ -3033,4 +3047,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "edb7145394dd61f5a665b5519cc0c091c8c1628200ea1170857cff1a6bdb829e"
content-hash = "c5da15520cef58124f6699007c81158036840469d4f9972592d72bd456c45e7e"

View File

@ -33,6 +33,7 @@ jinja2 = ">=3.1.2,<4.0"
psutil = ">=5.9.4,<7.0"
pydantic = ">=1.10.2,<3.0"
python-multipart = ">=0.0.5,<0.1"
python-dotenv = ">=1.0.1"
python-socketio = ">=5.7.0,<6.0"
redis = ">=4.3.5,<6.0"
rich = ">=13.0.0,<14.0"
@ -68,7 +69,7 @@ darglint = ">=1.8.1,<2.0"
toml = ">=0.10.2,<1.0"
pytest-asyncio = ">=0.24.0"
pytest-cov = ">=4.0.0,<6.0"
ruff = "^0.6.9"
ruff = "^0.7.0"
pandas = ">=2.1.1,<3.0"
pillow = ">=10.0.0,<12.0"
plotly = ">=5.13.0,<6.0"
@ -91,7 +92,7 @@ build-backend = "poetry.core.masonry.api"
[tool.ruff]
target-version = "py39"
lint.select = ["B", "D", "E", "F", "I", "SIM", "W"]
lint.ignore = ["B008", "D203", "D205", "D213", "D401", "D406", "D407", "E501", "F403", "F405", "F541"]
lint.ignore = ["B008", "D203", "D205", "D213", "D401", "D406", "D407", "E501", "F403", "F405", "F541", "SIM115"]
lint.pydocstyle.convention = "google"
[tool.ruff.lint.per-file-ignores]

View File

@ -0,0 +1,29 @@
import { useEffect, useState } from "react"
import { codeToHtml} from "shiki"
export function Code ({code, theme, language, transformers, ...divProps}) {
const [codeResult, setCodeResult] = useState("")
useEffect(() => {
async function fetchCode() {
let final_code;
if (Array.isArray(code)) {
final_code = code[0];
} else {
final_code = code;
}
const result = await codeToHtml(final_code, {
lang: language,
theme,
transformers
});
setCodeResult(result);
}
fetchCode();
}, [code, language, theme, transformers]
)
return (
<div dangerouslySetInnerHTML={{__html: codeResult}} {...divProps} ></div>
)
}

View File

@ -320,15 +320,18 @@ _MAPPING: dict = {
"upload_files",
"window_alert",
],
"istate.storage": [
"Cookie",
"LocalStorage",
"SessionStorage",
],
"middleware": ["middleware", "Middleware"],
"model": ["session", "Model"],
"state": [
"var",
"Cookie",
"LocalStorage",
"SessionStorage",
"ComponentState",
"State",
"dynamic",
],
"istate.wrappers": ["get_state"],
"style": ["Style", "toggle_color_mode"],

View File

@ -174,6 +174,9 @@ from .event import stop_propagation as stop_propagation
from .event import upload_files as upload_files
from .event import window_alert as window_alert
from .experimental import _x as _x
from .istate.storage import Cookie as Cookie
from .istate.storage import LocalStorage as LocalStorage
from .istate.storage import SessionStorage as SessionStorage
from .istate.wrappers import get_state as get_state
from .middleware import Middleware as Middleware
from .middleware import middleware as middleware
@ -181,10 +184,8 @@ from .model import Model as Model
from .model import session as session
from .page import page as page
from .state import ComponentState as ComponentState
from .state import Cookie as Cookie
from .state import LocalStorage as LocalStorage
from .state import SessionStorage as SessionStorage
from .state import State as State
from .state import dynamic as dynamic
from .state import var as var
from .style import Style as Style
from .style import toggle_color_mode as toggle_color_mode

View File

@ -28,7 +28,8 @@ from reflex.components.base import (
Title,
)
from reflex.components.component import Component, ComponentStyle, CustomComponent
from reflex.state import BaseState, Cookie, LocalStorage, SessionStorage
from reflex.istate.storage import Cookie, LocalStorage, SessionStorage
from reflex.state import BaseState
from reflex.style import Style
from reflex.utils import console, format, imports, path_ops
from reflex.utils.imports import ImportVar, ParsedImportDict

View File

@ -109,19 +109,6 @@ class DataEditorTheme(Base):
text_medium: Optional[str] = None
def on_edit_spec(pos, data: dict[str, Any]):
"""The on edit spec function.
Args:
pos: The position of the edit event.
data: The data of the edit event.
Returns:
The position and data.
"""
return [pos, data]
class Bounds(TypedDict):
"""The bounds of the group header."""
@ -149,7 +136,7 @@ class Rectangle(TypedDict):
class GridSelectionCurrent(TypedDict):
"""The current selection."""
cell: list[int]
cell: tuple[int, int]
range: Rectangle
rangeStack: list[Rectangle]
@ -167,7 +154,7 @@ class GroupHeaderClickedEventArgs(TypedDict):
kind: str
group: str
location: list[int]
location: tuple[int, int]
bounds: Bounds
isEdge: bool
shiftKey: bool
@ -178,7 +165,7 @@ class GroupHeaderClickedEventArgs(TypedDict):
localEventY: int
button: int
buttons: int
scrollEdge: list[int]
scrollEdge: tuple[int, int]
class GridCell(TypedDict):
@ -306,10 +293,10 @@ class DataEditor(NoSSRComponent):
on_cell_context_menu: EventHandler[identity_event(Tuple[int, int])]
# Fired when a cell is edited.
on_cell_edited: EventHandler[on_edit_spec]
on_cell_edited: EventHandler[identity_event(Tuple[int, int], GridCell)]
# Fired when a group header is clicked.
on_group_header_clicked: EventHandler[on_edit_spec]
on_group_header_clicked: EventHandler[identity_event(Tuple[int, int], GridCell)]
# Fired when a group header is right-clicked.
on_group_header_context_menu: EventHandler[
@ -335,7 +322,9 @@ class DataEditor(NoSSRComponent):
on_delete: EventHandler[identity_event(GridSelection)]
# Fired when editing is finished.
on_finished_editing: EventHandler[identity_event(Union[GridCell, None], list[int])]
on_finished_editing: EventHandler[
identity_event(Union[GridCell, None], tuple[int, int])
]
# Fired when a row is appended.
on_row_appended: EventHandler[empty_event]

View File

@ -78,8 +78,6 @@ class DataEditorTheme(Base):
text_light: Optional[str]
text_medium: Optional[str]
def on_edit_spec(pos, data: dict[str, Any]): ...
class Bounds(TypedDict):
x: int
y: int
@ -96,7 +94,7 @@ class Rectangle(TypedDict):
height: int
class GridSelectionCurrent(TypedDict):
cell: list[int]
cell: tuple[int, int]
range: Rectangle
rangeStack: list[Rectangle]
@ -108,7 +106,7 @@ class GridSelection(TypedDict):
class GroupHeaderClickedEventArgs(TypedDict):
kind: str
group: str
location: list[int]
location: tuple[int, int]
bounds: Bounds
isEdge: bool
shiftKey: bool
@ -119,7 +117,7 @@ class GroupHeaderClickedEventArgs(TypedDict):
localEventY: int
button: int
buttons: int
scrollEdge: list[int]
scrollEdge: tuple[int, int]
class GridCell(TypedDict):
span: Optional[List[int]]
@ -189,17 +187,17 @@ class DataEditor(NoSSRComponent):
on_cell_activated: Optional[EventType[tuple[int, int]]] = None,
on_cell_clicked: Optional[EventType[tuple[int, int]]] = None,
on_cell_context_menu: Optional[EventType[tuple[int, int]]] = None,
on_cell_edited: Optional[EventType] = None,
on_cell_edited: Optional[EventType[tuple[int, int], GridCell]] = None,
on_click: Optional[EventType[[]]] = None,
on_column_resize: Optional[EventType[GridColumn, int]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_delete: Optional[EventType[GridSelection]] = None,
on_double_click: Optional[EventType[[]]] = None,
on_finished_editing: Optional[
EventType[Union[GridCell, None], list[int]]
EventType[Union[GridCell, None], tuple[int, int]]
] = None,
on_focus: Optional[EventType[[]]] = None,
on_group_header_clicked: Optional[EventType] = None,
on_group_header_clicked: Optional[EventType[tuple[int, int], GridCell]] = None,
on_group_header_context_menu: Optional[
EventType[int, GroupHeaderClickedEventArgs]
] = None,

View File

@ -0,0 +1,813 @@
"""Shiki syntax hghlighter component."""
from __future__ import annotations
import re
from collections import defaultdict
from typing import Any, Literal, Optional, Union
from reflex.base import Base
from reflex.components.component import Component, ComponentNamespace
from reflex.components.core.colors import color
from reflex.components.core.cond import color_mode_cond
from reflex.components.el.elements.forms import Button
from reflex.components.lucide.icon import Icon
from reflex.components.radix.themes.layout.box import Box
from reflex.event import call_script, set_clipboard
from reflex.style import Style
from reflex.utils.exceptions import VarTypeError
from reflex.utils.imports import ImportVar
from reflex.vars.base import LiteralVar, Var
from reflex.vars.function import FunctionStringVar
from reflex.vars.sequence import StringVar, string_replace_operation
def copy_script() -> Any:
"""Copy script for the code block and modify the child SVG element.
Returns:
Any: The result of calling the script.
"""
return call_script(
f"""
// Event listener for the parent click
document.addEventListener('click', function(event) {{
// Find the closest div (parent element)
const parent = event.target.closest('div');
// If the parent is found
if (parent) {{
// Find the SVG element within the parent
const svgIcon = parent.querySelector('svg');
// If the SVG exists, proceed with the script
if (svgIcon) {{
const originalPath = svgIcon.innerHTML;
const checkmarkPath = '<polyline points="20 6 9 17 4 12"></polyline>'; // Checkmark SVG path
function transition(element, scale, opacity) {{
element.style.transform = `scale(${{scale}})`;
element.style.opacity = opacity;
}}
// Animate the SVG
transition(svgIcon, 0, '0');
setTimeout(() => {{
svgIcon.innerHTML = checkmarkPath; // Replace content with checkmark
svgIcon.setAttribute('viewBox', '0 0 24 24'); // Adjust viewBox if necessary
transition(svgIcon, 1, '1');
setTimeout(() => {{
transition(svgIcon, 0, '0');
setTimeout(() => {{
svgIcon.innerHTML = originalPath; // Restore original SVG content
transition(svgIcon, 1, '1');
}}, 125);
}}, 600);
}}, 125);
}} else {{
// console.error('SVG element not found within the parent.');
}}
}} else {{
// console.error('Parent element not found.');
}}
}});
"""
)
SHIKIJS_TRANSFORMER_FNS = {
"transformerNotationDiff",
"transformerNotationHighlight",
"transformerNotationWordHighlight",
"transformerNotationFocus",
"transformerNotationErrorLevel",
"transformerRenderWhitespace",
"transformerMetaHighlight",
"transformerMetaWordHighlight",
"transformerCompactLineOptions",
# TODO: this transformer when included adds a weird behavior which removes other code lines. Need to figure out why.
# "transformerRemoveLineBreak",
"transformerRemoveNotationEscape",
}
LINE_NUMBER_STYLING = {
"code": {
"counter-reset": "step",
"counter-increment": "step 0",
"display": "grid",
"line-height": "1.7",
"font-size": "0.875em",
},
"code .line::before": {
"content": "counter(step)",
"counter-increment": "step",
"width": "1rem",
"margin-right": "1.5rem",
"display": "inline-block",
"text-align": "right",
"color": "rgba(115,138,148,.4)",
},
}
BOX_PARENT_STYLING = {
"pre": {
"margin": "0",
"padding": "24px",
"background": "transparent",
"overflow-x": "auto",
"border-radius": "6px",
},
}
THEME_MAPPING = {
"light": "one-light",
"dark": "one-dark-pro",
"a11y-dark": "github-dark",
}
LANGUAGE_MAPPING = {"bash": "shellscript"}
LiteralCodeLanguage = Literal[
"abap",
"actionscript-3",
"ada",
"angular-html",
"angular-ts",
"apache",
"apex",
"apl",
"applescript",
"ara",
"asciidoc",
"asm",
"astro",
"awk",
"ballerina",
"bat",
"beancount",
"berry",
"bibtex",
"bicep",
"blade",
"c",
"cadence",
"clarity",
"clojure",
"cmake",
"cobol",
"codeowners",
"codeql",
"coffee",
"common-lisp",
"coq",
"cpp",
"crystal",
"csharp",
"css",
"csv",
"cue",
"cypher",
"d",
"dart",
"dax",
"desktop",
"diff",
"docker",
"dotenv",
"dream-maker",
"edge",
"elixir",
"elm",
"emacs-lisp",
"erb",
"erlang",
"fennel",
"fish",
"fluent",
"fortran-fixed-form",
"fortran-free-form",
"fsharp",
"gdresource",
"gdscript",
"gdshader",
"genie",
"gherkin",
"git-commit",
"git-rebase",
"gleam",
"glimmer-js",
"glimmer-ts",
"glsl",
"gnuplot",
"go",
"graphql",
"groovy",
"hack",
"haml",
"handlebars",
"haskell",
"haxe",
"hcl",
"hjson",
"hlsl",
"html",
"html-derivative",
"http",
"hxml",
"hy",
"imba",
"ini",
"java",
"javascript",
"jinja",
"jison",
"json",
"json5",
"jsonc",
"jsonl",
"jsonnet",
"jssm",
"jsx",
"julia",
"kotlin",
"kusto",
"latex",
"lean",
"less",
"liquid",
"log",
"logo",
"lua",
"luau",
"make",
"markdown",
"marko",
"matlab",
"mdc",
"mdx",
"mermaid",
"mojo",
"move",
"narrat",
"nextflow",
"nginx",
"nim",
"nix",
"nushell",
"objective-c",
"objective-cpp",
"ocaml",
"pascal",
"perl",
"php",
"plsql",
"po",
"postcss",
"powerquery",
"powershell",
"prisma",
"prolog",
"proto",
"pug",
"puppet",
"purescript",
"python",
"qml",
"qmldir",
"qss",
"r",
"racket",
"raku",
"razor",
"reg",
"regexp",
"rel",
"riscv",
"rst",
"ruby",
"rust",
"sas",
"sass",
"scala",
"scheme",
"scss",
"shaderlab",
"shellscript",
"shellsession",
"smalltalk",
"solidity",
"soy",
"sparql",
"splunk",
"sql",
"ssh-config",
"stata",
"stylus",
"svelte",
"swift",
"system-verilog",
"systemd",
"tasl",
"tcl",
"templ",
"terraform",
"tex",
"toml",
"ts-tags",
"tsv",
"tsx",
"turtle",
"twig",
"typescript",
"typespec",
"typst",
"v",
"vala",
"vb",
"verilog",
"vhdl",
"viml",
"vue",
"vue-html",
"vyper",
"wasm",
"wenyan",
"wgsl",
"wikitext",
"wolfram",
"xml",
"xsl",
"yaml",
"zenscript",
"zig",
]
LiteralCodeTheme = Literal[
"andromeeda",
"aurora-x",
"ayu-dark",
"catppuccin-frappe",
"catppuccin-latte",
"catppuccin-macchiato",
"catppuccin-mocha",
"dark-plus",
"dracula",
"dracula-soft",
"everforest-dark",
"everforest-light",
"github-dark",
"github-dark-default",
"github-dark-dimmed",
"github-dark-high-contrast",
"github-light",
"github-light-default",
"github-light-high-contrast",
"houston",
"laserwave",
"light-plus",
"material-theme",
"material-theme-darker",
"material-theme-lighter",
"material-theme-ocean",
"material-theme-palenight",
"min-dark",
"min-light",
"monokai",
"night-owl",
"nord",
"one-dark-pro",
"one-light",
"plain",
"plastic",
"poimandres",
"red",
"rose-pine",
"rose-pine-dawn",
"rose-pine-moon",
"slack-dark",
"slack-ochin",
"snazzy-light",
"solarized-dark",
"solarized-light",
"synthwave-84",
"tokyo-night",
"vesper",
"vitesse-black",
"vitesse-dark",
"vitesse-light",
]
class ShikiBaseTransformers(Base):
"""Base for creating transformers."""
library: str
fns: list[FunctionStringVar]
style: Optional[Style]
class ShikiJsTransformer(ShikiBaseTransformers):
"""A Wrapped shikijs transformer."""
library: str = "@shikijs/transformers"
fns: list[FunctionStringVar] = [
FunctionStringVar.create(fn) for fn in SHIKIJS_TRANSFORMER_FNS
]
style: Optional[Style] = Style(
{
"code": {"line-height": "1.7", "font-size": "0.875em", "display": "grid"},
# Diffs
".diff": {
"margin": "0 -24px",
"padding": "0 24px",
"width": "calc(100% + 48px)",
"display": "inline-block",
},
".diff.add": {
"background-color": "rgba(16, 185, 129, .14)",
"position": "relative",
},
".diff.remove": {
"background-color": "rgba(244, 63, 94, .14)",
"opacity": "0.7",
"position": "relative",
},
".diff.remove:after": {
"position": "absolute",
"left": "10px",
"content": "'-'",
"color": "#b34e52",
},
".diff.add:after": {
"position": "absolute",
"left": "10px",
"content": "'+'",
"color": "#18794e",
},
# Highlight
".highlighted": {
"background-color": "rgba(142, 150, 170, .14)",
"margin": "0 -24px",
"padding": "0 24px",
"width": "calc(100% + 48px)",
"display": "inline-block",
},
".highlighted.error": {
"background-color": "rgba(244, 63, 94, .14)",
},
".highlighted.warning": {
"background-color": "rgba(234, 179, 8, .14)",
},
# Highlighted Word
".highlighted-word": {
"background-color": color("gray", 2),
"border": f"1px solid {color('gray', 5)}",
"padding": "1px 3px",
"margin": "-1px -3px",
"border-radius": "4px",
},
# Focused Lines
".has-focused .line:not(.focused)": {
"opacity": "0.7",
"filter": "blur(0.095rem)",
"transition": "filter .35s, opacity .35s",
},
".has-focused:hover .line:not(.focused)": {
"opacity": "1",
"filter": "none",
},
# White Space
# ".tab, .space": {
# "position": "relative",
# },
# ".tab::before": {
# "content": "'⇥'",
# "position": "absolute",
# "opacity": "0.3",
# },
# ".space::before": {
# "content": "'·'",
# "position": "absolute",
# "opacity": "0.3",
# },
}
)
def __init__(self, **kwargs):
"""Initialize the transformer.
Args:
kwargs: Kwargs to initialize the props.
"""
fns = kwargs.pop("fns", None)
style = kwargs.pop("style", None)
if fns:
kwargs["fns"] = [
(
FunctionStringVar.create(x)
if not isinstance(x, FunctionStringVar)
else x
)
for x in fns
]
if style:
kwargs["style"] = Style(style)
super().__init__(**kwargs)
class ShikiCodeBlock(Component):
"""A Code block."""
library = "/components/shiki/code"
tag = "Code"
alias = "ShikiCode"
lib_dependencies: list[str] = ["shiki"]
# The language to use.
language: Var[LiteralCodeLanguage] = Var.create("python")
# The theme to use ("light" or "dark").
theme: Var[LiteralCodeTheme] = Var.create("one-light")
# The set of themes to use for different modes.
themes: Var[Union[list[dict[str, Any]], dict[str, str]]]
# The code to display.
code: Var[str]
# The transformers to use for the syntax highlighter.
transformers: Var[list[Union[ShikiBaseTransformers, dict[str, Any]]]] = Var.create(
[]
)
@classmethod
def create(
cls,
*children,
**props,
) -> Component:
"""Create a code block component using [shiki syntax highlighter](https://shiki.matsu.io/).
Args:
*children: The children of the component.
**props: The props to pass to the component.
Returns:
The code block component.
"""
# Separate props for the code block and the wrapper
code_block_props = {}
code_wrapper_props = {}
class_props = cls.get_props()
# Distribute props between the code block and wrapper
for key, value in props.items():
(code_block_props if key in class_props else code_wrapper_props)[key] = (
value
)
code_block_props["code"] = children[0]
code_block = super().create(**code_block_props)
transformer_styles = {}
# Collect styles from transformers and wrapper
for transformer in code_block.transformers._var_value: # type: ignore
if isinstance(transformer, ShikiBaseTransformers) and transformer.style:
transformer_styles.update(transformer.style)
transformer_styles.update(code_wrapper_props.pop("style", {}))
return Box.create(
code_block,
*children[1:],
style=Style({**transformer_styles, **BOX_PARENT_STYLING}),
**code_wrapper_props,
)
def add_imports(self) -> dict[str, list[str]]:
"""Add the necessary imports.
We add all referenced transformer functions as imports from their corresponding
libraries.
Returns:
Imports for the component.
"""
imports = defaultdict(list)
for transformer in self.transformers._var_value:
if isinstance(transformer, ShikiBaseTransformers):
imports[transformer.library].extend(
[ImportVar(tag=str(fn)) for fn in transformer.fns]
)
(
self.lib_dependencies.append(transformer.library)
if transformer.library not in self.lib_dependencies
else None
)
return imports
@classmethod
def create_transformer(cls, library: str, fns: list[str]) -> ShikiBaseTransformers:
"""Create a transformer from a third party library.
Args:
library: The name of the library.
fns: The str names of the functions/callables to invoke from the library.
Returns:
A transformer for the specified library.
Raises:
ValueError: If a supplied function name is not valid str.
"""
if any(not isinstance(fn_name, str) for fn_name in fns):
raise ValueError(
f"the function names should be str names of functions in the specified transformer: {library!r}"
)
return ShikiBaseTransformers( # type: ignore
library=library, fns=[FunctionStringVar.create(fn) for fn in fns]
)
def _render(self, props: dict[str, Any] | None = None):
"""Renders the component with the given properties, processing transformers if present.
Args:
props: Optional properties to pass to the render function.
Returns:
Rendered component output.
"""
# Ensure props is initialized from class attributes if not provided
props = props or {
attr.rstrip("_"): getattr(self, attr) for attr in self.get_props()
}
# Extract transformers and apply transformations
transformers = props.get("transformers")
if transformers is not None:
transformed_values = self._process_transformers(transformers._var_value)
props["transformers"] = LiteralVar.create(transformed_values)
return super()._render(props)
def _process_transformers(self, transformer_list: list) -> list:
"""Processes a list of transformers, applying transformations where necessary.
Args:
transformer_list: List of transformer objects or values.
Returns:
list: A list of transformed values.
"""
processed = []
for transformer in transformer_list:
if isinstance(transformer, ShikiBaseTransformers):
processed.extend(fn.call() for fn in transformer.fns)
else:
processed.append(transformer)
return processed
class ShikiHighLevelCodeBlock(ShikiCodeBlock):
"""High level component for the shiki syntax highlighter."""
# If this is enabled, the default transformers(shikijs transformer) will be used.
use_transformers: Var[bool]
# If this is enabled line numbers will be shown next to the code block.
show_line_numbers: Var[bool]
# Whether a copy button should appear.
can_copy: Var[bool] = Var.create(False)
# copy_button: A custom copy button to override the default one.
copy_button: Var[Optional[Union[Component, bool]]] = Var.create(None)
@classmethod
def create(
cls,
*children,
**props,
) -> Component:
"""Create a code block component using [shiki syntax highlighter](https://shiki.matsu.io/).
Args:
*children: The children of the component.
**props: The props to pass to the component.
Returns:
The code block component.
"""
use_transformers = props.pop("use_transformers", False)
show_line_numbers = props.pop("show_line_numbers", False)
language = props.pop("language", None)
can_copy = props.pop("can_copy", False)
copy_button = props.pop("copy_button", None)
if use_transformers:
props["transformers"] = [ShikiJsTransformer()]
if language is not None:
props["language"] = cls._map_languages(language)
# line numbers are generated via css
if show_line_numbers:
props["style"] = {**LINE_NUMBER_STYLING, **props.get("style", {})}
theme = props.pop("theme", None)
props["theme"] = props["theme"] = (
cls._map_themes(theme)
if theme is not None
else color_mode_cond( # Default color scheme responds to global color mode.
light="one-light",
dark="one-dark-pro",
)
)
if can_copy:
code = children[0]
copy_button = ( # type: ignore
copy_button
if copy_button is not None
else Button.create(
Icon.create(tag="copy", size=16, color=color("gray", 11)),
on_click=[
set_clipboard(cls._strip_transformer_triggers(code)), # type: ignore
copy_script(),
],
style=Style(
{
"position": "absolute",
"top": "4px",
"right": "4px",
"background": color("gray", 3),
"border": "1px solid",
"border-color": color("gray", 5),
"border-radius": "6px",
"padding": "5px",
"opacity": "1",
"cursor": "pointer",
"_hover": {
"background": color("gray", 4),
},
"transition": "background 0.250s ease-out",
"&>svg": {
"transition": "transform 0.250s ease-out, opacity 0.250s ease-out",
},
"_active": {
"background": color("gray", 5),
},
}
),
)
)
if copy_button:
return ShikiCodeBlock.create(
children[0], copy_button, position="relative", **props
)
else:
return ShikiCodeBlock.create(children[0], **props)
@staticmethod
def _map_themes(theme: str) -> str:
if isinstance(theme, str) and theme in THEME_MAPPING:
return THEME_MAPPING[theme]
return theme
@staticmethod
def _map_languages(language: str) -> str:
if isinstance(language, str) and language in LANGUAGE_MAPPING:
return LANGUAGE_MAPPING[language]
return language
@staticmethod
def _strip_transformer_triggers(code: str | StringVar) -> StringVar | str:
if not isinstance(code, (StringVar, str)):
raise VarTypeError(
f"code should be string literal or a StringVar type. Got {type(code)} instead."
)
regex_pattern = r"[\/#]+ *\[!code.*?\]"
if isinstance(code, Var):
return string_replace_operation(
code, StringVar(_js_expr=f"/{regex_pattern}/g", _var_type=str), ""
)
if isinstance(code, str):
return re.sub(regex_pattern, "", code)
class TransformerNamespace(ComponentNamespace):
"""Namespace for the Transformers."""
shikijs = ShikiJsTransformer
class CodeblockNamespace(ComponentNamespace):
"""Namespace for the CodeBlock component."""
root = staticmethod(ShikiCodeBlock.create)
create_transformer = staticmethod(ShikiCodeBlock.create_transformer)
transformers = TransformerNamespace()
__call__ = staticmethod(ShikiHighLevelCodeBlock.create)
code_block = CodeblockNamespace()

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,8 @@ from reflex.components.tags.tag import Tag
from reflex.utils import types
from reflex.utils.imports import ImportDict, ImportVar
from reflex.vars.base import LiteralVar, Var
from reflex.vars.function import ARRAY_ISARRAY
from reflex.vars.number import ternary_operation
# Special vars used in the component map.
_CHILDREN = Var(_js_expr="children", _var_type=str)
@ -199,7 +201,16 @@ class Markdown(Component):
raise ValueError(f"No markdown component found for tag: {tag}.")
special_props = [_PROPS_IN_TAG]
children = [_CHILDREN]
children = [
_CHILDREN
if tag != "codeblock"
# For codeblock, the mapping for some cases returns an array of elements. Let's join them into a string.
else ternary_operation(
ARRAY_ISARRAY.call(_CHILDREN), # type: ignore
_CHILDREN.to(list).join("\n"),
_CHILDREN,
).to(str)
]
# For certain tags, the props from the markdown renderer are not actually valid for the component.
if tag in NO_PROPS_TAGS:

View File

@ -5,7 +5,9 @@
# ------------------------------------------------------
from typing import Any, Dict, Literal, Optional, Union, overload
from reflex.event import EventType
from reflex.event import (
EventType,
)
from reflex.style import Style
from reflex.vars.base import Var

View File

@ -1,5 +1,6 @@
"""React Player component for audio and video."""
from . import react_player
from .audio import Audio
from .video import Video

View File

@ -5,6 +5,7 @@
# ------------------------------------------------------
from typing import Any, Dict, Optional, Union, overload
import reflex
from reflex.components.react_player.react_player import ReactPlayer
from reflex.event import EventType
from reflex.style import Style
@ -58,7 +59,9 @@ class Audio(ReactPlayer):
on_play: Optional[EventType[[]]] = None,
on_playback_quality_change: Optional[EventType[[]]] = None,
on_playback_rate_change: Optional[EventType[[]]] = None,
on_progress: Optional[EventType] = None,
on_progress: Optional[
EventType[reflex.components.react_player.react_player.Progress]
] = None,
on_ready: Optional[EventType[[]]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_seek: Optional[EventType[float]] = None,

View File

@ -2,11 +2,22 @@
from __future__ import annotations
from typing_extensions import TypedDict
from reflex.components.component import NoSSRComponent
from reflex.event import EventHandler, empty_event, identity_event
from reflex.vars.base import Var
class Progress(TypedDict):
"""Callback containing played and loaded progress as a fraction, and playedSeconds and loadedSeconds in seconds."""
played: float
playedSeconds: float
loaded: float
loadedSeconds: float
class ReactPlayer(NoSSRComponent):
"""Using react-player and not implement all props and callback yet.
reference: https://github.com/cookpete/react-player.
@ -55,7 +66,7 @@ class ReactPlayer(NoSSRComponent):
on_play: EventHandler[empty_event]
# Callback containing played and loaded progress as a fraction, and playedSeconds and loadedSeconds in seconds. eg { played: 0.12, playedSeconds: 11.3, loaded: 0.34, loadedSeconds: 16.7 }
on_progress: EventHandler[lambda progress: [progress]]
on_progress: EventHandler[identity_event(Progress)]
# Callback containing duration of the media, in seconds.
on_duration: EventHandler[identity_event(float)]

View File

@ -5,11 +5,19 @@
# ------------------------------------------------------
from typing import Any, Dict, Optional, Union, overload
from typing_extensions import TypedDict
from reflex.components.component import NoSSRComponent
from reflex.event import EventType
from reflex.style import Style
from reflex.vars.base import Var
class Progress(TypedDict):
played: float
playedSeconds: float
loaded: float
loadedSeconds: float
class ReactPlayer(NoSSRComponent):
@overload
@classmethod
@ -56,7 +64,7 @@ class ReactPlayer(NoSSRComponent):
on_play: Optional[EventType[[]]] = None,
on_playback_quality_change: Optional[EventType[[]]] = None,
on_playback_rate_change: Optional[EventType[[]]] = None,
on_progress: Optional[EventType] = None,
on_progress: Optional[EventType[Progress]] = None,
on_ready: Optional[EventType[[]]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_seek: Optional[EventType[float]] = None,

View File

@ -5,6 +5,7 @@
# ------------------------------------------------------
from typing import Any, Dict, Optional, Union, overload
import reflex
from reflex.components.react_player.react_player import ReactPlayer
from reflex.event import EventType
from reflex.style import Style
@ -58,7 +59,9 @@ class Video(ReactPlayer):
on_play: Optional[EventType[[]]] = None,
on_playback_quality_change: Optional[EventType[[]]] = None,
on_playback_rate_change: Optional[EventType[[]]] = None,
on_progress: Optional[EventType] = None,
on_progress: Optional[
EventType[reflex.components.react_player.react_player.Progress]
] = None,
on_ready: Optional[EventType[[]]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_seek: Optional[EventType[float]] = None,

View File

@ -418,6 +418,12 @@ class Config(Base):
# Number of gunicorn workers from user
gunicorn_workers: Optional[int] = None
# Number of requests before a worker is restarted
gunicorn_max_requests: int = 100
# Variance limit for max requests; gunicorn only
gunicorn_max_requests_jitter: int = 25
# Indicate which type of state manager to use
state_manager_mode: constants.StateManagerMode = constants.StateManagerMode.DISK
@ -430,6 +436,9 @@ class Config(Base):
# Attributes that were explicitly set by the user.
_non_default_attributes: Set[str] = pydantic.PrivateAttr(set())
# Path to file containing key-values pairs to override in the environment; Dotenv format.
env_file: Optional[str] = None
def __init__(self, *args, **kwargs):
"""Initialize the config values.
@ -471,6 +480,7 @@ class Config(Base):
def update_from_env(self) -> dict[str, Any]:
"""Update the config values based on set environment variables.
If there is a set env_file, it is loaded first.
Returns:
The updated config values.
@ -480,6 +490,12 @@ class Config(Base):
"""
from reflex.utils.exceptions import EnvVarValueError
if self.env_file:
from dotenv import load_dotenv
# load env file if exists
load_dotenv(self.env_file, override=True)
updated_values = {}
# Iterate over the fields.
for key, field in self.__fields__.items():

View File

@ -2,6 +2,7 @@
from types import SimpleNamespace
from reflex.components.datadisplay.shiki_code_block import code_block as code_block
from reflex.components.props import PropsBase
from reflex.components.radix.themes.components.progress import progress as progress
from reflex.components.sonner.toast import toast as toast
@ -67,4 +68,5 @@ _x = ExperimentalNamespace(
layout=layout,
PropsBase=PropsBase,
run_in_thread=run_in_thread,
code_block=code_block,
)

144
reflex/istate/storage.py Normal file
View File

@ -0,0 +1,144 @@
"""Client-side storage classes for reflex state variables."""
from __future__ import annotations
from typing import Any
from reflex.utils import format
class ClientStorageBase:
"""Base class for client-side storage."""
def options(self) -> dict[str, Any]:
"""Get the options for the storage.
Returns:
All set options for the storage (not None).
"""
return {
format.to_camel_case(k): v for k, v in vars(self).items() if v is not None
}
class Cookie(ClientStorageBase, str):
"""Represents a state Var that is stored as a cookie in the browser."""
name: str | None
path: str
max_age: int | None
domain: str | None
secure: bool | None
same_site: str
def __new__(
cls,
object: Any = "",
encoding: str | None = None,
errors: str | None = None,
/,
name: str | None = None,
path: str = "/",
max_age: int | None = None,
domain: str | None = None,
secure: bool | None = None,
same_site: str = "lax",
):
"""Create a client-side Cookie (str).
Args:
object: The initial object.
encoding: The encoding to use.
errors: The error handling scheme to use.
name: The name of the cookie on the client side.
path: Cookie path. Use / as the path if the cookie should be accessible on all pages.
max_age: Relative max age of the cookie in seconds from when the client receives it.
domain: Domain for the cookie (sub.domain.com or .allsubdomains.com).
secure: Is the cookie only accessible through HTTPS?
same_site: Whether the cookie is sent with third party requests.
One of (true|false|none|lax|strict)
Returns:
The client-side Cookie object.
Note: expires (absolute Date) is not supported at this time.
"""
if encoding or errors:
inst = super().__new__(cls, object, encoding or "utf-8", errors or "strict")
else:
inst = super().__new__(cls, object)
inst.name = name
inst.path = path
inst.max_age = max_age
inst.domain = domain
inst.secure = secure
inst.same_site = same_site
return inst
class LocalStorage(ClientStorageBase, str):
"""Represents a state Var that is stored in localStorage in the browser."""
name: str | None
sync: bool = False
def __new__(
cls,
object: Any = "",
encoding: str | None = None,
errors: str | None = None,
/,
name: str | None = None,
sync: bool = False,
) -> "LocalStorage":
"""Create a client-side localStorage (str).
Args:
object: The initial object.
encoding: The encoding to use.
errors: The error handling scheme to use.
name: The name of the storage key on the client side.
sync: Whether changes should be propagated to other tabs.
Returns:
The client-side localStorage object.
"""
if encoding or errors:
inst = super().__new__(cls, object, encoding or "utf-8", errors or "strict")
else:
inst = super().__new__(cls, object)
inst.name = name
inst.sync = sync
return inst
class SessionStorage(ClientStorageBase, str):
"""Represents a state Var that is stored in sessionStorage in the browser."""
name: str | None
def __new__(
cls,
object: Any = "",
encoding: str | None = None,
errors: str | None = None,
/,
name: str | None = None,
) -> "SessionStorage":
"""Create a client-side sessionStorage (str).
Args:
object: The initial object.
encoding: The encoding to use.
errors: The error handling scheme to use
name: The name of the storage on the client side
Returns:
The client-side sessionStorage object.
"""
if encoding or errors:
inst = super().__new__(cls, object, encoding or "utf-8", errors or "strict")
else:
inst = super().__new__(cls, object)
inst.name = name
return inst

View File

@ -30,6 +30,7 @@ from typing import (
Set,
Tuple,
Type,
TypeVar,
Union,
cast,
get_args,
@ -42,6 +43,9 @@ from typing_extensions import Self
from reflex import event
from reflex.config import get_config
from reflex.istate.data import RouterData
from reflex.istate.storage import (
ClientStorageBase,
)
from reflex.vars.base import (
ComputedVar,
DynamicRouteVar,
@ -76,6 +80,7 @@ from reflex.utils import console, format, path_ops, prerequisites, types
from reflex.utils.exceptions import (
ComputedVarShadowsBaseVars,
ComputedVarShadowsStateVar,
DynamicComponentInvalidSignature,
DynamicRouteArgShadowsStateVar,
EventHandlerShadowsBuiltInStateMethod,
ImmutableStateError,
@ -2092,6 +2097,51 @@ class State(BaseState):
is_hydrated: bool = False
T = TypeVar("T", bound=BaseState)
def dynamic(func: Callable[[T], Component]):
"""Create a dynamically generated components from a state class.
Args:
func: The function to generate the component.
Returns:
The dynamically generated component.
Raises:
DynamicComponentInvalidSignature: If the function does not have exactly one parameter.
DynamicComponentInvalidSignature: If the function does not have a type hint for the state class.
"""
number_of_parameters = len(inspect.signature(func).parameters)
func_signature = get_type_hints(func)
if "return" in func_signature:
func_signature.pop("return")
values = list(func_signature.values())
if number_of_parameters != 1:
raise DynamicComponentInvalidSignature(
"The function must have exactly one parameter, which is the state class."
)
if len(values) != 1:
raise DynamicComponentInvalidSignature(
"You must provide a type hint for the state class in the function."
)
state_class: Type[T] = values[0]
def wrapper() -> Component:
from reflex.components.base.fragment import fragment
return fragment(state_class._evaluate(lambda state: func(state)))
return wrapper
class FrontendEventExceptionState(State):
"""Substate for handling frontend exceptions."""
@ -3349,143 +3399,6 @@ def get_state_manager() -> StateManager:
return app.state_manager
class ClientStorageBase:
"""Base class for client-side storage."""
def options(self) -> dict[str, Any]:
"""Get the options for the storage.
Returns:
All set options for the storage (not None).
"""
return {
format.to_camel_case(k): v for k, v in vars(self).items() if v is not None
}
class Cookie(ClientStorageBase, str):
"""Represents a state Var that is stored as a cookie in the browser."""
name: str | None
path: str
max_age: int | None
domain: str | None
secure: bool | None
same_site: str
def __new__(
cls,
object: Any = "",
encoding: str | None = None,
errors: str | None = None,
/,
name: str | None = None,
path: str = "/",
max_age: int | None = None,
domain: str | None = None,
secure: bool | None = None,
same_site: str = "lax",
):
"""Create a client-side Cookie (str).
Args:
object: The initial object.
encoding: The encoding to use.
errors: The error handling scheme to use.
name: The name of the cookie on the client side.
path: Cookie path. Use / as the path if the cookie should be accessible on all pages.
max_age: Relative max age of the cookie in seconds from when the client receives it.
domain: Domain for the cookie (sub.domain.com or .allsubdomains.com).
secure: Is the cookie only accessible through HTTPS?
same_site: Whether the cookie is sent with third party requests.
One of (true|false|none|lax|strict)
Returns:
The client-side Cookie object.
Note: expires (absolute Date) is not supported at this time.
"""
if encoding or errors:
inst = super().__new__(cls, object, encoding or "utf-8", errors or "strict")
else:
inst = super().__new__(cls, object)
inst.name = name
inst.path = path
inst.max_age = max_age
inst.domain = domain
inst.secure = secure
inst.same_site = same_site
return inst
class LocalStorage(ClientStorageBase, str):
"""Represents a state Var that is stored in localStorage in the browser."""
name: str | None
sync: bool = False
def __new__(
cls,
object: Any = "",
encoding: str | None = None,
errors: str | None = None,
/,
name: str | None = None,
sync: bool = False,
) -> "LocalStorage":
"""Create a client-side localStorage (str).
Args:
object: The initial object.
encoding: The encoding to use.
errors: The error handling scheme to use.
name: The name of the storage key on the client side.
sync: Whether changes should be propagated to other tabs.
Returns:
The client-side localStorage object.
"""
if encoding or errors:
inst = super().__new__(cls, object, encoding or "utf-8", errors or "strict")
else:
inst = super().__new__(cls, object)
inst.name = name
inst.sync = sync
return inst
class SessionStorage(ClientStorageBase, str):
"""Represents a state Var that is stored in sessionStorage in the browser."""
name: str | None
def __new__(
cls,
object: Any = "",
encoding: str | None = None,
errors: str | None = None,
/,
name: str | None = None,
) -> "SessionStorage":
"""Create a client-side sessionStorage (str).
Args:
object: The initial object.
encoding: The encoding to use.
errors: The error handling scheme to use
name: The name of the storage on the client side
Returns:
The client-side sessionStorage object.
"""
if encoding or errors:
inst = super().__new__(cls, object, encoding or "utf-8", errors or "strict")
else:
inst = super().__new__(cls, object)
inst.name = name
return inst
class MutableProxy(wrapt.ObjectProxy):
"""A proxy for a mutable object that tracks changes."""

View File

@ -139,3 +139,7 @@ class StateSchemaMismatchError(ReflexError, TypeError):
class EnvironmentVarValueError(ReflexError, ValueError):
"""Raised when an environment variable is set to an invalid value."""
class DynamicComponentInvalidSignature(ReflexError, TypeError):
"""Raised when a dynamic component has an invalid signature."""

View File

@ -337,8 +337,8 @@ def run_uvicorn_backend_prod(host, port, loglevel):
app_module = get_app_module()
RUN_BACKEND_PROD = f"gunicorn --worker-class {config.gunicorn_worker_class} --preload --timeout {config.timeout} --log-level critical".split()
RUN_BACKEND_PROD_WINDOWS = f"uvicorn --timeout-keep-alive {config.timeout}".split()
RUN_BACKEND_PROD = f"gunicorn --worker-class {config.gunicorn_worker_class} --max-requests {config.gunicorn_max_requests} --max-requests-jitter {config.gunicorn_max_requests_jitter} --preload --timeout {config.timeout} --log-level critical".split()
RUN_BACKEND_PROD_WINDOWS = f"uvicorn --limit-max-requests {config.gunicorn_max_requests} --timeout-keep-alive {config.timeout}".split()
command = (
[
*RUN_BACKEND_PROD_WINDOWS,

View File

@ -214,7 +214,9 @@ def _get_type_hint(value, type_hint_globals, is_optional=True) -> str:
return res
def _generate_imports(typing_imports: Iterable[str]) -> list[ast.ImportFrom]:
def _generate_imports(
typing_imports: Iterable[str],
) -> list[ast.ImportFrom | ast.Import]:
"""Generate the import statements for the stub file.
Args:
@ -228,6 +230,7 @@ def _generate_imports(typing_imports: Iterable[str]) -> list[ast.ImportFrom]:
ast.ImportFrom(module=name, names=[ast.alias(name=val) for val in values])
for name, values in DEFAULT_IMPORTS.items()
],
ast.Import([ast.alias("reflex")]),
]
@ -372,12 +375,13 @@ def _extract_class_props_as_ast_nodes(
return kwargs
def type_to_ast(typ) -> ast.AST:
def type_to_ast(typ, cls: type) -> ast.AST:
"""Converts any type annotation into its AST representation.
Handles nested generic types, unions, etc.
Args:
typ: The type annotation to convert.
cls: The class where the type annotation is used.
Returns:
The AST representation of the type annotation.
@ -390,6 +394,16 @@ def type_to_ast(typ) -> ast.AST:
# Handle plain types (int, str, custom classes, etc.)
if origin is None:
if hasattr(typ, "__name__"):
if typ.__module__.startswith("reflex."):
typ_parts = typ.__module__.split(".")
cls_parts = cls.__module__.split(".")
zipped = list(zip(typ_parts, cls_parts, strict=False))
if all(a == b for a, b in zipped) and len(typ_parts) == len(cls_parts):
return ast.Name(id=typ.__name__)
return ast.Name(id=typ.__module__ + "." + typ.__name__)
return ast.Name(id=typ.__name__)
elif hasattr(typ, "_name"):
return ast.Name(id=typ._name)
@ -406,7 +420,7 @@ def type_to_ast(typ) -> ast.AST:
return ast.Name(id=base_name)
# Convert all type arguments recursively
arg_nodes = [type_to_ast(arg) for arg in args]
arg_nodes = [type_to_ast(arg, cls) for arg in args]
# Special case for single-argument types (like List[T] or Optional[T])
if len(arg_nodes) == 1:
@ -487,7 +501,7 @@ def _generate_component_create_functiondef(
]
# Convert each argument type to its AST representation
type_args = [type_to_ast(arg) for arg in arguments_without_var]
type_args = [type_to_ast(arg, cls=clz) for arg in arguments_without_var]
# Join the type arguments with commas for EventType
args_str = ", ".join(ast.unparse(arg) for arg in type_args)

View File

@ -180,6 +180,7 @@ class ArgsFunctionOperation(CachedVarOperation, FunctionVar):
JSON_STRINGIFY = FunctionStringVar.create("JSON.stringify")
ARRAY_ISARRAY = FunctionStringVar.create("Array.isArray")
PROTOTYPE_TO_STRING = FunctionStringVar.create(
"((__to_string) => __to_string.toString())"
)

View File

@ -529,6 +529,26 @@ def array_join_operation(array: ArrayVar, sep: StringVar[Any] | str = ""):
return var_operation_return(js_expression=f"{array}.join({sep})", var_type=str)
@var_operation
def string_replace_operation(
string: StringVar, search_value: StringVar | str, new_value: StringVar | str
):
"""Replace a string with a value.
Args:
string: The string.
search_value: The string to search.
new_value: The value to be replaced with.
Returns:
The string replace operation.
"""
return var_operation_return(
js_expression=f"{string}.replace({search_value}, {new_value})",
var_type=str,
)
# Compile regex for finding reflex var tags.
_decode_var_pattern_re = (
rf"{constants.REFLEX_VAR_OPENING_TAG}(.*?){constants.REFLEX_VAR_CLOSING_TAG}"

View File

@ -0,0 +1,172 @@
import pytest
from reflex.components.datadisplay.shiki_code_block import (
ShikiBaseTransformers,
ShikiCodeBlock,
ShikiHighLevelCodeBlock,
ShikiJsTransformer,
)
from reflex.components.el.elements.forms import Button
from reflex.components.lucide.icon import Icon
from reflex.components.radix.themes.layout.box import Box
from reflex.style import Style
from reflex.vars import Var
@pytest.mark.parametrize(
"library, fns, expected_output, raises_exception",
[
("some_library", ["function_one"], ["function_one"], False),
("some_library", [123], None, True),
("some_library", [], [], False),
(
"some_library",
["function_one", "function_two"],
["function_one", "function_two"],
False,
),
("", ["function_one"], ["function_one"], False),
("some_library", ["function_one", 789], None, True),
("", [], [], False),
],
)
def test_create_transformer(library, fns, expected_output, raises_exception):
if raises_exception:
# Ensure ValueError is raised for invalid cases
with pytest.raises(ValueError):
ShikiCodeBlock.create_transformer(library, fns)
else:
transformer = ShikiCodeBlock.create_transformer(library, fns)
assert isinstance(transformer, ShikiBaseTransformers)
assert transformer.library == library
# Verify that the functions are correctly wrapped in FunctionStringVar
function_names = [str(fn) for fn in transformer.fns]
assert function_names == expected_output
@pytest.mark.parametrize(
"code_block, children, props, expected_first_child, expected_styles",
[
("print('Hello')", ["print('Hello')"], {}, "print('Hello')", {}),
(
"print('Hello')",
["print('Hello')", "More content"],
{},
"print('Hello')",
{},
),
(
"print('Hello')",
["print('Hello')"],
{
"transformers": [
ShikiBaseTransformers(
library="lib", fns=[], style=Style({"color": "red"})
)
]
},
"print('Hello')",
{"color": "red"},
),
(
"print('Hello')",
["print('Hello')"],
{
"transformers": [
ShikiBaseTransformers(
library="lib", fns=[], style=Style({"color": "red"})
)
],
"style": {"background": "blue"},
},
"print('Hello')",
{"color": "red", "background": "blue"},
),
],
)
def test_create_shiki_code_block(
code_block, children, props, expected_first_child, expected_styles
):
component = ShikiCodeBlock.create(code_block, *children, **props)
# Test that the created component is a Box
assert isinstance(component, Box)
# Test that the first child is the code
code_block_component = component.children[0]
assert code_block_component.code._var_value == expected_first_child # type: ignore
applied_styles = component.style
for key, value in expected_styles.items():
assert Var.create(applied_styles[key])._var_value == value
@pytest.mark.parametrize(
"children, props, expected_transformers, expected_button_type",
[
(["print('Hello')"], {"use_transformers": True}, [ShikiJsTransformer], None),
(["print('Hello')"], {"can_copy": True}, None, Button),
(
["print('Hello')"],
{
"can_copy": True,
"copy_button": Button.create(Icon.create(tag="a_arrow_down")),
},
None,
Button,
),
],
)
def test_create_shiki_high_level_code_block(
children, props, expected_transformers, expected_button_type
):
component = ShikiHighLevelCodeBlock.create(*children, **props)
# Test that the created component is a Box
assert isinstance(component, Box)
# Test that the first child is the code block component
code_block_component = component.children[0]
assert code_block_component.code._var_value == children[0] # type: ignore
# Check if the transformer is set correctly if expected
if expected_transformers:
exp_trans_names = [t.__name__ for t in expected_transformers]
for transformer in code_block_component.transformers._var_value: # type: ignore
assert type(transformer).__name__ in exp_trans_names
# Check if the second child is the copy button if can_copy is True
if props.get("can_copy", False):
if props.get("copy_button"):
assert isinstance(component.children[1], expected_button_type)
assert component.children[1] == props["copy_button"]
else:
assert isinstance(component.children[1], expected_button_type)
else:
assert len(component.children) == 1
@pytest.mark.parametrize(
"children, props",
[
(["print('Hello')"], {"theme": "dark"}),
(["print('Hello')"], {"language": "javascript"}),
],
)
def test_shiki_high_level_code_block_theme_language_mapping(children, props):
component = ShikiHighLevelCodeBlock.create(*children, **props)
# Test that the theme is mapped correctly
if "theme" in props:
assert component.children[
0
].theme._var_value == ShikiHighLevelCodeBlock._map_themes(props["theme"]) # type: ignore
# Test that the language is mapped correctly
if "language" in props:
assert component.children[
0
].language._var_value == ShikiHighLevelCodeBlock._map_languages( # type: ignore
props["language"]
)