From b2c4d761d20636890ca9bb0f37e1c22d8b8bfba1 Mon Sep 17 00:00:00 2001 From: Kevin Matlock Date: Mon, 21 Oct 2024 19:00:00 -0700 Subject: [PATCH 1/9] Mappingproxy (#1) Adds in the MappingProxy type. --- pyo3-ffi/src/descrobject.rs | 7 +- src/conversions/std/map.rs | 31 +- src/prelude.rs | 1 + src/sealed.rs | 5 +- src/types/dict.rs | 2 +- src/types/mappingproxy.rs | 629 ++++++++++++++++++++++++++++++++++++ src/types/mod.rs | 2 + 7 files changed, 667 insertions(+), 10 deletions(-) create mode 100644 src/types/mappingproxy.rs diff --git a/pyo3-ffi/src/descrobject.rs b/pyo3-ffi/src/descrobject.rs index f4a5ce00bf6..dd8e1784726 100644 --- a/pyo3-ffi/src/descrobject.rs +++ b/pyo3-ffi/src/descrobject.rs @@ -1,6 +1,6 @@ use crate::methodobject::PyMethodDef; use crate::object::{PyObject, PyTypeObject}; -use crate::Py_ssize_t; +use crate::{PyObject_TypeCheck, Py_ssize_t}; use std::os::raw::{c_char, c_int, c_void}; use std::ptr; @@ -68,6 +68,11 @@ extern "C" { pub fn PyWrapper_New(arg1: *mut PyObject, arg2: *mut PyObject) -> *mut PyObject; } +#[inline] +pub unsafe fn PyDictProxy_Check(op: *mut PyObject) -> c_int { + PyObject_TypeCheck(op, std::ptr::addr_of_mut!(PyDictProxy_Type)) +} + /// Represents the [PyMemberDef](https://docs.python.org/3/c-api/structures.html#c.PyMemberDef) /// structure. /// diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index 888bd13c180..8f219b68950 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -7,7 +7,7 @@ use crate::ToPyObject; use crate::{ conversion::IntoPyObject, instance::Bound, - types::{any::PyAnyMethods, dict::PyDictMethods, PyDict}, + types::{any::PyAnyMethods, dict::PyDictMethods, PyDict, PyMappingProxy}, FromPyObject, IntoPy, PyAny, PyErr, PyObject, Python, }; @@ -162,9 +162,19 @@ where S: hash::BuildHasher + Default, { fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { - let dict = ob.downcast::()?; - let mut ret = collections::HashMap::with_capacity_and_hasher(dict.len(), S::default()); - for (k, v) in dict { + if let Ok(dict) = ob.downcast::() { + let mut ret = collections::HashMap::with_capacity_and_hasher(dict.len(), S::default()); + for (k, v) in dict { + ret.insert(k.extract()?, v.extract()?); + } + return Ok(ret); + } + + let mappingproxy = ob.downcast::()?; + let mut ret = + collections::HashMap::with_capacity_and_hasher(mappingproxy.len()?, S::default()); + for res in mappingproxy.clone() { + let (k, v) = res?; ret.insert(k.extract()?, v.extract()?); } Ok(ret) @@ -182,9 +192,18 @@ where V: FromPyObject<'py>, { fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { - let dict = ob.downcast::()?; + if let Ok(dict) = ob.downcast::() { + let mut ret = collections::BTreeMap::new(); + for (k, v) in dict { + ret.insert(k.extract()?, v.extract()?); + } + return Ok(ret); + } + + let mappingproxy = ob.downcast::()?; let mut ret = collections::BTreeMap::new(); - for (k, v) in dict { + for res in mappingproxy.clone() { + let (k, v) = res?; ret.insert(k.extract()?, v.extract()?); } Ok(ret) diff --git a/src/prelude.rs b/src/prelude.rs index 97f3e35afa1..a6df93032f0 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -38,6 +38,7 @@ pub use crate::types::float::PyFloatMethods; pub use crate::types::frozenset::PyFrozenSetMethods; pub use crate::types::list::PyListMethods; pub use crate::types::mapping::PyMappingMethods; +pub use crate::types::mappingproxy::PyMappingProxyMethods; pub use crate::types::module::PyModuleMethods; pub use crate::types::sequence::PySequenceMethods; pub use crate::types::set::PySetMethods; diff --git a/src/sealed.rs b/src/sealed.rs index 62b47e131a7..cc835bee3b8 100644 --- a/src/sealed.rs +++ b/src/sealed.rs @@ -1,7 +1,7 @@ use crate::types::{ PyBool, PyByteArray, PyBytes, PyCapsule, PyComplex, PyDict, PyFloat, PyFrozenSet, PyList, - PyMapping, PyModule, PySequence, PySet, PySlice, PyString, PyTraceback, PyTuple, PyType, - PyWeakref, PyWeakrefProxy, PyWeakrefReference, + PyMapping, PyMappingProxy, PyModule, PySequence, PySet, PySlice, PyString, PyTraceback, + PyTuple, PyType, PyWeakref, PyWeakrefProxy, PyWeakrefReference, }; use crate::{ffi, Bound, PyAny, PyResult}; @@ -33,6 +33,7 @@ impl Sealed for Bound<'_, PyFloat> {} impl Sealed for Bound<'_, PyFrozenSet> {} impl Sealed for Bound<'_, PyList> {} impl Sealed for Bound<'_, PyMapping> {} +impl Sealed for Bound<'_, PyMappingProxy> {} impl Sealed for Bound<'_, PyModule> {} impl Sealed for Bound<'_, PySequence> {} impl Sealed for Bound<'_, PySet> {} diff --git a/src/types/dict.rs b/src/types/dict.rs index 9b7d8697d20..ec59e48eac7 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -591,7 +591,7 @@ where } /// Represents a tuple which can be used as a PyDict item. -trait PyDictItem<'py> { +pub trait PyDictItem<'py> { type K: IntoPyObject<'py>; type V: IntoPyObject<'py>; fn unpack(self) -> (Self::K, Self::V); diff --git a/src/types/mappingproxy.rs b/src/types/mappingproxy.rs new file mode 100644 index 00000000000..d00d92cd36a --- /dev/null +++ b/src/types/mappingproxy.rs @@ -0,0 +1,629 @@ +// Copyright (c) 2017-present PyO3 Project and Contributors + +use super::PyMapping; +use crate::err::PyResult; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::instance::Bound; +use crate::types::any::PyAnyMethods; +use crate::types::{PyAny, PyIterator, PySequence}; +use crate::{ffi, Python}; + +/// Represents a Python `mappingproxy`. +#[repr(transparent)] +pub struct PyMappingProxy(PyAny); + +pyobject_native_type_core!( + PyMappingProxy, + pyobject_native_static_type_object!(ffi::PyDictProxy_Type), + #checkfunction=ffi::PyDictProxy_Check +); + +impl PyMappingProxy { + /// Creates a mappingproxy from an object. + pub fn new<'py>( + py: Python<'py>, + elements: &Bound<'py, PyMapping>, + ) -> Bound<'py, PyMappingProxy> { + unsafe { + ffi::PyDictProxy_New(elements.as_ptr()) + .assume_owned(py) + .downcast_into_unchecked() + } + } + + // /// Checks if the mappingproxy is empty, i.e. `len(self) == 0`. + // pub fn is_empty(&self) -> bool { + // self.len().unwrap_or_default() == 0 + // } +} + +/// Implementation of functionality for [`PyMappingProxy`]. +/// +/// These methods are defined for the `Bound<'py, PyMappingProxy>` smart pointer, so to use method call +/// syntax these methods are separated into a trait, because stable Rust does not yet support +/// `arbitrary_self_types`. +#[doc(alias = "PyMappingProxy")] +pub trait PyMappingProxyMethods<'py>: crate::sealed::Sealed { + /// Returns a new mappingproxy that contains the same key-value pairs as self. + /// + /// This is equivalent to the Python expression `self.copy()`. + fn copy(&self) -> PyResult>; + + /// Checks if the mappingproxy is empty, i.e. `len(self) == 0`. + fn is_empty(&self) -> PyResult; + + /// Returns a sequence containing all keys in the mapping. + fn keys(&self) -> PyResult>; + + /// Returns a sequence containing all values in the mapping. + fn values(&self) -> PyResult>; + + /// Returns a sequence of tuples of all (key, value) pairs in the mapping. + fn items(&self) -> PyResult>; + + /// Returns an iterator of `Result<(key, value)>` pairs in this MappingProxy. + /// + /// If PyO3 detects that the dictionary is mutated during iteration, it will panic. + /// It is allowed to modify values as you iterate over the dictionary, but only + /// so long as the set of keys does not change. + fn iter(self) -> BoundMappingProxyIterator<'py>; + + /// Returns `self` cast as a `PyMapping`. + fn as_mapping(&self) -> &Bound<'py, PyMapping>; +} + +impl<'py> PyMappingProxyMethods<'py> for Bound<'py, PyMappingProxy> { + fn copy(&self) -> PyResult> { + let res = self.call_method0("copy")?; + unsafe { Ok(res.downcast_into_unchecked::()) } + } + + fn is_empty(&self) -> PyResult { + Ok(self.len()? == 0) + } + + #[inline] + fn keys(&self) -> PyResult> { + unsafe { + Ok(ffi::PyMapping_Keys(self.as_ptr()) + .assume_owned_or_err(self.py())? + .downcast_into_unchecked()) + } + } + + #[inline] + fn values(&self) -> PyResult> { + unsafe { + Ok(ffi::PyMapping_Values(self.as_ptr()) + .assume_owned_or_err(self.py())? + .downcast_into_unchecked()) + } + } + + #[inline] + fn items(&self) -> PyResult> { + unsafe { + Ok(ffi::PyMapping_Items(self.as_ptr()) + .assume_owned_or_err(self.py())? + .downcast_into_unchecked()) + } + } + + fn iter(self) -> BoundMappingProxyIterator<'py> { + self.into_iter() + } + + fn as_mapping(&self) -> &Bound<'py, PyMapping> { + unsafe { self.downcast_unchecked() } + } +} + +pub struct BoundMappingProxyIterator<'py> { + iterator: Bound<'py, PyIterator>, + mappingproxy: Bound<'py, PyMappingProxy>, +} + +impl<'py> Iterator for BoundMappingProxyIterator<'py> { + type Item = PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)>; + + #[inline] + fn next(&mut self) -> Option { + self.iterator.next().map(|key| match key { + Ok(key) => match self.mappingproxy.get_item(&key) { + Ok(value) => Ok((key, value)), + Err(e) => Err(e), + }, + Err(e) => Err(e), + }) + } +} + +impl<'py> std::iter::IntoIterator for Bound<'py, PyMappingProxy> { + type Item = PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)>; + type IntoIter = BoundMappingProxyIterator<'py>; + + fn into_iter(self) -> Self::IntoIter { + BoundMappingProxyIterator { + iterator: PyIterator::from_object(&self).unwrap(), + mappingproxy: self, + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::types::dict::*; + use crate::Python; + use crate::{ + exceptions::PyKeyError, + types::{PyInt, PyTuple}, + }; + use std::collections::{BTreeMap, HashMap}; + + #[test] + fn test_new() { + Python::with_gil(|py| { + let pydict = [(7, 32)].into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, pydict.as_mapping()); + mappingproxy.get_item(7i32).unwrap(); + assert_eq!( + 32, + mappingproxy + .get_item(7i32) + .unwrap() + .extract::() + .unwrap() + ); + assert!(mappingproxy + .get_item(8i32) + .unwrap_err() + .is_instance_of::(py)); + let map: HashMap = [(7, 32)].iter().cloned().collect(); + assert_eq!(map, mappingproxy.extract().unwrap()); + let map: BTreeMap = [(7, 32)].iter().cloned().collect(); + assert_eq!(map, mappingproxy.extract().unwrap()); + }); + } + + #[test] + fn test_copy() { + Python::with_gil(|py| { + let dict = [(7, 32)].into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + + let new_dict = mappingproxy.copy().unwrap(); + assert_eq!( + 32, + new_dict.get_item(7i32).unwrap().extract::().unwrap() + ); + assert!(new_dict + .get_item(8i32) + .unwrap_err() + .is_instance_of::(py)); + }); + } + + #[test] + fn test_len() { + Python::with_gil(|py| { + let mut v = HashMap::new(); + let dict = v.clone().into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + assert_eq!(mappingproxy.len().unwrap(), 0); + v.insert(7, 32); + let dict2 = v.clone().into_py_dict(py).unwrap(); + let mp2 = PyMappingProxy::new(py, dict2.as_mapping()); + assert_eq!(mp2.len().unwrap(), 1); + }); + } + + #[test] + fn test_contains() { + Python::with_gil(|py| { + let mut v = HashMap::new(); + v.insert(7, 32); + let dict = v.clone().into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + assert!(mappingproxy.contains(7i32).unwrap()); + assert!(!mappingproxy.contains(8i32).unwrap()); + }); + } + + #[test] + fn test_get_item() { + Python::with_gil(|py| { + let mut v = HashMap::new(); + v.insert(7, 32); + let dict = v.clone().into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + assert_eq!( + 32, + mappingproxy + .get_item(7i32) + .unwrap() + .extract::() + .unwrap() + ); + assert!(mappingproxy + .get_item(8i32) + .unwrap_err() + .is_instance_of::(py)); + }); + } + + #[test] + fn test_set_item_refcnt() { + Python::with_gil(|py| { + let cnt; + { + let none = py.None(); + cnt = none.get_refcnt(py); + let dict = [(10, none)].into_py_dict(py).unwrap(); + let _mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + } + { + assert_eq!(cnt, py.None().get_refcnt(py)); + } + }); + } + + #[test] + fn test_isempty() { + Python::with_gil(|py| { + let map: HashMap = HashMap::new(); + let dict = map.into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + assert!(mappingproxy.is_empty().unwrap()); + }); + } + + #[test] + fn test_keys() { + Python::with_gil(|py| { + let mut v = HashMap::new(); + v.insert(7, 32); + v.insert(8, 42); + v.insert(9, 123); + let dict = v.into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. + let mut key_sum = 0; + for el in mappingproxy.keys().unwrap().try_iter().unwrap() { + key_sum += el.unwrap().extract::().unwrap(); + } + assert_eq!(7 + 8 + 9, key_sum); + }); + } + + #[test] + fn test_values() { + Python::with_gil(|py| { + let mut v: HashMap = HashMap::new(); + v.insert(7, 32); + v.insert(8, 42); + v.insert(9, 123); + let dict = v.into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. + let mut values_sum = 0; + for el in mappingproxy.values().unwrap().try_iter().unwrap() { + values_sum += el.unwrap().extract::().unwrap(); + } + assert_eq!(32 + 42 + 123, values_sum); + }); + } + + #[test] + fn test_items() { + Python::with_gil(|py| { + let mut v = HashMap::new(); + v.insert(7, 32); + v.insert(8, 42); + v.insert(9, 123); + let dict = v.into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. + let mut key_sum = 0; + let mut value_sum = 0; + for res in mappingproxy.items().unwrap().try_iter().unwrap() { + let el = res.unwrap(); + let tuple = el.downcast::().unwrap(); + key_sum += tuple.get_item(0).unwrap().extract::().unwrap(); + value_sum += tuple.get_item(1).unwrap().extract::().unwrap(); + } + assert_eq!(7 + 8 + 9, key_sum); + assert_eq!(32 + 42 + 123, value_sum); + }); + } + + #[test] + fn test_iter() { + Python::with_gil(|py| { + let mut v = HashMap::new(); + v.insert(7, 32); + v.insert(8, 42); + v.insert(9, 123); + let dict = v.into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + let mut key_sum = 0; + let mut value_sum = 0; + for res in mappingproxy.iter() { + let (key, value) = res.unwrap(); + key_sum += key.extract::().unwrap(); + value_sum += value.extract::().unwrap(); + } + assert_eq!(7 + 8 + 9, key_sum); + assert_eq!(32 + 42 + 123, value_sum); + }); + } + + #[test] + fn test_hashmap_to_python() { + Python::with_gil(|py| { + let mut map = HashMap::::new(); + map.insert(1, 1); + + let dict = map.clone().into_py_dict(py).unwrap(); + let py_map = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!(py_map.len().unwrap(), 1); + assert_eq!(py_map.get_item(1).unwrap().extract::().unwrap(), 1); + assert_eq!(map, py_map.extract().unwrap()); + }); + } + + #[test] + fn test_btreemap_to_python() { + Python::with_gil(|py| { + let mut map = BTreeMap::::new(); + map.insert(1, 1); + + let dict = map.clone().into_py_dict(py).unwrap(); + let py_map = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!(py_map.len().unwrap(), 1); + assert_eq!(py_map.get_item(1).unwrap().extract::().unwrap(), 1); + assert_eq!(map, py_map.extract().unwrap()); + }); + } + + #[test] + fn test_hashmap_into_python() { + Python::with_gil(|py| { + let mut map = HashMap::::new(); + map.insert(1, 1); + + let dict = map.clone().into_py_dict(py).unwrap(); + let py_map = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!(py_map.len().unwrap(), 1); + assert_eq!(py_map.get_item(1).unwrap().extract::().unwrap(), 1); + }); + } + + #[test] + fn test_hashmap_into_mappingproxy() { + Python::with_gil(|py| { + let mut map = HashMap::::new(); + map.insert(1, 1); + + let dict = map.clone().into_py_dict(py).unwrap(); + let py_map = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!(py_map.len().unwrap(), 1); + assert_eq!(py_map.get_item(1).unwrap().extract::().unwrap(), 1); + }); + } + + #[test] + fn test_btreemap_into_py() { + Python::with_gil(|py| { + let mut map = BTreeMap::::new(); + map.insert(1, 1); + + let dict = map.clone().into_py_dict(py).unwrap(); + let py_map = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!(py_map.len().unwrap(), 1); + assert_eq!(py_map.get_item(1).unwrap().extract::().unwrap(), 1); + }); + } + + #[test] + fn test_btreemap_into_mappingproxy() { + Python::with_gil(|py| { + let mut map = BTreeMap::::new(); + map.insert(1, 1); + + let dict = map.clone().into_py_dict(py).unwrap(); + let py_map = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!(py_map.len().unwrap(), 1); + assert_eq!(py_map.get_item(1).unwrap().extract::().unwrap(), 1); + }); + } + + #[test] + fn test_vec_into_mappingproxy() { + Python::with_gil(|py| { + let vec = vec![("a", 1), ("b", 2), ("c", 3)]; + let dict = vec.clone().into_py_dict(py).unwrap(); + let py_map = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!(py_map.len().unwrap(), 3); + assert_eq!(py_map.get_item("b").unwrap().extract::().unwrap(), 2); + }); + } + + #[test] + fn test_slice_into_mappingproxy() { + Python::with_gil(|py| { + let arr = [("a", 1), ("b", 2), ("c", 3)]; + + let dict = arr.into_py_dict(py).unwrap(); + let py_map = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!(py_map.len().unwrap(), 3); + assert_eq!(py_map.get_item("b").unwrap().extract::().unwrap(), 2); + }); + } + + #[test] + fn mappingproxy_as_mapping() { + Python::with_gil(|py| { + let mut map = HashMap::::new(); + map.insert(1, 1); + + let dict = map.clone().into_py_dict(py).unwrap(); + let py_map = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!(py_map.as_mapping().len().unwrap(), 1); + assert_eq!( + py_map + .as_mapping() + .get_item(1) + .unwrap() + .extract::() + .unwrap(), + 1 + ); + }); + } + + #[cfg(not(PyPy))] + fn abc_mappingproxy(py: Python<'_>) -> Bound<'_, PyMappingProxy> { + let mut map = HashMap::<&'static str, i32>::new(); + map.insert("a", 1); + map.insert("b", 2); + map.insert("c", 3); + let dict = map.clone().into_py_dict(py).unwrap(); + PyMappingProxy::new(py, dict.as_mapping()) + } + + #[test] + #[cfg(not(PyPy))] + fn mappingproxy_keys_view() { + Python::with_gil(|py| { + let mappingproxy = abc_mappingproxy(py); + let keys = mappingproxy.call_method0("keys").unwrap(); + assert!(keys.is_instance(&py.get_type::()).unwrap()); + }) + } + + #[test] + #[cfg(not(PyPy))] + fn mappingproxy_values_view() { + Python::with_gil(|py| { + let mappingproxy = abc_mappingproxy(py); + let values = mappingproxy.call_method0("values").unwrap(); + assert!(values.is_instance(&py.get_type::()).unwrap()); + }) + } + + #[test] + #[cfg(not(PyPy))] + fn mappingproxy_items_view() { + Python::with_gil(|py| { + let mappingproxy = abc_mappingproxy(py); + let items = mappingproxy.call_method0("items").unwrap(); + assert!(items.is_instance(&py.get_type::()).unwrap()); + }) + } + + #[test] + fn get_value_from_mappingproxy_of_strings() { + Python::with_gil(|py: Python<'_>| { + let mut map = HashMap::new(); + map.insert("first key".to_string(), "first value".to_string()); + map.insert("second key".to_string(), "second value".to_string()); + map.insert("third key".to_string(), "third value".to_string()); + + let dict = map.clone().into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!( + map.into_iter().collect::>(), + mappingproxy + .iter() + .map(|object| { + let tuple = object.unwrap(); + ( + tuple.0.extract::().unwrap(), + tuple.1.extract::().unwrap(), + ) + }) + .collect::>() + ); + }) + } + + #[test] + fn get_value_from_mappingproxy_of_integers() { + Python::with_gil(|py: Python<'_>| { + const LEN: usize = 10_000; + let items: Vec<(usize, usize)> = (1..LEN).map(|i| (i, i - 1)).collect(); + + let dict = items.clone().into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!( + items, + mappingproxy + .copy() + .unwrap() + .iter() + .map(|object| { + let tuple = object.unwrap(); + ( + tuple + .0 + .downcast::() + .unwrap() + .extract::() + .unwrap(), + tuple + .1 + .downcast::() + .unwrap() + .extract::() + .unwrap(), + ) + }) + .collect::>() + ); + for index in 1..LEN { + assert_eq!( + mappingproxy + .copy() + .unwrap() + .get_item(index) + .unwrap() + .extract::() + .unwrap(), + index - 1 + ); + } + }) + } + + #[test] + fn iter_mappingproxy_nosegv() { + Python::with_gil(|py| { + const LEN: usize = 10_000_000; + let items = (0..LEN as u64).map(|i| (i, i * 2)); + + let dict = items.clone().into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + + let mut sum = 0; + for result in mappingproxy { + let (k, _v) = result.unwrap(); + let i: u64 = k.extract().unwrap(); + sum += i; + } + assert_eq!(sum, 49_999_995_000_000); + }) + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index c074196ccc1..d84f099e773 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -28,6 +28,7 @@ pub use self::function::PyFunction; pub use self::iterator::PyIterator; pub use self::list::{PyList, PyListMethods}; pub use self::mapping::{PyMapping, PyMappingMethods}; +pub use self::mappingproxy::PyMappingProxy; pub use self::memoryview::PyMemoryView; pub use self::module::{PyModule, PyModuleMethods}; pub use self::none::PyNone; @@ -248,6 +249,7 @@ mod function; pub(crate) mod iterator; pub(crate) mod list; pub(crate) mod mapping; +pub(crate) mod mappingproxy; mod memoryview; pub(crate) mod module; mod none; From 8ae75b4407e3c5c6a8c5da1900d08db7696dc278 Mon Sep 17 00:00:00 2001 From: Kevin Matlock Date: Tue, 22 Oct 2024 18:28:34 -0700 Subject: [PATCH 2/9] Move over from `iter` to `try_iter`. --- src/types/mappingproxy.rs | 44 +++++++++++++++------------------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/src/types/mappingproxy.rs b/src/types/mappingproxy.rs index d00d92cd36a..bc633675a7a 100644 --- a/src/types/mappingproxy.rs +++ b/src/types/mappingproxy.rs @@ -61,15 +61,12 @@ pub trait PyMappingProxyMethods<'py>: crate::sealed::Sealed { /// Returns a sequence of tuples of all (key, value) pairs in the mapping. fn items(&self) -> PyResult>; - /// Returns an iterator of `Result<(key, value)>` pairs in this MappingProxy. - /// - /// If PyO3 detects that the dictionary is mutated during iteration, it will panic. - /// It is allowed to modify values as you iterate over the dictionary, but only - /// so long as the set of keys does not change. - fn iter(self) -> BoundMappingProxyIterator<'py>; - /// Returns `self` cast as a `PyMapping`. fn as_mapping(&self) -> &Bound<'py, PyMapping>; + + /// Takes an object and returns an iterator for it. Returns an error if the object is not + /// iterable. + fn try_iter(self) -> PyResult>; } impl<'py> PyMappingProxyMethods<'py> for Bound<'py, PyMappingProxy> { @@ -109,13 +106,16 @@ impl<'py> PyMappingProxyMethods<'py> for Bound<'py, PyMappingProxy> { } } - fn iter(self) -> BoundMappingProxyIterator<'py> { - self.into_iter() - } - fn as_mapping(&self) -> &Bound<'py, PyMapping> { unsafe { self.downcast_unchecked() } } + + fn try_iter(self) -> PyResult> { + Ok(BoundMappingProxyIterator { + iterator: PyIterator::from_object(&self)?, + mappingproxy: self, + }) + } } pub struct BoundMappingProxyIterator<'py> { @@ -138,18 +138,6 @@ impl<'py> Iterator for BoundMappingProxyIterator<'py> { } } -impl<'py> std::iter::IntoIterator for Bound<'py, PyMappingProxy> { - type Item = PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)>; - type IntoIter = BoundMappingProxyIterator<'py>; - - fn into_iter(self) -> Self::IntoIter { - BoundMappingProxyIterator { - iterator: PyIterator::from_object(&self).unwrap(), - mappingproxy: self, - } - } -} - #[cfg(test)] mod tests { @@ -349,7 +337,7 @@ mod tests { let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); let mut key_sum = 0; let mut value_sum = 0; - for res in mappingproxy.iter() { + for res in mappingproxy.try_iter().unwrap() { let (key, value) = res.unwrap(); key_sum += key.extract::().unwrap(); value_sum += value.extract::().unwrap(); @@ -546,7 +534,8 @@ mod tests { assert_eq!( map.into_iter().collect::>(), mappingproxy - .iter() + .try_iter() + .unwrap() .map(|object| { let tuple = object.unwrap(); ( @@ -573,7 +562,8 @@ mod tests { mappingproxy .copy() .unwrap() - .iter() + .try_iter() + .unwrap() .map(|object| { let tuple = object.unwrap(); ( @@ -618,7 +608,7 @@ mod tests { let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); let mut sum = 0; - for result in mappingproxy { + for result in mappingproxy.try_iter().unwrap() { let (k, _v) = result.unwrap(); let i: u64 = k.extract().unwrap(); sum += i; From 4bad990d8def7bc1095a96eb80442a66159a3987 Mon Sep 17 00:00:00 2001 From: Kevin Matlock Date: Tue, 22 Oct 2024 18:58:47 -0700 Subject: [PATCH 3/9] Added lifetime to `try_iter`, preventing need to clone when iterating. --- src/conversions/std/map.rs | 9 ++++++--- src/types/mappingproxy.rs | 19 +++++++------------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index 8f219b68950..1488d778ecc 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -7,7 +7,10 @@ use crate::ToPyObject; use crate::{ conversion::IntoPyObject, instance::Bound, - types::{any::PyAnyMethods, dict::PyDictMethods, PyDict, PyMappingProxy}, + types::{ + any::PyAnyMethods, dict::PyDictMethods, mappingproxy::PyMappingProxyMethods, PyDict, + PyMappingProxy, + }, FromPyObject, IntoPy, PyAny, PyErr, PyObject, Python, }; @@ -173,7 +176,7 @@ where let mappingproxy = ob.downcast::()?; let mut ret = collections::HashMap::with_capacity_and_hasher(mappingproxy.len()?, S::default()); - for res in mappingproxy.clone() { + for res in mappingproxy.try_iter()? { let (k, v) = res?; ret.insert(k.extract()?, v.extract()?); } @@ -202,7 +205,7 @@ where let mappingproxy = ob.downcast::()?; let mut ret = collections::BTreeMap::new(); - for res in mappingproxy.clone() { + for res in mappingproxy.try_iter()? { let (k, v) = res?; ret.insert(k.extract()?, v.extract()?); } diff --git a/src/types/mappingproxy.rs b/src/types/mappingproxy.rs index bc633675a7a..49f55435107 100644 --- a/src/types/mappingproxy.rs +++ b/src/types/mappingproxy.rs @@ -30,11 +30,6 @@ impl PyMappingProxy { .downcast_into_unchecked() } } - - // /// Checks if the mappingproxy is empty, i.e. `len(self) == 0`. - // pub fn is_empty(&self) -> bool { - // self.len().unwrap_or_default() == 0 - // } } /// Implementation of functionality for [`PyMappingProxy`]. @@ -43,7 +38,7 @@ impl PyMappingProxy { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyMappingProxy")] -pub trait PyMappingProxyMethods<'py>: crate::sealed::Sealed { +pub trait PyMappingProxyMethods<'py, 'a>: crate::sealed::Sealed { /// Returns a new mappingproxy that contains the same key-value pairs as self. /// /// This is equivalent to the Python expression `self.copy()`. @@ -66,10 +61,10 @@ pub trait PyMappingProxyMethods<'py>: crate::sealed::Sealed { /// Takes an object and returns an iterator for it. Returns an error if the object is not /// iterable. - fn try_iter(self) -> PyResult>; + fn try_iter(&'a self) -> PyResult>; } -impl<'py> PyMappingProxyMethods<'py> for Bound<'py, PyMappingProxy> { +impl<'py, 'a> PyMappingProxyMethods<'py, 'a> for Bound<'py, PyMappingProxy> { fn copy(&self) -> PyResult> { let res = self.call_method0("copy")?; unsafe { Ok(res.downcast_into_unchecked::()) } @@ -110,7 +105,7 @@ impl<'py> PyMappingProxyMethods<'py> for Bound<'py, PyMappingProxy> { unsafe { self.downcast_unchecked() } } - fn try_iter(self) -> PyResult> { + fn try_iter(&'a self) -> PyResult> { Ok(BoundMappingProxyIterator { iterator: PyIterator::from_object(&self)?, mappingproxy: self, @@ -118,12 +113,12 @@ impl<'py> PyMappingProxyMethods<'py> for Bound<'py, PyMappingProxy> { } } -pub struct BoundMappingProxyIterator<'py> { +pub struct BoundMappingProxyIterator<'py, 'a> { iterator: Bound<'py, PyIterator>, - mappingproxy: Bound<'py, PyMappingProxy>, + mappingproxy: &'a Bound<'py, PyMappingProxy>, } -impl<'py> Iterator for BoundMappingProxyIterator<'py> { +impl<'py, 'a> Iterator for BoundMappingProxyIterator<'py, 'a> { type Item = PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)>; #[inline] From a7126d0c1962d663f57051f84b9907da3deab279 Mon Sep 17 00:00:00 2001 From: Kevin Matlock Date: Tue, 22 Oct 2024 19:07:47 -0700 Subject: [PATCH 4/9] Remove unneccessary borrow. --- src/types/mappingproxy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/mappingproxy.rs b/src/types/mappingproxy.rs index 49f55435107..2dbd1d3552b 100644 --- a/src/types/mappingproxy.rs +++ b/src/types/mappingproxy.rs @@ -107,7 +107,7 @@ impl<'py, 'a> PyMappingProxyMethods<'py, 'a> for Bound<'py, PyMappingProxy> { fn try_iter(&'a self) -> PyResult> { Ok(BoundMappingProxyIterator { - iterator: PyIterator::from_object(&self)?, + iterator: PyIterator::from_object(self)?, mappingproxy: self, }) } From 878ed161b820503338b6c8e59ef034cbeee12734 Mon Sep 17 00:00:00 2001 From: Kevin Matlock Date: Thu, 24 Oct 2024 08:29:09 -0700 Subject: [PATCH 5/9] Add newsfragment --- newsfragments/4644.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/4644.added.md diff --git a/newsfragments/4644.added.md b/newsfragments/4644.added.md new file mode 100644 index 00000000000..3da2f4bc4f0 --- /dev/null +++ b/newsfragments/4644.added.md @@ -0,0 +1 @@ +New `PyMappingProxy` struct corresponing to the `mappingproxy` class in Python. \ No newline at end of file From de9743fce2ec34cbc5ab5c6054e89c22f3f4b2ef Mon Sep 17 00:00:00 2001 From: Kevin Matlock Date: Thu, 24 Oct 2024 09:04:22 -0700 Subject: [PATCH 6/9] Newline to newsfragment. --- newsfragments/4644.added.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/4644.added.md b/newsfragments/4644.added.md index 3da2f4bc4f0..4b4a277abf8 100644 --- a/newsfragments/4644.added.md +++ b/newsfragments/4644.added.md @@ -1 +1 @@ -New `PyMappingProxy` struct corresponing to the `mappingproxy` class in Python. \ No newline at end of file +New `PyMappingProxy` struct corresponing to the `mappingproxy` class in Python. From b209d582abff59a43f067e83842bb395d4c7915d Mon Sep 17 00:00:00 2001 From: Kevin Matlock Date: Thu, 24 Oct 2024 11:53:36 -0700 Subject: [PATCH 7/9] Remove explicit lifetime, --- src/types/mappingproxy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/mappingproxy.rs b/src/types/mappingproxy.rs index 2dbd1d3552b..833a82083b5 100644 --- a/src/types/mappingproxy.rs +++ b/src/types/mappingproxy.rs @@ -118,7 +118,7 @@ pub struct BoundMappingProxyIterator<'py, 'a> { mappingproxy: &'a Bound<'py, PyMappingProxy>, } -impl<'py, 'a> Iterator for BoundMappingProxyIterator<'py, 'a> { +impl<'py> Iterator for BoundMappingProxyIterator<'py, '_> { type Item = PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)>; #[inline] From 24304f9c6969d6c1f2f75298cd0d2822121089a7 Mon Sep 17 00:00:00 2001 From: Kevin Matlock Date: Sat, 26 Oct 2024 14:46:18 -0700 Subject: [PATCH 8/9] Review comments (#2) * Addressing more comments * Remove extract_bound. * Remove extract methods. --- pyo3-ffi/src/descrobject.rs | 7 +-- src/conversions/std/map.rs | 34 +++----------- src/types/dict.rs | 2 +- src/types/mappingproxy.rs | 91 +++++++------------------------------ 4 files changed, 25 insertions(+), 109 deletions(-) diff --git a/pyo3-ffi/src/descrobject.rs b/pyo3-ffi/src/descrobject.rs index dd8e1784726..f4a5ce00bf6 100644 --- a/pyo3-ffi/src/descrobject.rs +++ b/pyo3-ffi/src/descrobject.rs @@ -1,6 +1,6 @@ use crate::methodobject::PyMethodDef; use crate::object::{PyObject, PyTypeObject}; -use crate::{PyObject_TypeCheck, Py_ssize_t}; +use crate::Py_ssize_t; use std::os::raw::{c_char, c_int, c_void}; use std::ptr; @@ -68,11 +68,6 @@ extern "C" { pub fn PyWrapper_New(arg1: *mut PyObject, arg2: *mut PyObject) -> *mut PyObject; } -#[inline] -pub unsafe fn PyDictProxy_Check(op: *mut PyObject) -> c_int { - PyObject_TypeCheck(op, std::ptr::addr_of_mut!(PyDictProxy_Type)) -} - /// Represents the [PyMemberDef](https://docs.python.org/3/c-api/structures.html#c.PyMemberDef) /// structure. /// diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index 1488d778ecc..888bd13c180 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -7,10 +7,7 @@ use crate::ToPyObject; use crate::{ conversion::IntoPyObject, instance::Bound, - types::{ - any::PyAnyMethods, dict::PyDictMethods, mappingproxy::PyMappingProxyMethods, PyDict, - PyMappingProxy, - }, + types::{any::PyAnyMethods, dict::PyDictMethods, PyDict}, FromPyObject, IntoPy, PyAny, PyErr, PyObject, Python, }; @@ -165,19 +162,9 @@ where S: hash::BuildHasher + Default, { fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { - if let Ok(dict) = ob.downcast::() { - let mut ret = collections::HashMap::with_capacity_and_hasher(dict.len(), S::default()); - for (k, v) in dict { - ret.insert(k.extract()?, v.extract()?); - } - return Ok(ret); - } - - let mappingproxy = ob.downcast::()?; - let mut ret = - collections::HashMap::with_capacity_and_hasher(mappingproxy.len()?, S::default()); - for res in mappingproxy.try_iter()? { - let (k, v) = res?; + let dict = ob.downcast::()?; + let mut ret = collections::HashMap::with_capacity_and_hasher(dict.len(), S::default()); + for (k, v) in dict { ret.insert(k.extract()?, v.extract()?); } Ok(ret) @@ -195,18 +182,9 @@ where V: FromPyObject<'py>, { fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { - if let Ok(dict) = ob.downcast::() { - let mut ret = collections::BTreeMap::new(); - for (k, v) in dict { - ret.insert(k.extract()?, v.extract()?); - } - return Ok(ret); - } - - let mappingproxy = ob.downcast::()?; + let dict = ob.downcast::()?; let mut ret = collections::BTreeMap::new(); - for res in mappingproxy.try_iter()? { - let (k, v) = res?; + for (k, v) in dict { ret.insert(k.extract()?, v.extract()?); } Ok(ret) diff --git a/src/types/dict.rs b/src/types/dict.rs index 185074b3749..6987e4525ec 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -780,7 +780,7 @@ where } /// Represents a tuple which can be used as a PyDict item. -pub trait PyDictItem<'py> { +trait PyDictItem<'py> { type K: IntoPyObject<'py>; type V: IntoPyObject<'py>; fn unpack(self) -> (Self::K, Self::V); diff --git a/src/types/mappingproxy.rs b/src/types/mappingproxy.rs index 833a82083b5..4230c7159ce 100644 --- a/src/types/mappingproxy.rs +++ b/src/types/mappingproxy.rs @@ -5,17 +5,24 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::types::any::PyAnyMethods; -use crate::types::{PyAny, PyIterator, PySequence}; +use crate::types::{PyAny, PyIterator, PyList}; use crate::{ffi, Python}; +use std::os::raw::c_int; + /// Represents a Python `mappingproxy`. #[repr(transparent)] pub struct PyMappingProxy(PyAny); +#[inline] +unsafe fn dict_proxy_check(op: *mut ffi::PyObject) -> c_int { + ffi::Py_IS_TYPE(op, std::ptr::addr_of_mut!(ffi::PyDictProxy_Type)) +} + pyobject_native_type_core!( PyMappingProxy, pyobject_native_static_type_object!(ffi::PyDictProxy_Type), - #checkfunction=ffi::PyDictProxy_Check + #checkfunction=dict_proxy_check ); impl PyMappingProxy { @@ -39,22 +46,17 @@ impl PyMappingProxy { /// `arbitrary_self_types`. #[doc(alias = "PyMappingProxy")] pub trait PyMappingProxyMethods<'py, 'a>: crate::sealed::Sealed { - /// Returns a new mappingproxy that contains the same key-value pairs as self. - /// - /// This is equivalent to the Python expression `self.copy()`. - fn copy(&self) -> PyResult>; - /// Checks if the mappingproxy is empty, i.e. `len(self) == 0`. fn is_empty(&self) -> PyResult; /// Returns a sequence containing all keys in the mapping. - fn keys(&self) -> PyResult>; + fn keys(&self) -> PyResult>; /// Returns a sequence containing all values in the mapping. - fn values(&self) -> PyResult>; + fn values(&self) -> PyResult>; /// Returns a sequence of tuples of all (key, value) pairs in the mapping. - fn items(&self) -> PyResult>; + fn items(&self) -> PyResult>; /// Returns `self` cast as a `PyMapping`. fn as_mapping(&self) -> &Bound<'py, PyMapping>; @@ -65,17 +67,12 @@ pub trait PyMappingProxyMethods<'py, 'a>: crate::sealed::Sealed { } impl<'py, 'a> PyMappingProxyMethods<'py, 'a> for Bound<'py, PyMappingProxy> { - fn copy(&self) -> PyResult> { - let res = self.call_method0("copy")?; - unsafe { Ok(res.downcast_into_unchecked::()) } - } - fn is_empty(&self) -> PyResult { Ok(self.len()? == 0) } #[inline] - fn keys(&self) -> PyResult> { + fn keys(&self) -> PyResult> { unsafe { Ok(ffi::PyMapping_Keys(self.as_ptr()) .assume_owned_or_err(self.py())? @@ -84,7 +81,7 @@ impl<'py, 'a> PyMappingProxyMethods<'py, 'a> for Bound<'py, PyMappingProxy> { } #[inline] - fn values(&self) -> PyResult> { + fn values(&self) -> PyResult> { unsafe { Ok(ffi::PyMapping_Values(self.as_ptr()) .assume_owned_or_err(self.py())? @@ -93,7 +90,7 @@ impl<'py, 'a> PyMappingProxyMethods<'py, 'a> for Bound<'py, PyMappingProxy> { } #[inline] - fn items(&self) -> PyResult> { + fn items(&self) -> PyResult> { unsafe { Ok(ffi::PyMapping_Items(self.as_ptr()) .assume_owned_or_err(self.py())? @@ -163,28 +160,6 @@ mod tests { .get_item(8i32) .unwrap_err() .is_instance_of::(py)); - let map: HashMap = [(7, 32)].iter().cloned().collect(); - assert_eq!(map, mappingproxy.extract().unwrap()); - let map: BTreeMap = [(7, 32)].iter().cloned().collect(); - assert_eq!(map, mappingproxy.extract().unwrap()); - }); - } - - #[test] - fn test_copy() { - Python::with_gil(|py| { - let dict = [(7, 32)].into_py_dict(py).unwrap(); - let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); - - let new_dict = mappingproxy.copy().unwrap(); - assert_eq!( - 32, - new_dict.get_item(7i32).unwrap().extract::().unwrap() - ); - assert!(new_dict - .get_item(8i32) - .unwrap_err() - .is_instance_of::(py)); }); } @@ -342,36 +317,6 @@ mod tests { }); } - #[test] - fn test_hashmap_to_python() { - Python::with_gil(|py| { - let mut map = HashMap::::new(); - map.insert(1, 1); - - let dict = map.clone().into_py_dict(py).unwrap(); - let py_map = PyMappingProxy::new(py, dict.as_mapping()); - - assert_eq!(py_map.len().unwrap(), 1); - assert_eq!(py_map.get_item(1).unwrap().extract::().unwrap(), 1); - assert_eq!(map, py_map.extract().unwrap()); - }); - } - - #[test] - fn test_btreemap_to_python() { - Python::with_gil(|py| { - let mut map = BTreeMap::::new(); - map.insert(1, 1); - - let dict = map.clone().into_py_dict(py).unwrap(); - let py_map = PyMappingProxy::new(py, dict.as_mapping()); - - assert_eq!(py_map.len().unwrap(), 1); - assert_eq!(py_map.get_item(1).unwrap().extract::().unwrap(), 1); - assert_eq!(map, py_map.extract().unwrap()); - }); - } - #[test] fn test_hashmap_into_python() { Python::with_gil(|py| { @@ -555,8 +500,7 @@ mod tests { assert_eq!( items, mappingproxy - .copy() - .unwrap() + .clone() .try_iter() .unwrap() .map(|object| { @@ -581,8 +525,7 @@ mod tests { for index in 1..LEN { assert_eq!( mappingproxy - .copy() - .unwrap() + .clone() .get_item(index) .unwrap() .extract::() From 14a3751dd908a74714794273e41b61d1738b332c Mon Sep 17 00:00:00 2001 From: Kevin Matlock Date: Sat, 26 Oct 2024 15:01:44 -0700 Subject: [PATCH 9/9] Update comments for list return type. --- src/types/mappingproxy.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types/mappingproxy.rs b/src/types/mappingproxy.rs index 4230c7159ce..fc28687c561 100644 --- a/src/types/mappingproxy.rs +++ b/src/types/mappingproxy.rs @@ -49,13 +49,13 @@ pub trait PyMappingProxyMethods<'py, 'a>: crate::sealed::Sealed { /// Checks if the mappingproxy is empty, i.e. `len(self) == 0`. fn is_empty(&self) -> PyResult; - /// Returns a sequence containing all keys in the mapping. + /// Returns a list containing all keys in the mapping. fn keys(&self) -> PyResult>; - /// Returns a sequence containing all values in the mapping. + /// Returns a list containing all values in the mapping. fn values(&self) -> PyResult>; - /// Returns a sequence of tuples of all (key, value) pairs in the mapping. + /// Returns a list of tuples of all (key, value) pairs in the mapping. fn items(&self) -> PyResult>; /// Returns `self` cast as a `PyMapping`.