Skip to content

Commit e9f99cc

Browse files
committed
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
1 parent 0f93210 commit e9f99cc

5 files changed

Lines changed: 43 additions & 24 deletions

File tree

‎crates/stdlib/src/json.rs‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ mod _json {
55
use crate::vm::{
66
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine,
77
builtins::{
8-
PyBaseExceptionRef, PyDict, PyFloat, PyInt, PyList, PyStr, PyStrRef, PyTuple, PyType,
8+
PyAnyDict, PyBaseExceptionRef, PyDict, PyFloat, PyInt, PyList, PyStr, PyStrRef,
9+
PyTuple, PyType,
910
},
1011
convert::ToPyResult,
1112
function::{IntoFuncArgs, OptionalArg},
@@ -1126,18 +1127,20 @@ mod _json {
11261127
Ok(())
11271128
}
11281129

1129-
/// Fetch `dct.items()` for encoding. For an exact `dict` this reads
1130+
/// Fetch `dct.items()` for encoding. For an exact `dict` or `frozendict` this reads
11301131
/// the native table directly (matching CPython's fast path); for a
1131-
/// `dict` subclass (e.g. `collections.OrderedDict`) it calls the
1132+
/// subclass (e.g. `collections.OrderedDict`) it calls the
11321133
/// real (possibly overridden) `.items()` method instead, so a
11331134
/// subclass's custom ordering/behavior is respected.
11341135
fn dict_items(
11351136
&self,
11361137
obj: &PyObject,
1137-
dict: &Py<PyDict>,
1138+
dict: &PyAnyDict,
11381139
vm: &VirtualMachine,
11391140
) -> PyResult<Vec<(PyObjectRef, PyObjectRef)>> {
1140-
if obj.class().is(vm.ctx.types.dict_type) {
1141+
if obj.class().is(vm.ctx.types.dict_type)
1142+
|| obj.class().is(vm.ctx.types.frozendict_type)
1143+
{
11411144
return Ok(dict.items_vec());
11421145
}
11431146
let items_obj = obj.get_attr("items", vm)?.call((), vm)?;
@@ -1155,7 +1158,7 @@ mod _json {
11551158
fn encode_dict(
11561159
&self,
11571160
obj: &PyObject,
1158-
dict: &Py<PyDict>,
1161+
dict: &PyAnyDict,
11591162
level: isize,
11601163
out: &mut Wtf8Buf,
11611164
vm: &VirtualMachine,
@@ -1289,7 +1292,7 @@ mod _json {
12891292
} else if let Some(tuple) = obj.downcast_ref::<PyTuple>() {
12901293
let slice = tuple.as_slice();
12911294
self.encode_list(obj, level, out, vm, || slice.len(), |i| slice[i].clone())
1292-
} else if let Some(dict) = obj.downcast_ref::<PyDict>() {
1295+
} else if let Some(dict) = PyAnyDict::from_object(obj) {
12931296
self.encode_dict(obj, dict, level, out, vm)
12941297
} else {
12951298
self.encode_default(obj, level, out, vm)

‎crates/vm/src/builtins/mappingproxy.rs‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ use super::{PyDict, PyDictRef, PyGenericAlias, PyList, PyTuple, PyType, PyTypeRe
22
use crate::{
33
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
44
atomic_func,
5-
class::PyClassImpl,
5+
class::{PyClassImpl, StaticType},
66
common::{hash, lock::LazyLock},
77
convert::ToPyObject,
88
function::{ArgMapping, Either, OptionalArg, PyComparisonValue},
99
object::{Traverse, TraverseFn},
1010
protocol::{PyMappingMethods, PyNumberMethods, PySequenceMethods},
11+
stdlib::PyOrderedDict,
1112
types::{
1213
AsMapping, AsNumber, AsSequence, Constructor, Hashable, Iterable, PyComparisonOp,
1314
Representable,
@@ -202,8 +203,24 @@ impl Py<PyMappingProxy> {
202203
if !matches!(op, PyComparisonOp::Eq | PyComparisonOp::Ne) {
203204
return Ok(Either::B(PyComparisonValue::NotImplemented));
204205
}
205-
let obj = zelf.to_object(vm)?;
206-
// Equality comparisons preserve the underlying mapping's result.
206+
let mut obj = zelf.to_object(vm)?;
207+
// Unknown comparison methods must not receive a mutable dictionary
208+
// protected by the proxy. Class namespaces are already copied by
209+
// to_object; other exact dicts need the same protection here.
210+
if matches!(&zelf.mapping, MappingProxyInner::Mapping(_))
211+
&& obj.class().is(vm.ctx.types.dict_type)
212+
&& !(other.class().is(vm.ctx.types.dict_type)
213+
|| other.class().is(vm.ctx.types.frozendict_type)
214+
|| other.class().is(vm.ctx.types.mappingproxy_type)
215+
|| (other.downcastable::<PyOrderedDict>()
216+
&& other.class().is(PyOrderedDict::static_type())))
217+
{
218+
obj = obj
219+
.downcast_ref::<PyDict>()
220+
.unwrap()
221+
.copy()
222+
.into_pyobject(vm);
223+
}
207224
obj.rich_compare(other.to_owned(), op, vm).map(Either::A)
208225
}
209226

‎crates/vm/src/builtins/str.rs‎

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use super::{
2-
PositionIterInternal, PyBytesRef, PyDict, PyList, PyTuple, PyTupleRef, PyType, PyTypeRef,
2+
PositionIterInternal, PyAnyDict, PyBytesRef, PyDict, PyList, PyTuple, PyTupleRef, PyType,
3+
PyTypeRef,
34
int::PyInt,
45
iter::{IterStatus, builtins_iter},
56
};
@@ -1580,17 +1581,15 @@ impl Py<PyStr> {
15801581
)),
15811582
}
15821583
} else {
1583-
// dict_str must be a dict
1584-
match dict_or_str.downcast::<PyDict>() {
1585-
Ok(dict) => {
1584+
// The single-argument form accepts exact dict and frozendict objects.
1585+
match PyAnyDict::from_object(&dict_or_str).filter(|_| {
1586+
dict_or_str.class().is(vm.ctx.types.dict_type)
1587+
|| dict_or_str.class().is(vm.ctx.types.frozendict_type)
1588+
}) {
1589+
Some(dict) => {
15861590
for (key, val) in dict {
1587-
// FIXME: ints are key-compatible
1588-
if let Some(num) = key.downcast_ref::<PyInt>() {
1589-
new_dict.set_item(
1590-
&*num.as_bigint().to_i32().to_pyobject(vm),
1591-
val,
1592-
vm,
1593-
)?;
1591+
if key.downcast_ref::<PyInt>().is_some() {
1592+
new_dict.set_item(&*key, val, vm)?;
15941593
} else if let Some(string) = key.downcast_ref::<PyStr>() {
15951594
if string.len() == 1 {
15961595
let num_value =
@@ -1609,7 +1608,7 @@ impl Py<PyStr> {
16091608
}
16101609
Ok(new_dict.to_pyobject(vm))
16111610
}
1612-
_ => Err(vm.new_value_error(
1611+
_ => Err(vm.new_type_error(
16131612
"if you give only one argument to maketrans it must be a dict",
16141613
)),
16151614
}

‎crates/vm/src/stdlib/_collections/ordered_dict.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ pub(crate) mod ordered_dict {
138138
traverse = "manual"
139139
)]
140140
#[derive(Debug)]
141-
struct PyOrderedDict {
141+
pub(crate) struct PyOrderedDict {
142142
dict: PyDict,
143143
links: PyMutex<ODictLinks>,
144144
}

‎crates/vm/src/stdlib/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ mod _abc;
33
pub(crate) mod _ast;
44
mod _codecs;
55
mod _collections;
6-
pub(crate) use _collections::ordered_dict::ordered_dict::PyOrderedDictItems;
6+
pub(crate) use _collections::ordered_dict::ordered_dict::{PyOrderedDict, PyOrderedDictItems};
77
mod _functools;
88
mod _imp;
99
pub mod _io;

0 commit comments

Comments
 (0)