Make mappingproxy's nb_or slot symmetric for dict | mp (#7723)

* Make mappingproxy's nb_or slot symmetric for dict | mp

CPython's mappingproxy_or (Objects/descrobject.c) unwraps a mappingproxy
on either side of '|' to its underlying mapping and delegates back to
PyNumber_Or. This makes 'dict | mp', 'mp | dict', and 'mp | mp' all
produce a plain dict result without dict's own nb_or having to know
about mappingproxy.

RustPython's 'or' slot in 'AsNumber for PyMappingProxy' only handled
the case where the left operand is a mappingproxy. When the slot fired
with a=dict, b=mp (because dict.nb_or returned NotImplemented), it
returned NotImplemented again — so 'dict | mp' raised TypeError. This
propagated to 'UserDict | mappingproxy' (which calls self.data | other).

Rewrite the slot to unwrap a mappingproxy on either side (or both)
before delegating to vm._or on the underlying mappings. The
inplace_or slot is unchanged — '|=' is still rejected.

Unmasks test_userdict.UserDictTest.test_mixed_or.

* Unmask test_types.MappingProxyTests.test_union

The slot fix in this PR also enables this test, which was marked
expectedFailure for the same dict|mappingproxy TypeError. Caught as
UNEXPECTED SUCCESS in CI.
This commit is contained in:
Changjoon authored and GitHub committed 2026-04-29 18:16:27 +09:00
1 parent 8605702980
commit e7252b2c4e
3 files changed
+13 -7

No files matched your search

-1
View File
@@ -1362,7 +1362,6 @@ class MappingProxyTests(unittest.TestCase):
self.assertEqual(view['key1'], 70)
self.assertEqual(copy['key1'], 27)
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: unsupported operand type(s) for |: 'dict' and 'mappingproxy'
def test_union(self):
mapping = {'a': 0, 'b': 1, 'c': 2}
view = self.mappingproxy(mapping)
-1
View File
@@ -244,7 +244,6 @@ class UserDictTest(mapping_tests.TestHashMappingProtocol):
test_repr_deep = mapping_tests.TestHashMappingProtocol.test_repr_deep
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: unsupported operand type(s) for |: 'UserDict' and 'mappingproxy'
def test_mixed_or(self):
for t in UserDict, dict, types.MappingProxyType:
with self.subTest(t.__name__):
+13 -5
View File
@@ -258,11 +258,19 @@ impl AsNumber for PyMappingProxy {
fn as_number() -> &'static PyNumberMethods {
static AS_NUMBER: PyNumberMethods = PyNumberMethods {
or: Some(|a, b, vm| {
if let Some(a) = a.downcast_ref::<PyMappingProxy>() {
a.__or__(b.to_pyobject(vm), vm)
} else {
Ok(vm.ctx.not_implemented())
}
// Mirror CPython's mappingproxy_or: when either side is a
// mappingproxy, unwrap to its underlying mapping and delegate
// to PyNumber_Or so `dict | mp`, `mp | dict`, and `mp | mp`
// all produce a `dict` result.
let a_obj = match a.downcast_ref::<PyMappingProxy>() {
Some(mp) => mp.copy(vm)?,
None => a.to_pyobject(vm),
};
let b_obj = match b.downcast_ref::<PyMappingProxy>() {
Some(mp) => mp.copy(vm)?,
None => b.to_pyobject(vm),
};
vm._or(a_obj.as_ref(), b_obj.as_ref())
}),
inplace_or: Some(|a, b, vm| {
if let Some(a) = a.downcast_ref::<PyMappingProxy>() {