Merge branch 'main' into lendemor/expose_get_state_at_top_level
This commit is contained in:
commit
414bb01e9c
@ -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
80
poetry.lock
generated
@ -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"
|
||||
|
@ -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]
|
||||
|
29
reflex/.templates/web/components/shiki/code.js
Normal file
29
reflex/.templates/web/components/shiki/code.js
Normal 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>
|
||||
)
|
||||
}
|
@ -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"],
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
|
813
reflex/components/datadisplay/shiki_code_block.py
Normal file
813
reflex/components/datadisplay/shiki_code_block.py
Normal 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()
|
2211
reflex/components/datadisplay/shiki_code_block.pyi
Normal file
2211
reflex/components/datadisplay/shiki_code_block.pyi
Normal file
File diff suppressed because it is too large
Load Diff
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""React Player component for audio and video."""
|
||||
|
||||
from . import react_player
|
||||
from .audio import Audio
|
||||
from .video import Video
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)]
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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():
|
||||
|
@ -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
144
reflex/istate/storage.py
Normal 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
|
187
reflex/state.py
187
reflex/state.py
@ -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."""
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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())"
|
||||
)
|
||||
|
@ -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}"
|
||||
|
172
tests/units/components/datadisplay/test_shiki_code.py
Normal file
172
tests/units/components/datadisplay/test_shiki_code.py
Normal 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"]
|
||||
)
|
Loading…
Reference in New Issue
Block a user