diff --git a/reflex/components/markdown/markdown.py b/reflex/components/markdown/markdown.py index e4a3b0c47..16da45205 100644 --- a/reflex/components/markdown/markdown.py +++ b/reflex/components/markdown/markdown.py @@ -2,18 +2,18 @@ from __future__ import annotations +import dataclasses import textwrap from functools import lru_cache from hashlib import md5 -from typing import Any, Callable, Dict, Union -import dataclasses +from typing import Any, Callable, Dict, Sequence, Union from reflex.components.component import Component, CustomComponent 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, ArgsFunctionOperation +from reflex.vars.function import ARRAY_ISARRAY, ArgsFunctionOperation, DestructuredArg from reflex.vars.number import ternary_operation # Special vars used in the component map. @@ -79,6 +79,7 @@ def get_base_component_map() -> dict[str, Callable]: @dataclasses.dataclass(frozen=True) class MarkdownComponentMap: """Mixin class for handling custom component maps in Markdown components.""" + _explicit_return: bool = dataclasses.field(default=False) @classmethod @@ -92,7 +93,10 @@ class MarkdownComponentMap: @classmethod def create_map_fn_var( - cls, fn_body: Var | None = None, fn_args: tuple[str, ...] | None = None, explicit_return: bool | None = None + cls, + fn_body: Var | None = None, + fn_args: Sequence[str] | None = None, + explicit_return: bool | None = None, ) -> Var: """Create a function Var for the component map. @@ -108,10 +112,14 @@ class MarkdownComponentMap: fn_body = fn_body if fn_body is not None else cls.get_fn_body() explicit_return = explicit_return or cls._explicit_return - return ArgsFunctionOperation.create(args_names=fn_args, return_expr=fn_body, destructure_args=True, explicit_return=explicit_return) + return ArgsFunctionOperation.create( + args_names=(DestructuredArg(fields=tuple(fn_args)),), + return_expr=fn_body, + explicit_return=explicit_return, + ) @classmethod - def get_fn_args(cls) -> list[str]: + def get_fn_args(cls) -> Sequence[str]: """Get the function arguments for the component map. Returns: @@ -126,7 +134,7 @@ class MarkdownComponentMap: Returns: The function body as a string. """ - return Var(_js_expr="", _var_type=str) + return Var(_js_expr="undefined", _var_type=None) class Markdown(Component): @@ -290,7 +298,7 @@ class Markdown(Component): _PROPS._js_expr, ), fn_body=Var(_js_expr=formatted_code), - explicit_return=True + explicit_return=True, ) def get_component(self, tag: str, **props) -> Component: @@ -359,7 +367,9 @@ class Markdown(Component): Returns: The function Var for the component map. """ - formatted_component = Var(_js_expr=f"({self.format_component(tag)})", _var_type=str) + formatted_component = Var( + _js_expr=f"({self.format_component(tag)})", _var_type=str + ) if isinstance(component, MarkdownComponentMap): return component.create_map_fn_var(fn_body=formatted_component) diff --git a/reflex/event.py b/reflex/event.py index e51d1cc07..a64d4d6c1 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -45,6 +45,7 @@ from reflex.vars import VarData from reflex.vars.base import LiteralVar, Var from reflex.vars.function import ( ArgsFunctionOperation, + FunctionArgs, FunctionStringVar, FunctionVar, VarOperationCall, @@ -1643,7 +1644,7 @@ class LiteralEventChainVar(ArgsFunctionOperation, LiteralVar, EventChainVar): _js_expr="", _var_type=EventChain, _var_data=_var_data, - _args_names=arg_def, + _args=FunctionArgs(arg_def), _return_expr=invocation.call( LiteralVar.create([LiteralVar.create(event) for event in value.events]), arg_def_expr, diff --git a/reflex/vars/function.py b/reflex/vars/function.py index d60b3bce1..98f3b2335 100644 --- a/reflex/vars/function.py +++ b/reflex/vars/function.py @@ -4,10 +4,10 @@ from __future__ import annotations import dataclasses import sys -from typing import Any, Callable, Optional, Tuple, Type, Union +from typing import Any, Callable, Optional, Sequence, Tuple, Type, Union -from reflex.utils.types import GenericType from reflex.utils import format +from reflex.utils.types import GenericType from .base import CachedVarOperation, LiteralVar, Var, VarData, cached_property_no_lock @@ -127,6 +127,36 @@ class VarOperationCall(CachedVarOperation, Var): ) +@dataclasses.dataclass(frozen=True) +class DestructuredArg: + """Class for destructured arguments.""" + + fields: Tuple[str, ...] = tuple() + rest: Optional[str] = None + + def to_javascript(self) -> str: + """Convert the destructured argument to JavaScript. + + Returns: + The destructured argument in JavaScript. + """ + return format.wrap( + ", ".join(self.fields) + (f", ...{self.rest}" if self.rest else ""), + "{", + "}", + ) + + +@dataclasses.dataclass( + frozen=True, +) +class FunctionArgs: + """Class for function arguments.""" + + args: Tuple[Union[str, DestructuredArg], ...] = tuple() + rest: Optional[str] = None + + @dataclasses.dataclass( eq=False, frozen=True, @@ -135,10 +165,9 @@ class VarOperationCall(CachedVarOperation, Var): class ArgsFunctionOperation(CachedVarOperation, FunctionVar): """Base class for immutable function defined via arguments and return expression.""" - _args_names: Tuple[str, ...] = dataclasses.field(default_factory=tuple) + _args: FunctionArgs = dataclasses.field(default_factory=FunctionArgs) _return_expr: Union[Var, Any] = dataclasses.field(default=None) - _destructure_args: bool = dataclasses.field(default=False) - _explicit_return: bool = dataclasses.field(default=True) + _explicit_return: bool = dataclasses.field(default=False) @cached_property_no_lock def _cached_var_name(self) -> str: @@ -147,35 +176,40 @@ class ArgsFunctionOperation(CachedVarOperation, FunctionVar): Returns: The name of the var. """ - arg_names_str = ", ".join(self._args_names) + arg_names_str = ", ".join( + [ + arg if isinstance(arg, str) else arg.to_javascript() + for arg in self._args.args + ] + ) + (f", ...{self._args.rest}" if self._args.rest else "") + return_expr_str = str(LiteralVar.create(self._return_expr)) - if self._destructure_args: - arg_names_str = format.wrap(arg_names_str, "{", "}") - # Wrap return expression in curly braces if explicit return syntax is used. - return_expr_str = format.wrap(return_expr_str, "{", "}") if self._explicit_return else format.wrap(return_expr_str, "(", ")") - - return f"(({arg_names_str}) => {return_expr_str})" + return_expr_str_wrapped = ( + format.wrap(return_expr_str, "{", "}") + if self._explicit_return + else return_expr_str + ) + return f"(({arg_names_str}) => {return_expr_str_wrapped})" @classmethod def create( cls, - args_names: Tuple[str, ...], + args_names: Sequence[Union[str, DestructuredArg]], return_expr: Var | Any, - destructure_args: bool = False, + rest: str | None = None, explicit_return: bool = False, _var_type: GenericType = Callable, _var_data: VarData | None = None, - ) -> ArgsFunctionOperation: """Create a new function var. Args: args_names: The names of the arguments. return_expr: The return expression of the function. - destructure_args: Whether to destructure the arguments. + rest: The name of the rest argument. explicit_return: Whether to use explicit return syntax. _var_data: Additional hooks and imports associated with the Var. @@ -186,10 +220,9 @@ class ArgsFunctionOperation(CachedVarOperation, FunctionVar): _js_expr="", _var_type=_var_type, _var_data=_var_data, - _args_names=args_names, + _args=FunctionArgs(args=tuple(args_names), rest=rest), _return_expr=return_expr, - _destructure_args=destructure_args, - _explicit_return=explicit_return + _explicit_return=explicit_return, ) diff --git a/tests/units/components/base/test_script.py b/tests/units/components/base/test_script.py index b909b6c61..e9c40188b 100644 --- a/tests/units/components/base/test_script.py +++ b/tests/units/components/base/test_script.py @@ -62,14 +62,14 @@ def test_script_event_handler(): ) render_dict = component.render() assert ( - f'onReady={{((...args) => ((addEvents([(Event("{EvState.get_full_name()}.on_ready", ({{ }}), ({{ }})))], args, ({{ }})))))}}' + f'onReady={{((...args) => (addEvents([(Event("{EvState.get_full_name()}.on_ready", ({{ }}), ({{ }})))], args, ({{ }}))))}}' in render_dict["props"] ) assert ( - f'onLoad={{((...args) => ((addEvents([(Event("{EvState.get_full_name()}.on_load", ({{ }}), ({{ }})))], args, ({{ }})))))}}' + f'onLoad={{((...args) => (addEvents([(Event("{EvState.get_full_name()}.on_load", ({{ }}), ({{ }})))], args, ({{ }}))))}}' in render_dict["props"] ) assert ( - f'onError={{((...args) => ((addEvents([(Event("{EvState.get_full_name()}.on_error", ({{ }}), ({{ }})))], args, ({{ }})))))}}' + f'onError={{((...args) => (addEvents([(Event("{EvState.get_full_name()}.on_error", ({{ }}), ({{ }})))], args, ({{ }}))))}}' in render_dict["props"] ) diff --git a/tests/units/components/markdown/test_markdown.py b/tests/units/components/markdown/test_markdown.py index 95535f37a..920b0eea0 100644 --- a/tests/units/components/markdown/test_markdown.py +++ b/tests/units/components/markdown/test_markdown.py @@ -60,8 +60,13 @@ def syntax_highlighter_memoized_component(codeblock: Type[Component]): @pytest.mark.parametrize( "fn_body, fn_args, explicit_return, expected", [ - (None, None, False, Var(_js_expr="(({node, children, ...props}) => ())")), - ("return node", ("node", ), True, Var(_js_expr="(({node}) => {return node})")), + ( + None, + None, + False, + Var(_js_expr="(({node, children, ...props}) => undefined)"), + ), + ("return node", ("node",), True, Var(_js_expr="(({node}) => {return node})")), ( "return node + children", ("node", "children"), @@ -85,24 +90,28 @@ def syntax_highlighter_memoized_component(codeblock: Type[Component]): ], ) def test_create_map_fn_var(fn_body, fn_args, explicit_return, expected): - result = MarkdownComponentMap.create_map_fn_var(fn_body= Var(_js_expr=fn_body,_var_type=str) if fn_body else None, fn_args=fn_args, explicit_return=explicit_return) + result = MarkdownComponentMap.create_map_fn_var( + fn_body=Var(_js_expr=fn_body, _var_type=str) if fn_body else None, + fn_args=fn_args, + explicit_return=explicit_return, + ) assert result._js_expr == expected._js_expr @pytest.mark.parametrize( - "cls, fn_body, fn_args, explicit_return, expected", + ("cls", "fn_body", "fn_args", "explicit_return", "expected"), [ ( MarkdownComponentMap, None, None, False, - Var(_js_expr="(({node, children, ...props}) => ())"), + Var(_js_expr="(({node, children, ...props}) => undefined)"), ), ( MarkdownComponentMap, "return node", - ("node", ), + ("node",), True, Var(_js_expr="(({node}) => {return node})"), ), @@ -125,7 +134,11 @@ def test_create_map_fn_var(fn_body, fn_args, explicit_return, expected): ], ) def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expected): - result = cls.create_map_fn_var(fn_body= Var(_js_expr=fn_body, _var_type=int) if fn_body else None, fn_args=fn_args, explicit_return=explicit_return) + result = cls.create_map_fn_var( + fn_body=Var(_js_expr=fn_body, _var_type=int) if fn_body else None, + fn_args=fn_args, + explicit_return=explicit_return, + ) assert result._js_expr == expected._js_expr diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index e4744b9fb..a2485d10e 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -844,9 +844,9 @@ def test_component_event_trigger_arbitrary_args(): comp = C1.create(on_foo=C1State.mock_handler) assert comp.render()["props"][0] == ( - "onFoo={((__e, _alpha, _bravo, _charlie) => ((addEvents(" + "onFoo={((__e, _alpha, _bravo, _charlie) => (addEvents(" f'[(Event("{C1State.get_full_name()}.mock_handler", ({{ ["_e"] : __e["target"]["value"], ["_bravo"] : _bravo["nested"], ["_charlie"] : (_charlie["custom"] + 42) }}), ({{ }})))], ' - "[__e, _alpha, _bravo, _charlie], ({ })))))}" + "[__e, _alpha, _bravo, _charlie], ({ }))))}" ) diff --git a/tests/units/test_var.py b/tests/units/test_var.py index 64cad6c00..7e7ed545d 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -22,8 +22,16 @@ from reflex.vars.base import ( var_operation, var_operation_return, ) -from reflex.vars.function import ArgsFunctionOperation, FunctionStringVar -from reflex.vars.number import LiteralBooleanVar, LiteralNumberVar, NumberVar +from reflex.vars.function import ( + ArgsFunctionOperation, + DestructuredArg, + FunctionStringVar, +) +from reflex.vars.number import ( + LiteralBooleanVar, + LiteralNumberVar, + NumberVar, +) from reflex.vars.object import LiteralObjectVar, ObjectVar from reflex.vars.sequence import ( ArrayVar, @@ -921,13 +929,13 @@ def test_function_var(): ) assert ( str(manual_addition_func.call(1, 2)) - == '(((a, b) => (({ ["args"] : [a, b], ["result"] : a + b })))(1, 2))' + == '(((a, b) => ({ ["args"] : [a, b], ["result"] : a + b }))(1, 2))' ) increment_func = addition_func(1) assert ( str(increment_func.call(2)) - == "(((...args) => ((((a, b) => a + b)(1, ...args))))(2))" + == "(((...args) => (((a, b) => a + b)(1, ...args)))(2))" ) create_hello_statement = ArgsFunctionOperation.create( @@ -937,20 +945,24 @@ def test_function_var(): last_name = LiteralStringVar.create("Universe") assert ( str(create_hello_statement.call(f"{first_name} {last_name}")) - == '(((name) => (("Hello, "+name+"!")))("Steven Universe"))' + == '(((name) => ("Hello, "+name+"!"))("Steven Universe"))' ) # Test with destructured arguments destructured_func = ArgsFunctionOperation.create( - ("a","b"), Var(_js_expr="a + b"), destructure_args=True + (DestructuredArg(fields=("a", "b")),), + Var(_js_expr="a + b"), + ) + assert ( + str(destructured_func.call({"a": 1, "b": 2})) + == '((({a, b}) => a + b)(({ ["a"] : 1, ["b"] : 2 })))' ) - assert str(destructured_func.call({"a": 1, "b": 2})) == '(({a, b}) => (a + b))({"a": 1, "b": 2})' # Test with explicit return explicit_return_func = ArgsFunctionOperation.create( ("a", "b"), Var(_js_expr="return a + b"), explicit_return=True ) - assert str(explicit_return_func.call(1, 2)) == '((a, b) => {return a + b})(1, 2)' + assert str(explicit_return_func.call(1, 2)) == "(((a, b) => {return a + b})(1, 2))" def test_var_operation(): diff --git a/tests/units/utils/test_format.py b/tests/units/utils/test_format.py index f8b605541..cd1d0179d 100644 --- a/tests/units/utils/test_format.py +++ b/tests/units/utils/test_format.py @@ -374,7 +374,7 @@ def test_format_match( events=[EventSpec(handler=EventHandler(fn=mock_event))], args_spec=lambda: [], ), - '((...args) => ((addEvents([(Event("mock_event", ({ }), ({ })))], args, ({ })))))', + '((...args) => (addEvents([(Event("mock_event", ({ }), ({ })))], args, ({ }))))', ), ( EventChain( @@ -395,7 +395,7 @@ def test_format_match( ], args_spec=lambda e: [e.target.value], ), - '((_e) => ((addEvents([(Event("mock_event", ({ ["arg"] : _e["target"]["value"] }), ({ })))], [_e], ({ })))))', + '((_e) => (addEvents([(Event("mock_event", ({ ["arg"] : _e["target"]["value"] }), ({ })))], [_e], ({ }))))', ), ( EventChain( @@ -403,7 +403,7 @@ def test_format_match( args_spec=lambda: [], event_actions={"stopPropagation": True}, ), - '((...args) => ((addEvents([(Event("mock_event", ({ }), ({ })))], args, ({ ["stopPropagation"] : true })))))', + '((...args) => (addEvents([(Event("mock_event", ({ }), ({ })))], args, ({ ["stopPropagation"] : true }))))', ), ( EventChain( @@ -415,7 +415,7 @@ def test_format_match( ], args_spec=lambda: [], ), - '((...args) => ((addEvents([(Event("mock_event", ({ }), ({ ["stopPropagation"] : true })))], args, ({ })))))', + '((...args) => (addEvents([(Event("mock_event", ({ }), ({ ["stopPropagation"] : true })))], args, ({ }))))', ), ( EventChain( @@ -423,7 +423,7 @@ def test_format_match( args_spec=lambda: [], event_actions={"preventDefault": True}, ), - '((...args) => ((addEvents([(Event("mock_event", ({ }), ({ })))], args, ({ ["preventDefault"] : true })))))', + '((...args) => (addEvents([(Event("mock_event", ({ }), ({ })))], args, ({ ["preventDefault"] : true }))))', ), ({"a": "red", "b": "blue"}, '({ ["a"] : "red", ["b"] : "blue" })'), (Var(_js_expr="var", _var_type=int).guess_type(), "var"),