diff --git a/reflex/components/datadisplay/shiki_code_block.py b/reflex/components/datadisplay/shiki_code_block.py index 43e138f2d..48d595327 100644 --- a/reflex/components/datadisplay/shiki_code_block.py +++ b/reflex/components/datadisplay/shiki_code_block.py @@ -12,6 +12,7 @@ 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.props import NoExtrasAllowedProps from reflex.components.radix.themes.layout.box import Box from reflex.event import call_script, set_clipboard from reflex.style import Style @@ -390,6 +391,23 @@ LiteralCodeTheme = Literal[ ] +class Position(NoExtrasAllowedProps): + """Position of the decoration.""" + + line: str + character: str + + +class ShikiDecorations(NoExtrasAllowedProps): + """Decorations for the code block.""" + + start: Union[int, Position] + end: Union[int, Position] + tag_name: str = "span" + properties: dict[str, Any] = {} + always_wrap: bool = False + + class ShikiBaseTransformers(Base): """Base for creating transformers.""" @@ -538,7 +556,7 @@ class ShikiCodeBlock(Component): ) # The decorations to use for the syntax highlighter. - decorations: Var[list[dict[str, Any]]] = Var.create([]) + decorations: Var[list[ShikiDecorations]] = Var.create([]) @classmethod def create( @@ -558,6 +576,7 @@ class ShikiCodeBlock(Component): # Separate props for the code block and the wrapper code_block_props = {} code_wrapper_props = {} + decorations = props.pop("decorations", []) class_props = cls.get_props() @@ -577,6 +596,9 @@ class ShikiCodeBlock(Component): transformer_styles.update(transformer.style) transformer_styles.update(code_wrapper_props.pop("style", {})) + decorations = [ShikiDecorations(**decoration) for decoration in decorations] + code_block_props["decorations"] = decorations + return Box.create( code_block, *children[1:], diff --git a/reflex/components/datadisplay/shiki_code_block.pyi b/reflex/components/datadisplay/shiki_code_block.pyi index d2ed995ff..91a092836 100644 --- a/reflex/components/datadisplay/shiki_code_block.pyi +++ b/reflex/components/datadisplay/shiki_code_block.pyi @@ -7,6 +7,7 @@ from typing import Any, Dict, Literal, Optional, Union, overload from reflex.base import Base from reflex.components.component import Component, ComponentNamespace +from reflex.components.props import NoExtrasAllowedProps from reflex.event import EventType from reflex.style import Style from reflex.vars.base import Var @@ -328,6 +329,17 @@ LiteralCodeTheme = Literal[ "vitesse-light", ] +class Position(NoExtrasAllowedProps): + line: str + character: str + +class ShikiDecorations(NoExtrasAllowedProps): + start: Union[int, Position] + end: Union[int, Position] + tag_name: str + properties: dict[str, Any] + always_wrap: bool + class ShikiBaseTransformers(Base): library: str fns: list[FunctionStringVar] @@ -907,7 +919,7 @@ class ShikiCodeBlock(Component): ] ] = None, decorations: Optional[ - Union[Var[list[dict[str, Any]]], list[dict[str, Any]]] + Union[Var[list[ShikiDecorations]], list[ShikiDecorations]] ] = None, style: Optional[Style] = None, key: Optional[Any] = None, @@ -1534,7 +1546,7 @@ class ShikiHighLevelCodeBlock(ShikiCodeBlock): ] ] = None, decorations: Optional[ - Union[Var[list[dict[str, Any]]], list[dict[str, Any]]] + Union[Var[list[ShikiDecorations]], list[ShikiDecorations]] ] = None, style: Optional[Style] = None, key: Optional[Any] = None, @@ -2164,7 +2176,7 @@ class CodeblockNamespace(ComponentNamespace): ] ] = None, decorations: Optional[ - Union[Var[list[dict[str, Any]]], list[dict[str, Any]]] + Union[Var[list[ShikiDecorations]], list[ShikiDecorations]] ] = None, style: Optional[Style] = None, key: Optional[Any] = None, diff --git a/reflex/components/props.py b/reflex/components/props.py index 92dbe8144..adce134fc 100644 --- a/reflex/components/props.py +++ b/reflex/components/props.py @@ -2,8 +2,11 @@ from __future__ import annotations +from pydantic import ValidationError + from reflex.base import Base from reflex.utils import format +from reflex.utils.exceptions import InvalidPropValueError from reflex.vars.object import LiteralObjectVar @@ -40,3 +43,34 @@ class PropsBase(Base): format.to_camel_case(key): value for key, value in super().dict(*args, **kwargs).items() } + + +class NoExtrasAllowedProps(Base): + """A class that holds props to be passed or applied to a component with no extra props allowed.""" + + def __init__(self, component_name=None, **kwargs): + """Initialize the props. + + Args: + component_name: The custom name of the component. + kwargs: Kwargs to initialize the props. + + Raises: + InvalidPropValueError: If invalid props are passed on instantiation. + """ + component_name = component_name or type(self).__name__ + try: + super().__init__(**kwargs) + except ValidationError as e: + invalid_fields = ", ".join([error["loc"][0] for error in e.errors()]) # type: ignore + supported_props_str = ", ".join(f'"{field}"' for field in self.get_fields()) + raise InvalidPropValueError( + f"Invalid prop(s) {invalid_fields} for {component_name!r}. Supported props are {supported_props_str}" + ) from None + + class Config: + """Pydantic config.""" + + arbitrary_types_allowed = True + use_enum_values = True + extra = "forbid" diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py index 02c320ac6..88487c381 100644 --- a/reflex/components/sonner/toast.py +++ b/reflex/components/sonner/toast.py @@ -4,12 +4,10 @@ from __future__ import annotations from typing import Any, ClassVar, Literal, Optional, Union -from pydantic import ValidationError - from reflex.base import Base from reflex.components.component import Component, ComponentNamespace from reflex.components.lucide.icon import Icon -from reflex.components.props import PropsBase +from reflex.components.props import NoExtrasAllowedProps, PropsBase from reflex.event import ( EventSpec, call_script, @@ -72,7 +70,7 @@ def _toast_callback_signature(toast: Var) -> list[Var]: ] -class ToastProps(PropsBase): +class ToastProps(PropsBase, NoExtrasAllowedProps): """Props for the toast component.""" # Toast's title, renders above the description. @@ -132,24 +130,6 @@ class ToastProps(PropsBase): # Function that gets called when the toast disappears automatically after it's timeout (duration` prop). on_auto_close: Optional[Any] - def __init__(self, **kwargs): - """Initialize the props. - - Args: - kwargs: Kwargs to initialize the props. - - Raises: - ValueError: If invalid props are passed on instantiation. - """ - try: - super().__init__(**kwargs) - except ValidationError as e: - invalid_fields = ", ".join([error["loc"][0] for error in e.errors()]) # type: ignore - supported_props_str = ", ".join(f'"{field}"' for field in self.get_fields()) - raise ValueError( - f"Invalid prop(s) {invalid_fields} for rx.toast. Supported props are {supported_props_str}" - ) from None - def dict(self, *args, **kwargs) -> dict[str, Any]: """Convert the object to a dictionary. @@ -181,13 +161,6 @@ class ToastProps(PropsBase): ) return d - class Config: - """Pydantic config.""" - - arbitrary_types_allowed = True - use_enum_values = True - extra = "forbid" - class Toaster(Component): """A Toaster Component for displaying toast notifications.""" @@ -281,7 +254,7 @@ class Toaster(Component): if message == "" and ("title" not in props or "description" not in props): raise ValueError("Toast message or title or description must be provided.") if props: - args = LiteralVar.create(ToastProps(**props)) + args = LiteralVar.create(ToastProps(component_name="rx.toast", **props)) toast = f"{toast_command}(`{message}`, {str(args)})" else: toast = f"{toast_command}(`{message}`)" diff --git a/reflex/components/sonner/toast.pyi b/reflex/components/sonner/toast.pyi index 976f9f310..10168257e 100644 --- a/reflex/components/sonner/toast.pyi +++ b/reflex/components/sonner/toast.pyi @@ -8,7 +8,7 @@ from typing import Any, ClassVar, Dict, Literal, Optional, Union, overload from reflex.base import Base from reflex.components.component import Component, ComponentNamespace from reflex.components.lucide.icon import Icon -from reflex.components.props import PropsBase +from reflex.components.props import NoExtrasAllowedProps, PropsBase from reflex.event import EventSpec, EventType from reflex.style import Style from reflex.utils.serializers import serializer @@ -31,7 +31,7 @@ class ToastAction(Base): @serializer def serialize_action(action: ToastAction) -> dict: ... -class ToastProps(PropsBase): +class ToastProps(PropsBase, NoExtrasAllowedProps): title: Optional[Union[str, Var]] description: Optional[Union[str, Var]] close_button: Optional[bool] @@ -52,11 +52,6 @@ class ToastProps(PropsBase): def dict(self, *args, **kwargs) -> dict[str, Any]: ... - class Config: - arbitrary_types_allowed = True - use_enum_values = True - extra = "forbid" - class Toaster(Component): is_used: ClassVar[bool] = False diff --git a/reflex/utils/exceptions.py b/reflex/utils/exceptions.py index f16338513..a9788aa9a 100644 --- a/reflex/utils/exceptions.py +++ b/reflex/utils/exceptions.py @@ -143,3 +143,7 @@ class EnvironmentVarValueError(ReflexError, ValueError): class DynamicComponentInvalidSignature(ReflexError, TypeError): """Raised when a dynamic component has an invalid signature.""" + + +class InvalidPropValueError(ReflexError, ValueError): + """Raised when a prop value is invalid."""