diff --git a/extra_tests/snippets/builtin_dict.py b/extra_tests/snippets/builtin_dict.py index abd93539a..83a8c5f99 100644 --- a/extra_tests/snippets/builtin_dict.py +++ b/extra_tests/snippets/builtin_dict.py @@ -358,3 +358,53 @@ next(it) assert it.__length_hint__() == 0 assert_raises(StopIteration, next, it) assert it.__length_hint__() == 0 + +# Test dictionary unpacking with non-mapping objects +# This should raise TypeError for non-mapping objects +with assert_raises(TypeError) as cm: + {**[1, 2]} +assert "'list' object is not a mapping" in str(cm.exception) + +with assert_raises(TypeError) as cm: + {**[[1, 2], [3, 4]]} +assert "'list' object is not a mapping" in str(cm.exception) + +with assert_raises(TypeError) as cm: + {**"string"} +assert "'str' object is not a mapping" in str(cm.exception) + +with assert_raises(TypeError) as cm: + {**(1, 2, 3)} +assert "'tuple' object is not a mapping" in str(cm.exception) + +# Test that valid mappings still work +assert {**{"a": 1}, **{"b": 2}} == {"a": 1, "b": 2} + +# Test OrderedDict unpacking preserves order +import collections + +od = collections.OrderedDict([("a", 1), ("b", 2)]) +od.move_to_end("a") # Move 'a' to end: ['b', 'a'] +expected_order = list(od.items()) # [('b', 2), ('a', 1)] + + +def test_func(**kwargs): + return kwargs + + +result = test_func(**od) +assert list(result.items()) == expected_order, ( + f"Expected {expected_order}, got {list(result.items())}" +) + +# Test multiple OrderedDict unpacking +od1 = collections.OrderedDict([("x", 10), ("y", 20)]) +od2 = collections.OrderedDict([("z", 30), ("w", 40)]) +od2.move_to_end("z") # Move 'z' to end: ['w', 'z'] + +result = test_func(**od1, **od2) +# Should preserve order: x, y, w, z +expected_keys = ["x", "y", "w", "z"] +assert list(result.keys()) == expected_keys, ( + f"Expected {expected_keys}, got {list(result.keys())}" +) diff --git a/vm/src/frame.rs b/vm/src/frame.rs index 29598c75e..3c9766bde 100644 --- a/vm/src/frame.rs +++ b/vm/src/frame.rs @@ -796,6 +796,19 @@ impl ExecutingFrame<'_> { .top_value() .downcast_ref::() .expect("exact dict expected"); + + // For dictionary unpacking {**x}, x must be a mapping + // Check if the object has the mapping protocol (keys method) + if vm + .get_method(other.clone(), vm.ctx.intern_str("keys")) + .is_none() + { + return Err(vm.new_type_error(format!( + "'{}' object is not a mapping", + other.class().name() + ))); + } + dict.merge_object(other, vm)?; Ok(None) }