Skip to content

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

Merged
youknowone merged 2 commits intoRustPython:mainfrom
changjoon-park:fix-mappingproxy-or-slot
Apr 29, 2026
Merged

Make mappingproxy's nb_or slot symmetric for dict | mp#7723
youknowone merged 2 commits intoRustPython:mainfrom
changjoon-park:fix-mappingproxy-or-slot

Conversation

@changjoon-park
Copy link
Copy Markdown
Contributor

@changjoon-park changjoon-park commented Apr 29, 2026

Background

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).

Repro

import types
from collections import UserDict
mp = types.MappingProxyType({1: 'c'})

{0: 'a'} | mp
# CPython 3.14: {0: 'a', 1: 'c'}
# RustPython:   TypeError: unsupported operand type(s) for |: 'dict' and 'mappingproxy'  (before this PR)

UserDict({0: 'a'}) | mp
# CPython 3.14: UserDict({0: 'a', 1: 'c'})
# RustPython:   TypeError: unsupported operand type(s) for |: 'UserDict' and 'mappingproxy'  (before this PR)

Fix

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

Tests unmasked

  • test_userdict.UserDictTest.test_mixed_or

Verification

  • CPython 3.14.4 byte-identical for all 6 mixed cases (dict | mp, mp | dict, mp | mp, UserDict | mp, mp | UserDict, plus subclass propagation: UserDict | Sub, Sub | UserDict, Sub | Sub2)
  • mp |= dict still raises TypeError (inplace_or slot unchanged)
  • mp | list and list | mp still raise TypeError (non-mapping operands)
  • Class-dict mappingproxy (C.__dict__ | dict) works for the other MappingProxyInner variant too
  • No regressions across 9 modules (~2,147 tests): test_userdict, test_dict, test_collections, test_descr, test_class, test_typing, test_userlist, test_ordered_dict, test_set

Summary by CodeRabbit

Bug Fixes

  • Fixed the | (or) operator behavior with mapping proxies. Operations combining dictionaries and mapping proxies (dict | proxy, proxy | dict, proxy | proxy) now consistently return dictionaries as expected.

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.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • Lib/test/test_types.py is excluded by !Lib/**

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: e4f7077d-be10-4d4c-a4dd-b1a086e605b1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The | ("or") operator for PyMappingProxy was updated to mirror CPython's behavior. Instead of only supporting PyMappingProxy as the left operand, the implementation now unwraps underlying mappings when either operand is a PyMappingProxy and delegates to the VM's generic _or method for consistent results.

Changes

Cohort / File(s) Summary
MappingProxy OR Operator
crates/vm/src/builtins/mappingproxy.rs
Updated the | operator to unwrap/copy underlying mappings when either operand is a PyMappingProxy, delegating to vm._or to consistently resolve dict | mp, mp | dict, and mp | mp operations to a dict result.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 Hop, hop, the mappings now align,
Both left and right, they now combine,
With Python's way, we've set things straight,
PyMappingProxy now delegates—so great!
The | now flows both ways with grace,

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main change: making mappingproxy's nb_or slot symmetric to handle dict | mp cases, which mirrors the PR's objective of fixing asymmetric behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 29, 2026

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

(module 'types test_userdict' not found)

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/vm/src/builtins/mappingproxy.rs (1)

265-272: Extract duplicated mappingproxy-unwrapping logic into one helper.

a_obj and b_obj use identical match logic. Please extract once and call it for both operands to keep this slot easier to maintain.

♻️ Suggested refactor
             or: Some(|a, b, vm| {
                 // 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),
-                };
+                let unwrap_mappingproxy = |obj: &PyObject| -> PyResult<PyObjectRef> {
+                    match obj.downcast_ref::<PyMappingProxy>() {
+                        Some(mp) => mp.copy(vm),
+                        None => Ok(obj.to_pyobject(vm)),
+                    }
+                };
+                let a_obj = unwrap_mappingproxy(a)?;
+                let b_obj = unwrap_mappingproxy(b)?;
                 vm._or(a_obj.as_ref(), b_obj.as_ref())
             }),

As per coding guidelines, when branches differ only in a value but share common logic, extract the differing value first, then call the common logic once to avoid duplicate code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/builtins/mappingproxy.rs` around lines 265 - 272, Extract the
duplicated match into a small helper (e.g., unwrap_mappingproxy_or_convert) that
takes the operand (the value currently named `a` or `b`) and `vm` and returns
the same result type as the current branches, using
`downcast_ref::<PyMappingProxy>()` to call `mp.copy(vm)?` or falling back to
`to_pyobject(vm)`; then call that helper for both `a` and `b` to produce `a_obj`
and `b_obj`, preserving the `?` error propagation and return types so all
existing callers (`PyMappingProxy`, `copy`, `to_pyobject`) behave identically.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/vm/src/builtins/mappingproxy.rs`:
- Around line 265-272: Extract the duplicated match into a small helper (e.g.,
unwrap_mappingproxy_or_convert) that takes the operand (the value currently
named `a` or `b`) and `vm` and returns the same result type as the current
branches, using `downcast_ref::<PyMappingProxy>()` to call `mp.copy(vm)?` or
falling back to `to_pyobject(vm)`; then call that helper for both `a` and `b` to
produce `a_obj` and `b_obj`, preserving the `?` error propagation and return
types so all existing callers (`PyMappingProxy`, `copy`, `to_pyobject`) behave
identically.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 40f31df5-08cf-4a0d-a3f8-9e4dfe5cfeb0

📥 Commits

Reviewing files that changed from the base of the PR and between 3f718f9 and e29736a.

⛔ Files ignored due to path filters (1)
  • Lib/test/test_userdict.py is excluded by !Lib/**
📒 Files selected for processing (1)
  • crates/vm/src/builtins/mappingproxy.rs

@ShaharNaveh
Copy link
Copy Markdown
Contributor

ShaharNaveh commented Apr 29, 2026

UNEXPECTED SUCCESS: test_union (test.test_types.MappingProxyTests.test_union)

CI failing because of that, can you please remove that test patch?

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.
@changjoon-park
Copy link
Copy Markdown
Contributor Author

Done — removed the decorator on test_types.MappingProxyTests.test_union in 45fbe2c. test_types now runs with 0 unexpected successes.

@changjoon-park
Copy link
Copy Markdown
Contributor Author

UNEXPECTED SUCCESS: test_union (test.test_types.MappingProxyTests.test_union)

CI failing because of that, can you please remove that test patch?

my bad thanks for catching ! updated with correct test patch :)

@youknowone
Copy link
Copy Markdown
Member

my bad thanks for catching ! updated with correct test patch :)

Don't worry. Nobody expects you run entire test on local machine. This is perfectly common workflow.

@youknowone youknowone merged commit 3ebcab7 into RustPython:main Apr 29, 2026
38 of 39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants