diff --git a/pyproject.toml b/pyproject.toml index c152f8c58..67bcef1cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,5 +103,5 @@ asyncio_default_fixture_loop_scope = "function" asyncio_mode = "auto" [tool.codespell] -skip = "docs/*,*.html,examples/*, *.pyi" +skip = "docs/*,*.html,examples/*, *.pyi, *.lock" ignore-words-list = "te, TreeE" diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 4f2f4c046..3cec050a8 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -300,10 +300,7 @@ export const applyEvent = async (event, socket) => { // Send the event to the server. if (socket) { - socket.emit( - "event", - event, - ); + socket.emit("event", event); return true; } @@ -410,7 +407,7 @@ export const connect = async ( autoUnref: false, }); // Ensure undefined fields in events are sent as null instead of removed - socket.current.io.encoder.replacer = (k, v) => (v === undefined ? null : v) + socket.current.io.encoder.replacer = (k, v) => (v === undefined ? null : v); function checkVisibility() { if (document.visibilityState === "visible") { @@ -488,7 +485,7 @@ export const uploadFiles = async ( return false; } - const upload_ref_name = `__upload_controllers_${upload_id}` + const upload_ref_name = `__upload_controllers_${upload_id}`; if (refs[upload_ref_name]) { console.log("Upload already in progress for ", upload_id); @@ -924,6 +921,18 @@ export const atSlice = (arrayLike, slice) => { .filter((_, i) => i % step === 0); }; +/** + * Get the value at a slice or index. + * @param {Array | string} arrayLike The array to get the value from. + * @param {number | [number, number, number]} sliceOrIndex The slice or index to get the value at. + * @returns The value at the slice or index. + */ +export const atSliceOrIndex = (arrayLike, sliceOrIndex) => { + return Array.isArray(sliceOrIndex) + ? atSlice(arrayLike, sliceOrIndex) + : arrayLike.at(sliceOrIndex); +}; + /** * Get the value from a ref. * @param ref The ref to get the value from. diff --git a/reflex/components/base/bare.py b/reflex/components/base/bare.py index ae1a48224..b6d2befd5 100644 --- a/reflex/components/base/bare.py +++ b/reflex/components/base/bare.py @@ -61,13 +61,13 @@ class Bare(Component): hooks |= component._get_all_hooks() return hooks - def _get_all_imports(self) -> ParsedImportDict: + def _get_all_imports(self, collapse: bool = False) -> ParsedImportDict: """Include the imports for the component. Returns: The imports for the component. """ - imports = super()._get_all_imports() + imports = super()._get_all_imports(collapse=collapse) if isinstance(self.contents, Var): var_data = self.contents._get_all_var_data() if var_data: diff --git a/reflex/components/component.py b/reflex/components/component.py index 68a43d889..015406d92 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -930,6 +930,7 @@ class Component(BaseComponent, ABC): children: The children of the component. """ + from reflex.components.base.bare import Bare from reflex.components.base.fragment import Fragment from reflex.components.core.cond import Cond from reflex.components.core.foreach import Foreach @@ -960,6 +961,16 @@ class Component(BaseComponent, ABC): validate_child(child.comp1) validate_child(child.comp2) + if ( + isinstance(child, Bare) + and child.contents is not None + and isinstance(child.contents, Var) + ): + var_data = child.contents._get_all_var_data() + if var_data is not None: + for c in var_data.components: + validate_child(c) + if isinstance(child, Match): for cases in child.match_cases: validate_child(cases[-1]) @@ -970,10 +981,23 @@ class Component(BaseComponent, ABC): f"The component `{comp_name}` cannot have `{child_name}` as a child component" ) - if self._valid_children and child_name not in [ - *self._valid_children, - *allowed_components, - ]: + valid_children = self._valid_children + allowed_components + + def child_is_in_valid(child): + if type(child).__name__ in valid_children: + return True + + if ( + not isinstance(child, Bare) + or child.contents is None + or not isinstance(child.contents, Var) + or (var_data := child.contents._get_all_var_data()) is None + ): + return False + + return all(child_is_in_valid(c) for c in var_data.components) + + if self._valid_children and not child_is_in_valid(child): valid_child_list = ", ".join( [f"`{v_child}`" for v_child in self._valid_children] ) diff --git a/reflex/components/core/banner.py b/reflex/components/core/banner.py index b7b6fae6c..e25951fb0 100644 --- a/reflex/components/core/banner.py +++ b/reflex/components/core/banner.py @@ -175,6 +175,8 @@ class ConnectionBanner(Component): Returns: The connection banner component. """ + from reflex.components.base.bare import Bare + if not comp: comp = Flex.create( Text.create( @@ -189,7 +191,7 @@ class ConnectionBanner(Component): position="fixed", ) - return cond(has_connection_errors, comp) + return Bare.create(cond(has_connection_errors, comp)) class ConnectionModal(Component): @@ -205,18 +207,22 @@ class ConnectionModal(Component): Returns: The connection banner component. """ + from reflex.components.base.bare import Bare + if not comp: comp = Text.create(*default_connection_error()) - return cond( - has_too_many_connection_errors, - DialogRoot.create( - DialogContent.create( - DialogTitle.create("Connection Error"), - comp, + return Bare.create( + cond( + has_too_many_connection_errors, + DialogRoot.create( + DialogContent.create( + DialogTitle.create("Connection Error"), + comp, + ), + open=has_too_many_connection_errors, + z_index=9999, ), - open=has_too_many_connection_errors, - z_index=9999, - ), + ) ) diff --git a/reflex/components/core/cond.py b/reflex/components/core/cond.py index 5f70fef22..ccb13c17b 100644 --- a/reflex/components/core/cond.py +++ b/reflex/components/core/cond.py @@ -12,7 +12,8 @@ from reflex.style import LIGHT_COLOR_MODE, resolved_color_mode from reflex.utils.imports import ImportDict, ImportVar from reflex.utils.types import safe_issubclass from reflex.vars import VarData -from reflex.vars.base import LiteralVar, Var +from reflex.vars.base import LiteralVar, ReflexCallable, Var +from reflex.vars.function import ArgsFunctionOperation from reflex.vars.number import ternary_operation _IS_TRUE_IMPORT: ImportDict = { @@ -150,12 +151,23 @@ def cond(condition: Any, c1: Any, c2: Any = None) -> Component | Var: if c2 is None: raise ValueError("For conditional vars, the second argument must be set.") + c1 = Var.create(c1) + c2 = Var.create(c2) + # Create the conditional var. return ternary_operation( cond_var.bool(), - c1, - c2, - ) + ArgsFunctionOperation.create( + (), + c1, + _var_type=ReflexCallable[[], c1._var_type], + ), + ArgsFunctionOperation.create( + (), + c2, + _var_type=ReflexCallable[[], c2._var_type], + ), + ).call() @overload diff --git a/reflex/components/core/match.py b/reflex/components/core/match.py index 8b9382c89..3c1e27004 100644 --- a/reflex/components/core/match.py +++ b/reflex/components/core/match.py @@ -184,11 +184,17 @@ class Match(MemoizationLeaf): return_type = Var for index, case in enumerate(match_cases): - if not types._issubclass(type(case[-1]), return_type): + if not ( + types._issubclass(type(case[-1]), return_type) + or ( + isinstance(case[-1], Var) + and types.typehint_issubclass(case[-1]._var_type, return_type) + ) + ): raise MatchTypeError( f"Match cases should have the same return types. Case {index} with return " f"value `{case[-1]._js_expr if isinstance(case[-1], Var) else textwrap.shorten(str(case[-1]), width=250)}`" - f" of type {type(case[-1])!r} is not {return_type}" + f" of type {(type(case[-1]) if not isinstance(case[-1], Var) else case[-1]._var_type)!r} is not {return_type}" ) @classmethod diff --git a/reflex/components/datadisplay/code.py b/reflex/components/datadisplay/code.py index 8a433c18c..dfccad149 100644 --- a/reflex/components/datadisplay/code.py +++ b/reflex/components/datadisplay/code.py @@ -447,7 +447,7 @@ class CodeBlock(Component, MarkdownComponentMap): # react-syntax-highlighter doesn't have an explicit "light" or "dark" theme so we use one-light and one-dark # themes respectively to ensure code compatibility. - if "theme" in props and not isinstance(props["theme"], Var): + if props.get("theme") is not None and not isinstance(props["theme"], Var): props["theme"] = getattr(Theme, format.to_snake_case(props["theme"])) # type: ignore console.deprecate( feature_name="theme prop as string", diff --git a/reflex/components/markdown/markdown.py b/reflex/components/markdown/markdown.py index 7c65c0d43..bf94f5f70 100644 --- a/reflex/components/markdown/markdown.py +++ b/reflex/components/markdown/markdown.py @@ -113,8 +113,8 @@ class MarkdownComponentMap: explicit_return = explicit_return or cls._explicit_return return ArgsFunctionOperation.create( - args_names=(DestructuredArg(fields=tuple(fn_args)),), - return_expr=fn_body, + (DestructuredArg(fields=tuple(fn_args)),), + fn_body, explicit_return=explicit_return, ) diff --git a/reflex/utils/types.py b/reflex/utils/types.py index d6c8729ef..6503be8be 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -850,6 +850,22 @@ def safe_issubclass(cls: Any, class_or_tuple: Any, /) -> bool: ) from e +def infallible_issubclass(cls: Any, class_or_tuple: Any, /) -> bool: + """Check if a class is a subclass of another class or a tuple of classes. + + Args: + cls: The class to check. + class_or_tuple: The class or tuple of classes to check against. + + Returns: + Whether the class is a subclass of the other class or tuple of classes. + """ + try: + return issubclass(cls, class_or_tuple) + except TypeError: + return False + + def typehint_issubclass(possible_subclass: Any, possible_superclass: Any) -> bool: """Check if a type hint is a subclass of another type hint. diff --git a/reflex/vars/base.py b/reflex/vars/base.py index db2790977..397d540ed 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -51,7 +51,7 @@ from typing_extensions import ( from reflex import constants from reflex.base import Base from reflex.constants.compiler import Hooks -from reflex.utils import console, exceptions, imports, serializers, types +from reflex.utils import console, imports, serializers, types from reflex.utils.exceptions import ( VarAttributeError, VarDependencyError, @@ -72,6 +72,7 @@ from reflex.utils.types import ( _isinstance, get_origin, has_args, + infallible_issubclass, typehint_issubclass, unionize, ) @@ -125,8 +126,25 @@ def unwrap_reflex_callalbe( """ if callable_type is ReflexCallable: return Ellipsis, Any - if get_origin(callable_type) is not ReflexCallable: + + origin = get_origin(callable_type) + + if origin is not ReflexCallable: + if origin in types.UnionTypes: + args = get_args(callable_type) + params: List[ReflexCallableParams] = [] + return_types: List[GenericType] = [] + for arg in args: + param, return_type = unwrap_reflex_callalbe(arg) + if param not in params: + params.append(param) + return_types.append(return_type) + return ( + Ellipsis if len(params) > 1 else params[0], + unionize(*return_types), + ) return Ellipsis, Any + args = get_args(callable_type) if not args or len(args) != 2: return Ellipsis, Any @@ -143,6 +161,7 @@ class VarSubclassEntry: var_subclass: Type[Var] to_var_subclass: Type[ToOperation] python_types: Tuple[GenericType, ...] + is_subclass: Callable[[GenericType], bool] | None _var_subclasses: List[VarSubclassEntry] = [] @@ -208,7 +227,7 @@ class VarData: object.__setattr__(self, "imports", immutable_imports) object.__setattr__(self, "hooks", tuple(hooks or {})) object.__setattr__( - self, "components", tuple(components) if components is not None else tuple() + self, "components", tuple(components) if components is not None else () ) object.__setattr__(self, "deps", tuple(deps or [])) object.__setattr__(self, "position", position or None) @@ -444,6 +463,7 @@ class Var(Generic[VAR_TYPE]): cls, python_types: Tuple[GenericType, ...] | GenericType = types.Unset(), default_type: GenericType = types.Unset(), + is_subclass: Callable[[GenericType], bool] | types.Unset = types.Unset(), **kwargs, ): """Initialize the subclass. @@ -451,11 +471,12 @@ class Var(Generic[VAR_TYPE]): Args: python_types: The python types that the var represents. default_type: The default type of the var. Defaults to the first python type. + is_subclass: A function to check if a type is a subclass of the var. **kwargs: Additional keyword arguments. """ super().__init_subclass__(**kwargs) - if python_types or default_type: + if python_types or default_type or is_subclass: python_types = ( (python_types if isinstance(python_types, tuple) else (python_types,)) if python_types @@ -480,7 +501,14 @@ class Var(Generic[VAR_TYPE]): ToVarOperation.__name__ = f'To{cls.__name__.removesuffix("Var")}Operation' - _var_subclasses.append(VarSubclassEntry(cls, ToVarOperation, python_types)) + _var_subclasses.append( + VarSubclassEntry( + cls, + ToVarOperation, + python_types, + is_subclass if not isinstance(is_subclass, types.Unset) else None, + ) + ) def __post_init__(self): """Post-initialize the var.""" @@ -726,7 +754,12 @@ class Var(Generic[VAR_TYPE]): # If the first argument is a python type, we map it to the corresponding Var type. for var_subclass in _var_subclasses[::-1]: - if fixed_output_type in var_subclass.python_types: + if ( + var_subclass.python_types + and infallible_issubclass(fixed_output_type, var_subclass.python_types) + ) or ( + var_subclass.is_subclass and var_subclass.is_subclass(fixed_output_type) + ): return self.to(var_subclass.var_subclass, output) if fixed_output_type is None: @@ -801,12 +834,13 @@ class Var(Generic[VAR_TYPE]): Raises: TypeError: If the type is not supported for guessing. """ - from .number import NumberVar from .object import ObjectVar var_type = self._var_type + if var_type is None: return self.to(None) + if types.is_optional(var_type): var_type = types.get_args(var_type)[0] @@ -818,10 +852,15 @@ class Var(Generic[VAR_TYPE]): if fixed_type in types.UnionTypes: inner_types = get_args(var_type) - if all( - inspect.isclass(t) and issubclass(t, (int, float)) for t in inner_types - ): - return self.to(NumberVar, self._var_type) + for var_subclass in _var_subclasses: + if all( + ( + infallible_issubclass(t, var_subclass.python_types) + or (var_subclass.is_subclass and var_subclass.is_subclass(t)) + ) + for t in inner_types + ): + return self.to(var_subclass.var_subclass, self._var_type) if can_use_in_object_var(var_type): return self.to(ObjectVar, self._var_type) @@ -839,7 +878,9 @@ class Var(Generic[VAR_TYPE]): return self.to(None) for var_subclass in _var_subclasses[::-1]: - if issubclass(fixed_type, var_subclass.python_types): + if infallible_issubclass(fixed_type, var_subclass.python_types) or ( + var_subclass.is_subclass and var_subclass.is_subclass(fixed_type) + ): return self.to(var_subclass.var_subclass, self._var_type) if can_use_in_object_var(fixed_type): @@ -1799,6 +1840,7 @@ def var_operation( ), function_name=func_name, type_computer=custom_operation_return._type_computer, + _raw_js_function=custom_operation_return._raw_js_function, _var_type=ReflexCallable[ tuple( arg_python_type @@ -2541,15 +2583,17 @@ RETURN = TypeVar("RETURN") class CustomVarOperationReturn(Var[RETURN]): """Base class for custom var operations.""" - _type_computer: Optional[TypeComputer] = dataclasses.field(default=None) + _type_computer: TypeComputer | None = dataclasses.field(default=None) + _raw_js_function: str | None = dataclasses.field(default=None) @classmethod def create( cls, js_expression: str, _var_type: Type[RETURN] | None = None, - _type_computer: Optional[TypeComputer] = None, + _type_computer: TypeComputer | None = None, _var_data: VarData | None = None, + _raw_js_function: str | None = None, ) -> CustomVarOperationReturn[RETURN]: """Create a CustomVarOperation. @@ -2558,6 +2602,7 @@ class CustomVarOperationReturn(Var[RETURN]): _var_type: The type of the var. _type_computer: A function to compute the type of the var given the arguments. _var_data: Additional hooks and imports associated with the Var. + _raw_js_function: If provided, it will be used when the operation is being called with all of its arguments at once. Returns: The CustomVarOperation. @@ -2567,6 +2612,7 @@ class CustomVarOperationReturn(Var[RETURN]): _var_type=_var_type or Any, _type_computer=_type_computer, _var_data=_var_data, + _raw_js_function=_raw_js_function, ) @@ -2575,6 +2621,7 @@ def var_operation_return( var_type: Type[RETURN] | None = None, type_computer: Optional[TypeComputer] = None, var_data: VarData | None = None, + _raw_js_function: str | None = None, ) -> CustomVarOperationReturn[RETURN]: """Shortcut for creating a CustomVarOperationReturn. @@ -2583,6 +2630,7 @@ def var_operation_return( var_type: The type of the var. type_computer: A function to compute the type of the var given the arguments. var_data: Additional hooks and imports associated with the Var. + _raw_js_function: If provided, it will be used when the operation is being called with all of its arguments at once. Returns: The CustomVarOperationReturn. @@ -2592,6 +2640,7 @@ def var_operation_return( _var_type=var_type, _type_computer=type_computer, _var_data=var_data, + _raw_js_function=_raw_js_function, ) diff --git a/reflex/vars/function.py b/reflex/vars/function.py index b023cb9b2..b62a1e1be 100644 --- a/reflex/vars/function.py +++ b/reflex/vars/function.py @@ -52,7 +52,23 @@ OTHER_CALLABLE_TYPE = TypeVar( ) -class FunctionVar(Var[CALLABLE_TYPE], default_type=ReflexCallable[Any, Any]): +def type_is_reflex_callable(type_: Any) -> bool: + """Check if a type is a ReflexCallable. + + Args: + type_: The type to check. + + Returns: + True if the type is a ReflexCallable. + """ + return type_ is ReflexCallable or get_origin(type_) is ReflexCallable + + +class FunctionVar( + Var[CALLABLE_TYPE], + default_type=ReflexCallable[Any, Any], + is_subclass=type_is_reflex_callable, +): """Base class for immutable function vars.""" @overload @@ -304,15 +320,27 @@ class FunctionVar(Var[CALLABLE_TYPE], default_type=ReflexCallable[Any, Any]): if arg_len is not None: if len(args) < required_arg_len: raise VarTypeError( - f"Passed {len(args)} arguments, expected at least {required_arg_len} for {str(self)}" + f"Passed {len(args)} arguments, expected at least {required_arg_len} for {self!s}" ) if len(args) > arg_len: raise VarTypeError( - f"Passed {len(args)} arguments, expected at most {arg_len} for {str(self)}" + f"Passed {len(args)} arguments, expected at most {arg_len} for {self!s}" ) args = tuple(map(LiteralVar.create, args)) self._pre_check(*args) return_type = self._return_type(*args) + if ( + isinstance(self, (ArgsFunctionOperation, ArgsFunctionOperationBuilder)) + and self._raw_js_function + ): + return VarOperationCall.create( + FunctionStringVar.create( + self._raw_js_function, _var_type=self._var_type + ), + *args, + _var_type=return_type, + ).guess_type() + return VarOperationCall.create(self, *args, _var_type=return_type).guess_type() def chain( @@ -412,7 +440,7 @@ class FunctionVar(Var[CALLABLE_TYPE], default_type=ReflexCallable[Any, Any]): Returns: True if the function can be called with the given arguments. """ - return tuple() + return () @overload def __get__(self, instance: None, owner: Any) -> FunctionVar[CALLABLE_TYPE]: ... @@ -588,7 +616,7 @@ def format_args_function_operation( [ (arg if isinstance(arg, str) else arg.to_javascript()) + ( - f" = {str(default_value.default)}" + f" = {default_value.default!s}" if i < len(self._default_values) and not isinstance( (default_value := self._default_values[i]), inspect.Parameter.empty @@ -632,10 +660,10 @@ def pre_check_args( arg_name = self._args.args[i] if i < len(self._args.args) else None if arg_name is not None: raise VarTypeError( - f"Invalid argument {str(arg)} provided to {arg_name} in {self._function_name or 'var operation'}. {validation_message}" + f"Invalid argument {arg!s} provided to {arg_name} in {self._function_name or 'var operation'}. {validation_message}" ) raise VarTypeError( - f"Invalid argument {str(arg)} provided to argument {i} in {self._function_name or 'var operation'}. {validation_message}" + f"Invalid argument {arg!s} provided to argument {i} in {self._function_name or 'var operation'}. {validation_message}" ) return self._validators[len(args) :] @@ -679,6 +707,7 @@ class ArgsFunctionOperation(CachedVarOperation, FunctionVar[CALLABLE_TYPE]): _function_name: str = dataclasses.field(default="") _type_computer: Optional[TypeComputer] = dataclasses.field(default=None) _explicit_return: bool = dataclasses.field(default=False) + _raw_js_function: str | None = dataclasses.field(default=None) _cached_var_name = cached_property_no_lock(format_args_function_operation) @@ -698,6 +727,7 @@ class ArgsFunctionOperation(CachedVarOperation, FunctionVar[CALLABLE_TYPE]): function_name: str = "", explicit_return: bool = False, type_computer: Optional[TypeComputer] = None, + _raw_js_function: str | None = None, _var_type: GenericType = Callable, _var_data: VarData | None = None, ): @@ -712,6 +742,7 @@ class ArgsFunctionOperation(CachedVarOperation, FunctionVar[CALLABLE_TYPE]): function_name: The name of the function. explicit_return: Whether to use explicit return syntax. type_computer: A function to compute the return type. + _raw_js_function: If provided, it will be used when the operation is being called with all of its arguments at once. _var_type: The type of the var. _var_data: Additional hooks and imports associated with the Var. @@ -723,6 +754,7 @@ class ArgsFunctionOperation(CachedVarOperation, FunctionVar[CALLABLE_TYPE]): _var_type=_var_type, _var_data=_var_data, _args=FunctionArgs(args=tuple(args_names), rest=rest), + _raw_js_function=_raw_js_function, _default_values=tuple(default_values), _function_name=function_name, _validators=tuple(validators), @@ -753,6 +785,7 @@ class ArgsFunctionOperationBuilder( _function_name: str = dataclasses.field(default="") _type_computer: Optional[TypeComputer] = dataclasses.field(default=None) _explicit_return: bool = dataclasses.field(default=False) + _raw_js_function: str | None = dataclasses.field(default=None) _cached_var_name = cached_property_no_lock(format_args_function_operation) @@ -772,6 +805,7 @@ class ArgsFunctionOperationBuilder( function_name: str = "", explicit_return: bool = False, type_computer: Optional[TypeComputer] = None, + _raw_js_function: str | None = None, _var_type: GenericType = Callable, _var_data: VarData | None = None, ): @@ -788,6 +822,7 @@ class ArgsFunctionOperationBuilder( type_computer: A function to compute the return type. _var_type: The type of the var. _var_data: Additional hooks and imports associated with the Var. + _raw_js_function: If provided, it will be used when the operation is being called with all of its arguments at once. Returns: The function var. @@ -797,6 +832,7 @@ class ArgsFunctionOperationBuilder( _var_type=_var_type, _var_data=_var_data, _args=FunctionArgs(args=tuple(args_names), rest=rest), + _raw_js_function=_raw_js_function, _default_values=tuple(default_values), _function_name=function_name, _validators=tuple(validators), diff --git a/reflex/vars/number.py b/reflex/vars/number.py index 865ce5cba..25d1520ea 100644 --- a/reflex/vars/number.py +++ b/reflex/vars/number.py @@ -530,7 +530,9 @@ def number_abs_operation( The number absolute operation. """ return var_operation_return( - js_expression=f"Math.abs({value})", type_computer=unary_operation_type_computer + js_expression=f"Math.abs({value})", + type_computer=unary_operation_type_computer, + _raw_js_function="Math.abs", ) @@ -657,7 +659,11 @@ def number_floor_operation(value: Var[int | float]): Returns: The number floor operation. """ - return var_operation_return(js_expression=f"Math.floor({value})", var_type=int) + return var_operation_return( + js_expression=f"Math.floor({value})", + var_type=int, + _raw_js_function="Math.floor", + ) @var_operation @@ -763,7 +769,9 @@ def boolean_to_number_operation(value: Var[bool]): Returns: The boolean to number operation. """ - return var_operation_return(js_expression=f"Number({value})", var_type=int) + return var_operation_return( + js_expression=f"Number({value})", var_type=int, _raw_js_function="Number" + ) def comparison_operator( @@ -1002,6 +1010,10 @@ _AT_SLICE_IMPORT: ImportDict = { f"$/{Dirs.STATE_PATH}": [ImportVar(tag="atSlice")], } +_AT_SLICE_OR_INDEX: ImportDict = { + f"$/{Dirs.STATE_PATH}": [ImportVar(tag="atSliceOrIndex")], +} + _RANGE_IMPORT: ImportDict = { f"$/{Dirs.UTILS}/helpers/range": [ImportVar(tag="range", is_default=True)], } @@ -1021,6 +1033,7 @@ def boolify(value: Var): js_expression=f"isTrue({value})", var_type=bool, var_data=VarData(imports=_IS_TRUE_IMPORT), + _raw_js_function="isTrue", ) diff --git a/reflex/vars/object.py b/reflex/vars/object.py index dfc76d4cf..7c793a0fb 100644 --- a/reflex/vars/object.py +++ b/reflex/vars/object.py @@ -415,6 +415,7 @@ def object_keys_operation(value: Var): return var_operation_return( js_expression=f"Object.keys({value})", var_type=List[str], + _raw_js_function="Object.keys", ) @@ -435,6 +436,7 @@ def object_values_operation(value: Var): lambda x: List[x.to(ObjectVar)._value_type()], ), var_type=List[Any], + _raw_js_function="Object.values", ) @@ -456,6 +458,7 @@ def object_entries_operation(value: Var): lambda x: List[Tuple[str, x.to(ObjectVar)._value_type()]], ), var_type=List[Tuple[str, Any]], + _raw_js_function="Object.entries", ) diff --git a/reflex/vars/sequence.py b/reflex/vars/sequence.py index 6136e2398..997a5a00e 100644 --- a/reflex/vars/sequence.py +++ b/reflex/vars/sequence.py @@ -30,8 +30,7 @@ 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 ( +from reflex.vars.base import ( CachedVarOperation, CustomVarOperationReturn, LiteralVar, @@ -51,8 +50,10 @@ from .base import ( var_operation, var_operation_return, ) + from .number import ( _AT_SLICE_IMPORT, + _AT_SLICE_OR_INDEX, _IS_TRUE_IMPORT, _RANGE_IMPORT, LiteralNumberVar, @@ -88,7 +89,7 @@ def string_lt_operation(lhs: Var[str], rhs: Var[str]): Returns: The string less than operation. """ - return var_operation_return(js_expression=f"{lhs} < {rhs}", var_type=bool) + return var_operation_return(js_expression=f"({lhs} < {rhs})", var_type=bool) @var_operation @@ -102,7 +103,7 @@ def string_gt_operation(lhs: Var[str], rhs: Var[str]): Returns: The string greater than operation. """ - return var_operation_return(js_expression=f"{lhs} > {rhs}", var_type=bool) + return var_operation_return(js_expression=f"({lhs} > {rhs})", var_type=bool) @var_operation @@ -116,7 +117,7 @@ def string_le_operation(lhs: Var[str], rhs: Var[str]): Returns: The string less than or equal operation. """ - return var_operation_return(js_expression=f"{lhs} <= {rhs}", var_type=bool) + return var_operation_return(js_expression=f"({lhs} <= {rhs})", var_type=bool) @var_operation @@ -130,7 +131,7 @@ def string_ge_operation(lhs: Var[str], rhs: Var[str]): Returns: The string greater than or equal operation. """ - return var_operation_return(js_expression=f"{lhs} >= {rhs}", var_type=bool) + return var_operation_return(js_expression=f"({lhs} >= {rhs})", var_type=bool) @var_operation @@ -143,7 +144,11 @@ def string_lower_operation(string: Var[str]): Returns: The lowercase string. """ - return var_operation_return(js_expression=f"{string}.toLowerCase()", var_type=str) + return var_operation_return( + js_expression=f"String.prototype.toLowerCase.apply({string})", + var_type=str, + _raw_js_function="String.prototype.toLowerCase.apply", + ) @var_operation @@ -156,7 +161,11 @@ def string_upper_operation(string: Var[str]): Returns: The uppercase string. """ - return var_operation_return(js_expression=f"{string}.toUpperCase()", var_type=str) + return var_operation_return( + js_expression=f"String.prototype.toUpperCase.apply({string})", + var_type=str, + _raw_js_function="String.prototype.toUpperCase.apply", + ) @var_operation @@ -169,7 +178,11 @@ def string_strip_operation(string: Var[str]): Returns: The stripped string. """ - return var_operation_return(js_expression=f"{string}.trim()", var_type=str) + return var_operation_return( + js_expression=f"String.prototype.trim.apply({string})", + var_type=str, + _raw_js_function="String.prototype.trim.apply", + ) @var_operation @@ -259,6 +272,59 @@ def string_item_operation(string: Var[str], index: Var[int]): return var_operation_return(js_expression=f"{string}.at({index})", var_type=str) +@var_operation +def string_slice_operation( + string: Var[str], slice: Var[slice] +) -> CustomVarOperationReturn[str]: + """Get a slice from a string. + + Args: + string: The string. + slice: The slice. + + Returns: + The sliced string. + """ + return var_operation_return( + js_expression=f'atSlice({string}.split(""), {slice}).join("")', + type_computer=nary_type_computer( + ReflexCallable[[List[str], slice], str], + ReflexCallable[[slice], str], + computer=lambda args: str, + ), + var_data=VarData( + imports=_AT_SLICE_IMPORT, + ), + ) + + +@var_operation +def string_index_or_slice_operation( + string: Var[str], index_or_slice: Var[Union[int, slice]] +) -> CustomVarOperationReturn[Union[str, Sequence[str]]]: + """Get an item or slice from a string. + + Args: + string: The string. + index_or_slice: The index or slice. + + Returns: + The item or slice from the string. + """ + return var_operation_return( + js_expression=f"Array.prototype.join.apply(atSliceOrIndex({string}, {index_or_slice}), [''])", + _raw_js_function="atSliceOrIndex", + type_computer=nary_type_computer( + ReflexCallable[[List[str], Union[int, slice]], str], + ReflexCallable[[Union[int, slice]], str], + computer=lambda args: str, + ), + var_data=VarData( + imports=_AT_SLICE_OR_INDEX, + ), + ) + + @var_operation def string_replace_operation( string: Var[str], search_value: Var[str], new_value: Var[str] @@ -454,7 +520,8 @@ def array_item_or_slice_operation( The item or slice from the array. """ return var_operation_return( - js_expression=f"Array.isArray({index_or_slice}) ? at_slice({array}, {index_or_slice}) : {array}.at({index_or_slice})", + js_expression=f"atSliceOrIndex({array}, {index_or_slice})", + _raw_js_function="atSliceOrIndex", type_computer=nary_type_computer( ReflexCallable[[Sequence, Union[int, slice]], Any], ReflexCallable[[Union[int, slice]], Any], @@ -465,7 +532,7 @@ def array_item_or_slice_operation( ), ), var_data=VarData( - imports=_AT_SLICE_IMPORT, + imports=_AT_SLICE_OR_INDEX, ), ) @@ -1073,7 +1140,11 @@ class StringVar(Var[STRING_TYPE], python_types=str): __radd__ = reverse_string_concat_operation - __getitem__ = string_item_operation + __getitem__ = string_index_or_slice_operation + + at = string_item_operation + + slice = string_slice_operation lower = string_lower_operation diff --git a/tests/units/components/core/test_banner.py b/tests/units/components/core/test_banner.py index e1498d12c..fc572e9a6 100644 --- a/tests/units/components/core/test_banner.py +++ b/tests/units/components/core/test_banner.py @@ -25,6 +25,7 @@ def test_connection_banner(): "react", "$/utils/context", "$/utils/state", + "@emotion/react", RadixThemesComponent().library or "", "$/env.json", ) @@ -43,6 +44,7 @@ def test_connection_modal(): "react", "$/utils/context", "$/utils/state", + "@emotion/react", RadixThemesComponent().library or "", "$/env.json", ) diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index 674873b69..766f96e61 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -846,7 +846,7 @@ def test_component_event_trigger_arbitrary_args(): assert comp.render()["props"][0] == ( "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) }}), ({{ }})))], ' + f'[(Event("{C1State.get_full_name()}.mock_handler", ({{ ["_e"] : __e["target"]["value"], ["_bravo"] : _bravo["nested"], ["_charlie"] : (((_lhs, _rhs) => (_lhs + _rhs))(_charlie["custom"], 42)) }}), ({{ }})))], ' "[__e, _alpha, _bravo, _charlie], ({ }))))}" ) diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 41fac443e..3898e4658 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -432,12 +432,15 @@ def test_default_setters(test_state): def test_class_indexing_with_vars(): """Test that we can index into a state var with another var.""" prop = TestState.array[TestState.num1] - assert str(prop) == f"{TestState.get_name()}.array.at({TestState.get_name()}.num1)" + assert ( + str(prop) + == f"(((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))({TestState.get_name()}.array, ...args)))({TestState.get_name()}.num1))" + ) prop = TestState.mapping["a"][TestState.num1] assert ( str(prop) - == f'{TestState.get_name()}.mapping["a"].at({TestState.get_name()}.num1)' + == f'(((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))({TestState.get_name()}.mapping["a"], ...args)))({TestState.get_name()}.num1))' ) prop = TestState.mapping[TestState.map_key] diff --git a/tests/units/test_var.py b/tests/units/test_var.py index f3d22f632..0aa0d2f37 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -307,30 +307,65 @@ def test_basic_operations(TestObj): Args: TestObj: The test object. """ - assert str(v(1) == v(2)) == "(1 === 2)" - assert str(v(1) != v(2)) == "(1 !== 2)" - assert str(LiteralNumberVar.create(1) < 2) == "(1 < 2)" - assert str(LiteralNumberVar.create(1) <= 2) == "(1 <= 2)" - assert str(LiteralNumberVar.create(1) > 2) == "(1 > 2)" - assert str(LiteralNumberVar.create(1) >= 2) == "(1 >= 2)" - assert str(LiteralNumberVar.create(1) + 2) == "(1 + 2)" - assert str(LiteralNumberVar.create(1) - 2) == "(1 - 2)" - assert str(LiteralNumberVar.create(1) * 2) == "(1 * 2)" - assert str(LiteralNumberVar.create(1) / 2) == "(1 / 2)" - assert str(LiteralNumberVar.create(1) // 2) == "Math.floor(1 / 2)" - assert str(LiteralNumberVar.create(1) % 2) == "(1 % 2)" - assert str(LiteralNumberVar.create(1) ** 2) == "(1 ** 2)" - assert str(LiteralNumberVar.create(1) & v(2)) == "(1 && 2)" - assert str(LiteralNumberVar.create(1) | v(2)) == "(1 || 2)" - assert str(LiteralArrayVar.create([1, 2, 3])[0]) == "[1, 2, 3].at(0)" + assert str(v(1) == v(2)) == "(((_lhs, _rhs) => (_lhs === _rhs))(1, 2))" + assert str(v(1) != v(2)) == "(((_lhs, _rhs) => (_lhs !== _rhs))(1, 2))" + assert ( + str(LiteralNumberVar.create(1) < 2) == "(((_lhs, _rhs) => (_lhs < _rhs))(1, 2))" + ) + assert ( + str(LiteralNumberVar.create(1) <= 2) + == "(((_lhs, _rhs) => (_lhs <= _rhs))(1, 2))" + ) + assert ( + str(LiteralNumberVar.create(1) > 2) == "(((_lhs, _rhs) => (_lhs > _rhs))(1, 2))" + ) + assert ( + str(LiteralNumberVar.create(1) >= 2) + == "(((_lhs, _rhs) => (_lhs >= _rhs))(1, 2))" + ) + assert ( + str(LiteralNumberVar.create(1) + 2) == "(((_lhs, _rhs) => (_lhs + _rhs))(1, 2))" + ) + assert ( + str(LiteralNumberVar.create(1) - 2) == "(((_lhs, _rhs) => (_lhs - _rhs))(1, 2))" + ) + assert ( + str(LiteralNumberVar.create(1) * 2) == "(((_lhs, _rhs) => (_lhs * _rhs))(1, 2))" + ) + assert ( + str(LiteralNumberVar.create(1) / 2) == "(((_lhs, _rhs) => (_lhs / _rhs))(1, 2))" + ) + assert ( + str(LiteralNumberVar.create(1) // 2) + == "(((_lhs, _rhs) => Math.floor(_lhs / _rhs))(1, 2))" + ) + assert ( + str(LiteralNumberVar.create(1) % 2) == "(((_lhs, _rhs) => (_lhs % _rhs))(1, 2))" + ) + assert ( + str(LiteralNumberVar.create(1) ** 2) + == "(((_lhs, _rhs) => (_lhs ** _rhs))(1, 2))" + ) + assert str(LiteralNumberVar.create(1) & v(2)) == "(((_a, _b) => (_a && _b))(1, 2))" + assert str(LiteralNumberVar.create(1) | v(2)) == "(((_a, _b) => (_a || _b))(1, 2))" + assert ( + str(LiteralArrayVar.create([1, 2, 3])[0]) + == "(((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))([1, 2, 3], ...args)))(0))" + ) assert ( str(LiteralObjectVar.create({"a": 1, "b": 2})["a"]) == '({ ["a"] : 1, ["b"] : 2 })["a"]' ) - assert str(v("foo") == v("bar")) == '("foo" === "bar")' - assert str(Var(_js_expr="foo") == Var(_js_expr="bar")) == "(foo === bar)" assert ( - str(LiteralVar.create("foo") == LiteralVar.create("bar")) == '("foo" === "bar")' + str(v("foo") == v("bar")) == '(((_lhs, _rhs) => (_lhs === _rhs))("foo", "bar"))' + ) + assert ( + str(Var(_js_expr="foo") == Var(_js_expr="bar")) + == "(((_lhs, _rhs) => (_lhs === _rhs))(foo, bar))" + ) + assert ( + str(LiteralVar.create("foo") == LiteralVar.create("bar")) + == '(((_lhs, _rhs) => (_lhs === _rhs))("foo", "bar"))' ) print(Var(_js_expr="foo").to(ObjectVar, TestObj)._var_set_state("state")) assert ( @@ -338,33 +373,39 @@ def test_basic_operations(TestObj): Var(_js_expr="foo").to(ObjectVar, TestObj)._var_set_state("state").bar == LiteralVar.create("bar") ) - == '(state.foo["bar"] === "bar")' + == '(((_lhs, _rhs) => (_lhs === _rhs))(state.foo["bar"], "bar"))' ) assert ( str(Var(_js_expr="foo").to(ObjectVar, TestObj)._var_set_state("state").bar) == 'state.foo["bar"]' ) - assert str(abs(LiteralNumberVar.create(1))) == "Math.abs(1)" - assert str(LiteralArrayVar.create([1, 2, 3]).length()) == "[1, 2, 3].length" + assert str(abs(LiteralNumberVar.create(1))) == "(Math.abs(1))" + assert ( + str(LiteralArrayVar.create([1, 2, 3]).length()) + == "(((...args) => (((_array) => _array.length)([1, 2, 3], ...args)))())" + ) assert ( str(LiteralArrayVar.create([1, 2]) + LiteralArrayVar.create([3, 4])) - == "[...[1, 2], ...[3, 4]]" + == "(((...args) => (((_lhs, _rhs) => [..._lhs, ..._rhs])([1, 2], ...args)))([3, 4]))" ) # Tests for reverse operation assert ( str(LiteralArrayVar.create([1, 2, 3]).reverse()) - == "[1, 2, 3].slice().reverse()" + == "(((...args) => (((_array) => _array.slice().reverse())([1, 2, 3], ...args)))())" ) assert ( str(LiteralArrayVar.create(["1", "2", "3"]).reverse()) - == '["1", "2", "3"].slice().reverse()' + == '(((...args) => (((_array) => _array.slice().reverse())(["1", "2", "3"], ...args)))())' ) assert ( str(Var(_js_expr="foo")._var_set_state("state").to(list).reverse()) - == "state.foo.slice().reverse()" + == "(((...args) => (((_array) => _array.slice().reverse())(state.foo, ...args)))())" + ) + assert ( + str(Var(_js_expr="foo").to(list).reverse()) + == "(((...args) => (((_array) => _array.slice().reverse())(foo, ...args)))())" ) - assert str(Var(_js_expr="foo").to(list).reverse()) == "foo.slice().reverse()" assert str(Var(_js_expr="foo", _var_type=str).js_type()) == "(typeof(foo))" @@ -389,14 +430,32 @@ def test_basic_operations(TestObj): ], ) def test_list_tuple_contains(var, expected): - assert str(var.contains(1)) == f"{expected}.includes(1)" - assert str(var.contains("1")) == f'{expected}.includes("1")' - assert str(var.contains(v(1))) == f"{expected}.includes(1)" - assert str(var.contains(v("1"))) == f'{expected}.includes("1")' + assert ( + str(var.contains(1)) + == f'(((...args) => (((_haystack, _needle, _field = "") => isTrue(_field) ? _haystack.some(obj => obj[_field] === _needle) : _haystack.some(obj => obj === _needle))({expected!s}, ...args)))(1))' + ) + assert ( + str(var.contains("1")) + == f'(((...args) => (((_haystack, _needle, _field = "") => isTrue(_field) ? _haystack.some(obj => obj[_field] === _needle) : _haystack.some(obj => obj === _needle))({expected!s}, ...args)))("1"))' + ) + assert ( + str(var.contains(v(1))) + == f'(((...args) => (((_haystack, _needle, _field = "") => isTrue(_field) ? _haystack.some(obj => obj[_field] === _needle) : _haystack.some(obj => obj === _needle))({expected!s}, ...args)))(1))' + ) + assert ( + str(var.contains(v("1"))) + == f'(((...args) => (((_haystack, _needle, _field = "") => isTrue(_field) ? _haystack.some(obj => obj[_field] === _needle) : _haystack.some(obj => obj === _needle))({expected!s}, ...args)))("1"))' + ) other_state_var = Var(_js_expr="other", _var_type=str)._var_set_state("state") other_var = Var(_js_expr="other", _var_type=str) - assert str(var.contains(other_state_var)) == f"{expected}.includes(state.other)" - assert str(var.contains(other_var)) == f"{expected}.includes(other)" + assert ( + str(var.contains(other_state_var)) + == f'(((...args) => (((_haystack, _needle, _field = "") => isTrue(_field) ? _haystack.some(obj => obj[_field] === _needle) : _haystack.some(obj => obj === _needle))({expected!s}, ...args)))(state.other))' + ) + assert ( + str(var.contains(other_var)) + == f'(((...args) => (((_haystack, _needle, _field = "") => isTrue(_field) ? _haystack.some(obj => obj[_field] === _needle) : _haystack.some(obj => obj === _needle))({expected!s}, ...args)))(other))' + ) class Foo(rx.Base): @@ -446,15 +505,27 @@ def test_var_types(var, var_type): ], ) def test_str_contains(var, expected): - assert str(var.contains("1")) == f'{expected}.includes("1")' - assert str(var.contains(v("1"))) == f'{expected}.includes("1")' + assert ( + str(var.contains("1")) + == f'(((...args) => (((_haystack, _needle, _field = "") => isTrue(_field) ? _haystack.some(obj => obj[_field] === _needle) : _haystack.some(obj => obj === _needle))({expected!s}, ...args)))("1"))' + ) + assert ( + str(var.contains(v("1"))) + == f'(((...args) => (((_haystack, _needle, _field = "") => isTrue(_field) ? _haystack.some(obj => obj[_field] === _needle) : _haystack.some(obj => obj === _needle))({expected!s}, ...args)))("1"))' + ) other_state_var = Var(_js_expr="other")._var_set_state("state").to(str) other_var = Var(_js_expr="other").to(str) - assert str(var.contains(other_state_var)) == f"{expected}.includes(state.other)" - assert str(var.contains(other_var)) == f"{expected}.includes(other)" + assert ( + str(var.contains(other_state_var)) + == f'(((...args) => (((_haystack, _needle, _field = "") => isTrue(_field) ? _haystack.some(obj => obj[_field] === _needle) : _haystack.some(obj => obj === _needle))({expected!s}, ...args)))(state.other))' + ) + assert ( + str(var.contains(other_var)) + == f'(((...args) => (((_haystack, _needle, _field = "") => isTrue(_field) ? _haystack.some(obj => obj[_field] === _needle) : _haystack.some(obj => obj === _needle))({expected!s}, ...args)))(other))' + ) assert ( str(var.contains("1", "hello")) - == f'{expected}.some(obj => obj["hello"] === "1")' + == f'(((...args) => (((_haystack, _needle, _field = "") => isTrue(_field) ? _haystack.some(obj => obj[_field] === _needle) : _haystack.some(obj => obj === _needle))({expected!s}, ...args)))("1", "hello"))' ) @@ -467,16 +538,32 @@ def test_str_contains(var, expected): ], ) def test_dict_contains(var, expected): - assert str(var.contains(1)) == f"{expected}.hasOwnProperty(1)" - assert str(var.contains("1")) == f'{expected}.hasOwnProperty("1")' - assert str(var.contains(v(1))) == f"{expected}.hasOwnProperty(1)" - assert str(var.contains(v("1"))) == f'{expected}.hasOwnProperty("1")' + assert ( + str(var.contains(1)) + == f"(((_object, _key) => _object.hasOwnProperty(_key))({expected!s}, 1))" + ) + assert ( + str(var.contains("1")) + == f'(((_object, _key) => _object.hasOwnProperty(_key))({expected!s}, "1"))' + ) + assert ( + str(var.contains(v(1))) + == f"(((_object, _key) => _object.hasOwnProperty(_key))({expected!s}, 1))" + ) + assert ( + str(var.contains(v("1"))) + == f'(((_object, _key) => _object.hasOwnProperty(_key))({expected!s}, "1"))' + ) other_state_var = Var(_js_expr="other")._var_set_state("state").to(str) other_var = Var(_js_expr="other").to(str) assert ( - str(var.contains(other_state_var)) == f"{expected}.hasOwnProperty(state.other)" + str(var.contains(other_state_var)) + == f"(((_object, _key) => _object.hasOwnProperty(_key))({expected!s}, state.other))" + ) + assert ( + str(var.contains(other_var)) + == f"(((_object, _key) => _object.hasOwnProperty(_key))({expected!s}, other))" ) - assert str(var.contains(other_var)) == f"{expected}.hasOwnProperty(other)" @pytest.mark.parametrize( @@ -484,7 +571,6 @@ def test_dict_contains(var, expected): [ Var(_js_expr="list", _var_type=List[int]).guess_type(), Var(_js_expr="tuple", _var_type=Tuple[int, int]).guess_type(), - Var(_js_expr="str", _var_type=str).guess_type(), ], ) def test_var_indexing_lists(var): @@ -494,11 +580,20 @@ def test_var_indexing_lists(var): var : The str, list or tuple base var. """ # Test basic indexing. - assert str(var[0]) == f"{var._js_expr}.at(0)" - assert str(var[1]) == f"{var._js_expr}.at(1)" + assert ( + str(var[0]) + == f"(((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))({var!s}, ...args)))(0))" + ) + assert ( + str(var[1]) + == f"(((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))({var!s}, ...args)))(1))" + ) # Test negative indexing. - assert str(var[-1]) == f"{var._js_expr}.at(-1)" + assert ( + str(var[-1]) + == f"(((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))({var!s}, ...args)))(-1))" + ) @pytest.mark.parametrize( @@ -532,11 +627,20 @@ def test_var_indexing_str(): assert str_var[0]._var_type is str # Test basic indexing. - assert str(str_var[0]) == "str.at(0)" - assert str(str_var[1]) == "str.at(1)" + assert ( + str(str_var[0]) + == "(((...args) => (((_string, _index_or_slice) => Array.prototype.join.apply(atSliceOrIndex(_string, _index_or_slice), ['']))(str, ...args)))(0))" + ) + assert ( + str(str_var[1]) + == "(((...args) => (((_string, _index_or_slice) => Array.prototype.join.apply(atSliceOrIndex(_string, _index_or_slice), ['']))(str, ...args)))(1))" + ) # Test negative indexing. - assert str(str_var[-1]) == "str.at(-1)" + assert ( + str(str_var[-1]) + == "(((...args) => (((_string, _index_or_slice) => Array.prototype.join.apply(atSliceOrIndex(_string, _index_or_slice), ['']))(str, ...args)))(-1))" + ) @pytest.mark.parametrize( @@ -651,9 +755,18 @@ def test_var_list_slicing(var): Args: var : The str, list or tuple base var. """ - assert str(var[:1]) == f"{var._js_expr}.slice(undefined, 1)" - assert str(var[1:]) == f"{var._js_expr}.slice(1, undefined)" - assert str(var[:]) == f"{var._js_expr}.slice(undefined, undefined)" + assert ( + str(var[:1]) + == f"(((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))({var!s}, ...args)))([null, 1, null]))" + ) + assert ( + str(var[1:]) + == f"(((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))({var!s}, ...args)))([1, null, null]))" + ) + assert ( + str(var[:]) + == f"(((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))({var!s}, ...args)))([null, null, null]))" + ) def test_str_var_slicing(): @@ -665,16 +778,40 @@ def test_str_var_slicing(): assert str_var[:1]._var_type is str # Test basic slicing. - assert str(str_var[:1]) == 'str.split("").slice(undefined, 1).join("")' - assert str(str_var[1:]) == 'str.split("").slice(1, undefined).join("")' - assert str(str_var[:]) == 'str.split("").slice(undefined, undefined).join("")' - assert str(str_var[1:2]) == 'str.split("").slice(1, 2).join("")' + assert ( + str(str_var[:1]) + == "(((...args) => (((_string, _index_or_slice) => Array.prototype.join.apply(atSliceOrIndex(_string, _index_or_slice), ['']))(str, ...args)))([null, 1, null]))" + ) + assert ( + str(str_var[1:]) + == "(((...args) => (((_string, _index_or_slice) => Array.prototype.join.apply(atSliceOrIndex(_string, _index_or_slice), ['']))(str, ...args)))([1, null, null]))" + ) + assert ( + str(str_var[:]) + == "(((...args) => (((_string, _index_or_slice) => Array.prototype.join.apply(atSliceOrIndex(_string, _index_or_slice), ['']))(str, ...args)))([null, null, null]))" + ) + assert ( + str(str_var[1:2]) + == "(((...args) => (((_string, _index_or_slice) => Array.prototype.join.apply(atSliceOrIndex(_string, _index_or_slice), ['']))(str, ...args)))([1, 2, null]))" + ) # Test negative slicing. - assert str(str_var[:-1]) == 'str.split("").slice(undefined, -1).join("")' - assert str(str_var[-1:]) == 'str.split("").slice(-1, undefined).join("")' - assert str(str_var[:-2]) == 'str.split("").slice(undefined, -2).join("")' - assert str(str_var[-2:]) == 'str.split("").slice(-2, undefined).join("")' + assert ( + str(str_var[:-1]) + == "(((...args) => (((_string, _index_or_slice) => Array.prototype.join.apply(atSliceOrIndex(_string, _index_or_slice), ['']))(str, ...args)))([null, -1, null]))" + ) + assert ( + str(str_var[-1:]) + == "(((...args) => (((_string, _index_or_slice) => Array.prototype.join.apply(atSliceOrIndex(_string, _index_or_slice), ['']))(str, ...args)))([-1, null, null]))" + ) + assert ( + str(str_var[:-2]) + == "(((...args) => (((_string, _index_or_slice) => Array.prototype.join.apply(atSliceOrIndex(_string, _index_or_slice), ['']))(str, ...args)))([null, -2, null]))" + ) + assert ( + str(str_var[-2:]) + == "(((...args) => (((_string, _index_or_slice) => Array.prototype.join.apply(atSliceOrIndex(_string, _index_or_slice), ['']))(str, ...args)))([-2, null, null]))" + ) def test_dict_indexing(): @@ -966,8 +1103,8 @@ def test_var_operation(): def add(a: Var[int], b: Var[int]): return var_operation_return(js_expression=f"({a} + {b})", var_type=int) - assert str(add(1, 2)) == "(1 + 2)" - assert str(add(4, -9)) == "(4 + -9)" + assert str(add(1, 2)) == "(((_a, _b) => (_a + _b))(1, 2))" + assert str(add(4, -9)) == "(((_a, _b) => (_a + _b))(4, -9))" five = LiteralNumberVar.create(5) seven = add(2, five) @@ -978,13 +1115,29 @@ def test_var_operation(): def test_string_operations(): basic_string = LiteralStringVar.create("Hello, World!") - assert str(basic_string.length()) == '"Hello, World!".split("").length' - assert str(basic_string.lower()) == '"Hello, World!".toLowerCase()' - assert str(basic_string.upper()) == '"Hello, World!".toUpperCase()' - assert str(basic_string.strip()) == '"Hello, World!".trim()' - assert str(basic_string.contains("World")) == '"Hello, World!".includes("World")' assert ( - str(basic_string.split(" ").join(",")) == '"Hello, World!".split(" ").join(",")' + str(basic_string.length()) + == '(((...args) => (((...arg) => (((_array) => _array.length)((((_string, _sep = "") => isTrue(_sep) ? _string.split(_sep) : [..._string])(...args)))))("Hello, World!", ...args)))())' + ) + assert ( + str(basic_string.lower()) + == '(((...args) => (((_string) => String.prototype.toLowerCase.apply(_string))("Hello, World!", ...args)))())' + ) + assert ( + str(basic_string.upper()) + == '(((...args) => (((_string) => String.prototype.toUpperCase.apply(_string))("Hello, World!", ...args)))())' + ) + assert ( + str(basic_string.strip()) + == '(((...args) => (((_string) => String.prototype.trim.apply(_string))("Hello, World!", ...args)))())' + ) + assert ( + str(basic_string.contains("World")) + == '(((...args) => (((_haystack, _needle, _field = "") => isTrue(_field) ? _haystack.some(obj => obj[_field] === _needle) : _haystack.some(obj => obj === _needle))("Hello, World!", ...args)))("World"))' + ) + assert ( + str(basic_string.split(" ").join(",")) + == '(((...args) => (((_array, _sep = "") => _array.join(_sep))((((...args) => (((_string, _sep = "") => isTrue(_sep) ? _string.split(_sep) : [..._string])("Hello, World!", ...args)))(" ")), ...args)))(","))' ) @@ -995,7 +1148,7 @@ def test_all_number_operations(): assert ( str(complicated_number) - == "((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2)" + == "(((_lhs, _rhs) => (_lhs ** _rhs))((((_lhs, _rhs) => (_lhs % _rhs))((((_lhs, _rhs) => Math.floor(_lhs / _rhs))((((_lhs, _rhs) => (_lhs / _rhs))((((_lhs, _rhs) => (_lhs * _rhs))((((_value) => -(_value))((((_lhs, _rhs) => (_lhs + _rhs))(-5.4, 1)))), 2)), 3)), 2)), 3)), 2))" ) even_more_complicated_number = ~( @@ -1004,14 +1157,20 @@ def test_all_number_operations(): assert ( str(even_more_complicated_number) - == "!(((Math.abs(Math.floor(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))) || (2 && Math.round(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2)))) !== 0))" + == "(((_value) => !(_value))((((_lhs, _rhs) => (_lhs !== _rhs))((((_a, _b) => (_a || _b))((Math.abs((Math.floor((((_lhs, _rhs) => (_lhs ** _rhs))((((_lhs, _rhs) => (_lhs % _rhs))((((_lhs, _rhs) => Math.floor(_lhs / _rhs))((((_lhs, _rhs) => (_lhs / _rhs))((((_lhs, _rhs) => (_lhs * _rhs))((((_value) => -(_value))((((_lhs, _rhs) => (_lhs + _rhs))(-5.4, 1)))), 2)), 3)), 2)), 3)), 2)))))), (((_a, _b) => (_a && _b))(2, (((_value) => Math.round(_value))((((_lhs, _rhs) => (_lhs ** _rhs))((((_lhs, _rhs) => (_lhs % _rhs))((((_lhs, _rhs) => Math.floor(_lhs / _rhs))((((_lhs, _rhs) => (_lhs / _rhs))((((_lhs, _rhs) => (_lhs * _rhs))((((_value) => -(_value))((((_lhs, _rhs) => (_lhs + _rhs))(-5.4, 1)))), 2)), 3)), 2)), 3)), 2)))))))), 0))))" ) - assert str(LiteralNumberVar.create(5) > False) == "(5 > 0)" - assert str(LiteralBooleanVar.create(False) < 5) == "(Number(false) < 5)" + assert ( + str(LiteralNumberVar.create(5) > False) + == "(((_lhs, _rhs) => (_lhs > _rhs))(5, 0))" + ) + assert ( + str(LiteralBooleanVar.create(False) < 5) + == "(((_lhs, _rhs) => (_lhs < _rhs))((Number(false)), 5))" + ) assert ( str(LiteralBooleanVar.create(False) < LiteralBooleanVar.create(True)) - == "(Number(false) < Number(true))" + == "(((_lhs, _rhs) => (_lhs < _rhs))((Number(false)), (Number(true))))" ) @@ -1020,10 +1179,10 @@ def test_all_number_operations(): [ (Var.create(False), "false"), (Var.create(True), "true"), - (Var.create("false"), 'isTrue("false")'), - (Var.create([1, 2, 3]), "isTrue([1, 2, 3])"), - (Var.create({"a": 1, "b": 2}), 'isTrue(({ ["a"] : 1, ["b"] : 2 }))'), - (Var("mysterious_var"), "isTrue(mysterious_var)"), + (Var.create("false"), '(isTrue("false"))'), + (Var.create([1, 2, 3]), "(isTrue([1, 2, 3]))"), + (Var.create({"a": 1, "b": 2}), '(isTrue(({ ["a"] : 1, ["b"] : 2 })))'), + (Var("mysterious_var"), "(isTrue(mysterious_var))"), ], ) def test_boolify_operations(var, expected): @@ -1032,18 +1191,30 @@ def test_boolify_operations(var, expected): def test_index_operation(): array_var = LiteralArrayVar.create([1, 2, 3, 4, 5]) - assert str(array_var[0]) == "[1, 2, 3, 4, 5].at(0)" - assert str(array_var[1:2]) == "[1, 2, 3, 4, 5].slice(1, 2)" + assert ( + str(array_var[0]) + == "(((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))([1, 2, 3, 4, 5], ...args)))(0))" + ) + assert ( + str(array_var[1:2]) + == "(((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))([1, 2, 3, 4, 5], ...args)))([1, 2, null]))" + ) assert ( str(array_var[1:4:2]) - == "[1, 2, 3, 4, 5].slice(1, 4).filter((_, i) => i % 2 === 0)" + == "(((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))([1, 2, 3, 4, 5], ...args)))([1, 4, 2]))" ) assert ( str(array_var[::-1]) - == "[1, 2, 3, 4, 5].slice(0, [1, 2, 3, 4, 5].length).slice().reverse().slice(undefined, undefined).filter((_, i) => i % 1 === 0)" + == "(((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))([1, 2, 3, 4, 5], ...args)))([null, null, -1]))" + ) + assert ( + str(array_var.reverse()) + == "(((...args) => (((_array) => _array.slice().reverse())([1, 2, 3, 4, 5], ...args)))())" + ) + assert ( + str(array_var[0].to(NumberVar) + 9) + == "(((_lhs, _rhs) => (_lhs + _rhs))((((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))([1, 2, 3, 4, 5], ...args)))(0)), 9))" ) - assert str(array_var.reverse()) == "[1, 2, 3, 4, 5].slice().reverse()" - assert str(array_var[0].to(NumberVar) + 9) == "([1, 2, 3, 4, 5].at(0) + 9)" @pytest.mark.parametrize( @@ -1065,24 +1236,33 @@ def test_inf_and_nan(var, expected_js): def test_array_operations(): array_var = LiteralArrayVar.create([1, 2, 3, 4, 5]) - assert str(array_var.length()) == "[1, 2, 3, 4, 5].length" - assert str(array_var.contains(3)) == "[1, 2, 3, 4, 5].includes(3)" - assert str(array_var.reverse()) == "[1, 2, 3, 4, 5].slice().reverse()" + assert ( + str(array_var.length()) + == "(((...args) => (((_array) => _array.length)([1, 2, 3, 4, 5], ...args)))())" + ) + assert ( + str(array_var.contains(3)) + == '(((...args) => (((_haystack, _needle, _field = "") => isTrue(_field) ? _haystack.some(obj => obj[_field] === _needle) : _haystack.some(obj => obj === _needle))([1, 2, 3, 4, 5], ...args)))(3))' + ) + assert ( + str(array_var.reverse()) + == "(((...args) => (((_array) => _array.slice().reverse())([1, 2, 3, 4, 5], ...args)))())" + ) assert ( str(ArrayVar.range(10)) - == "Array.from({ length: (10 - 0) / 1 }, (_, i) => 0 + i * 1)" + == "(((_e1, _e2 = null, _step = 1) => range(_e1, _e2, _step))(10))" ) assert ( str(ArrayVar.range(1, 10)) - == "Array.from({ length: (10 - 1) / 1 }, (_, i) => 1 + i * 1)" + == "(((_e1, _e2 = null, _step = 1) => range(_e1, _e2, _step))(1, 10))" ) assert ( str(ArrayVar.range(1, 10, 2)) - == "Array.from({ length: (10 - 1) / 2 }, (_, i) => 1 + i * 2)" + == "(((_e1, _e2 = null, _step = 1) => range(_e1, _e2, _step))(1, 10, 2))" ) assert ( str(ArrayVar.range(1, 10, -1)) - == "Array.from({ length: (10 - 1) / -1 }, (_, i) => 1 + i * -1)" + == "(((_e1, _e2 = null, _step = 1) => range(_e1, _e2, _step))(1, 10, -1))" ) @@ -1090,21 +1270,21 @@ def test_object_operations(): object_var = LiteralObjectVar.create({"a": 1, "b": 2, "c": 3}) assert ( - str(object_var.keys()) == 'Object.keys(({ ["a"] : 1, ["b"] : 2, ["c"] : 3 }))' + str(object_var.keys()) == '(Object.keys(({ ["a"] : 1, ["b"] : 2, ["c"] : 3 })))' ) assert ( str(object_var.values()) - == 'Object.values(({ ["a"] : 1, ["b"] : 2, ["c"] : 3 }))' + == '(Object.values(({ ["a"] : 1, ["b"] : 2, ["c"] : 3 })))' ) assert ( str(object_var.entries()) - == 'Object.entries(({ ["a"] : 1, ["b"] : 2, ["c"] : 3 }))' + == '(Object.entries(({ ["a"] : 1, ["b"] : 2, ["c"] : 3 })))' ) assert str(object_var.a) == '({ ["a"] : 1, ["b"] : 2, ["c"] : 3 })["a"]' assert str(object_var["a"]) == '({ ["a"] : 1, ["b"] : 2, ["c"] : 3 })["a"]' assert ( str(object_var.merge(LiteralObjectVar.create({"c": 4, "d": 5}))) - == '({...({ ["a"] : 1, ["b"] : 2, ["c"] : 3 }), ...({ ["c"] : 4, ["d"] : 5 })})' + == '(((_lhs, _rhs) => ({..._lhs, ..._rhs}))(({ ["a"] : 1, ["b"] : 2, ["c"] : 3 }), ({ ["c"] : 4, ["d"] : 5 })))' ) @@ -1140,23 +1320,27 @@ def test_type_chains(): ) assert ( str(object_var.keys()[0].upper()) # type: ignore - == 'Object.keys(({ ["a"] : 1, ["b"] : 2, ["c"] : 3 })).at(0).toUpperCase()' + == '(((...args) => (((_string) => String.prototype.toUpperCase.apply(_string))((((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))((Object.keys(({ ["a"] : 1, ["b"] : 2, ["c"] : 3 }))), ...args)))(0)), ...args)))())' ) assert ( str(object_var.entries()[1][1] - 1) # type: ignore - == '(Object.entries(({ ["a"] : 1, ["b"] : 2, ["c"] : 3 })).at(1).at(1) - 1)' + == '(((_lhs, _rhs) => (_lhs - _rhs))((((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))((((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))((Object.entries(({ ["a"] : 1, ["b"] : 2, ["c"] : 3 }))), ...args)))(1)), ...args)))(1)), 1))' ) assert ( str(object_var["c"] + object_var["b"]) # type: ignore - == '(({ ["a"] : 1, ["b"] : 2, ["c"] : 3 })["c"] + ({ ["a"] : 1, ["b"] : 2, ["c"] : 3 })["b"])' + == '(((_lhs, _rhs) => (_lhs + _rhs))(({ ["a"] : 1, ["b"] : 2, ["c"] : 3 })["c"], ({ ["a"] : 1, ["b"] : 2, ["c"] : 3 })["b"]))' ) def test_nested_dict(): - arr = LiteralArrayVar.create([{"bar": ["foo", "bar"]}], List[Dict[str, List[str]]]) + arr = Var.create([{"bar": ["foo", "bar"]}]).to(List[Dict[str, List[str]]]) + first_dict = arr.at(0) + bar_element = first_dict["bar"] + first_bar_element = bar_element[0] assert ( - str(arr[0]["bar"][0]) == '[({ ["bar"] : ["foo", "bar"] })].at(0)["bar"].at(0)' + str(first_bar_element) + == '(((...args) => (((_array, _index_or_slice) => atSliceOrIndex(_array, _index_or_slice))((((...args) => (((_array, _index) => _array.at(_index))([({ ["bar"] : ["foo", "bar"] })], ...args)))(0))["bar"], ...args)))(0))' ) @@ -1376,7 +1560,7 @@ def test_unsupported_types_for_string_contains(other): assert Var(_js_expr="var").to(str).contains(other) assert ( err.value.args[0] - == f"Unsupported Operand type(s) for contains: ToStringOperation, {type(other).__name__}" + == f"Invalid argument other provided to argument 0 in var operation. Expected but got {other._var_type}." ) @@ -1608,17 +1792,12 @@ def test_valid_var_operations(operand1_var: Var, operand2_var, operators: List[s LiteralVar.create([10, 20]), LiteralVar.create("5"), [ - "+", "-", "/", "//", "*", "%", "**", - ">", - "<", - "<=", - ">=", "^", "<<", ">>",