Skip to content

Commit efc6b5d

Browse files
authored
Merge pull request #5809 from youknowone/fix-dict-unpack
Fix dict unpack
2 parents f0e7427 + d973e6d commit efc6b5d

File tree

4 files changed

+105
-16
lines changed

4 files changed

+105
-16
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Run `./whats_left.py` to get a list of unimplemented methods, which is helpful w
102102

103103
### Python Code
104104

105+
- **IMPORTANT**: In most cases, Python code should not be edited. Bug fixes should be made through Rust code modifications only
105106
- Follow PEP 8 style for custom Python code
106107
- Use ruff for linting Python code
107108
- Minimize modifications to CPython standard library files

Lib/test/test_call.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313

1414
class FunctionCalls(unittest.TestCase):
1515

16-
# TODO: RUSTPYTHON
17-
@unittest.expectedFailure
1816
def test_kwargs_order(self):
1917
# bpo-34320: **kwargs should preserve order of passed OrderedDict
2018
od = collections.OrderedDict([('a', 1), ('b', 2)])

extra_tests/snippets/builtin_dict.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,3 +358,53 @@ def __eq__(self, other):
358358
assert it.__length_hint__() == 0
359359
assert_raises(StopIteration, next, it)
360360
assert it.__length_hint__() == 0
361+
362+
# Test dictionary unpacking with non-mapping objects
363+
# This should raise TypeError for non-mapping objects
364+
with assert_raises(TypeError) as cm:
365+
{**[1, 2]}
366+
assert "'list' object is not a mapping" in str(cm.exception)
367+
368+
with assert_raises(TypeError) as cm:
369+
{**[[1, 2], [3, 4]]}
370+
assert "'list' object is not a mapping" in str(cm.exception)
371+
372+
with assert_raises(TypeError) as cm:
373+
{**"string"}
374+
assert "'str' object is not a mapping" in str(cm.exception)
375+
376+
with assert_raises(TypeError) as cm:
377+
{**(1, 2, 3)}
378+
assert "'tuple' object is not a mapping" in str(cm.exception)
379+
380+
# Test that valid mappings still work
381+
assert {**{"a": 1}, **{"b": 2}} == {"a": 1, "b": 2}
382+
383+
# Test OrderedDict unpacking preserves order
384+
import collections
385+
386+
od = collections.OrderedDict([("a", 1), ("b", 2)])
387+
od.move_to_end("a") # Move 'a' to end: ['b', 'a']
388+
expected_order = list(od.items()) # [('b', 2), ('a', 1)]
389+
390+
391+
def test_func(**kwargs):
392+
return kwargs
393+
394+
395+
result = test_func(**od)
396+
assert list(result.items()) == expected_order, (
397+
f"Expected {expected_order}, got {list(result.items())}"
398+
)
399+
400+
# Test multiple OrderedDict unpacking
401+
od1 = collections.OrderedDict([("x", 10), ("y", 20)])
402+
od2 = collections.OrderedDict([("z", 30), ("w", 40)])
403+
od2.move_to_end("z") # Move 'z' to end: ['w', 'z']
404+
405+
result = test_func(**od1, **od2)
406+
# Should preserve order: x, y, w, z
407+
expected_keys = ["x", "y", "w", "z"]
408+
assert list(result.keys()) == expected_keys, (
409+
f"Expected {expected_keys}, got {list(result.keys())}"
410+
)

