From 6c173677ab9fe269962d11d8e877a959f90ec016 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 2 Jul 2026 13:26:58 -0700 Subject: [PATCH] Implement ArrayVar operations: filter, reduce, flat_map, foreach -> map implements Array -> new Array ops as var operations on ArrayVar renames `.foreach` to `.map` for consistency with js and python --- docs/vars/var-operations.md | 29 ++ .../reflex_base/.templates/web/utils/state.js | 17 ++ .../src/reflex_base/vars/sequence.py | 278 ++++++++++++++++-- .../src/reflex_components_core/core/upload.py | 4 +- .../themes/components/segmented_control.py | 2 +- tests/integration/test_var_operations.py | 74 +++++ tests/units/test_var.py | 127 +++++++- 7 files changed, 497 insertions(+), 34 deletions(-) diff --git a/docs/vars/var-operations.md b/docs/vars/var-operations.md index 17d40be40b3..d6a4a424d42 100644 --- a/docs/vars/var-operations.md +++ b/docs/vars/var-operations.md @@ -397,6 +397,35 @@ def var_list_example(): ) ``` +### Map, Filter, Reduce and Flat Map + +The `map` operation applies a function to each element of a list var. The function is called with a var representing one element and must build its result out of other var operations. (`foreach` is a deprecated alias for `map`.) + +The `filter` operation keeps the elements for which the function returns a truthy value, following Python truthiness semantics: `0`, `""`, empty lists, and empty dicts are all falsy. Calling `filter()` with no arguments keeps the truthy elements themselves, like `filter(None, xs)` in Python. + +The `reduce` operation combines the elements into a single value like `functools.reduce`, calling the function with the accumulator and the current element. Pass a second argument to provide the initial accumulator value; without it, the first element is used, and reducing an empty list raises a `TypeError` in the browser. + +The `flat_map` operation maps a function over the list and flattens the results one level, iterating each result the way Python does: lists yield their elements, strings yield their characters, and dicts yield their keys. + +```python demo exec +class MapFilterReduceState(rx.State): + numbers: list[int] = [1, 2, 3, 4, 5] + words: list[str] = ["hello", "", "world"] + nested: list[list[int]] = [[1, 2], [3], [4, 5]] + + +def var_map_filter_reduce_example(): + return rx.vstack( + rx.text(f"Doubled: {MapFilterReduceState.numbers.map(lambda x: x * 2)}"), + rx.text(f"Evens: {MapFilterReduceState.numbers.filter(lambda x: x % 2 == 0)}"), + rx.text(f"Non-empty words: {MapFilterReduceState.words.filter()}"), + rx.text( + f"Total: {MapFilterReduceState.numbers.reduce(lambda total, x: total + x, 0)}" + ), + rx.text(f"Flattened: {MapFilterReduceState.nested.flat_map(lambda x: x)}"), + ) +``` + ### Lower, Upper, Split The `lower` operator converts a string var to lowercase. The `upper` operator converts a string var to uppercase. The `split` operator splits a string var into a list. diff --git a/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js b/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js index 65b11492758..05af3acc362 100644 --- a/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js +++ b/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js @@ -1229,6 +1229,23 @@ export const pyRstrip = (s, chars) => { export const pyStrip = (s, chars) => chars == null ? s.trim() : pyRstrip(pyLstrip(s, chars), chars); +/*** + * Python-semantics flat map: map each element through fn and concatenate the + * results, iterating each one the way Python does (arrays yield their + * elements, strings yield characters, objects yield their keys). + * @param {Array} arr The array to map over. + * @param {Function} fn The mapping function applied to each element. + * @returns {Array} The flattened array of mapped results. + */ +export const pyFlatMap = (arr, fn) => + arr.flatMap((element) => { + const value = fn(element); + if (Array.isArray(value)) return value; + if (typeof value === "string") return Array.from(value); + if (value === Object(value)) return Object.keys(value); + throw new TypeError(`flat_map value is not iterable: ${value}`); + }); + /** * Get the value from a ref. * @param ref The ref to get the value from. diff --git a/packages/reflex-base/src/reflex_base/vars/sequence.py b/packages/reflex-base/src/reflex_base/vars/sequence.py index b1f990ed3f1..0b621e25fb8 100644 --- a/packages/reflex-base/src/reflex_base/vars/sequence.py +++ b/packages/reflex-base/src/reflex_base/vars/sequence.py @@ -8,17 +8,17 @@ import inspect import json import re -from collections.abc import Iterable, Mapping, Sequence +from collections.abc import Callable, Iterable, Mapping, Sequence from typing import TYPE_CHECKING, Any, Literal, TypeVar, get_args, overload from typing_extensions import TypeVar as TypingExtensionsTypeVar from reflex_base import constants from reflex_base.constants.base import REFLEX_VAR_OPENING_TAG, Dirs -from reflex_base.utils import types +from reflex_base.utils import console, types from reflex_base.utils.exceptions import VarTypeError from reflex_base.utils.imports import ImportDict, ImportVar -from reflex_base.utils.types import GenericType, get_origin +from reflex_base.utils.types import GenericType, get_origin, unionize from .base import ( CachedVarOperation, @@ -30,18 +30,21 @@ cached_property_no_lock, figure_out_type, get_unique_variable_name, - unionize, var_operation, var_operation_return, ) from .number import ( + _IS_TRUE_IMPORT, BooleanVar, LiteralNumberVar, NumberVar, + boolify, raise_unsupported_operand_types, ) if TYPE_CHECKING: + from typing_extensions import deprecated + from .base import DATACLASS_TYPE, SQLA_TYPE from .function import FunctionVar from .object import ObjectVar @@ -418,51 +421,185 @@ def __ge__(self, other: Any): return array_ge_operation(self, other) - def foreach(self, fn: Any): - """Apply a function to each element of the array. + def _element_placeholder(self) -> Var: + """Create a placeholder var typed like this array's elements. + + Returns: + A var with a unique name and the array's element type. + """ + element_type = self[Var("").to(NumberVar, int)]._var_type + return Var( + _js_expr=get_unique_variable_name(), + _var_type=element_type, + ).guess_type() + + def _trace_element_fn( + self, fn: Any, operation_name: str + ) -> tuple[tuple[str, ...], Var]: + """Call fn with a placeholder element to get function arg names and return expression. Args: - fn: The function to apply. + fn: The function to trace, taking at most one argument. + operation_name: The name of the calling operation, for error messages. Returns: - The array after applying the function. + A tuple of the function's argument names and its return expression. Raises: VarTypeError: If the function takes more than one argument. """ - from .function import ArgsFunctionOperation - if not callable(fn): - raise_unsupported_operand_types("foreach", (type(self), type(fn))) - # get the number of arguments of the function + raise_unsupported_operand_types(operation_name, (type(self), type(fn))) num_args = len(inspect.signature(fn).parameters) if num_args > 1: - msg = "The function passed to foreach should take at most one argument." + msg = f"The function passed to {operation_name} should take at most one argument." raise VarTypeError(msg) - if num_args == 0: - return_value = fn() - function_var = ArgsFunctionOperation.create((), return_value) - else: - # generic number var - number_var = Var("").to(NumberVar, int) + return (), Var.create(fn()) + element = self._element_placeholder() + return (element._js_expr,), Var.create(fn(element)) + + def map(self, fn: Any): + """Apply a function to each element of the array. + + Args: + fn: The function to apply, taking at most one argument. + + Returns: + The array after applying the function. + """ + from .function import ArgsFunctionOperation + + args, return_expr = self._trace_element_fn(fn, "map") + return map_array_operation( + self, ArgsFunctionOperation.create(args, return_expr) + ) + + if TYPE_CHECKING: + + @deprecated("Use `ArrayVar.map` instead.") + def foreach(self, fn: Any) -> Var[list[Any]]: + """Apply a function to each element of the array (deprecated, use map). + + Args: + fn: The function to apply, taking at most one argument. + + Returns: + The array after applying the function. + """ + ... - first_arg_type = self[number_var]._var_type + else: - arg_name = get_unique_variable_name() + def foreach(self, fn: Any): + """Apply a function to each element of the array (deprecated, use map). - # get first argument type - first_arg = Var( - _js_expr=arg_name, - _var_type=first_arg_type, - ).guess_type() + Args: + fn: The function to apply, taking at most one argument. - function_var = ArgsFunctionOperation.create( - (arg_name,), - Var.create(fn(first_arg)), + Returns: + The array after applying the function. + """ + console.deprecate( + feature_name="ArrayVar.foreach", + reason="Use ArrayVar.map instead.", + deprecation_version="0.9.7", + removal_version="1.0", ) + return self.map(fn) + + def filter(self, fn: Any = None): + """Filter the array to the elements for which fn returns a truthy value. + + Truthiness follows Python semantics: 0, empty strings, empty arrays, + and empty objects are falsy. + + Args: + fn: The predicate, taking at most one argument. If None, filter by the truthiness of the elements themselves, like ``filter(None, xs)``. + + Returns: + The filtered array. + """ + from .function import ArgsFunctionOperation, FunctionVar + + if fn is None: + predicate = Var( + _js_expr="isTrue", + _var_data=VarData(imports=_IS_TRUE_IMPORT), + ).to(FunctionVar) + else: + args, return_expr = self._trace_element_fn(fn, "filter") + if not isinstance(return_expr, BooleanVar): + return_expr = boolify(return_expr) + predicate = ArgsFunctionOperation.create(args, return_expr) + return filter_array_operation(self, predicate) + + def reduce(self, fn: Any, initial: Any = types.Unset()): + """Reduce the array to a single value, like functools.reduce. + + The reducer is applied from left to right. When initial is not + provided, the first element is the initial accumulator, and reducing + an empty array raises a TypeError at runtime. + + Args: + fn: The reducer, taking the accumulator and the current element. + initial: The initial accumulator value. + + Returns: + The reduced value. + + Raises: + VarTypeError: If the function does not take exactly two arguments. + """ + from .function import ArgsFunctionOperation + + if not callable(fn): + raise_unsupported_operand_types("reduce", (type(self), type(fn))) + if len(inspect.signature(fn).parameters) != 2: + msg = "The function passed to reduce should take exactly two arguments (accumulator, element)." + raise VarTypeError(msg) + + element = self._element_placeholder() + initial_var = None if isinstance(initial, types.Unset) else Var.create(initial) + accumulator_type = ( + element._var_type if initial_var is None else initial_var._var_type + ) + accumulator = Var( + _js_expr=get_unique_variable_name(), + _var_type=accumulator_type, + ).guess_type() + return_expr = Var.create(fn(accumulator, element)) + function_var = ArgsFunctionOperation.create( + (accumulator._js_expr, element._js_expr), + return_expr, + _var_type=Callable[ + [accumulator_type, element._var_type], return_expr._var_type + ], + ) + if initial_var is None: + return reduce_array_operation(self, function_var) + return reduce_array_operation(self, function_var, initial_var) - return map_array_operation(self, function_var) + def flat_map(self, fn: Any): + """Apply a function to each element of the array and flatten the results. + + Each result is flattened one level, iterating it the way Python + does: arrays yield their elements, strings yield characters, and + objects yield their keys. A non-iterable result raises a TypeError + at runtime. + + Args: + fn: The function to apply, taking at most one argument. + + Returns: + The flattened array of the mapped results. + """ + from .function import ArgsFunctionOperation + + args, return_expr = self._trace_element_fn(fn, "flat_map") + return flat_map_array_operation( + self, ArgsFunctionOperation.create(args, return_expr) + ) @dataclasses.dataclass( @@ -1842,6 +1979,87 @@ def map_array_operation( ) +@var_operation +def filter_array_operation( + array: ArrayVar[ARRAY_VAR_TYPE], + predicate: FunctionVar, +) -> CustomVarOperationReturn[ARRAY_VAR_TYPE]: + """Filter an array with a predicate function. + + Args: + array: The array to filter. + predicate: The predicate deciding which elements to keep. + + Returns: + The filtered array. + """ + return var_operation_return( + js_expression=f"{array}.filter({predicate})", + var_type=array._var_type, + ) + + +@var_operation +def reduce_array_operation( + array: ArrayVar[ARRAY_VAR_TYPE], + function: FunctionVar, + initial: Var | None = None, +) -> CustomVarOperationReturn[Any]: + """Reduce an array to a single value with a two-argument function. + + Args: + array: The array to reduce. + function: The reducer, called with the accumulator and the current element. + initial: The initial accumulator value. If None, the first element is used. + + Returns: + The reduced value. + """ + callable_args = get_args(function._var_type) + return_type = callable_args[-1] if callable_args else Any + if initial is not None: + return var_operation_return( + js_expression=f"{array}.reduce({function}, {initial})", + var_type=unionize(return_type, initial._var_type), + ) + param_types = callable_args[0] if callable_args else None + element_type = ( + param_types[1] + if isinstance(param_types, list) and len(param_types) == 2 + else Any + ) + return var_operation_return( + js_expression=f"{array}.reduce({function})", + var_type=unionize(return_type, element_type), + ) + + +_PY_FLAT_MAP_IMPORT: ImportDict = { + f"$/{Dirs.STATE_PATH}": [ImportVar(tag="pyFlatMap")], +} + + +@var_operation +def flat_map_array_operation( + array: ArrayVar[ARRAY_VAR_TYPE], + function: FunctionVar, +) -> CustomVarOperationReturn[list[Any]]: + """Map a function over an array and flatten the iterable results one level. + + Args: + array: The array to map over. + function: The function to apply to each element. + + Returns: + The flattened array of mapped results. + """ + return var_operation_return( + js_expression=f"pyFlatMap({array}, {function})", + var_type=list[Any], + var_data=VarData(imports=_PY_FLAT_MAP_IMPORT), + ) + + @var_operation def array_concat_operation( lhs: ArrayVar[ARRAY_VAR_TYPE], rhs: ArrayVar[ARRAY_VAR_TYPE] diff --git a/packages/reflex-components-core/src/reflex_components_core/core/upload.py b/packages/reflex-components-core/src/reflex_components_core/core/upload.py index 7951ea60abd..fa75810148b 100644 --- a/packages/reflex-components-core/src/reflex_components_core/core/upload.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/upload.py @@ -210,13 +210,13 @@ def _format_rejected_file_record(rf: ObjectVar[dict[str, Any]]) -> str: rf = rf.to(ObjectVar, dict[str, dict[str, Any]]) file = rf["file"].to(ObjectVar, dict[str, Any]) errors = rf["errors"].to(ArrayVar, list[dict[str, Any]]) - return f"{file['path']}: {errors.foreach(lambda kv: kv['message']).join(', ')}" # noqa: FURB118 + return f"{file['path']}: {errors.map(lambda kv: kv['message']).join(', ')}" # noqa: FURB118 return toast.error( title="Files not Accepted", description=rejected_files .to(ArrayVar) - .foreach(_format_rejected_file_record) + .map(_format_rejected_file_record) .join("\n\n"), close_button=True, style={"white_space": "pre-line"}, diff --git a/packages/reflex-components-radix/src/reflex_components_radix/themes/components/segmented_control.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/segmented_control.py index b0d64aae38a..8881ca87cd5 100644 --- a/packages/reflex-components-radix/src/reflex_components_radix/themes/components/segmented_control.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/segmented_control.py @@ -48,7 +48,7 @@ def _collect_item_values(children: Sequence[Any]) -> ArrayVar | None: if len(children) == 1 and isinstance(children[0], Foreach): foreach = children[0] iterable = cast("ArrayVar", foreach.iterable) - return iterable.foreach( + return iterable.map( lambda element: ( cast("SegmentedControlItem", foreach.render_fn(element)).value ) diff --git a/tests/integration/test_var_operations.py b/tests/integration/test_var_operations.py index f943dfb5eb9..987b0902c1d 100644 --- a/tests/integration/test_var_operations.py +++ b/tests/integration/test_var_operations.py @@ -602,6 +602,68 @@ def index(): rx.text(ArrayVar.range(2, 10, 2).join(","), id="list_join_range2"), rx.text(ArrayVar.range(5, 0, -1).join(","), id="list_join_range3"), rx.text(ArrayVar.range(0, 3).join(","), id="list_join_range4"), + rx.text( + ArrayVar.range(1, 5).map(lambda x: x * 2).to_string(), + id="list_map", + ), + rx.text( + ArrayVar.range(1, 5).foreach(lambda x: x * 2).to_string(), + id="list_foreach_alias", + ), + rx.text( + LiteralVar + .create([0, 1, "", "hello", [], [1], {}, {"key": 1}]) + .filter() + .to_string(), + id="list_filter_truthiness", + ), + rx.text( + ArrayVar.range(1, 6).filter(lambda x: x % 2 == 0).to_string(), + id="list_filter_predicate", + ), + rx.text( + ArrayVar.range(1, 6).filter(lambda x: x % 3).to_string(), + id="list_filter_nonbool_predicate", + ), + rx.text( + ArrayVar.range(1, 5).reduce(lambda acc, x: acc + x), # noqa: FURB118 + id="list_reduce", + ), + rx.text( + ArrayVar.range(1, 5).reduce( + lambda acc, x: acc + x, # noqa: FURB118 + 100, + ), + id="list_reduce_initial", + ), + rx.text( + LiteralVar.create(["a", "b", "c"]).reduce( + lambda acc, x: acc + x # noqa: FURB118 + ), + id="list_reduce_str", + ), + rx.text( + LiteralVar + .create([[1, 2], [3], [4, 5]]) + .flat_map(lambda x: x) + .to_string(), + id="list_flat_map", + ), + rx.text( + LiteralVar.create(["ab", "cd"]).flat_map(lambda x: x).to_string(), + id="list_flat_map_str", + ), + rx.text( + ArrayVar.range(1, 4).flat_map(lambda x: [x, x * 10]).to_string(), + id="list_flat_map_fn", + ), + rx.text( + LiteralVar + .create([{"a": 1}, {"b": 2}]) + .flat_map(lambda x: x) + .to_string(), + id="list_flat_map_dict", + ), rx.box( # Test that foreach works with various non-array inputs without throwing rx.foreach( @@ -1003,6 +1065,18 @@ def test_var_operations(driver, var_operations: AppHarness): ("list_join_range2", "2,4,6,8"), ("list_join_range3", "5,4,3,2,1"), ("list_join_range4", "0,1,2"), + ("list_map", "[2,4,6,8]"), + ("list_foreach_alias", "[2,4,6,8]"), + ("list_filter_truthiness", '[1,"hello",[1],{"key":1}]'), + ("list_filter_predicate", "[2,4]"), + ("list_filter_nonbool_predicate", "[1,2,4,5]"), + ("list_reduce", "10"), + ("list_reduce_initial", "110"), + ("list_reduce_str", "abc"), + ("list_flat_map", "[1,2,3,4,5]"), + ("list_flat_map_str", '["a","b","c","d"]'), + ("list_flat_map_fn", "[1,10,2,20,3,30]"), + ("list_flat_map_dict", '["a","b"]'), # list, int ("list_mult_int", "[1,2,1,2,1,2,1,2,1,2]"), ("list_or_int", "[1,2]"), diff --git a/tests/units/test_var.py b/tests/units/test_var.py index be097011729..c7a64902084 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -1,6 +1,7 @@ import decimal import json import math +import re import typing from collections.abc import Mapping, Sequence from typing import cast @@ -8,12 +9,17 @@ import pytest from pandas import DataFrame from pytest_mock import MockerFixture -from reflex_base.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG +from reflex_base.constants.base import ( + REFLEX_VAR_CLOSING_TAG, + REFLEX_VAR_OPENING_TAG, + Dirs, +) from reflex_base.constants.state import FIELD_MARKER from reflex_base.utils.exceptions import ( PrimitiveUnserializableToJSONError, ReflexError, UntypedComputedVarError, + VarTypeError, ) from reflex_base.utils.imports import ImportVar from reflex_base.utils.types import get_default_value_for_type @@ -1117,6 +1123,125 @@ def test_array_operations(): ) +def _assert_var_imports(var: Var, tag: str): + """Assert that the var's VarData includes an import of tag from the state module. + + Args: + var: The var to check. + tag: The expected import tag. + """ + var_data = var._get_all_var_data() + assert var_data is not None + assert any( + import_var.tag == tag + for import_var in dict(var_data.imports).get(f"$/{Dirs.STATE_PATH}", ()) + ) + + +def test_array_map(mocker: MockerFixture): + array_var = LiteralArrayVar.create([1, 2, 3]) + + mapped = array_var.map(lambda x: x * 2) + assert re.fullmatch( + r"\[1, 2, 3\]\.map\(\(\((\w+)\) => \(\1 \* 2\)\)\)", str(mapped) + ) + assert mapped._var_type == list[typing.Any] + + # 0-argument functions are supported + assert str(array_var.map(lambda: 42)) == "[1, 2, 3].map((() => 42))" + + # foreach is a deprecated alias of map + mock_deprecate = mocker.patch("reflex_base.utils.console.deprecate") + assert re.fullmatch( + r"\[1, 2, 3\]\.map\(\(\((\w+)\) => \(\1 \* 2\)\)\)", + str(array_var.foreach(lambda x: x * 2)), # pyright: ignore[reportDeprecated] + ) + mock_deprecate.assert_called_once() + assert mock_deprecate.call_args.kwargs["feature_name"] == "ArrayVar.foreach" + + with pytest.raises(VarTypeError): + array_var.map(lambda x, y: x) + with pytest.raises(VarTypeError): + array_var.map(42) + + +def test_array_filter(): + array_var = LiteralArrayVar.create([1, 2, 3]) + + # no predicate: python truthiness of the elements themselves + truthy = array_var.filter() + assert str(truthy) == "[1, 2, 3].filter(isTrue)" + assert truthy._var_type == array_var._var_type + _assert_var_imports(truthy, "isTrue") + + # boolean-returning predicates are used as-is + predicate = array_var.filter(lambda x: x > 1) + assert re.fullmatch( + r"\[1, 2, 3\]\.filter\(\(\((\w+)\) => \(\1 > 1\)\)\)", str(predicate) + ) + + # non-boolean results are evaluated with python truthiness + truthy_predicate = array_var.filter(lambda x: x % 2) + assert re.fullmatch( + r"\[1, 2, 3\]\.filter\(\(\((\w+)\) => isTrue\(\(\1 % 2\)\)\)\)", + str(truthy_predicate), + ) + _assert_var_imports(truthy_predicate, "isTrue") + + with pytest.raises(VarTypeError): + array_var.filter(lambda x, y: x) + with pytest.raises(VarTypeError): + array_var.filter(42) + + +def test_array_reduce(): + array_var = LiteralArrayVar.create([1, 2, 3]) + + summed = array_var.reduce(lambda acc, x: acc + x) # noqa: FURB118 + assert re.fullmatch( + r"\[1, 2, 3\]\.reduce\(\(\((\w+), (\w+)\) => \(\1 \+ \2\)\)\)", str(summed) + ) + assert summed._var_type is int + assert isinstance(summed, NumberVar) + + with_initial = array_var.reduce(lambda acc, x: acc + x, 10) # noqa: FURB118 + assert re.fullmatch( + r"\[1, 2, 3\]\.reduce\(\(\((\w+), (\w+)\) => \(\1 \+ \2\)\), 10\)", + str(with_initial), + ) + assert with_initial._var_type is int + + # None is a valid initial value, distinct from no initial + with_none_initial = array_var.reduce(lambda acc, x: x, None) + assert re.fullmatch( + r"\[1, 2, 3\]\.reduce\(\(\((\w+), (\w+)\) => \2\), null\)", + str(with_none_initial), + ) + + with pytest.raises(VarTypeError): + array_var.reduce(lambda x: x) + with pytest.raises(VarTypeError): + array_var.reduce(lambda x, y, z: x) + with pytest.raises(VarTypeError): + array_var.reduce(42) + + +def test_array_flat_map(): + array_var = LiteralArrayVar.create([[1, 2], [3]]) + + flattened = array_var.flat_map(lambda x: x) + assert re.fullmatch( + r"pyFlatMap\(\[\[1, 2\], \[3\]\], \(\((\w+)\) => \1\)\)", str(flattened) + ) + assert flattened._var_type == list[typing.Any] + _assert_var_imports(flattened, "pyFlatMap") + + with pytest.raises(VarTypeError): + array_var.flat_map(lambda x, y: x) + with pytest.raises(VarTypeError): + array_var.flat_map(42) + + def test_object_operations(): object_var = LiteralObjectVar.create({"a": 1, "b": 2, "c": 3})