From e9f99cc3888aa174f6989ff41e3221ac3010be6b Mon Sep 17 00:00:00 2001 From: youknowdot Date: Thu, 15 Oct 2026 03:38:20 +0900 Subject: [PATCH] Align mapping boundaries with Python 3.15 final Support frozen mappings in str.maketrans and native JSON encoding, preserve integer translation keys, and protect mappingproxy dictionaries from unknown reflected comparisons. Assisted-by: OpenAI Codex:model version unavailable --- crates/stdlib/src/json.rs | 17 ++++++++------ crates/vm/src/builtins/mappingproxy.rs | 23 ++++++++++++++++--- crates/vm/src/builtins/str.rs | 23 +++++++++---------- .../src/stdlib/_collections/ordered_dict.rs | 2 +- crates/vm/src/stdlib/mod.rs | 2 +- 5 files changed, 43 insertions(+), 24 deletions(-) diff --git a/crates/stdlib/src/json.rs b/crates/stdlib/src/json.rs index 1716d673f15..58461f90c26 100644 --- a/crates/stdlib/src/json.rs +++ b/crates/stdlib/src/json.rs @@ -5,7 +5,8 @@ mod _json { use crate::vm::{ AsObject, Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine, builtins::{ - PyBaseExceptionRef, PyDict, PyFloat, PyInt, PyList, PyStr, PyStrRef, PyTuple, PyType, + PyAnyDict, PyBaseExceptionRef, PyDict, PyFloat, PyInt, PyList, PyStr, PyStrRef, + PyTuple, PyType, }, convert::ToPyResult, function::{IntoFuncArgs, OptionalArg}, @@ -1126,18 +1127,20 @@ mod _json { Ok(()) } - /// Fetch `dct.items()` for encoding. For an exact `dict` this reads + /// Fetch `dct.items()` for encoding. For an exact `dict` or `frozendict` this reads /// the native table directly (matching CPython's fast path); for a - /// `dict` subclass (e.g. `collections.OrderedDict`) it calls the + /// subclass (e.g. `collections.OrderedDict`) it calls the /// real (possibly overridden) `.items()` method instead, so a /// subclass's custom ordering/behavior is respected. fn dict_items( &self, obj: &PyObject, - dict: &Py, + dict: &PyAnyDict, vm: &VirtualMachine, ) -> PyResult> { - if obj.class().is(vm.ctx.types.dict_type) { + if obj.class().is(vm.ctx.types.dict_type) + || obj.class().is(vm.ctx.types.frozendict_type) + { return Ok(dict.items_vec()); } let items_obj = obj.get_attr("items", vm)?.call((), vm)?; @@ -1155,7 +1158,7 @@ mod _json { fn encode_dict( &self, obj: &PyObject, - dict: &Py, + dict: &PyAnyDict, level: isize, out: &mut Wtf8Buf, vm: &VirtualMachine, @@ -1289,7 +1292,7 @@ mod _json { } else if let Some(tuple) = obj.downcast_ref::() { let slice = tuple.as_slice(); self.encode_list(obj, level, out, vm, || slice.len(), |i| slice[i].clone()) - } else if let Some(dict) = obj.downcast_ref::() { + } else if let Some(dict) = PyAnyDict::from_object(obj) { self.encode_dict(obj, dict, level, out, vm) } else { self.encode_default(obj, level, out, vm) diff --git a/crates/vm/src/builtins/mappingproxy.rs b/crates/vm/src/builtins/mappingproxy.rs index e1e0283d189..168bdebed5e 100644 --- a/crates/vm/src/builtins/mappingproxy.rs +++ b/crates/vm/src/builtins/mappingproxy.rs @@ -2,12 +2,13 @@ use super::{PyDict, PyDictRef, PyGenericAlias, PyList, PyTuple, PyType, PyTypeRe use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, atomic_func, - class::PyClassImpl, + class::{PyClassImpl, StaticType}, common::{hash, lock::LazyLock}, convert::ToPyObject, function::{ArgMapping, Either, OptionalArg, PyComparisonValue}, object::{Traverse, TraverseFn}, protocol::{PyMappingMethods, PyNumberMethods, PySequenceMethods}, + stdlib::PyOrderedDict, types::{ AsMapping, AsNumber, AsSequence, Constructor, Hashable, Iterable, PyComparisonOp, Representable, @@ -202,8 +203,24 @@ impl Py { if !matches!(op, PyComparisonOp::Eq | PyComparisonOp::Ne) { return Ok(Either::B(PyComparisonValue::NotImplemented)); } - let obj = zelf.to_object(vm)?; - // Equality comparisons preserve the underlying mapping's result. + let mut obj = zelf.to_object(vm)?; + // Unknown comparison methods must not receive a mutable dictionary + // protected by the proxy. Class namespaces are already copied by + // to_object; other exact dicts need the same protection here. + if matches!(&zelf.mapping, MappingProxyInner::Mapping(_)) + && obj.class().is(vm.ctx.types.dict_type) + && !(other.class().is(vm.ctx.types.dict_type) + || other.class().is(vm.ctx.types.frozendict_type) + || other.class().is(vm.ctx.types.mappingproxy_type) + || (other.downcastable::() + && other.class().is(PyOrderedDict::static_type()))) + { + obj = obj + .downcast_ref::() + .unwrap() + .copy() + .into_pyobject(vm); + } obj.rich_compare(other.to_owned(), op, vm).map(Either::A) } diff --git a/crates/vm/src/builtins/str.rs b/crates/vm/src/builtins/str.rs index 5e3c10f77a0..27a6059d283 100644 --- a/crates/vm/src/builtins/str.rs +++ b/crates/vm/src/builtins/str.rs @@ -1,5 +1,6 @@ use super::{ - PositionIterInternal, PyBytesRef, PyDict, PyList, PyTuple, PyTupleRef, PyType, PyTypeRef, + PositionIterInternal, PyAnyDict, PyBytesRef, PyDict, PyList, PyTuple, PyTupleRef, PyType, + PyTypeRef, int::PyInt, iter::{IterStatus, builtins_iter}, }; @@ -1580,17 +1581,15 @@ impl Py { )), } } else { - // dict_str must be a dict - match dict_or_str.downcast::() { - Ok(dict) => { + // The single-argument form accepts exact dict and frozendict objects. + match PyAnyDict::from_object(&dict_or_str).filter(|_| { + dict_or_str.class().is(vm.ctx.types.dict_type) + || dict_or_str.class().is(vm.ctx.types.frozendict_type) + }) { + Some(dict) => { for (key, val) in dict { - // FIXME: ints are key-compatible - if let Some(num) = key.downcast_ref::() { - new_dict.set_item( - &*num.as_bigint().to_i32().to_pyobject(vm), - val, - vm, - )?; + if key.downcast_ref::().is_some() { + new_dict.set_item(&*key, val, vm)?; } else if let Some(string) = key.downcast_ref::() { if string.len() == 1 { let num_value = @@ -1609,7 +1608,7 @@ impl Py { } Ok(new_dict.to_pyobject(vm)) } - _ => Err(vm.new_value_error( + _ => Err(vm.new_type_error( "if you give only one argument to maketrans it must be a dict", )), } diff --git a/crates/vm/src/stdlib/_collections/ordered_dict.rs b/crates/vm/src/stdlib/_collections/ordered_dict.rs index 8c3079c3bb1..568658b846a 100644 --- a/crates/vm/src/stdlib/_collections/ordered_dict.rs +++ b/crates/vm/src/stdlib/_collections/ordered_dict.rs @@ -138,7 +138,7 @@ pub(crate) mod ordered_dict { traverse = "manual" )] #[derive(Debug)] - struct PyOrderedDict { + pub(crate) struct PyOrderedDict { dict: PyDict, links: PyMutex, } diff --git a/crates/vm/src/stdlib/mod.rs b/crates/vm/src/stdlib/mod.rs index 73b816c579c..c89ea3101c2 100644 --- a/crates/vm/src/stdlib/mod.rs +++ b/crates/vm/src/stdlib/mod.rs @@ -3,7 +3,7 @@ mod _abc; pub(crate) mod _ast; mod _codecs; mod _collections; -pub(crate) use _collections::ordered_dict::ordered_dict::PyOrderedDictItems; +pub(crate) use _collections::ordered_dict::ordered_dict::{PyOrderedDict, PyOrderedDictItems}; mod _functools; mod _imp; pub mod _io;