This commit is contained in:
Khaleel Al-Adhami 2024-10-22 13:32:43 -07:00
commit fcf6aa6cf3
86 changed files with 5891 additions and 1798 deletions

View File

@ -74,7 +74,7 @@ jobs:
echo "$outdated"
# Ignore 3rd party dependencies that are not updated.
filtered_outdated=$(echo "$outdated" | grep -vE 'Package|@chakra-ui|lucide-react|@splinetool/runtime|ag-grid-react|framer-motion|react-markdown|remark-math|remark-gfm|rehype-katex|rehype-raw' || true)
filtered_outdated=$(echo "$outdated" | grep -vE 'Package|@chakra-ui|lucide-react|@splinetool/runtime|ag-grid-react|framer-motion|react-markdown|remark-math|remark-gfm|rehype-katex|rehype-raw|remark-unwrap-images' || true)
no_extra=$(echo "$filtered_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-' || true)

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]

1126
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "reflex"
version = "0.6.3dev1"
version = "0.6.4dev1"
description = "Web apps in pure Python."
license = "Apache-2.0"
authors = [
@ -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,9 +69,9 @@ 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,<11.0"
pillow = ">=10.0.0,<12.0"
plotly = ">=5.13.0,<6.0"
asynctest = ">=0.13.0,<1.0"
pre-commit = ">=3.2.1"
@ -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

@ -743,6 +743,7 @@ export const useEventLoop = (
addEvents([
Event(`${exception_state_name}.handle_frontend_exception`, {
stack: error.stack,
component_stack: "",
}),
]);
return false;
@ -754,6 +755,7 @@ export const useEventLoop = (
addEvents([
Event(`${exception_state_name}.handle_frontend_exception`, {
stack: event.reason.stack,
component_stack: "",
}),
]);
return false;

View File

@ -320,13 +320,15 @@ _MAPPING: dict = {
"upload_files",
"window_alert",
],
"istate.storage": [
"Cookie",
"LocalStorage",
"SessionStorage",
],
"middleware": ["middleware", "Middleware"],
"model": ["session", "Model"],
"state": [
"var",
"Cookie",
"LocalStorage",
"SessionStorage",
"ComponentState",
"State",
],

View File

@ -174,15 +174,15 @@ 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 .middleware import Middleware as Middleware
from .middleware import middleware as middleware
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 var as var
from .style import Style as Style

View File

@ -64,7 +64,7 @@ from reflex.components.core.client_side_routing import (
)
from reflex.components.core.upload import Upload, get_upload_dir
from reflex.components.radix import themes
from reflex.config import get_config
from reflex.config import environment, get_config
from reflex.event import Event, EventHandler, EventSpec, window_alert
from reflex.model import Model, get_db_status
from reflex.page import (
@ -957,15 +957,16 @@ class App(MiddlewareMixin, LifespanMixin, Base):
executor = None
if (
platform.system() in ("Linux", "Darwin")
and os.environ.get("REFLEX_COMPILE_PROCESSES") is not None
and (number_of_processes := environment.REFLEX_COMPILE_PROCESSES)
is not None
):
executor = concurrent.futures.ProcessPoolExecutor(
max_workers=int(os.environ.get("REFLEX_COMPILE_PROCESSES", 0)) or None,
max_workers=number_of_processes,
mp_context=multiprocessing.get_context("fork"),
)
else:
executor = concurrent.futures.ThreadPoolExecutor(
max_workers=int(os.environ.get("REFLEX_COMPILE_THREADS", 0)) or None,
max_workers=environment.REFLEX_COMPILE_THREADS
)
with executor:

View File

@ -2,7 +2,6 @@
from __future__ import annotations
import os
from datetime import datetime
from pathlib import Path
from typing import Dict, Iterable, Optional, Type, Union
@ -16,7 +15,7 @@ from reflex.components.component import (
CustomComponent,
StatefulComponent,
)
from reflex.config import get_config
from reflex.config import environment, get_config
from reflex.state import BaseState
from reflex.style import SYSTEM_COLOR_MODE
from reflex.utils.exec import is_prod_mode
@ -527,7 +526,7 @@ def remove_tailwind_from_postcss() -> tuple[str, str]:
def purge_web_pages_dir():
"""Empty out .web/pages directory."""
if not is_prod_mode() and os.environ.get("REFLEX_PERSIST_WEB_DIR"):
if not is_prod_mode() and environment.REFLEX_PERSIST_WEB_DIR:
# Skip purging the web directory in dev mode if REFLEX_PERSIST_WEB_DIR is set.
return

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

@ -2,16 +2,32 @@
from __future__ import annotations
from typing import List
from typing import Dict, List, Tuple
from reflex.compiler.compiler import _compile_component
from reflex.components.component import Component
from reflex.components.el import div, p
from reflex.constants import Hooks, Imports
from reflex.event import EventChain, EventHandler
from reflex.utils.imports import ImportVar
from reflex.event import EventHandler
from reflex.state import FrontendEventExceptionState
from reflex.vars.base import Var
from reflex.vars.function import FunctionVar
def on_error_spec(
error: Var[Dict[str, str]], info: Var[Dict[str, str]]
) -> Tuple[Var[str], Var[str]]:
"""The spec for the on_error event handler.
Args:
error: The error message.
info: Additional information about the error.
Returns:
The arguments for the event handler.
"""
return (
error.stack,
info.componentStack,
)
class ErrorBoundary(Component):
@ -21,31 +37,13 @@ class ErrorBoundary(Component):
tag = "ErrorBoundary"
# Fired when the boundary catches an error.
on_error: EventHandler[lambda error, info: [error, info]] = Var( # type: ignore
"logFrontendError"
).to(FunctionVar, EventChain)
on_error: EventHandler[on_error_spec]
# Rendered instead of the children when an error is caught.
Fallback_component: Var[Component] = Var(_js_expr="Fallback")._replace(
_var_type=Component
)
def add_imports(self) -> dict[str, list[ImportVar]]:
"""Add imports for the component.
Returns:
The imports to add.
"""
return Imports.EVENTS
def add_hooks(self) -> List[str | Var]:
"""Add hooks for the component.
Returns:
The hooks to add.
"""
return [Hooks.EVENTS, Hooks.FRONTEND_ERRORS]
def add_custom_code(self) -> List[str]:
"""Add custom Javascript code into the page that contains this component.
@ -75,5 +73,20 @@ class ErrorBoundary(Component):
"""
]
@classmethod
def create(cls, *children, **props):
"""Create an ErrorBoundary component.
Args:
*children: The children of the component.
**props: The props of the component.
Returns:
The ErrorBoundary component.
"""
if "on_error" not in props:
props["on_error"] = FrontendEventExceptionState.handle_frontend_exception
return super().create(*children, **props)
error_boundary = ErrorBoundary.create

View File

@ -3,17 +3,18 @@
# ------------------- DO NOT EDIT ----------------------
# This file was generated by `reflex/utils/pyi_generator.py`!
# ------------------------------------------------------
from typing import Any, Dict, List, Optional, Union, overload
from typing import Any, Dict, List, Optional, Tuple, Union, overload
from reflex.components.component import Component
from reflex.event import EventType
from reflex.style import Style
from reflex.utils.imports import ImportVar
from reflex.vars.base import Var
def on_error_spec(
error: Var[Dict[str, str]], info: Var[Dict[str, str]]
) -> Tuple[Var[str], Var[str]]: ...
class ErrorBoundary(Component):
def add_imports(self) -> dict[str, list[ImportVar]]: ...
def add_hooks(self) -> List[str | Var]: ...
def add_custom_code(self) -> List[str]: ...
@overload
@classmethod
@ -31,7 +32,7 @@ class ErrorBoundary(Component):
on_click: Optional[EventType[[]]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,
on_error: Optional[EventType[[]]] = None,
on_error: Optional[EventType[str, str]] = None,
on_focus: Optional[EventType[[]]] = None,
on_mount: Optional[EventType[[]]] = None,
on_mouse_down: Optional[EventType[[]]] = None,
@ -45,7 +46,7 @@ class ErrorBoundary(Component):
on_unmount: Optional[EventType[[]]] = None,
**props,
) -> "ErrorBoundary":
"""Create the component.
"""Create an ErrorBoundary component.
Args:
*children: The children of the component.
@ -59,7 +60,7 @@ class ErrorBoundary(Component):
**props: The props of the component.
Returns:
The component.
The ErrorBoundary component.
"""
...

View File

@ -45,6 +45,7 @@ from reflex.event import (
EventVar,
call_event_fn,
call_event_handler,
empty_event,
get_handler_args,
)
from reflex.style import Style, format_as_emotion
@ -626,21 +627,21 @@ class Component(BaseComponent, ABC):
"""
default_triggers = {
EventTriggers.ON_FOCUS: lambda: [],
EventTriggers.ON_BLUR: lambda: [],
EventTriggers.ON_CLICK: lambda: [],
EventTriggers.ON_CONTEXT_MENU: lambda: [],
EventTriggers.ON_DOUBLE_CLICK: lambda: [],
EventTriggers.ON_MOUSE_DOWN: lambda: [],
EventTriggers.ON_MOUSE_ENTER: lambda: [],
EventTriggers.ON_MOUSE_LEAVE: lambda: [],
EventTriggers.ON_MOUSE_MOVE: lambda: [],
EventTriggers.ON_MOUSE_OUT: lambda: [],
EventTriggers.ON_MOUSE_OVER: lambda: [],
EventTriggers.ON_MOUSE_UP: lambda: [],
EventTriggers.ON_SCROLL: lambda: [],
EventTriggers.ON_MOUNT: lambda: [],
EventTriggers.ON_UNMOUNT: lambda: [],
EventTriggers.ON_FOCUS: empty_event,
EventTriggers.ON_BLUR: empty_event,
EventTriggers.ON_CLICK: empty_event,
EventTriggers.ON_CONTEXT_MENU: empty_event,
EventTriggers.ON_DOUBLE_CLICK: empty_event,
EventTriggers.ON_MOUSE_DOWN: empty_event,
EventTriggers.ON_MOUSE_ENTER: empty_event,
EventTriggers.ON_MOUSE_LEAVE: empty_event,
EventTriggers.ON_MOUSE_MOVE: empty_event,
EventTriggers.ON_MOUSE_OUT: empty_event,
EventTriggers.ON_MOUSE_OVER: empty_event,
EventTriggers.ON_MOUSE_UP: empty_event,
EventTriggers.ON_SCROLL: empty_event,
EventTriggers.ON_MOUNT: empty_event,
EventTriggers.ON_UNMOUNT: empty_event,
}
# Look for component specific triggers,
@ -651,7 +652,7 @@ class Component(BaseComponent, ABC):
annotation = field.annotation
if (metadata := getattr(annotation, "__metadata__", None)) is not None:
args_spec = metadata[0]
default_triggers[field.name] = args_spec or (lambda: [])
default_triggers[field.name] = args_spec or (empty_event) # type: ignore
return default_triggers
def __repr__(self) -> str:
@ -1708,7 +1709,7 @@ class CustomComponent(Component):
value = self._create_event_chain(
value=value,
args_spec=event_triggers_in_component_declaration.get(
key, lambda: []
key, empty_event
),
key=key,
)

View File

@ -40,7 +40,7 @@ class Clipboard(Fragment):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_paste: Optional[EventType] = None,
on_paste: Optional[EventType[list[tuple[str, str]]]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,

View File

@ -2,13 +2,13 @@
from __future__ import annotations
import os
from pathlib import Path
from typing import Any, Callable, ClassVar, Dict, List, Optional, Tuple
from reflex.components.component import Component, ComponentNamespace, MemoizationLeaf
from reflex.components.el.elements.forms import Input
from reflex.components.radix.themes.layout.box import Box
from reflex.config import environment
from reflex.constants import Dirs
from reflex.event import (
CallableEventSpec,
@ -125,9 +125,7 @@ def get_upload_dir() -> Path:
"""
Upload.is_used = True
uploaded_files_dir = Path(
os.environ.get("REFLEX_UPLOADED_FILES_DIR", "./uploaded_files")
)
uploaded_files_dir = environment.REFLEX_UPLOADED_FILES_DIR
uploaded_files_dir.mkdir(parents=True, exist_ok=True)
return uploaded_files_dir
@ -179,7 +177,7 @@ class UploadFilesProvider(Component):
class Upload(MemoizationLeaf):
"""A file upload component."""
library = "react-dropzone@14.2.9"
library = "react-dropzone@14.2.10"
tag = "ReactDropzone"

View File

@ -381,7 +381,7 @@ for theme_name in dir(Theme):
class CodeBlock(Component):
"""A code block."""
library = "react-syntax-highlighter@15.5.0"
library = "react-syntax-highlighter@15.6.1"
tag = "PrismAsyncLight"

View File

@ -5,6 +5,8 @@ from __future__ import annotations
from enum import Enum
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
from typing_extensions import TypedDict
from reflex.base import Base
from reflex.components.component import Component, NoSSRComponent
from reflex.components.literals import LiteralRowMarker
@ -107,17 +109,76 @@ class DataEditorTheme(Base):
text_medium: Optional[str] = None
def on_edit_spec(pos, data: dict[str, Any]):
"""The on edit spec function.
class Bounds(TypedDict):
"""The bounds of the group header."""
Args:
pos: The position of the edit event.
data: The data of the edit event.
x: int
y: int
width: int
height: int
Returns:
The position and data.
"""
return [pos, data]
class CompatSelection(TypedDict):
"""The selection."""
items: list
class Rectangle(TypedDict):
"""The bounds of the group header."""
x: int
y: int
width: int
height: int
class GridSelectionCurrent(TypedDict):
"""The current selection."""
cell: tuple[int, int]
range: Rectangle
rangeStack: list[Rectangle]
class GridSelection(TypedDict):
"""The grid selection."""
current: Optional[GridSelectionCurrent]
columns: CompatSelection
rows: CompatSelection
class GroupHeaderClickedEventArgs(TypedDict):
"""The arguments for the group header clicked event."""
kind: str
group: str
location: tuple[int, int]
bounds: Bounds
isEdge: bool
shiftKey: bool
ctrlKey: bool
metaKey: bool
isTouch: bool
localEventX: int
localEventY: int
button: int
buttons: int
scrollEdge: tuple[int, int]
class GridCell(TypedDict):
"""The grid cell."""
span: Optional[List[int]]
class GridColumn(TypedDict):
"""The grid column."""
title: str
group: Optional[str]
class DataEditor(NoSSRComponent):
@ -232,16 +293,18 @@ 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[lambda grp_idx, data: [grp_idx, data]]
on_group_header_context_menu: EventHandler[
identity_event(int, GroupHeaderClickedEventArgs)
]
# Fired when a group header is renamed.
on_group_header_renamed: EventHandler[lambda idx, val: [idx, val]]
on_group_header_renamed: EventHandler[identity_event(str, str)]
# Fired when a header is clicked.
on_header_clicked: EventHandler[identity_event(Tuple[int, int])]
@ -250,16 +313,18 @@ class DataEditor(NoSSRComponent):
on_header_context_menu: EventHandler[identity_event(Tuple[int, int])]
# Fired when a header menu item is clicked.
on_header_menu_click: EventHandler[lambda col, pos: [col, pos]]
on_header_menu_click: EventHandler[identity_event(int, Rectangle)]
# Fired when an item is hovered.
on_item_hovered: EventHandler[identity_event(Tuple[int, int])]
# Fired when a selection is deleted.
on_delete: EventHandler[lambda selection: [selection]]
on_delete: EventHandler[identity_event(GridSelection)]
# Fired when editing is finished.
on_finished_editing: EventHandler[lambda new_value, movement: [new_value, movement]]
on_finished_editing: EventHandler[
identity_event(Union[GridCell, None], tuple[int, int])
]
# Fired when a row is appended.
on_row_appended: EventHandler[empty_event]
@ -268,7 +333,7 @@ class DataEditor(NoSSRComponent):
on_selection_cleared: EventHandler[empty_event]
# Fired when a column is resized.
on_column_resize: EventHandler[lambda col, width: [col, width]]
on_column_resize: EventHandler[identity_event(GridColumn, int)]
def add_imports(self) -> ImportDict:
"""Add imports for the component.

View File

@ -6,6 +6,8 @@
from enum import Enum
from typing import Any, Dict, List, Literal, Optional, Union, overload
from typing_extensions import TypedDict
from reflex.base import Base
from reflex.components.component import NoSSRComponent
from reflex.event import EventType
@ -76,7 +78,53 @@ 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
width: int
height: int
class CompatSelection(TypedDict):
items: list
class Rectangle(TypedDict):
x: int
y: int
width: int
height: int
class GridSelectionCurrent(TypedDict):
cell: tuple[int, int]
range: Rectangle
rangeStack: list[Rectangle]
class GridSelection(TypedDict):
current: Optional[GridSelectionCurrent]
columns: CompatSelection
rows: CompatSelection
class GroupHeaderClickedEventArgs(TypedDict):
kind: str
group: str
location: tuple[int, int]
bounds: Bounds
isEdge: bool
shiftKey: bool
ctrlKey: bool
metaKey: bool
isTouch: bool
localEventX: int
localEventY: int
button: int
buttons: int
scrollEdge: tuple[int, int]
class GridCell(TypedDict):
span: Optional[List[int]]
class GridColumn(TypedDict):
title: str
group: Optional[str]
class DataEditor(NoSSRComponent):
def add_imports(self) -> ImportDict: ...
@ -136,24 +184,28 @@ class DataEditor(NoSSRComponent):
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[EventType[[]]] = None,
on_cell_activated: Optional[EventType] = None,
on_cell_clicked: Optional[EventType] = None,
on_cell_context_menu: Optional[EventType] = None,
on_cell_edited: Optional[EventType[[]]] = None,
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[tuple[int, int], GridCell]] = None,
on_click: Optional[EventType[[]]] = None,
on_column_resize: Optional[EventType[[]]] = None,
on_column_resize: Optional[EventType[GridColumn, int]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_delete: Optional[EventType[[]]] = None,
on_delete: Optional[EventType[GridSelection]] = None,
on_double_click: Optional[EventType[[]]] = None,
on_finished_editing: Optional[EventType[[]]] = None,
on_finished_editing: Optional[
EventType[Union[GridCell, None], tuple[int, int]]
] = None,
on_focus: Optional[EventType[[]]] = None,
on_group_header_clicked: Optional[EventType[[]]] = None,
on_group_header_context_menu: Optional[EventType[[]]] = None,
on_group_header_renamed: Optional[EventType[[]]] = None,
on_header_clicked: Optional[EventType] = None,
on_header_context_menu: Optional[EventType] = None,
on_header_menu_click: Optional[EventType[[]]] = None,
on_item_hovered: Optional[EventType] = None,
on_group_header_clicked: Optional[EventType[tuple[int, int], GridCell]] = None,
on_group_header_context_menu: Optional[
EventType[int, GroupHeaderClickedEventArgs]
] = None,
on_group_header_renamed: Optional[EventType[str, str]] = None,
on_header_clicked: Optional[EventType[tuple[int, int]]] = None,
on_header_context_menu: Optional[EventType[tuple[int, int]]] = None,
on_header_menu_click: Optional[EventType[int, Rectangle]] = None,
on_item_hovered: Optional[EventType[tuple[int, int]]] = None,
on_mount: Optional[EventType[[]]] = None,
on_mouse_down: Optional[EventType[[]]] = None,
on_mouse_enter: Optional[EventType[[]]] = 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

@ -58,7 +58,7 @@ class Moment(NoSSRComponent):
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[EventType[[]]] = None,
on_change: Optional[EventType] = None,
on_change: Optional[EventType[str]] = None,
on_click: Optional[EventType[[]]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,

View File

@ -101,7 +101,7 @@ class DrawerRoot(DrawerComponent):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_open_change: Optional[EventType] = None,
on_open_change: Optional[EventType[bool]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,
@ -511,7 +511,7 @@ class Drawer(ComponentNamespace):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_open_change: Optional[EventType] = None,
on_open_change: Optional[EventType[bool]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,

View File

@ -383,7 +383,7 @@ class ColorModeSwitch(Switch):
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[EventType[[]]] = None,
on_change: Optional[EventType] = None,
on_change: Optional[EventType[bool]] = None,
on_click: Optional[EventType[[]]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,

View File

@ -42,7 +42,7 @@ class AlertDialogRoot(RadixThemesComponent):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_open_change: Optional[EventType] = None,
on_open_change: Optional[EventType[bool]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,

View File

@ -116,7 +116,7 @@ class Checkbox(RadixThemesComponent):
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[EventType[[]]] = None,
on_change: Optional[EventType] = None,
on_change: Optional[EventType[bool]] = None,
on_click: Optional[EventType[[]]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,
@ -263,7 +263,7 @@ class HighLevelCheckbox(RadixThemesComponent):
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[EventType[[]]] = None,
on_change: Optional[EventType] = None,
on_change: Optional[EventType[bool]] = None,
on_click: Optional[EventType[[]]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,
@ -407,7 +407,7 @@ class CheckboxNamespace(ComponentNamespace):
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[EventType[[]]] = None,
on_change: Optional[EventType] = None,
on_change: Optional[EventType[bool]] = None,
on_click: Optional[EventType[[]]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,

View File

@ -39,7 +39,7 @@ class ContextMenuRoot(RadixThemesComponent):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_open_change: Optional[EventType] = None,
on_open_change: Optional[EventType[bool]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,

View File

@ -40,7 +40,7 @@ class DialogRoot(RadixThemesComponent):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_open_change: Optional[EventType] = None,
on_open_change: Optional[EventType[bool]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,
@ -382,7 +382,7 @@ class Dialog(ComponentNamespace):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_open_change: Optional[EventType] = None,
on_open_change: Optional[EventType[bool]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,

View File

@ -49,7 +49,7 @@ class DropdownMenuRoot(RadixThemesComponent):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_open_change: Optional[EventType] = None,
on_open_change: Optional[EventType[bool]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,
@ -363,7 +363,7 @@ class DropdownMenuSub(RadixThemesComponent):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_open_change: Optional[EventType] = None,
on_open_change: Optional[EventType[bool]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,

View File

@ -43,7 +43,7 @@ class HoverCardRoot(RadixThemesComponent):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_open_change: Optional[EventType] = None,
on_open_change: Optional[EventType[bool]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,
@ -256,7 +256,7 @@ class HoverCard(ComponentNamespace):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_open_change: Optional[EventType] = None,
on_open_change: Optional[EventType[bool]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,

View File

@ -41,7 +41,7 @@ class PopoverRoot(RadixThemesComponent):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_open_change: Optional[EventType] = None,
on_open_change: Optional[EventType[bool]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,

View File

@ -177,7 +177,7 @@ class RadioCardsRoot(RadixThemesComponent):
on_mouse_up: Optional[EventType[[]]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
on_value_change: Optional[EventType] = None,
on_value_change: Optional[EventType[str]] = None,
**props,
) -> "RadioCardsRoot":
"""Create a new component instance.

View File

@ -113,7 +113,7 @@ class RadioGroupRoot(RadixThemesComponent):
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[EventType[[]]] = None,
on_change: Optional[EventType] = None,
on_change: Optional[EventType[str]] = None,
on_click: Optional[EventType[[]]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,

View File

@ -44,7 +44,7 @@ class SelectRoot(RadixThemesComponent):
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[EventType[[]]] = None,
on_change: Optional[EventType] = None,
on_change: Optional[EventType[str]] = None,
on_click: Optional[EventType[[]]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,
@ -57,7 +57,7 @@ class SelectRoot(RadixThemesComponent):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_open_change: Optional[EventType] = None,
on_open_change: Optional[EventType[bool]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,
@ -680,7 +680,7 @@ class HighLevelSelect(SelectRoot):
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[EventType[[]]] = None,
on_change: Optional[EventType] = None,
on_change: Optional[EventType[str]] = None,
on_click: Optional[EventType[[]]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,
@ -693,7 +693,7 @@ class HighLevelSelect(SelectRoot):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_open_change: Optional[EventType] = None,
on_open_change: Optional[EventType[bool]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,
@ -854,7 +854,7 @@ class Select(ComponentNamespace):
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[EventType[[]]] = None,
on_change: Optional[EventType] = None,
on_change: Optional[EventType[str]] = None,
on_click: Optional[EventType[[]]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,
@ -867,7 +867,7 @@ class Select(ComponentNamespace):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_open_change: Optional[EventType] = None,
on_open_change: Optional[EventType[bool]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,

View File

@ -119,7 +119,7 @@ class Switch(RadixThemesComponent):
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[EventType[[]]] = None,
on_change: Optional[EventType] = None,
on_change: Optional[EventType[bool]] = None,
on_click: Optional[EventType[[]]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,

View File

@ -41,7 +41,7 @@ class TabsRoot(RadixThemesComponent):
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[EventType[[]]] = None,
on_change: Optional[EventType] = None,
on_change: Optional[EventType[str]] = None,
on_click: Optional[EventType[[]]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,
@ -340,7 +340,7 @@ class Tabs(ComponentNamespace):
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[EventType[[]]] = None,
on_change: Optional[EventType] = None,
on_change: Optional[EventType[str]] = None,
on_click: Optional[EventType[[]]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,

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
@ -76,7 +78,7 @@ class Tooltip(RadixThemesComponent):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_open_change: Optional[EventType] = None,
on_open_change: Optional[EventType[bool]] = None,
on_pointer_down_outside: Optional[EventType[[]]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,

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
@ -41,7 +42,7 @@ class Audio(ReactPlayer):
on_context_menu: Optional[EventType[[]]] = None,
on_disable_pip: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,
on_duration: Optional[EventType] = None,
on_duration: Optional[EventType[float]] = None,
on_enable_pip: Optional[EventType[[]]] = None,
on_ended: Optional[EventType[[]]] = None,
on_error: Optional[EventType[[]]] = None,
@ -58,10 +59,12 @@ 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] = None,
on_seek: Optional[EventType[float]] = None,
on_start: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,

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
@ -39,7 +47,7 @@ class ReactPlayer(NoSSRComponent):
on_context_menu: Optional[EventType[[]]] = None,
on_disable_pip: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,
on_duration: Optional[EventType] = None,
on_duration: Optional[EventType[float]] = None,
on_enable_pip: Optional[EventType[[]]] = None,
on_ended: Optional[EventType[[]]] = None,
on_error: Optional[EventType[[]]] = None,
@ -56,10 +64,10 @@ 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] = None,
on_seek: Optional[EventType[float]] = None,
on_start: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,

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
@ -41,7 +42,7 @@ class Video(ReactPlayer):
on_context_menu: Optional[EventType[[]]] = None,
on_disable_pip: Optional[EventType[[]]] = None,
on_double_click: Optional[EventType[[]]] = None,
on_duration: Optional[EventType] = None,
on_duration: Optional[EventType[float]] = None,
on_enable_pip: Optional[EventType[[]]] = None,
on_ended: Optional[EventType[[]]] = None,
on_error: Optional[EventType[[]]] = None,
@ -58,10 +59,12 @@ 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] = None,
on_seek: Optional[EventType[float]] = None,
on_start: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,

View File

@ -252,7 +252,7 @@ class Brush(Recharts):
A dict mapping the event trigger to the var that is passed to the handler.
"""
return {
EventTriggers.ON_CHANGE: lambda: [],
EventTriggers.ON_CHANGE: empty_event,
}
@ -293,10 +293,10 @@ class Cartesian(Recharts):
name: Var[Union[str, int]]
# The customized event handler of animation start
on_animation_start: EventHandler[lambda: []]
on_animation_start: EventHandler[empty_event]
# The customized event handler of animation end
on_animation_end: EventHandler[lambda: []]
on_animation_end: EventHandler[empty_event]
# The customized event handler of click on the component in this group
on_click: EventHandler[empty_event]

View File

@ -330,9 +330,9 @@ class RadarChart(ChartBase):
A dict mapping the event trigger to the var that is passed to the handler.
"""
return {
EventTriggers.ON_CLICK: lambda: [],
EventTriggers.ON_MOUSE_ENTER: lambda: [],
EventTriggers.ON_MOUSE_LEAVE: lambda: [],
EventTriggers.ON_CLICK: empty_event,
EventTriggers.ON_MOUSE_ENTER: empty_event,
EventTriggers.ON_MOUSE_LEAVE: empty_event,
}
@ -419,14 +419,14 @@ class ScatterChart(ChartBase):
A dict mapping the event trigger to the var that is passed to the handler.
"""
return {
EventTriggers.ON_CLICK: lambda: [],
EventTriggers.ON_MOUSE_DOWN: lambda: [],
EventTriggers.ON_MOUSE_UP: lambda: [],
EventTriggers.ON_MOUSE_MOVE: lambda: [],
EventTriggers.ON_MOUSE_OVER: lambda: [],
EventTriggers.ON_MOUSE_OUT: lambda: [],
EventTriggers.ON_MOUSE_ENTER: lambda: [],
EventTriggers.ON_MOUSE_LEAVE: lambda: [],
EventTriggers.ON_CLICK: empty_event,
EventTriggers.ON_MOUSE_DOWN: empty_event,
EventTriggers.ON_MOUSE_UP: empty_event,
EventTriggers.ON_MOUSE_MOVE: empty_event,
EventTriggers.ON_MOUSE_OVER: empty_event,
EventTriggers.ON_MOUSE_OUT: empty_event,
EventTriggers.ON_MOUSE_ENTER: empty_event,
EventTriggers.ON_MOUSE_LEAVE: empty_event,
}

View File

@ -103,14 +103,14 @@ class Pie(Recharts):
A dict mapping the event trigger to the var that is passed to the handler.
"""
return {
EventTriggers.ON_ANIMATION_START: lambda: [],
EventTriggers.ON_ANIMATION_END: lambda: [],
EventTriggers.ON_CLICK: lambda: [],
EventTriggers.ON_MOUSE_MOVE: lambda: [],
EventTriggers.ON_MOUSE_OVER: lambda: [],
EventTriggers.ON_MOUSE_OUT: lambda: [],
EventTriggers.ON_MOUSE_ENTER: lambda: [],
EventTriggers.ON_MOUSE_LEAVE: lambda: [],
EventTriggers.ON_ANIMATION_START: empty_event,
EventTriggers.ON_ANIMATION_END: empty_event,
EventTriggers.ON_CLICK: empty_event,
EventTriggers.ON_MOUSE_MOVE: empty_event,
EventTriggers.ON_MOUSE_OVER: empty_event,
EventTriggers.ON_MOUSE_OUT: empty_event,
EventTriggers.ON_MOUSE_ENTER: empty_event,
EventTriggers.ON_MOUSE_LEAVE: empty_event,
}
@ -167,8 +167,8 @@ class Radar(Recharts):
A dict mapping the event trigger to the var that is passed to the handler.
"""
return {
EventTriggers.ON_ANIMATION_START: lambda: [],
EventTriggers.ON_ANIMATION_END: lambda: [],
EventTriggers.ON_ANIMATION_START: empty_event,
EventTriggers.ON_ANIMATION_END: empty_event,
}
@ -219,14 +219,14 @@ class RadialBar(Recharts):
A dict mapping the event trigger to the var that is passed to the handler.
"""
return {
EventTriggers.ON_CLICK: lambda: [],
EventTriggers.ON_MOUSE_MOVE: lambda: [],
EventTriggers.ON_MOUSE_OVER: lambda: [],
EventTriggers.ON_MOUSE_OUT: lambda: [],
EventTriggers.ON_MOUSE_ENTER: lambda: [],
EventTriggers.ON_MOUSE_LEAVE: lambda: [],
EventTriggers.ON_ANIMATION_START: lambda: [],
EventTriggers.ON_ANIMATION_END: lambda: [],
EventTriggers.ON_CLICK: empty_event,
EventTriggers.ON_MOUSE_MOVE: empty_event,
EventTriggers.ON_MOUSE_OVER: empty_event,
EventTriggers.ON_MOUSE_OUT: empty_event,
EventTriggers.ON_MOUSE_ENTER: empty_event,
EventTriggers.ON_MOUSE_LEAVE: empty_event,
EventTriggers.ON_ANIMATION_START: empty_event,
EventTriggers.ON_ANIMATION_END: empty_event,
}
@ -392,12 +392,12 @@ class PolarRadiusAxis(Recharts):
A dict mapping the event trigger to the var that is passed to the handler.
"""
return {
EventTriggers.ON_CLICK: lambda: [],
EventTriggers.ON_MOUSE_MOVE: lambda: [],
EventTriggers.ON_MOUSE_OVER: lambda: [],
EventTriggers.ON_MOUSE_OUT: lambda: [],
EventTriggers.ON_MOUSE_ENTER: lambda: [],
EventTriggers.ON_MOUSE_LEAVE: lambda: [],
EventTriggers.ON_CLICK: empty_event,
EventTriggers.ON_MOUSE_MOVE: empty_event,
EventTriggers.ON_MOUSE_OVER: empty_event,
EventTriggers.ON_MOUSE_OUT: empty_event,
EventTriggers.ON_MOUSE_ENTER: empty_event,
EventTriggers.ON_MOUSE_LEAVE: empty_event,
}

View File

@ -9,7 +9,7 @@ from reflex.utils import console
class Recharts(Component):
"""A component that wraps a recharts lib."""
library = "recharts@2.12.7"
library = "recharts@2.13.0"
def render(self) -> Dict:
"""Render the tag.
@ -29,7 +29,7 @@ class Recharts(Component):
class RechartsCharts(NoSSRComponent, MemoizationLeaf):
"""A component that wraps a recharts lib."""
library = "recharts@2.12.7"
library = "recharts@2.13.0"
LiteralAnimationEasing = Literal["ease", "ease-in", "ease-out", "ease-in-out", "linear"]

View File

@ -3,11 +3,11 @@
from __future__ import annotations
import enum
from typing import Dict, List, Literal, Optional, Union
from typing import Dict, List, Literal, Optional, Tuple, Union
from reflex.base import Base
from reflex.components.component import Component, NoSSRComponent
from reflex.event import EventHandler
from reflex.event import EventHandler, empty_event, identity_event
from reflex.utils.format import to_camel_case
from reflex.utils.imports import ImportDict, ImportVar
from reflex.vars.base import Var
@ -68,6 +68,35 @@ class EditorOptions(Base):
button_list: Optional[List[Union[List[str], str]]]
def on_blur_spec(e: Var, content: Var[str]) -> Tuple[Var[str]]:
"""A helper function to specify the on_blur event handler.
Args:
e: The event.
content: The content of the editor.
Returns:
A tuple containing the content of the editor.
"""
return (content,)
def on_paste_spec(
e: Var, clean_data: Var[str], max_char_count: Var[bool]
) -> Tuple[Var[str], Var[bool]]:
"""A helper function to specify the on_paste event handler.
Args:
e: The event.
clean_data: The clean data.
max_char_count: The maximum character count.
Returns:
A tuple containing the clean data and the maximum character count.
"""
return (clean_data, max_char_count)
class Editor(NoSSRComponent):
"""A Rich Text Editor component based on SunEditor.
Not every JS prop is listed here (some are not easily usable from python),
@ -178,36 +207,31 @@ class Editor(NoSSRComponent):
disable_toolbar: Var[bool]
# Fired when the editor content changes.
on_change: EventHandler[lambda content: [content]]
on_change: EventHandler[identity_event(str)]
# Fired when the something is inputted in the editor.
on_input: EventHandler[lambda e: [e]]
on_input: EventHandler[empty_event]
# Fired when the editor loses focus.
on_blur: EventHandler[lambda e, content: [content]]
on_blur: EventHandler[on_blur_spec]
# Fired when the editor is loaded.
on_load: EventHandler[lambda reload: [reload]]
# Fired when the editor is resized.
on_resize_editor: EventHandler[lambda height, prev_height: [height, prev_height]]
on_load: EventHandler[identity_event(bool)]
# Fired when the editor content is copied.
on_copy: EventHandler[lambda e, clipboard_data: [clipboard_data]]
on_copy: EventHandler[empty_event]
# Fired when the editor content is cut.
on_cut: EventHandler[lambda e, clipboard_data: [clipboard_data]]
on_cut: EventHandler[empty_event]
# Fired when the editor content is pasted.
on_paste: EventHandler[
lambda e, clean_data, max_char_count: [clean_data, max_char_count]
]
on_paste: EventHandler[on_paste_spec]
# Fired when the code view is toggled.
toggle_code_view: EventHandler[lambda is_code_view: [is_code_view]]
toggle_code_view: EventHandler[identity_event(bool)]
# Fired when the full screen mode is toggled.
toggle_full_screen: EventHandler[lambda is_full_screen: [is_full_screen]]
toggle_full_screen: EventHandler[identity_event(bool)]
def add_imports(self) -> ImportDict:
"""Add imports for the Editor component.

View File

@ -4,7 +4,7 @@
# This file was generated by `reflex/utils/pyi_generator.py`!
# ------------------------------------------------------
import enum
from typing import Any, Dict, List, Literal, Optional, Union, overload
from typing import Any, Dict, List, Literal, Optional, Tuple, Union, overload
from reflex.base import Base
from reflex.components.component import NoSSRComponent
@ -44,6 +44,11 @@ class EditorOptions(Base):
rtl: Optional[bool]
button_list: Optional[List[Union[List[str], str]]]
def on_blur_spec(e: Var, content: Var[str]) -> Tuple[Var[str]]: ...
def on_paste_spec(
e: Var, clean_data: Var[str], max_char_count: Var[bool]
) -> Tuple[Var[str], Var[bool]]: ...
class Editor(NoSSRComponent):
def add_imports(self) -> ImportDict: ...
@overload
@ -122,8 +127,8 @@ class Editor(NoSSRComponent):
class_name: Optional[Any] = None,
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[EventType[[]]] = None,
on_change: Optional[EventType[[]]] = None,
on_blur: Optional[EventType[str]] = None,
on_change: Optional[EventType[str]] = None,
on_click: Optional[EventType[[]]] = None,
on_context_menu: Optional[EventType[[]]] = None,
on_copy: Optional[EventType[[]]] = None,
@ -131,7 +136,7 @@ class Editor(NoSSRComponent):
on_double_click: Optional[EventType[[]]] = None,
on_focus: Optional[EventType[[]]] = None,
on_input: Optional[EventType[[]]] = None,
on_load: Optional[EventType[[]]] = None,
on_load: Optional[EventType[bool]] = None,
on_mount: Optional[EventType[[]]] = None,
on_mouse_down: Optional[EventType[[]]] = None,
on_mouse_enter: Optional[EventType[[]]] = None,
@ -140,12 +145,11 @@ class Editor(NoSSRComponent):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_paste: Optional[EventType[[]]] = None,
on_resize_editor: Optional[EventType[[]]] = None,
on_paste: Optional[EventType[str, bool]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
toggle_code_view: Optional[EventType[[]]] = None,
toggle_full_screen: Optional[EventType[[]]] = None,
toggle_code_view: Optional[EventType[bool]] = None,
toggle_full_screen: Optional[EventType[bool]] = None,
**props,
) -> "Editor":
"""Create an instance of Editor. No children allowed.

View File

@ -2,6 +2,7 @@
from __future__ import annotations
import dataclasses
import importlib
import os
import sys
@ -9,7 +10,10 @@ import urllib.parse
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Union
from reflex.utils.exceptions import ConfigError
from typing_extensions import get_type_hints
from reflex.utils.exceptions import ConfigError, EnvironmentVarValueError
from reflex.utils.types import value_inside_optional
try:
import pydantic.v1 as pydantic
@ -131,6 +135,198 @@ class DBConfig(Base):
return f"{self.engine}://{path}/{self.database}"
def get_default_value_for_field(field: dataclasses.Field) -> Any:
"""Get the default value for a field.
Args:
field: The field.
Returns:
The default value.
Raises:
ValueError: If no default value is found.
"""
if field.default != dataclasses.MISSING:
return field.default
elif field.default_factory != dataclasses.MISSING:
return field.default_factory()
else:
raise ValueError(
f"Missing value for environment variable {field.name} and no default value found"
)
def interpret_boolean_env(value: str) -> bool:
"""Interpret a boolean environment variable value.
Args:
value: The environment variable value.
Returns:
The interpreted value.
Raises:
EnvironmentVarValueError: If the value is invalid.
"""
true_values = ["true", "1", "yes", "y"]
false_values = ["false", "0", "no", "n"]
if value.lower() in true_values:
return True
elif value.lower() in false_values:
return False
raise EnvironmentVarValueError(f"Invalid boolean value: {value}")
def interpret_int_env(value: str) -> int:
"""Interpret an integer environment variable value.
Args:
value: The environment variable value.
Returns:
The interpreted value.
Raises:
EnvironmentVarValueError: If the value is invalid.
"""
try:
return int(value)
except ValueError as ve:
raise EnvironmentVarValueError(f"Invalid integer value: {value}") from ve
def interpret_path_env(value: str) -> Path:
"""Interpret a path environment variable value.
Args:
value: The environment variable value.
Returns:
The interpreted value.
Raises:
EnvironmentVarValueError: If the path does not exist.
"""
path = Path(value)
if not path.exists():
raise EnvironmentVarValueError(f"Path does not exist: {path}")
return path
def interpret_env_var_value(value: str, field: dataclasses.Field) -> Any:
"""Interpret an environment variable value based on the field type.
Args:
value: The environment variable value.
field: The field.
Returns:
The interpreted value.
Raises:
ValueError: If the value is invalid.
"""
field_type = value_inside_optional(field.type)
if field_type is bool:
return interpret_boolean_env(value)
elif field_type is str:
return value
elif field_type is int:
return interpret_int_env(value)
elif field_type is Path:
return interpret_path_env(value)
else:
raise ValueError(
f"Invalid type for environment variable {field.name}: {field_type}. This is probably an issue in Reflex."
)
@dataclasses.dataclass(init=False)
class EnvironmentVariables:
"""Environment variables class to instantiate environment variables."""
# Whether to use npm over bun to install frontend packages.
REFLEX_USE_NPM: bool = False
# The npm registry to use.
NPM_CONFIG_REGISTRY: Optional[str] = None
# Whether to use Granian for the backend. Otherwise, use Uvicorn.
REFLEX_USE_GRANIAN: bool = False
# The username to use for authentication on python package repository. Username and password must both be provided.
TWINE_USERNAME: Optional[str] = None
# The password to use for authentication on python package repository. Username and password must both be provided.
TWINE_PASSWORD: Optional[str] = None
# Whether to use the system installed bun. If set to false, bun will be bundled with the app.
REFLEX_USE_SYSTEM_BUN: bool = False
# Whether to use the system installed node and npm. If set to false, node and npm will be bundled with the app.
REFLEX_USE_SYSTEM_NODE: bool = False
# The working directory for the next.js commands.
REFLEX_WEB_WORKDIR: Path = Path(constants.Dirs.WEB)
# Path to the alembic config file
ALEMBIC_CONFIG: Path = Path(constants.ALEMBIC_CONFIG)
# Disable SSL verification for HTTPX requests.
SSL_NO_VERIFY: bool = False
# The directory to store uploaded files.
REFLEX_UPLOADED_FILES_DIR: Path = Path(constants.Dirs.UPLOADED_FILES)
# Whether to use seperate processes to compile the frontend and how many. If not set, defaults to thread executor.
REFLEX_COMPILE_PROCESSES: Optional[int] = None
# Whether to use seperate threads to compile the frontend and how many. Defaults to `min(32, os.cpu_count() + 4)`.
REFLEX_COMPILE_THREADS: Optional[int] = None
# The directory to store reflex dependencies.
REFLEX_DIR: Path = Path(constants.Reflex.DIR)
# Whether to print the SQL queries if the log level is INFO or lower.
SQLALCHEMY_ECHO: bool = False
# Whether to ignore the redis config error. Some redis servers only allow out-of-band configuration.
REFLEX_IGNORE_REDIS_CONFIG_ERROR: bool = False
# Whether to skip purging the web directory in dev mode.
REFLEX_PERSIST_WEB_DIR: bool = False
# The reflex.build frontend host.
REFLEX_BUILD_FRONTEND: str = constants.Templates.REFLEX_BUILD_FRONTEND
# The reflex.build backend host.
REFLEX_BUILD_BACKEND: str = constants.Templates.REFLEX_BUILD_BACKEND
def __init__(self):
"""Initialize the environment variables."""
type_hints = get_type_hints(type(self))
for field in dataclasses.fields(self):
raw_value = os.getenv(field.name, None)
field.type = type_hints.get(field.name) or field.type
value = (
interpret_env_var_value(raw_value, field)
if raw_value is not None
else get_default_value_for_field(field)
)
setattr(self, field.name, value)
environment = EnvironmentVariables()
class Config(Base):
"""The config defines runtime settings for the app.
@ -222,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
@ -234,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.
@ -275,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.
@ -284,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,8 @@
from .base import (
COOKIES,
ENV_BACKEND_ONLY_ENV_VAR,
ENV_FRONTEND_ONLY_ENV_VAR,
ENV_MODE_ENV_VAR,
IS_WINDOWS,
LOCAL_STORAGE,

View File

@ -2,7 +2,6 @@
from __future__ import annotations
import os
import platform
from enum import Enum
from importlib import metadata
@ -11,6 +10,8 @@ from types import SimpleNamespace
from platformdirs import PlatformDirs
from .utils import classproperty
IS_WINDOWS = platform.system() == "Windows"
@ -20,6 +21,8 @@ class Dirs(SimpleNamespace):
# The frontend directories in a project.
# The web folder where the NextJS app is compiled to.
WEB = ".web"
# The directory where uploaded files are stored.
UPLOADED_FILES = "uploaded_files"
# The name of the assets directory.
APP_ASSETS = "assets"
# The name of the assets directory for external ressource (a subfolder of APP_ASSETS).
@ -64,21 +67,13 @@ class Reflex(SimpleNamespace):
# Files and directories used to init a new project.
# The directory to store reflex dependencies.
# Get directory value from enviroment variables if it exists.
_dir = os.environ.get("REFLEX_DIR", "")
# on windows, we use C:/Users/<username>/AppData/Local/reflex.
# on macOS, we use ~/Library/Application Support/reflex.
# on linux, we use ~/.local/share/reflex.
# If user sets REFLEX_DIR envroment variable use that instead.
DIR = PlatformDirs(MODULE_NAME, False).user_data_path
DIR = Path(
_dir
or (
# on windows, we use C:/Users/<username>/AppData/Local/reflex.
# on macOS, we use ~/Library/Application Support/reflex.
# on linux, we use ~/.local/share/reflex.
# If user sets REFLEX_DIR envroment variable use that instead.
PlatformDirs(MODULE_NAME, False).user_data_dir
)
)
# The root directory of the reflex library.
ROOT_DIR = Path(__file__).parents[2]
RELEASES_URL = f"https://api.github.com/repos/reflex-dev/templates/releases"
@ -101,27 +96,51 @@ class Templates(SimpleNamespace):
DEFAULT = "blank"
# The reflex.build frontend host
REFLEX_BUILD_FRONTEND = os.environ.get(
"REFLEX_BUILD_FRONTEND", "https://flexgen.reflex.run"
)
REFLEX_BUILD_FRONTEND = "https://flexgen.reflex.run"
# The reflex.build backend host
REFLEX_BUILD_BACKEND = os.environ.get(
"REFLEX_BUILD_BACKEND", "https://flexgen-prod-flexgen.fly.dev"
)
REFLEX_BUILD_BACKEND = "https://flexgen-prod-flexgen.fly.dev"
# The URL to redirect to reflex.build
REFLEX_BUILD_URL = (
REFLEX_BUILD_FRONTEND + "/gen?reflex_init_token={reflex_init_token}"
)
@classproperty
@classmethod
def REFLEX_BUILD_URL(cls):
"""The URL to redirect to reflex.build.
# The URL to poll waiting for the user to select a generation.
REFLEX_BUILD_POLL_URL = REFLEX_BUILD_BACKEND + "/api/init/{reflex_init_token}"
Returns:
The URL to redirect to reflex.build.
"""
from reflex.config import environment
# The URL to fetch the generation's reflex code
REFLEX_BUILD_CODE_URL = (
REFLEX_BUILD_BACKEND + "/api/gen/{generation_hash}/refactored"
)
return (
environment.REFLEX_BUILD_FRONTEND
+ "/gen?reflex_init_token={reflex_init_token}"
)
@classproperty
@classmethod
def REFLEX_BUILD_POLL_URL(cls):
"""The URL to poll waiting for the user to select a generation.
Returns:
The URL to poll waiting for the user to select a generation.
"""
from reflex.config import environment
return environment.REFLEX_BUILD_BACKEND + "/api/init/{reflex_init_token}"
@classproperty
@classmethod
def REFLEX_BUILD_CODE_URL(cls):
"""The URL to fetch the generation's reflex code.
Returns:
The URL to fetch the generation's reflex code.
"""
from reflex.config import environment
return (
environment.REFLEX_BUILD_BACKEND + "/api/gen/{generation_hash}/refactored"
)
class Dirs(SimpleNamespace):
"""Folders used by the template system of Reflex."""
@ -226,6 +245,9 @@ SKIP_COMPILE_ENV_VAR = "__REFLEX_SKIP_COMPILE"
# This env var stores the execution mode of the app
ENV_MODE_ENV_VAR = "REFLEX_ENV_MODE"
ENV_BACKEND_ONLY_ENV_VAR = "REFLEX_BACKEND_ONLY"
ENV_FRONTEND_ONLY_ENV_VAR = "REFLEX_FRONTEND_ONLY"
# Testing variables.
# Testing os env set by pytest when running a test case.
PYTEST_CURRENT_TEST = "PYTEST_CURRENT_TEST"

View File

@ -132,16 +132,6 @@ class Hooks(SimpleNamespace):
}
})"""
FRONTEND_ERRORS = f"""
const logFrontendError = (error, info) => {{
if (process.env.NODE_ENV === "production") {{
addEvents([Event("{CompileVars.FRONTEND_EXCEPTION_STATE_FULL}.handle_frontend_exception", {{
stack: error.stack,
}})])
}}
}}
"""
class MemoizationDisposition(enum.Enum):
"""The conditions under which a component should be memoized."""

View File

@ -1,6 +1,5 @@
"""Config constants."""
import os
from pathlib import Path
from types import SimpleNamespace
@ -9,7 +8,7 @@ from reflex.constants.base import Dirs, Reflex
from .compiler import Ext
# Alembic migrations
ALEMBIC_CONFIG = os.environ.get("ALEMBIC_CONFIG", "alembic.ini")
ALEMBIC_CONFIG = "alembic.ini"
class Config(SimpleNamespace):

View File

@ -3,9 +3,11 @@
from __future__ import annotations
import platform
from pathlib import Path
from types import SimpleNamespace
from .base import IS_WINDOWS, Reflex
from .base import IS_WINDOWS
from .utils import classproperty
def get_fnm_name() -> str | None:
@ -36,12 +38,9 @@ class Bun(SimpleNamespace):
# The Bun version.
VERSION = "1.1.29"
# Min Bun Version
MIN_VERSION = "0.7.0"
# The directory to store the bun.
ROOT_PATH = Reflex.DIR / "bun"
# Default bun path.
DEFAULT_PATH = ROOT_PATH / "bin" / ("bun" if not IS_WINDOWS else "bun.exe")
# URL to bun install script.
INSTALL_URL = "https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/bun_install.sh"
@ -50,11 +49,31 @@ class Bun(SimpleNamespace):
WINDOWS_INSTALL_URL = (
"https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/install.ps1"
)
# Path of the bunfig file
CONFIG_PATH = "bunfig.toml"
# The environment variable to use the system installed bun.
USE_SYSTEM_VAR = "REFLEX_USE_SYSTEM_BUN"
@classproperty
@classmethod
def ROOT_PATH(cls):
"""The directory to store the bun.
Returns:
The directory to store the bun.
"""
from reflex.config import environment
return environment.REFLEX_DIR / "bun"
@classproperty
@classmethod
def DEFAULT_PATH(cls):
"""Default bun path.
Returns:
The default bun path.
"""
return cls.ROOT_PATH / "bin" / ("bun" if not IS_WINDOWS else "bun.exe")
# FNM config.
@ -63,44 +82,81 @@ class Fnm(SimpleNamespace):
# The FNM version.
VERSION = "1.35.1"
# The directory to store fnm.
DIR = Reflex.DIR / "fnm"
FILENAME = get_fnm_name()
# The fnm executable binary.
EXE = DIR / ("fnm.exe" if IS_WINDOWS else "fnm")
# The URL to the fnm release binary
INSTALL_URL = (
f"https://github.com/Schniz/fnm/releases/download/v{VERSION}/{FILENAME}.zip"
)
@classproperty
@classmethod
def DIR(cls) -> Path:
"""The directory to store fnm.
Returns:
The directory to store fnm.
"""
from reflex.config import environment
return environment.REFLEX_DIR / "fnm"
@classproperty
@classmethod
def EXE(cls):
"""The fnm executable binary.
Returns:
The fnm executable binary.
"""
return cls.DIR / ("fnm.exe" if IS_WINDOWS else "fnm")
# Node / NPM config
class Node(SimpleNamespace):
"""Node/ NPM constants."""
# The Node version.
VERSION = "20.18.0"
VERSION = "22.10.0"
# The minimum required node version.
MIN_VERSION = "18.17.0"
# The node bin path.
BIN_PATH = (
Fnm.DIR
/ "node-versions"
/ f"v{VERSION}"
/ "installation"
/ ("bin" if not IS_WINDOWS else "")
)
@classproperty
@classmethod
def BIN_PATH(cls):
"""The node bin path.
# The default path where node is installed.
PATH = BIN_PATH / ("node.exe" if IS_WINDOWS else "node")
Returns:
The node bin path.
"""
return (
Fnm.DIR
/ "node-versions"
/ f"v{cls.VERSION}"
/ "installation"
/ ("bin" if not IS_WINDOWS else "")
)
# The default path where npm is installed.
NPM_PATH = BIN_PATH / "npm"
@classproperty
@classmethod
def PATH(cls):
"""The default path where node is installed.
# The environment variable to use the system installed node.
USE_SYSTEM_VAR = "REFLEX_USE_SYSTEM_NODE"
Returns:
The default path where node is installed.
"""
return cls.BIN_PATH / ("node.exe" if IS_WINDOWS else "node")
@classproperty
@classmethod
def NPM_PATH(cls):
"""The default path where npm is installed.
Returns:
The default path where npm is installed.
"""
return cls.BIN_PATH / "npm"
class PackageJson(SimpleNamespace):
@ -117,18 +173,18 @@ class PackageJson(SimpleNamespace):
PATH = "package.json"
DEPENDENCIES = {
"@babel/standalone": "7.25.7",
"@babel/standalone": "7.25.8",
"@emotion/react": "11.13.3",
"axios": "1.7.7",
"json5": "2.2.3",
"next": "14.2.14",
"next": "14.2.15",
"next-sitemap": "4.2.3",
"next-themes": "0.3.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-focus-lock": "2.13.2",
"socket.io-client": "4.8.0",
"universal-cookie": "7.2.0",
"universal-cookie": "7.2.1",
}
DEV_DEPENDENCIES = {
"autoprefixer": "10.4.20",

View File

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

32
reflex/constants/utils.py Normal file
View File

@ -0,0 +1,32 @@
"""Utility functions for constants."""
from typing import Any, Callable, Generic, Type
from typing_extensions import TypeVar
T = TypeVar("T")
V = TypeVar("V")
class classproperty(Generic[T, V]):
"""A class property decorator."""
def __init__(self, getter: Callable[[Type[T]], V]) -> None:
"""Initialize the class property.
Args:
getter: The getter function.
"""
self.getter = getattr(getter, "__func__", getter)
def __get__(self, instance: Any, owner: Type[T]) -> V:
"""Get the value of the class property.
Args:
instance: The instance of the class.
owner: The class itself.
Returns:
The value of the class property.
"""
return self.getter(owner)

View File

@ -17,7 +17,7 @@ import typer
from tomlkit.exceptions import TOMLKitError
from reflex import constants
from reflex.config import get_config
from reflex.config import environment, get_config
from reflex.constants import CustomComponents
from reflex.utils import console
@ -609,14 +609,14 @@ def publish(
help="The API token to use for authentication on python package repository. If token is provided, no username/password should be provided at the same time",
),
username: Optional[str] = typer.Option(
os.getenv("TWINE_USERNAME"),
environment.TWINE_USERNAME,
"-u",
"--username",
show_default="TWINE_USERNAME environment variable value if set",
help="The username to use for authentication on python package repository. Username and password must both be provided.",
),
password: Optional[str] = typer.Option(
os.getenv("TWINE_PASSWORD"),
environment.TWINE_PASSWORD,
"-p",
"--password",
show_default="TWINE_PASSWORD environment variable value if set",

View File

@ -12,7 +12,6 @@ from functools import partial
from typing import (
Any,
Callable,
ClassVar,
Dict,
Generic,
List,
@ -22,9 +21,10 @@ from typing import (
TypeVar,
Union,
get_type_hints,
overload,
)
from typing_extensions import ParamSpec, get_args, get_origin
from typing_extensions import ParamSpec, Protocol, get_args, get_origin
from reflex import constants
from reflex.utils import console, format
@ -36,14 +36,15 @@ from reflex.utils.exceptions import (
from reflex.utils.types import ArgsSpec, GenericType
from reflex.vars import VarData
from reflex.vars.base import (
CachedVarOperation,
LiteralNoneVar,
LiteralVar,
ToOperation,
Var,
cached_property_no_lock,
)
from reflex.vars.function import ArgsFunctionOperation, FunctionStringVar, FunctionVar
from reflex.vars.function import (
ArgsFunctionOperation,
FunctionStringVar,
FunctionVar,
VarOperationCall,
)
from reflex.vars.object import ObjectVar
try:
@ -399,11 +400,6 @@ class EventChain(EventActionsMixin):
invocation: Optional[Var] = dataclasses.field(default=None)
# These chains can be used for their side effects when no other events are desired.
stop_propagation = EventChain(events=[], args_spec=lambda: []).stop_propagation
prevent_default = EventChain(events=[], args_spec=lambda: []).prevent_default
@dataclasses.dataclass(
init=True,
frozen=True,
@ -467,34 +463,103 @@ def empty_event() -> Tuple[()]:
return tuple() # type: ignore
# These chains can be used for their side effects when no other events are desired.
stop_propagation = EventChain(events=[], args_spec=empty_event).stop_propagation
prevent_default = EventChain(events=[], args_spec=empty_event).prevent_default
T = TypeVar("T")
U = TypeVar("U")
def identity_event(event_type: Type[T]) -> Callable[[Var[T]], Tuple[Var[T]]]:
# def identity_event(event_type: Type[T]) -> Callable[[Var[T]], Tuple[Var[T]]]:
# """A helper function that returns the input event as output.
# Args:
# event_type: The type of the event.
# Returns:
# A function that returns the input event as output.
# """
# def inner(ev: Var[T]) -> Tuple[Var[T]]:
# return (ev,)
# inner.__signature__ = inspect.signature(inner).replace( # type: ignore
# parameters=[
# inspect.Parameter(
# "ev",
# kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
# annotation=Var[event_type],
# )
# ],
# return_annotation=Tuple[Var[event_type]],
# )
# inner.__annotations__["ev"] = Var[event_type]
# inner.__annotations__["return"] = Tuple[Var[event_type]]
# return inner
class IdentityEventReturn(Generic[T], Protocol):
"""Protocol for an identity event return."""
def __call__(self, *values: Var[T]) -> Tuple[Var[T], ...]:
"""Return the input values.
Args:
*values: The values to return.
Returns:
The input values.
"""
return values
@overload
def identity_event(event_type: Type[T], /) -> Callable[[Var[T]], Tuple[Var[T]]]: ... # type: ignore
@overload
def identity_event(
event_type_1: Type[T], event_type2: Type[U], /
) -> Callable[[Var[T], Var[U]], Tuple[Var[T], Var[U]]]: ...
@overload
def identity_event(*event_types: Type[T]) -> IdentityEventReturn[T]: ...
def identity_event(*event_types: Type[T]) -> IdentityEventReturn[T]: # type: ignore
"""A helper function that returns the input event as output.
Args:
event_type: The type of the event.
*event_types: The types of the events.
Returns:
A function that returns the input event as output.
"""
def inner(ev: Var[T]) -> Tuple[Var[T]]:
return (ev,)
def inner(*values: Var[T]) -> Tuple[Var[T], ...]:
return values
inner_type = tuple(Var[event_type] for event_type in event_types)
return_annotation = Tuple[inner_type] # type: ignore
inner.__signature__ = inspect.signature(inner).replace( # type: ignore
parameters=[
inspect.Parameter(
"ev",
f"ev_{i}",
kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
annotation=Var[event_type],
)
for i, event_type in enumerate(event_types)
],
return_annotation=Tuple[Var[event_type]],
return_annotation=return_annotation,
)
inner.__annotations__["ev"] = Var[event_type]
inner.__annotations__["return"] = Tuple[Var[event_type]]
for i, event_type in enumerate(event_types):
inner.__annotations__[f"ev_{i}"] = Var[event_type]
inner.__annotations__["return"] = return_annotation
return inner
@ -1116,7 +1181,8 @@ def resolve_annotation(annotations: dict[str, Any], arg_name: str):
deprecation_version="0.6.3",
removal_version="0.7.0",
)
return JavascriptInputEvent
# Allow arbitrary attribute access two levels deep until removed.
return Dict[str, dict]
return annotation
@ -1334,7 +1400,7 @@ def get_fn_signature(fn: Callable) -> inspect.Signature:
return signature.replace(parameters=(new_param, *signature.parameters.values()))
class EventVar(ObjectVar):
class EventVar(ObjectVar, python_types=EventSpec):
"""Base class for event vars."""
@ -1343,7 +1409,7 @@ class EventVar(ObjectVar):
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class LiteralEventVar(CachedVarOperation, LiteralVar, EventVar):
class LiteralEventVar(VarOperationCall, LiteralVar, EventVar):
"""A literal event var."""
_var_value: EventSpec = dataclasses.field(default=None) # type: ignore
@ -1356,35 +1422,6 @@ class LiteralEventVar(CachedVarOperation, LiteralVar, EventVar):
"""
return hash((self.__class__.__name__, self._js_expr))
@cached_property_no_lock
def _cached_var_name(self) -> str:
"""The name of the var.
Returns:
The name of the var.
"""
return str(
FunctionStringVar("Event").call(
# event handler name
".".join(
filter(
None,
format.get_event_handler_parts(self._var_value.handler),
)
),
# event handler args
{str(name): value for name, value in self._var_value.args},
# event actions
self._var_value.event_actions,
# client handler name
*(
[self._var_value.client_handler_name]
if self._var_value.client_handler_name
else []
),
)
)
@classmethod
def create(
cls,
@ -1405,10 +1442,26 @@ class LiteralEventVar(CachedVarOperation, LiteralVar, EventVar):
_var_type=EventSpec,
_var_data=_var_data,
_var_value=value,
_func=FunctionStringVar("Event"),
_args=(
# event handler name
".".join(
filter(
None,
format.get_event_handler_parts(value.handler),
)
),
# event handler args
{str(name): value for name, value in value.args},
# event actions
value.event_actions,
# client handler name
*([value.client_handler_name] if value.client_handler_name else []),
),
)
class EventChainVar(FunctionVar):
class EventChainVar(FunctionVar, python_types=EventChain):
"""Base class for event chain vars."""
@ -1417,7 +1470,10 @@ class EventChainVar(FunctionVar):
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class LiteralEventChainVar(CachedVarOperation, LiteralVar, EventChainVar):
# Note: LiteralVar is second in the inheritance list allowing it act like a
# CachedVarOperation (ArgsFunctionOperation) and get the _js_expr from the
# _cached_var_name property.
class LiteralEventChainVar(ArgsFunctionOperation, LiteralVar, EventChainVar):
"""A literal event chain var."""
_var_value: EventChain = dataclasses.field(default=None) # type: ignore
@ -1430,41 +1486,6 @@ class LiteralEventChainVar(CachedVarOperation, LiteralVar, EventChainVar):
"""
return hash((self.__class__.__name__, self._js_expr))
@cached_property_no_lock
def _cached_var_name(self) -> str:
"""The name of the var.
Returns:
The name of the var.
"""
sig = inspect.signature(self._var_value.args_spec) # type: ignore
if sig.parameters:
arg_def = tuple((f"_{p}" for p in sig.parameters))
arg_def_expr = LiteralVar.create([Var(_js_expr=arg) for arg in arg_def])
else:
# add a default argument for addEvents if none were specified in value.args_spec
# used to trigger the preventDefault() on the event.
arg_def = ("...args",)
arg_def_expr = Var(_js_expr="args")
if self._var_value.invocation is None:
invocation = FunctionStringVar.create("addEvents")
else:
invocation = self._var_value.invocation
return str(
ArgsFunctionOperation.create(
arg_def,
invocation.call(
LiteralVar.create(
[LiteralVar.create(event) for event in self._var_value.events]
),
arg_def_expr,
self._var_value.event_actions,
),
)
)
@classmethod
def create(
cls,
@ -1480,48 +1501,48 @@ class LiteralEventChainVar(CachedVarOperation, LiteralVar, EventChainVar):
Returns:
The created LiteralEventChainVar instance.
"""
sig = inspect.signature(value.args_spec) # type: ignore
if sig.parameters:
arg_def = tuple((f"_{p}" for p in sig.parameters))
arg_def_expr = LiteralVar.create([Var(_js_expr=arg) for arg in arg_def])
else:
# add a default argument for addEvents if none were specified in value.args_spec
# used to trigger the preventDefault() on the event.
arg_def = ("...args",)
arg_def_expr = Var(_js_expr="args")
if value.invocation is None:
invocation = FunctionStringVar.create("addEvents")
else:
invocation = value.invocation
return cls(
_js_expr="",
_var_type=EventChain,
_var_data=_var_data,
_args_names=arg_def,
_return_expr=invocation.call(
LiteralVar.create([LiteralVar.create(event) for event in value.events]),
arg_def_expr,
value.event_actions,
),
_var_value=value,
)
@dataclasses.dataclass(
eq=False,
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class ToEventVarOperation(ToOperation, EventVar):
"""Result of a cast to an event var."""
_original: Var = dataclasses.field(default_factory=lambda: LiteralNoneVar.create())
_default_var_type: ClassVar[Type] = EventSpec
@dataclasses.dataclass(
eq=False,
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class ToEventChainVarOperation(ToOperation, EventChainVar):
"""Result of a cast to an event chain var."""
_original: Var = dataclasses.field(default_factory=lambda: LiteralNoneVar.create())
_default_var_type: ClassVar[Type] = EventChain
G = ParamSpec("G")
IndividualEventType = Union[EventSpec, EventHandler, Callable[G, Any], Var]
IndividualEventType = Union[EventSpec, EventHandler, Callable[G, Any], Var[Any]]
EventType = Union[IndividualEventType[G], List[IndividualEventType[G]]]
P = ParamSpec("P")
T = TypeVar("T")
V = TypeVar("V")
V2 = TypeVar("V2")
V3 = TypeVar("V3")
V4 = TypeVar("V4")
V5 = TypeVar("V5")
if sys.version_info >= (3, 10):
from typing import Concatenate
@ -1537,7 +1558,55 @@ if sys.version_info >= (3, 10):
"""
self.func = func
def __get__(self, instance, owner) -> Callable[P, T]:
@overload
def __get__(
self: EventCallback[[V], T], instance: None, owner
) -> Callable[[Union[Var[V], V]], EventSpec]: ...
@overload
def __get__(
self: EventCallback[[V, V2], T], instance: None, owner
) -> Callable[[Union[Var[V], V], Union[Var[V2], V2]], EventSpec]: ...
@overload
def __get__(
self: EventCallback[[V, V2, V3], T], instance: None, owner
) -> Callable[
[Union[Var[V], V], Union[Var[V2], V2], Union[Var[V3], V3]],
EventSpec,
]: ...
@overload
def __get__(
self: EventCallback[[V, V2, V3, V4], T], instance: None, owner
) -> Callable[
[
Union[Var[V], V],
Union[Var[V2], V2],
Union[Var[V3], V3],
Union[Var[V4], V4],
],
EventSpec,
]: ...
@overload
def __get__(
self: EventCallback[[V, V2, V3, V4, V5], T], instance: None, owner
) -> Callable[
[
Union[Var[V], V],
Union[Var[V2], V2],
Union[Var[V3], V3],
Union[Var[V4], V4],
Union[Var[V5], V5],
],
EventSpec,
]: ...
@overload
def __get__(self, instance, owner) -> Callable[P, T]: ...
def __get__(self, instance, owner) -> Callable:
"""Get the function with the instance bound to it.
Args:
@ -1588,8 +1657,6 @@ class EventNamespace(types.SimpleNamespace):
LiteralEventVar = LiteralEventVar
EventChainVar = EventChainVar
LiteralEventChainVar = LiteralEventChainVar
ToEventVarOperation = ToEventVarOperation
ToEventChainVarOperation = ToEventChainVarOperation
EventType = EventType
__call__ = staticmethod(event_handler)

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

View File

@ -129,7 +129,7 @@ class DrawerSidebar(DrawerRoot):
on_mouse_out: Optional[EventType[[]]] = None,
on_mouse_over: Optional[EventType[[]]] = None,
on_mouse_up: Optional[EventType[[]]] = None,
on_open_change: Optional[EventType] = None,
on_open_change: Optional[EventType[bool]] = None,
on_scroll: Optional[EventType[[]]] = None,
on_unmount: Optional[EventType[[]]] = None,
**props,

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

@ -2,9 +2,7 @@
from __future__ import annotations
import os
from collections import defaultdict
from pathlib import Path
from typing import Any, ClassVar, Optional, Type, Union
import alembic.autogenerate
@ -18,9 +16,8 @@ import sqlalchemy
import sqlalchemy.exc
import sqlalchemy.orm
from reflex import constants
from reflex.base import Base
from reflex.config import get_config
from reflex.config import environment, get_config
from reflex.utils import console
from reflex.utils.compat import sqlmodel, sqlmodel_field_has_primary_key
@ -41,12 +38,12 @@ def get_engine(url: str | None = None) -> sqlalchemy.engine.Engine:
url = url or conf.db_url
if url is None:
raise ValueError("No database url configured")
if not Path(constants.ALEMBIC_CONFIG).exists():
if environment.ALEMBIC_CONFIG.exists():
console.warn(
"Database is not initialized, run [bold]reflex db init[/bold] first."
)
# Print the SQL queries if the log level is INFO or lower.
echo_db_query = os.environ.get("SQLALCHEMY_ECHO") == "True"
echo_db_query = environment.SQLALCHEMY_ECHO
# Needed for the admin dash on sqlite.
connect_args = {"check_same_thread": False} if url.startswith("sqlite") else {}
return sqlmodel.create_engine(url, echo=echo_db_query, connect_args=connect_args)
@ -234,7 +231,7 @@ class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssue
Returns:
tuple of (config, script_directory)
"""
config = alembic.config.Config(constants.ALEMBIC_CONFIG)
config = alembic.config.Config(environment.ALEMBIC_CONFIG)
return config, alembic.script.ScriptDirectory(
config.get_main_option("script_location", default="version"),
)
@ -269,8 +266,8 @@ class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssue
def alembic_init(cls):
"""Initialize alembic for the project."""
alembic.command.init(
config=alembic.config.Config(constants.ALEMBIC_CONFIG),
directory=str(Path(constants.ALEMBIC_CONFIG).parent / "alembic"),
config=alembic.config.Config(environment.ALEMBIC_CONFIG),
directory=str(environment.ALEMBIC_CONFIG.parent / "alembic"),
)
@classmethod
@ -290,7 +287,7 @@ class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssue
Returns:
True when changes have been detected.
"""
if not Path(constants.ALEMBIC_CONFIG).exists():
if not environment.ALEMBIC_CONFIG.exists():
return False
config, script_directory = cls._alembic_config()
@ -391,7 +388,7 @@ class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssue
True - indicating the process was successful.
None - indicating the process was skipped.
"""
if not Path(constants.ALEMBIC_CONFIG).exists():
if not environment.ALEMBIC_CONFIG.exists():
return
with cls.get_db_engine().connect() as connection:

View File

@ -4,7 +4,6 @@ from __future__ import annotations
import atexit
import os
import webbrowser
from pathlib import Path
from typing import List, Optional
@ -14,7 +13,7 @@ from reflex_cli.deployments import deployments_cli
from reflex_cli.utils import dependency
from reflex import constants
from reflex.config import get_config
from reflex.config import environment, get_config
from reflex.custom_components.custom_components import custom_components_cli
from reflex.state import reset_disk_state_manager
from reflex.utils import console, redir, telemetry
@ -275,9 +274,17 @@ def run(
constants.Env.DEV, help="The environment to run the app in."
),
frontend: bool = typer.Option(
False, "--frontend-only", help="Execute only frontend."
False,
"--frontend-only",
help="Execute only frontend.",
envvar=constants.ENV_FRONTEND_ONLY_ENV_VAR,
),
backend: bool = typer.Option(
False,
"--backend-only",
help="Execute only backend.",
envvar=constants.ENV_BACKEND_ONLY_ENV_VAR,
),
backend: bool = typer.Option(False, "--backend-only", help="Execute only backend."),
frontend_port: str = typer.Option(
config.frontend_port, help="Specify a different frontend port."
),
@ -292,6 +299,12 @@ def run(
),
):
"""Run the app in the current directory."""
if frontend and backend:
console.error("Cannot use both --frontend-only and --backend-only options.")
raise typer.Exit(1)
os.environ[constants.ENV_BACKEND_ONLY_ENV_VAR] = str(backend).lower()
os.environ[constants.ENV_FRONTEND_ONLY_ENV_VAR] = str(frontend).lower()
_run(env, frontend, backend, frontend_port, backend_port, backend_host, loglevel)
@ -407,7 +420,7 @@ def db_init():
return
# Check the alembic config.
if Path(constants.ALEMBIC_CONFIG).exists():
if environment.ALEMBIC_CONFIG.exists():
console.error(
"Database is already initialized. Use "
"[bold]reflex db makemigrations[/bold] to create schema change "
@ -586,18 +599,6 @@ def deploy(
)
@cli.command()
def demo(
frontend_port: str = typer.Option(
"3001", help="Specify a different frontend port."
),
backend_port: str = typer.Option("8001", help="Specify a different backend port."),
):
"""Run the demo app."""
# Open the demo app in a terminal.
webbrowser.open("https://demo.reflex.run")
cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.")
cli.add_typer(

View File

@ -8,7 +8,6 @@ import copy
import dataclasses
import functools
import inspect
import os
import pickle
import sys
import uuid
@ -40,8 +39,12 @@ from typing import (
from sqlalchemy.orm import DeclarativeBase
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,
@ -64,6 +67,7 @@ from redis.exceptions import ResponseError
import reflex.istate.dynamic
from reflex import constants
from reflex.base import Base
from reflex.config import environment
from reflex.event import (
BACKGROUND_TASK_MARKER,
Event,
@ -2094,7 +2098,8 @@ class State(BaseState):
class FrontendEventExceptionState(State):
"""Substate for handling frontend exceptions."""
def handle_frontend_exception(self, stack: str) -> None:
@event
def handle_frontend_exception(self, stack: str, component_stack: str) -> None:
"""Handle frontend exceptions.
If a frontend exception handler is provided, it will be called.
@ -2102,6 +2107,7 @@ class FrontendEventExceptionState(State):
Args:
stack: The stack trace of the exception.
component_stack: The stack trace of the component where the exception occurred.
"""
app_instance = getattr(prerequisites.get_app(), constants.CompileVars.APP)
@ -2566,9 +2572,11 @@ class StateManager(Base, ABC):
The state manager (either disk, memory or redis).
"""
config = get_config()
if config.state_manager_mode == constants.StateManagerMode.DISK:
return StateManagerMemory(state=state)
if prerequisites.parse_redis_url() is not None:
config.state_manager_mode = constants.StateManagerMode.REDIS
if config.state_manager_mode == constants.StateManagerMode.MEMORY:
return StateManagerMemory(state=state)
if config.state_manager_mode == constants.StateManagerMode.DISK:
return StateManagerDisk(state=state)
if config.state_manager_mode == constants.StateManagerMode.REDIS:
redis = prerequisites.get_redis()
@ -3272,11 +3280,7 @@ class StateManagerRedis(StateManager):
)
except ResponseError:
# Some redis servers only allow out-of-band configuration, so ignore errors here.
ignore_config_error = os.environ.get(
"REFLEX_IGNORE_REDIS_CONFIG_ERROR",
None,
)
if not ignore_config_error:
if not environment.REFLEX_IGNORE_REDIS_CONFIG_ERROR:
raise
async with self.redis.pubsub() as pubsub:
await pubsub.psubscribe(lock_key_channel)
@ -3348,143 +3352,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

@ -10,6 +10,7 @@ from reflex.event import EventChain, EventHandler
from reflex.utils import format
from reflex.utils.exceptions import ReflexError
from reflex.utils.imports import ImportVar
from reflex.utils.types import get_origin
from reflex.vars import VarData
from reflex.vars.base import CallableVar, LiteralVar, Var
from reflex.vars.function import FunctionVar
@ -196,6 +197,10 @@ def convert(
isinstance(value, Breakpoints)
and all(not isinstance(v, dict) for v in value.values())
)
or (
isinstance(value, ObjectVar)
and not issubclass(get_origin(value._var_type) or value._var_type, dict)
)
else (key,)
)

View File

@ -23,18 +23,6 @@ def set_env_json():
)
def set_os_env(**kwargs):
"""Set os environment variables.
Args:
kwargs: env key word args.
"""
for key, value in kwargs.items():
if not value:
continue
os.environ[key.upper()] = value
def generate_sitemap_config(deploy_url: str, export=False):
"""Generate the sitemap config file.

View File

@ -139,3 +139,7 @@ class SetUndefinedStateVarError(ReflexError, AttributeError):
class StateSchemaMismatchError(ReflexError, TypeError):
"""Raised when the serialized schema of a state class does not match the current schema."""
class EnvironmentVarValueError(ReflexError, ValueError):
"""Raised when an environment variable is set to an invalid value."""

View File

@ -15,7 +15,7 @@ from urllib.parse import urljoin
import psutil
from reflex import constants
from reflex.config import get_config
from reflex.config import environment, get_config
from reflex.constants.base import LogLevel
from reflex.utils import console, path_ops
from reflex.utils.prerequisites import get_web_dir
@ -184,7 +184,7 @@ def should_use_granian():
Returns:
True if Granian should be used.
"""
return os.getenv("REFLEX_USE_GRANIAN", "0") == "1"
return environment.REFLEX_USE_GRANIAN
def get_app_module():
@ -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,
@ -496,6 +496,24 @@ def is_prod_mode() -> bool:
return current_mode == constants.Env.PROD.value
def is_frontend_only() -> bool:
"""Check if the app is running in frontend-only mode.
Returns:
True if the app is running in frontend-only mode.
"""
return os.environ.get(constants.ENV_FRONTEND_ONLY_ENV_VAR, "").lower() == "true"
def is_backend_only() -> bool:
"""Check if the app is running in backend-only mode.
Returns:
True if the app is running in backend-only mode.
"""
return os.environ.get(constants.ENV_BACKEND_ONLY_ENV_VAR, "").lower() == "true"
def should_skip_compile() -> bool:
"""Whether the app should skip compile.

View File

@ -1,9 +1,8 @@
"""Helpers for downloading files from the network."""
import os
import httpx
from ..config import environment
from . import console
@ -13,8 +12,7 @@ def _httpx_verify_kwarg() -> bool:
Returns:
True if SSL verification is enabled, False otherwise
"""
ssl_no_verify = os.environ.get("SSL_NO_VERIFY", "").lower() in ["true", "1", "yes"]
return not ssl_no_verify
return not environment.SSL_NO_VERIFY
def get(url: str, **kwargs) -> httpx.Response:

View File

@ -9,6 +9,7 @@ import shutil
from pathlib import Path
from reflex import constants
from reflex.config import environment
# Shorthand for join.
join = os.linesep.join
@ -129,30 +130,13 @@ def which(program: str | Path) -> str | Path | None:
return shutil.which(str(program))
def use_system_install(var_name: str) -> bool:
"""Check if the system install should be used.
Args:
var_name: The name of the environment variable.
Raises:
ValueError: If the variable name is invalid.
Returns:
Whether the associated env var should use the system install.
"""
if not var_name.startswith("REFLEX_USE_SYSTEM_"):
raise ValueError("Invalid system install variable name.")
return os.getenv(var_name, "").lower() in ["true", "1", "yes"]
def use_system_node() -> bool:
"""Check if the system node should be used.
Returns:
Whether the system node should be used.
"""
return use_system_install(constants.Node.USE_SYSTEM_VAR)
return environment.REFLEX_USE_SYSTEM_NODE
def use_system_bun() -> bool:
@ -161,7 +145,7 @@ def use_system_bun() -> bool:
Returns:
Whether the system bun should be used.
"""
return use_system_install(constants.Bun.USE_SYSTEM_VAR)
return environment.REFLEX_USE_SYSTEM_BUN
def get_node_bin_path() -> Path | None:
@ -185,7 +169,8 @@ def get_node_path() -> str | None:
"""
node_path = Path(constants.Node.PATH)
if use_system_node() or not node_path.exists():
return str(which("node"))
system_node_path = which("node")
return str(system_node_path) if system_node_path else None
return str(node_path)
@ -197,7 +182,8 @@ def get_npm_path() -> str | None:
"""
npm_path = Path(constants.Node.NPM_PATH)
if use_system_node() or not npm_path.exists():
return str(which("npm"))
system_npm_path = which("npm")
return str(system_npm_path) if system_npm_path else None
return str(npm_path)

View File

@ -33,7 +33,7 @@ from redis.asyncio import Redis
from reflex import constants, model
from reflex.compiler import templates
from reflex.config import Config, get_config
from reflex.config import Config, environment, get_config
from reflex.utils import console, net, path_ops, processes
from reflex.utils.exceptions import GeneratedCodeHasNoFunctionDefs
from reflex.utils.format import format_library_name
@ -69,8 +69,7 @@ def get_web_dir() -> Path:
Returns:
The working directory.
"""
workdir = Path(os.getenv("REFLEX_WEB_WORKDIR", constants.Dirs.WEB))
return workdir
return environment.REFLEX_WEB_WORKDIR
def _python_version_check():
@ -146,14 +145,9 @@ def check_node_version() -> bool:
Whether the version of Node.js is valid.
"""
current_version = get_node_version()
if current_version:
# Compare the version numbers
return (
current_version >= version.parse(constants.Node.MIN_VERSION)
if constants.IS_WINDOWS or path_ops.use_system_node()
else current_version == version.parse(constants.Node.VERSION)
)
return False
return current_version is not None and current_version >= version.parse(
constants.Node.MIN_VERSION
)
def get_node_version() -> version.Version | None:
@ -255,7 +249,7 @@ def windows_npm_escape_hatch() -> bool:
Returns:
If the user has set REFLEX_USE_NPM.
"""
return os.environ.get("REFLEX_USE_NPM", "").lower() in ["true", "1", "yes"]
return environment.REFLEX_USE_NPM
def get_app(reload: bool = False) -> ModuleType:
@ -997,7 +991,7 @@ def needs_reinit(frontend: bool = True) -> bool:
return False
# Make sure the .reflex directory exists.
if not constants.Reflex.DIR.exists():
if not environment.REFLEX_DIR.exists():
return True
# Make sure the .web directory exists in frontend mode.
@ -1102,7 +1096,7 @@ def ensure_reflex_installation_id() -> Optional[int]:
"""
try:
initialize_reflex_user_directory()
installation_id_file = constants.Reflex.DIR / "installation_id"
installation_id_file = environment.REFLEX_DIR / "installation_id"
installation_id = None
if installation_id_file.exists():
@ -1127,7 +1121,7 @@ def ensure_reflex_installation_id() -> Optional[int]:
def initialize_reflex_user_directory():
"""Initialize the reflex user directory."""
# Create the reflex directory.
path_ops.mkdir(constants.Reflex.DIR)
path_ops.mkdir(environment.REFLEX_DIR)
def initialize_frontend_dependencies():
@ -1150,7 +1144,7 @@ def check_db_initialized() -> bool:
Returns:
True if alembic is initialized (or if database is not used).
"""
if get_config().db_url is not None and not Path(constants.ALEMBIC_CONFIG).exists():
if get_config().db_url is not None and not environment.ALEMBIC_CONFIG.exists():
console.error(
"Database is not initialized. Run [bold]reflex db init[/bold] first."
)
@ -1160,7 +1154,7 @@ def check_db_initialized() -> bool:
def check_schema_up_to_date():
"""Check if the sqlmodel metadata matches the current database schema."""
if get_config().db_url is None or not Path(constants.ALEMBIC_CONFIG).exists():
if get_config().db_url is None or not environment.ALEMBIC_CONFIG.exists():
return
with model.Model.get_db_engine().connect() as connection:
try:

View File

@ -16,7 +16,7 @@ from itertools import chain
from multiprocessing import Pool, cpu_count
from pathlib import Path
from types import ModuleType, SimpleNamespace
from typing import Any, Callable, Iterable, Type, get_args
from typing import Any, Callable, Iterable, Type, get_args, get_origin
from reflex.components.component import Component
from reflex.utils import types as rx_types
@ -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,6 +375,64 @@ def _extract_class_props_as_ast_nodes(
return kwargs
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.
"""
if typ is type(None):
return ast.Name(id="None")
origin = get_origin(typ)
# 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)
return ast.Name(id=str(typ))
# Get the base type name (List, Dict, Optional, etc.)
base_name = origin._name if hasattr(origin, "_name") else origin.__name__
# Get type arguments
args = get_args(typ)
# Handle empty type arguments
if not args:
return ast.Name(id=base_name)
# Convert all type arguments recursively
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:
slice_value = arg_nodes[0]
else:
slice_value = ast.Tuple(elts=arg_nodes, ctx=ast.Load())
return ast.Subscript(
value=ast.Name(id=base_name), slice=ast.Index(value=slice_value), ctx=ast.Load()
)
def _get_parent_imports(func):
_imports = {"reflex.vars": ["Var"]}
for type_hint in inspect.get_annotations(func).values():
@ -429,14 +490,41 @@ def _generate_component_create_functiondef(
def figure_out_return_type(annotation: Any):
if inspect.isclass(annotation) and issubclass(annotation, inspect._empty):
return ast.Name(id="Optional[EventType[[]]]")
return ast.Name(id="Optional[EventType]")
if not isinstance(annotation, str) and get_origin(annotation) is tuple:
arguments = get_args(annotation)
arguments_without_var = [
get_args(argument)[0] if get_origin(argument) == Var else argument
for argument in arguments
]
# Convert each argument type to its AST representation
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)
# Create EventType using the joined string
event_type = ast.Name(id=f"EventType[{args_str}]")
# Wrap in Optional
optional_type = ast.Subscript(
value=ast.Name(id="Optional"),
slice=ast.Index(value=event_type),
ctx=ast.Load(),
)
return ast.Name(id=ast.unparse(optional_type))
if isinstance(annotation, str) and annotation.startswith("Tuple["):
inside_of_tuple = annotation.removeprefix("Tuple[").removesuffix("]")
if inside_of_tuple == "()":
return ast.Name(id="Optional[EventType[[]]]")
arguments: list[str] = [""]
arguments = [""]
bracket_count = 0

View File

@ -1,9 +1,8 @@
"""Utilities for working with registries."""
import os
import httpx
from reflex.config import environment
from reflex.utils import console, net
@ -56,7 +55,4 @@ def _get_npm_registry() -> str:
Returns:
str:
"""
if npm_registry := os.environ.get("NPM_CONFIG_REGISTRY", ""):
return npm_registry
else:
return get_best_registry()
return environment.NPM_CONFIG_REGISTRY or get_best_registry()

View File

@ -274,6 +274,20 @@ def is_optional(cls: GenericType) -> bool:
return is_union(cls) and type(None) in get_args(cls)
def value_inside_optional(cls: GenericType) -> GenericType:
"""Get the value inside an Optional type or the original type.
Args:
cls: The class to check.
Returns:
The value inside the Optional type or the original type.
"""
if is_union(cls) and len(args := get_args(cls)) >= 2 and type(None) in args:
return unionize(*[arg for arg in args if arg is not type(None)])
return cls
def get_property_hint(attr: Any | None) -> GenericType | None:
"""Check if an attribute is a property and return its type hint.

File diff suppressed because it is too large Load Diff

View File

@ -4,21 +4,20 @@ from __future__ import annotations
import dataclasses
import sys
from typing import Any, Callable, ClassVar, Optional, Tuple, Type, Union
from typing import Any, Callable, Optional, Tuple, Type, Union
from reflex.utils.types import GenericType
from .base import (
CachedVarOperation,
LiteralVar,
ToOperation,
Var,
VarData,
cached_property_no_lock,
)
class FunctionVar(Var[Callable]):
class FunctionVar(Var[Callable], python_types=Callable):
"""Base class for immutable function vars."""
def __call__(self, *args: Var | Any) -> ArgsFunctionOperation:
@ -180,17 +179,8 @@ class ArgsFunctionOperation(CachedVarOperation, FunctionVar):
)
@dataclasses.dataclass(
eq=False,
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class ToFunctionOperation(ToOperation, FunctionVar):
"""Base class of converting a var to a function."""
_original: Var = dataclasses.field(default_factory=lambda: LiteralVar.create(None))
_default_var_type: ClassVar[GenericType] = Callable
JSON_STRINGIFY = FunctionStringVar.create("JSON.stringify")
ARRAY_ISARRAY = FunctionStringVar.create("Array.isArray")
PROTOTYPE_TO_STRING = FunctionStringVar.create(
"((__to_string) => __to_string.toString())"
)

View File

@ -10,7 +10,6 @@ from typing import (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
NoReturn,
Type,
TypeVar,
@ -25,9 +24,7 @@ from reflex.utils.types import is_optional
from .base import (
CustomVarOperationReturn,
LiteralNoneVar,
LiteralVar,
ToOperation,
Var,
VarData,
unionize,
@ -58,7 +55,7 @@ def raise_unsupported_operand_types(
)
class NumberVar(Var[NUMBER_T]):
class NumberVar(Var[NUMBER_T], python_types=(int, float)):
"""Base class for immutable number vars."""
@overload
@ -760,7 +757,7 @@ def number_trunc_operation(value: NumberVar):
return var_operation_return(js_expression=f"Math.trunc({value})", var_type=int)
class BooleanVar(NumberVar[bool]):
class BooleanVar(NumberVar[bool], python_types=bool):
"""Base class for immutable boolean vars."""
def __invert__(self):
@ -984,51 +981,6 @@ def boolean_not_operation(value: BooleanVar):
return var_operation_return(js_expression=f"!({value})", var_type=bool)
@dataclasses.dataclass(
eq=False,
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class LiteralBooleanVar(LiteralVar, BooleanVar):
"""Base class for immutable literal boolean vars."""
_var_value: bool = dataclasses.field(default=False)
def json(self) -> str:
"""Get the JSON representation of the var.
Returns:
The JSON representation of the var.
"""
return "true" if self._var_value else "false"
def __hash__(self) -> int:
"""Calculate the hash value of the object.
Returns:
int: The hash value of the object.
"""
return hash((self.__class__.__name__, self._var_value))
@classmethod
def create(cls, value: bool, _var_data: VarData | None = None):
"""Create the boolean var.
Args:
value: The value of the var.
_var_data: Additional hooks and imports associated with the Var.
Returns:
The boolean var.
"""
return cls(
_js_expr="true" if value else "false",
_var_type=bool,
_var_data=_var_data,
_var_value=value,
)
@dataclasses.dataclass(
eq=False,
frozen=True,
@ -1088,36 +1040,55 @@ class LiteralNumberVar(LiteralVar, NumberVar):
)
@dataclasses.dataclass(
eq=False,
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class LiteralBooleanVar(LiteralVar, BooleanVar):
"""Base class for immutable literal boolean vars."""
_var_value: bool = dataclasses.field(default=False)
def json(self) -> str:
"""Get the JSON representation of the var.
Returns:
The JSON representation of the var.
"""
return "true" if self._var_value else "false"
def __hash__(self) -> int:
"""Calculate the hash value of the object.
Returns:
int: The hash value of the object.
"""
return hash((self.__class__.__name__, self._var_value))
@classmethod
def create(cls, value: bool, _var_data: VarData | None = None):
"""Create the boolean var.
Args:
value: The value of the var.
_var_data: Additional hooks and imports associated with the Var.
Returns:
The boolean var.
"""
return cls(
_js_expr="true" if value else "false",
_var_type=bool,
_var_data=_var_data,
_var_value=value,
)
number_types = Union[NumberVar, int, float]
boolean_types = Union[BooleanVar, bool]
@dataclasses.dataclass(
eq=False,
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class ToNumberVarOperation(ToOperation, NumberVar):
"""Base class for immutable number vars that are the result of a number operation."""
_original: Var = dataclasses.field(default_factory=lambda: LiteralNoneVar.create())
_default_var_type: ClassVar[Type] = float
@dataclasses.dataclass(
eq=False,
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class ToBooleanVarOperation(ToOperation, BooleanVar):
"""Base class for immutable boolean vars that are the result of a boolean operation."""
_original: Var = dataclasses.field(default_factory=lambda: LiteralNoneVar.create())
_default_var_type: ClassVar[Type] = bool
_IS_TRUE_IMPORT: ImportDict = {
f"/{Dirs.STATE_PATH}": [ImportVar(tag="isTrue")],
}
@ -1140,8 +1111,12 @@ def boolify(value: Var):
)
T = TypeVar("T")
U = TypeVar("U")
@var_operation
def ternary_operation(condition: BooleanVar, if_true: Var, if_false: Var):
def ternary_operation(condition: BooleanVar, if_true: Var[T], if_false: Var[U]):
"""Create a ternary operation.
Args:
@ -1152,10 +1127,14 @@ def ternary_operation(condition: BooleanVar, if_true: Var, if_false: Var):
Returns:
The ternary operation.
"""
return var_operation_return(
js_expression=f"({condition} ? {if_true} : {if_false})",
var_type=unionize(if_true._var_type, if_false._var_type),
type_value: Union[Type[T], Type[U]] = unionize(
if_true._var_type, if_false._var_type
)
value: CustomVarOperationReturn[Union[T, U]] = var_operation_return(
js_expression=f"({condition} ? {if_true} : {if_false})",
var_type=type_value,
)
return value
NUMBER_TYPES = (int, float, NumberVar)

View File

@ -8,7 +8,6 @@ import typing
from inspect import isclass
from typing import (
Any,
ClassVar,
Dict,
List,
NoReturn,
@ -27,7 +26,6 @@ from reflex.utils.types import GenericType, get_attribute_access_type, get_origi
from .base import (
CachedVarOperation,
LiteralVar,
ToOperation,
Var,
VarData,
cached_property_no_lock,
@ -48,7 +46,7 @@ ARRAY_INNER_TYPE = TypeVar("ARRAY_INNER_TYPE")
OTHER_KEY_TYPE = TypeVar("OTHER_KEY_TYPE")
class ObjectVar(Var[OBJECT_TYPE]):
class ObjectVar(Var[OBJECT_TYPE], python_types=dict):
"""Base class for immutable object vars."""
def _key_type(self) -> Type:
@ -521,34 +519,6 @@ class ObjectItemOperation(CachedVarOperation, Var):
)
@dataclasses.dataclass(
eq=False,
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class ToObjectOperation(ToOperation, ObjectVar):
"""Operation to convert a var to an object."""
_original: Var = dataclasses.field(
default_factory=lambda: LiteralObjectVar.create({})
)
_default_var_type: ClassVar[GenericType] = dict
def __getattr__(self, name: str) -> Any:
"""Get an attribute of the var.
Args:
name: The name of the attribute.
Returns:
The attribute of the var.
"""
if name == "_js_expr":
return self._original._js_expr
return ObjectVar.__getattr__(self, name)
@var_operation
def object_has_own_property_operation(object: ObjectVar, key: Var):
"""Check if an object has a key.

View File

@ -11,7 +11,6 @@ import typing
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Dict,
List,
Literal,
@ -19,27 +18,28 @@ from typing import (
Set,
Tuple,
Type,
TypeVar,
Union,
overload,
)
from typing_extensions import TypeVar
from reflex import constants
from reflex.constants.base import REFLEX_VAR_OPENING_TAG
from reflex.constants.colors import Color
from reflex.utils.exceptions import VarTypeError
from reflex.utils.types import GenericType, get_origin
from .base import (
CachedVarOperation,
CustomVarOperationReturn,
LiteralNoneVar,
LiteralVar,
ToOperation,
Var,
VarData,
_global_vars,
cached_property_no_lock,
figure_out_type,
get_python_literal,
get_unique_variable_name,
unionize,
var_operation,
@ -50,13 +50,16 @@ from .number import (
LiteralNumberVar,
NumberVar,
raise_unsupported_operand_types,
ternary_operation,
)
if TYPE_CHECKING:
from .object import ObjectVar
STRING_TYPE = TypeVar("STRING_TYPE", default=str)
class StringVar(Var[str]):
class StringVar(Var[STRING_TYPE], python_types=str):
"""Base class for immutable string vars."""
@overload
@ -350,7 +353,7 @@ class StringVar(Var[str]):
@var_operation
def string_lt_operation(lhs: StringVar | str, rhs: StringVar | str):
def string_lt_operation(lhs: StringVar[Any] | str, rhs: StringVar[Any] | str):
"""Check if a string is less than another string.
Args:
@ -364,7 +367,7 @@ def string_lt_operation(lhs: StringVar | str, rhs: StringVar | str):
@var_operation
def string_gt_operation(lhs: StringVar | str, rhs: StringVar | str):
def string_gt_operation(lhs: StringVar[Any] | str, rhs: StringVar[Any] | str):
"""Check if a string is greater than another string.
Args:
@ -378,7 +381,7 @@ def string_gt_operation(lhs: StringVar | str, rhs: StringVar | str):
@var_operation
def string_le_operation(lhs: StringVar | str, rhs: StringVar | str):
def string_le_operation(lhs: StringVar[Any] | str, rhs: StringVar[Any] | str):
"""Check if a string is less than or equal to another string.
Args:
@ -392,7 +395,7 @@ def string_le_operation(lhs: StringVar | str, rhs: StringVar | str):
@var_operation
def string_ge_operation(lhs: StringVar | str, rhs: StringVar | str):
def string_ge_operation(lhs: StringVar[Any] | str, rhs: StringVar[Any] | str):
"""Check if a string is greater than or equal to another string.
Args:
@ -406,7 +409,7 @@ def string_ge_operation(lhs: StringVar | str, rhs: StringVar | str):
@var_operation
def string_lower_operation(string: StringVar):
def string_lower_operation(string: StringVar[Any]):
"""Convert a string to lowercase.
Args:
@ -419,7 +422,7 @@ def string_lower_operation(string: StringVar):
@var_operation
def string_upper_operation(string: StringVar):
def string_upper_operation(string: StringVar[Any]):
"""Convert a string to uppercase.
Args:
@ -432,7 +435,7 @@ def string_upper_operation(string: StringVar):
@var_operation
def string_strip_operation(string: StringVar):
def string_strip_operation(string: StringVar[Any]):
"""Strip a string.
Args:
@ -446,7 +449,7 @@ def string_strip_operation(string: StringVar):
@var_operation
def string_contains_field_operation(
haystack: StringVar, needle: StringVar | str, field: StringVar | str
haystack: StringVar[Any], needle: StringVar[Any] | str, field: StringVar[Any] | str
):
"""Check if a string contains another string.
@ -465,7 +468,7 @@ def string_contains_field_operation(
@var_operation
def string_contains_operation(haystack: StringVar, needle: StringVar | str):
def string_contains_operation(haystack: StringVar[Any], needle: StringVar[Any] | str):
"""Check if a string contains another string.
Args:
@ -481,7 +484,9 @@ def string_contains_operation(haystack: StringVar, needle: StringVar | str):
@var_operation
def string_starts_with_operation(full_string: StringVar, prefix: StringVar | str):
def string_starts_with_operation(
full_string: StringVar[Any], prefix: StringVar[Any] | str
):
"""Check if a string starts with a prefix.
Args:
@ -497,7 +502,7 @@ def string_starts_with_operation(full_string: StringVar, prefix: StringVar | str
@var_operation
def string_item_operation(string: StringVar, index: NumberVar | int):
def string_item_operation(string: StringVar[Any], index: NumberVar | int):
"""Get an item from a string.
Args:
@ -511,7 +516,7 @@ def string_item_operation(string: StringVar, index: NumberVar | int):
@var_operation
def array_join_operation(array: ArrayVar, sep: StringVar | str = ""):
def array_join_operation(array: ArrayVar, sep: StringVar[Any] | str = ""):
"""Join the elements of an array.
Args:
@ -524,6 +529,26 @@ def array_join_operation(array: ArrayVar, sep: StringVar | 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}"
@ -536,7 +561,7 @@ _decode_var_pattern = re.compile(_decode_var_pattern_re, flags=re.DOTALL)
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class LiteralStringVar(LiteralVar, StringVar):
class LiteralStringVar(LiteralVar, StringVar[str]):
"""Base class for immutable literal string vars."""
_var_value: str = dataclasses.field(default="")
@ -658,7 +683,7 @@ class LiteralStringVar(LiteralVar, StringVar):
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class ConcatVarOperation(CachedVarOperation, StringVar):
class ConcatVarOperation(CachedVarOperation, StringVar[str]):
"""Representing a concatenation of literal string vars."""
_var_value: Tuple[Var, ...] = dataclasses.field(default_factory=tuple)
@ -742,7 +767,7 @@ KEY_TYPE = TypeVar("KEY_TYPE")
VALUE_TYPE = TypeVar("VALUE_TYPE")
class ArrayVar(Var[ARRAY_VAR_TYPE]):
class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(list, tuple, set)):
"""Base class for immutable array vars."""
@overload
@ -1155,7 +1180,7 @@ class ArrayVar(Var[ARRAY_VAR_TYPE]):
function_var = ArgsFunctionOperation.create(tuple(), return_value)
else:
# generic number var
number_var = Var("").to(NumberVar)
number_var = Var("").to(NumberVar, int)
first_arg_type = self[number_var]._var_type
@ -1167,7 +1192,10 @@ class ArrayVar(Var[ARRAY_VAR_TYPE]):
_var_type=first_arg_type,
).guess_type()
function_var = ArgsFunctionOperation.create((arg_name,), fn(first_arg))
function_var = ArgsFunctionOperation.create(
(arg_name,),
Var.create(fn(first_arg)),
)
return map_array_operation(self, function_var)
@ -1272,7 +1300,7 @@ class LiteralArrayVar(CachedVarOperation, LiteralVar, ArrayVar[ARRAY_VAR_TYPE]):
@var_operation
def string_split_operation(string: StringVar, sep: StringVar | str = ""):
def string_split_operation(string: StringVar[Any], sep: StringVar | str = ""):
"""Split a string.
Args:
@ -1569,32 +1597,6 @@ def array_contains_operation(haystack: ArrayVar, needle: Any | Var):
)
@dataclasses.dataclass(
eq=False,
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class ToStringOperation(ToOperation, StringVar):
"""Base class for immutable string vars that are the result of a to string operation."""
_original: Var = dataclasses.field(default_factory=lambda: LiteralNoneVar.create())
_default_var_type: ClassVar[Type] = str
@dataclasses.dataclass(
eq=False,
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class ToArrayOperation(ToOperation, ArrayVar):
"""Base class for immutable array vars that are the result of a to array operation."""
_original: Var = dataclasses.field(default_factory=lambda: LiteralNoneVar.create())
_default_var_type: ClassVar[Type] = List[Any]
@var_operation
def repeat_array_operation(
array: ArrayVar[ARRAY_VAR_TYPE], count: NumberVar | int
@ -1654,3 +1656,134 @@ def array_concat_operation(
js_expression=f"[...{lhs}, ...{rhs}]",
var_type=Union[lhs._var_type, rhs._var_type],
)
class ColorVar(StringVar[Color], python_types=Color):
"""Base class for immutable color vars."""
@dataclasses.dataclass(
eq=False,
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class LiteralColorVar(CachedVarOperation, LiteralVar, ColorVar):
"""Base class for immutable literal color vars."""
_var_value: Color = dataclasses.field(default_factory=lambda: Color(color="black"))
@classmethod
def create(
cls,
value: Color,
_var_type: Type[Color] | None = None,
_var_data: VarData | None = None,
) -> ColorVar:
"""Create a var from a string value.
Args:
value: The value to create the var from.
_var_type: The type of the var.
_var_data: Additional hooks and imports associated with the Var.
Returns:
The var.
"""
return cls(
_js_expr="",
_var_type=_var_type or Color,
_var_data=_var_data,
_var_value=value,
)
def __hash__(self) -> int:
"""Get the hash of the var.
Returns:
The hash of the var.
"""
return hash(
(
self.__class__.__name__,
self._var_value.color,
self._var_value.alpha,
self._var_value.shade,
)
)
@cached_property_no_lock
def _cached_var_name(self) -> str:
"""The name of the var.
Returns:
The name of the var.
"""
alpha = self._var_value.alpha
alpha = (
ternary_operation(
alpha,
LiteralStringVar.create("a"),
LiteralStringVar.create(""),
)
if isinstance(alpha, Var)
else LiteralStringVar.create("a" if alpha else "")
)
shade = self._var_value.shade
shade = (
shade.to_string(use_json=False)
if isinstance(shade, Var)
else LiteralStringVar.create(str(shade))
)
return str(
ConcatVarOperation.create(
LiteralStringVar.create("var(--"),
self._var_value.color,
LiteralStringVar.create("-"),
alpha,
shade,
LiteralStringVar.create(")"),
)
)
@cached_property_no_lock
def _cached_get_all_var_data(self) -> VarData | None:
"""Get all the var data.
Returns:
The var data.
"""
return VarData.merge(
*[
LiteralVar.create(var)._get_all_var_data()
for var in (
self._var_value.color,
self._var_value.alpha,
self._var_value.shade,
)
],
self._var_data,
)
def json(self) -> str:
"""Get the JSON representation of the var.
Returns:
The JSON representation of the var.
Raises:
TypeError: If the color is not a valid color.
"""
color, alpha, shade = map(
get_python_literal,
(self._var_value.color, self._var_value.alpha, self._var_value.shade),
)
if color is None or alpha is None or shade is None:
raise TypeError("Cannot serialize color that contains non-literal vars.")
if (
not isinstance(color, str)
or not isinstance(alpha, bool)
or not isinstance(shade, int)
):
raise TypeError("Color is not a valid color.")
return f"var(--{color}-{'a' if alpha else ''}{shade})"

View File

@ -51,6 +51,7 @@ def LifespanApp():
def context_global(self) -> int:
return lifespan_context_global
@rx.event
def tick(self, date):
pass

View File

@ -14,6 +14,7 @@ class ColorState(rx.State):
color: str = "mint"
color_part: str = "tom"
shade: int = 4
alpha: bool = False
color_state_name = ColorState.get_full_name().replace(".", "__")
@ -31,7 +32,14 @@ def create_color_var(color):
(create_color_var(rx.color("mint", 3, True)), '"var(--mint-a3)"', Color),
(
create_color_var(rx.color(ColorState.color, ColorState.shade)), # type: ignore
f'("var(--"+{str(color_state_name)}.color+"-"+{str(color_state_name)}.shade+")")',
f'("var(--"+{str(color_state_name)}.color+"-"+(((__to_string) => __to_string.toString())({str(color_state_name)}.shade))+")")',
Color,
),
(
create_color_var(
rx.color(ColorState.color, ColorState.shade, ColorState.alpha) # type: ignore
),
f'("var(--"+{str(color_state_name)}.color+"-"+({str(color_state_name)}.alpha ? "a" : "")+(((__to_string) => __to_string.toString())({str(color_state_name)}.shade))+")")',
Color,
),
(
@ -43,7 +51,7 @@ def create_color_var(color):
create_color_var(
rx.color(f"{ColorState.color_part}ato", f"{ColorState.shade}") # type: ignore
),
f'("var(--"+{str(color_state_name)}.color_part+"ato-"+{str(color_state_name)}.shade+")")',
f'("var(--"+({str(color_state_name)}.color_part+"ato")+"-"+{str(color_state_name)}.shade+")")',
Color,
),
(

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

View File

@ -5,6 +5,7 @@ import pytest
import reflex as rx
import reflex.config
from reflex.config import environment
from reflex.constants import Endpoint
@ -178,7 +179,7 @@ def test_replace_defaults(
def reflex_dir_constant():
return rx.constants.Reflex.DIR
return environment.REFLEX_DIR
def test_reflex_dir_env_var(monkeypatch, tmp_path):

View File

@ -2,11 +2,18 @@ from typing import List
import pytest
from reflex import event
from reflex.event import Event, EventHandler, EventSpec, call_event_handler, fix_events
from reflex.event import (
Event,
EventChain,
EventHandler,
EventSpec,
call_event_handler,
event,
fix_events,
)
from reflex.state import BaseState
from reflex.utils import format
from reflex.vars.base import LiteralVar, Var
from reflex.vars.base import Field, LiteralVar, Var, field
def make_var(value) -> Var:
@ -388,3 +395,28 @@ def test_event_actions_on_state():
assert sp_handler.event_actions == {"stopPropagation": True}
# should NOT affect other references to the handler
assert not handler.event_actions
def test_event_var_data():
class S(BaseState):
x: Field[int] = field(0)
@event
def s(self, value: int):
pass
# Handler doesn't have any _var_data because it's just a str
handler_var = Var.create(S.s)
assert handler_var._get_all_var_data() is None
# Ensure spec carries _var_data
spec_var = Var.create(S.s(S.x))
assert spec_var._get_all_var_data() == S.x._get_all_var_data()
# Needed to instantiate the EventChain
def _args_spec(value: Var[int]) -> tuple[Var[int]]:
return (value,)
# Ensure chain carries _var_data
chain_var = Var.create(EventChain(events=[S.s(S.x)], args_spec=_args_spec))
assert chain_var._get_all_var_data() == S.x._get_all_var_data()

View File

@ -519,8 +519,8 @@ def test_var_indexing_types(var, type_):
type_ : The type on indexed object.
"""
assert var[2]._var_type == type_[0]
assert var[3]._var_type == type_[1]
assert var[0]._var_type == type_[0]
assert var[1]._var_type == type_[1]
def test_var_indexing_str():