vm/src/frame.rs

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,19 @@ impl ExecutingFrame<'_> {
796796
.top_value()
797797
.downcast_ref::<PyDict>()
798798
.expect("exact dict expected");
799+
800+
// For dictionary unpacking {**x}, x must be a mapping
801+
// Check if the object has the mapping protocol (keys method)
802+
if vm
803+
.get_method(other.clone(), vm.ctx.intern_str("keys"))
804+
.is_none()
805+
{
806+
return Err(vm.new_type_error(format!(
807+
"'{}' object is not a mapping",
808+
other.class().name()
809+
)));
810+
}
811+
799812
dict.merge_object(other, vm)?;
800813
Ok(None)
801814
}
@@ -1527,11 +1540,12 @@ impl ExecutingFrame<'_> {
15271540
let size = size as usize;
15281541
let map_obj = vm.ctx.new_dict();
15291542
for obj in self.pop_multiple(size) {
1530-
// Take all key-value pairs from the dict:
1531-
let dict: PyDictRef = obj.downcast().map_err(|obj| {
1532-
vm.new_type_error(format!("'{}' object is not a mapping", obj.class().name()))
1533-
})?;
1534-
for (key, value) in dict {
1543+
// Use keys() method for all mapping objects to preserve order
1544+
Self::iterate_mapping_keys(vm, &obj, "keyword argument", |key| {
1545+
// Check for keyword argument restrictions
1546+
if key.downcast_ref::<PyStr>().is_none() {
1547+
return Err(vm.new_type_error("keywords must be strings".to_owned()));
1548+
}
15351549
if map_obj.contains_key(&*key, vm) {
15361550
let key_repr = &key.repr(vm)?;
15371551
let msg = format!(
@@ -1540,8 +1554,11 @@ impl ExecutingFrame<'_> {
15401554
);
15411555
return Err(vm.new_type_error(msg));
15421556
}
1557+
1558+
let value = obj.get_item(&*key, vm)?;
15431559
map_obj.set_item(&*key, value, vm)?;
1544-
}
1560+
Ok(())
1561+
})?;
15451562
}
15461563

15471564
self.push_value(map_obj.into());
@@ -1586,17 +1603,18 @@ impl ExecutingFrame<'_> {
15861603

15871604
fn collect_ex_args(&mut self, vm: &VirtualMachine, has_kwargs: bool) -> PyResult<FuncArgs> {
15881605
let kwargs = if has_kwargs {
1589-
let kw_dict: PyDictRef = self.pop_value().downcast().map_err(|_| {
1590-
// TODO: check collections.abc.Mapping
1591-
vm.new_type_error("Kwargs must be a dict.".to_owned())
1592-
})?;
1606+
let kw_obj = self.pop_value();
15931607
let mut kwargs = IndexMap::new();
1594-
for (key, value) in kw_dict.into_iter() {
1595-
let key = key
1608+
1609+
// Use keys() method for all mapping objects to preserve order
1610+
Self::iterate_mapping_keys(vm, &kw_obj, "argument after **", |key| {
1611+
let key_str = key
15961612
.payload_if_subclass::<PyStr>(vm)
15971613
.ok_or_else(|| vm.new_type_error("keywords must be strings".to_owned()))?;
1598-
kwargs.insert(key.as_str().to_owned(), value);
1599-
}
1614+
let value = kw_obj.get_item(&*key, vm)?;
1615+
kwargs.insert(key_str.as_str().to_owned(), value);
1616+
Ok(())
1617+
})?;
16001618
kwargs
16011619
} else {
16021620
IndexMap::new()
@@ -1608,6 +1626,28 @@ impl ExecutingFrame<'_> {
16081626
Ok(FuncArgs { args, kwargs })
16091627
}
16101628

1629+
/// Helper function to iterate over mapping keys using the keys() method.
1630+
/// This ensures proper order preservation for OrderedDict and other custom mappings.
1631+
fn iterate_mapping_keys<F>(
1632+
vm: &VirtualMachine,
1633+
mapping: &PyObjectRef,
1634+
error_prefix: &str,
1635+
mut key_handler: F,
1636+
) -> PyResult<()>
1637+
where
1638+
F: FnMut(PyObjectRef) -> PyResult<()>,
1639+
{
1640+
let Some(keys_method) = vm.get_method(mapping.clone(), vm.ctx.intern_str("keys")) else {
1641+
return Err(vm.new_type_error(format!("{} must be a mapping", error_prefix)));
1642+
};
1643+
1644+
let keys = keys_method?.call((), vm)?.get_iter(vm)?;
1645+
while let PyIterReturn::Return(key) = keys.next(vm)? {
1646+
key_handler(key)?;
1647+
}
1648+
Ok(())
1649+
}
1650+
16111651
#[inline]
16121652
fn execute_call(&mut self, args: FuncArgs, vm: &VirtualMachine) -> FrameResult {
16131653
let func_ref = self.pop_value();

0 commit comments

Comments
 (0)