From 7eef5d78c06a9d33ae827ac0a0e3582193473536 Mon Sep 17 00:00:00 2001 From: himanshu748 Date: Mon, 20 Jul 2026 12:22:32 +0530 Subject: [PATCH 1/2] Support dict unpacking in dict literals in local executor --- src/smolagents/local_python_executor.py | 16 ++++++++++++---- tests/test_local_python_executor.py | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/smolagents/local_python_executor.py b/src/smolagents/local_python_executor.py index 8e150cd5c..2c52576f7 100644 --- a/src/smolagents/local_python_executor.py +++ b/src/smolagents/local_python_executor.py @@ -1493,10 +1493,18 @@ def evaluate_ast( elif isinstance(expression, ast.FunctionDef): return evaluate_function_def(expression, *common_params) elif isinstance(expression, ast.Dict): - # Dict -> evaluate all keys and values - keys = (evaluate_ast(k, *common_params) for k in expression.keys) - values = (evaluate_ast(v, *common_params) for v in expression.values) - return dict(zip(keys, values)) + # Dict -> evaluate all keys and values; a None key marks a `**mapping` entry to merge + result = {} + for key_node, value_node in zip(expression.keys, expression.values): + if key_node is None: + value = evaluate_ast(value_node, *common_params) + if not isinstance(value, Mapping): + raise InterpreterError(f"'{type(value).__name__}' object is not a mapping") + result.update(value) + else: + key = evaluate_ast(key_node, *common_params) + result[key] = evaluate_ast(value_node, *common_params) + return result elif isinstance(expression, ast.Expr): # Expression -> evaluate the content return evaluate_ast(expression.value, *common_params) diff --git a/tests/test_local_python_executor.py b/tests/test_local_python_executor.py index aede6cd2d..5ca979584 100644 --- a/tests/test_local_python_executor.py +++ b/tests/test_local_python_executor.py @@ -263,6 +263,25 @@ def test_evaluate_dict(self): state, {"x": 3, "test_dict": {"x": 3, "y": 5}, "_operations_count": {"counter": 7}} ) + @pytest.mark.parametrize( + "code, expected", + [ + ("{**{'a': 1}, 'b': 2}", {"a": 1, "b": 2}), + ("{**{'a': 1}, **{'b': 2}}", {"a": 1, "b": 2}), + ("{**{'a': 1}, 'a': 2}", {"a": 2}), + ("{'a': 0, **{'a': 1}}", {"a": 1}), + ("base = {'x': 1}\n{**base, 'y': 2}", {"x": 1, "y": 2}), + ("{**{}}", {}), + ], + ) + def test_evaluate_dict_unpacking(self, code, expected): + result, _ = evaluate_python_code(code, {}, state={}) + assert result == expected + + def test_evaluate_dict_unpacking_non_mapping_raises(self): + with pytest.raises(InterpreterError, match="'list' object is not a mapping"): + evaluate_python_code("{**[1, 2]}", {}, state={}) + def test_evaluate_expression(self): code = "x = 3\ny = 5" state = {} From f8793c09f4143b1232f2b433c2aba24c4f83e12a Mon Sep 17 00:00:00 2001 From: himanshu748 Date: Mon, 20 Jul 2026 12:38:21 +0530 Subject: [PATCH 2/2] Accept mapping-protocol objects in dict unpacking --- src/smolagents/local_python_executor.py | 4 +++- tests/test_local_python_executor.py | 23 ++++++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/smolagents/local_python_executor.py b/src/smolagents/local_python_executor.py index 2c52576f7..4ffe19d25 100644 --- a/src/smolagents/local_python_executor.py +++ b/src/smolagents/local_python_executor.py @@ -1498,7 +1498,9 @@ def evaluate_ast( for key_node, value_node in zip(expression.keys, expression.values): if key_node is None: value = evaluate_ast(value_node, *common_params) - if not isinstance(value, Mapping): + # CPython gates `{**obj}` on the mapping protocol, not the Mapping ABC: + # any object with `keys()` is accepted, pair iterables are not. + if not hasattr(value, "keys"): raise InterpreterError(f"'{type(value).__name__}' object is not a mapping") result.update(value) else: diff --git a/tests/test_local_python_executor.py b/tests/test_local_python_executor.py index 5ca979584..93483bbad 100644 --- a/tests/test_local_python_executor.py +++ b/tests/test_local_python_executor.py @@ -278,9 +278,26 @@ def test_evaluate_dict_unpacking(self, code, expected): result, _ = evaluate_python_code(code, {}, state={}) assert result == expected - def test_evaluate_dict_unpacking_non_mapping_raises(self): - with pytest.raises(InterpreterError, match="'list' object is not a mapping"): - evaluate_python_code("{**[1, 2]}", {}, state={}) + def test_evaluate_dict_unpacking_accepts_mapping_protocol_objects(self): + code = dedent( + """ + class MyMap: + def keys(self): + return ["a"] + + def __getitem__(self, key): + return 1 + + {**MyMap(), "b": 2} + """ + ) + result, _ = evaluate_python_code(code, {}, state={}) + assert result == {"a": 1, "b": 2} + + @pytest.mark.parametrize("code", ["{**[1, 2]}", "{**[('a', 1)]}"]) + def test_evaluate_dict_unpacking_non_mapping_raises(self, code): + with pytest.raises(InterpreterError, match="object is not a mapping"): + evaluate_python_code(code, {}, state={}) def test_evaluate_expression(self): code = "x = 3\ny = 5"