From e7252b2c4ef5086396d71c84141286496d5f1b55 Mon Sep 17 00:00:00 2001 From: Changjoon Date: Wed, 29 Apr 2026 18:16:27 +0900 Subject: [PATCH] Make mappingproxy's nb_or slot symmetric for dict | mp (#7723) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- Lib/test/test_types.py | 1 - Lib/test/test_userdict.py | 1 - crates/vm/src/builtins/mappingproxy.rs | 18 +++++++++++++----- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 1bd8efa6a..588edc376 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -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) diff --git a/Lib/test/test_userdict.py b/Lib/test/test_userdict.py index 5f996893e..13285c9b2 100644 --- a/Lib/test/test_userdict.py +++ b/Lib/test/test_userdict.py @@ -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__): diff --git a/crates/vm/src/builtins/mappingproxy.rs b/crates/vm/src/builtins/mappingproxy.rs index df9c61e0a..fe7e0e178 100644 --- a/crates/vm/src/builtins/mappingproxy.rs +++ b/crates/vm/src/builtins/mappingproxy.rs @@ -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::() { - 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::() { + Some(mp) => mp.copy(vm)?, + None => a.to_pyobject(vm), + }; + let b_obj = match b.downcast_ref::() { + 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::() {