pyo3/types/
mapping.rs

1use crate::conversion::IntoPyObject;
2use crate::err::PyResult;
3use crate::ffi_ptr_ext::FfiPtrExt;
4#[cfg(feature = "experimental-inspect")]
5use crate::inspect::TypeHint;
6use crate::instance::Bound;
7use crate::py_result_ext::PyResultExt;
8use crate::sync::PyOnceLock;
9use crate::type_object::PyTypeInfo;
10use crate::types::any::PyAnyMethods;
11use crate::types::{PyAny, PyDict, PyList, PyType, PyTypeMethods};
12use crate::{ffi, Py, Python};
13
14/// Represents a reference to a Python object supporting the mapping protocol.
15///
16/// Values of this type are accessed via PyO3's smart pointers, e.g. as
17/// [`Py<PyMapping>`][crate::Py] or [`Bound<'py, PyMapping>`][Bound].
18///
19/// For APIs available on mapping objects, see the [`PyMappingMethods`] trait which is implemented for
20/// [`Bound<'py, PyMapping>`][Bound].
21#[repr(transparent)]
22pub struct PyMapping(PyAny);
23
24pyobject_native_type_named!(PyMapping);
25
26unsafe impl PyTypeInfo for PyMapping {
27    const NAME: &'static str = "Mapping";
28    const MODULE: Option<&'static str> = Some("collections.abc");
29
30    #[cfg(feature = "experimental-inspect")]
31    const TYPE_HINT: TypeHint = TypeHint::module_attr("collections.abc", "Mapping");
32
33    #[inline]
34    #[allow(clippy::redundant_closure_call)]
35    fn type_object_raw(py: Python<'_>) -> *mut ffi::PyTypeObject {
36        static TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();
37        TYPE.import(py, "collections.abc", "Mapping")
38            .unwrap()
39            .as_type_ptr()
40    }
41
42    #[inline]
43    fn is_type_of(object: &Bound<'_, PyAny>) -> bool {
44        // Using `is_instance` for `collections.abc.Mapping` is slow, so provide
45        // optimized case dict as a well-known mapping
46        PyDict::is_type_of(object)
47            || object
48                .is_instance(&Self::type_object(object.py()).into_any())
49                .unwrap_or_else(|err| {
50                    err.write_unraisable(object.py(), Some(object));
51                    false
52                })
53    }
54}
55
56impl PyMapping {
57    /// Register a pyclass as a subclass of `collections.abc.Mapping` (from the Python standard
58    /// library). This is equivalent to `collections.abc.Mapping.register(T)` in Python.
59    /// This registration is required for a pyclass to be castable from `PyAny` to `PyMapping`.
60    pub fn register<T: PyTypeInfo>(py: Python<'_>) -> PyResult<()> {
61        let ty = T::type_object(py);
62        Self::type_object(py).call_method1("register", (ty,))?;
63        Ok(())
64    }
65}
66
67/// Implementation of functionality for [`PyMapping`].
68///
69/// These methods are defined for the `Bound<'py, PyMapping>` smart pointer, so to use method call
70/// syntax these methods are separated into a trait, because stable Rust does not yet support
71/// `arbitrary_self_types`.
72#[doc(alias = "PyMapping")]
73pub trait PyMappingMethods<'py>: crate::sealed::Sealed {
74    /// Returns the number of objects in the mapping.
75    ///
76    /// This is equivalent to the Python expression `len(self)`.
77    fn len(&self) -> PyResult<usize>;
78
79    /// Returns whether the mapping is empty.
80    fn is_empty(&self) -> PyResult<bool>;
81
82    /// Determines if the mapping contains the specified key.
83    ///
84    /// This is equivalent to the Python expression `key in self`.
85    fn contains<K>(&self, key: K) -> PyResult<bool>
86    where
87        K: IntoPyObject<'py>;
88
89    /// Gets the item in self with key `key`.
90    ///
91    /// Returns an `Err` if the item with specified key is not found, usually `KeyError`.
92    ///
93    /// This is equivalent to the Python expression `self[key]`.
94    fn get_item<K>(&self, key: K) -> PyResult<Bound<'py, PyAny>>
95    where
96        K: IntoPyObject<'py>;
97
98    /// Sets the item in self with key `key`.
99    ///
100    /// This is equivalent to the Python expression `self[key] = value`.
101    fn set_item<K, V>(&self, key: K, value: V) -> PyResult<()>
102    where
103        K: IntoPyObject<'py>,
104        V: IntoPyObject<'py>;
105
106    /// Deletes the item with key `key`.
107    ///
108    /// This is equivalent to the Python statement `del self[key]`.
109    fn del_item<K>(&self, key: K) -> PyResult<()>
110    where
111        K: IntoPyObject<'py>;
112
113    /// Returns a list containing all keys in the mapping.
114    fn keys(&self) -> PyResult<Bound<'py, PyList>>;
115
116    /// Returns a list containing all values in the mapping.
117    fn values(&self) -> PyResult<Bound<'py, PyList>>;
118
119    /// Returns a list of all (key, value) pairs in the mapping.
120    fn items(&self) -> PyResult<Bound<'py, PyList>>;
121}
122
123impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> {
124    #[inline]
125    fn len(&self) -> PyResult<usize> {
126        let v = unsafe { ffi::PyMapping_Size(self.as_ptr()) };
127        crate::err::error_on_minusone(self.py(), v)?;
128        Ok(v as usize)
129    }
130
131    #[inline]
132    fn is_empty(&self) -> PyResult<bool> {
133        self.len().map(|l| l == 0)
134    }
135
136    fn contains<K>(&self, key: K) -> PyResult<bool>
137    where
138        K: IntoPyObject<'py>,
139    {
140        PyAnyMethods::contains(&**self, key)
141    }
142
143    #[inline]
144    fn get_item<K>(&self, key: K) -> PyResult<Bound<'py, PyAny>>
145    where
146        K: IntoPyObject<'py>,
147    {
148        PyAnyMethods::get_item(&**self, key)
149    }
150
151    #[inline]
152    fn set_item<K, V>(&self, key: K, value: V) -> PyResult<()>
153    where
154        K: IntoPyObject<'py>,
155        V: IntoPyObject<'py>,
156    {
157        PyAnyMethods::set_item(&**self, key, value)
158    }
159
160    #[inline]
161    fn del_item<K>(&self, key: K) -> PyResult<()>
162    where
163        K: IntoPyObject<'py>,
164    {
165        PyAnyMethods::del_item(&**self, key)
166    }
167
168    #[inline]
169    fn keys(&self) -> PyResult<Bound<'py, PyList>> {
170        unsafe {
171            ffi::PyMapping_Keys(self.as_ptr())
172                .assume_owned_or_err(self.py())
173                .cast_into_unchecked()
174        }
175    }
176
177    #[inline]
178    fn values(&self) -> PyResult<Bound<'py, PyList>> {
179        unsafe {
180            ffi::PyMapping_Values(self.as_ptr())
181                .assume_owned_or_err(self.py())
182                .cast_into_unchecked()
183        }
184    }
185
186    #[inline]
187    fn items(&self) -> PyResult<Bound<'py, PyList>> {
188        unsafe {
189            ffi::PyMapping_Items(self.as_ptr())
190                .assume_owned_or_err(self.py())
191                .cast_into_unchecked()
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use std::collections::HashMap;
199
200    use crate::{exceptions::PyKeyError, types::PyTuple};
201
202    use super::*;
203    use crate::conversion::IntoPyObject;
204
205    #[test]
206    fn test_len() {
207        Python::attach(|py| {
208            let mut v = HashMap::<i32, i32>::new();
209            let ob = (&v).into_pyobject(py).unwrap();
210            let mapping = ob.cast::<PyMapping>().unwrap();
211            assert_eq!(0, mapping.len().unwrap());
212            assert!(mapping.is_empty().unwrap());
213
214            v.insert(7, 32);
215            let ob = v.into_pyobject(py).unwrap();
216            let mapping2 = ob.cast::<PyMapping>().unwrap();
217            assert_eq!(1, mapping2.len().unwrap());
218            assert!(!mapping2.is_empty().unwrap());
219        });
220    }
221
222    #[test]
223    fn test_contains() {
224        Python::attach(|py| {
225            let mut v = HashMap::new();
226            v.insert("key0", 1234);
227            let ob = v.into_pyobject(py).unwrap();
228            let mapping = ob.cast::<PyMapping>().unwrap();
229            mapping.set_item("key1", "foo").unwrap();
230
231            assert!(mapping.contains("key0").unwrap());
232            assert!(mapping.contains("key1").unwrap());
233            assert!(!mapping.contains("key2").unwrap());
234        });
235    }
236
237    #[test]
238    fn test_get_item() {
239        Python::attach(|py| {
240            let mut v = HashMap::new();
241            v.insert(7, 32);
242            let ob = v.into_pyobject(py).unwrap();
243            let mapping = ob.cast::<PyMapping>().unwrap();
244            assert_eq!(
245                32,
246                mapping.get_item(7i32).unwrap().extract::<i32>().unwrap()
247            );
248            assert!(mapping
249                .get_item(8i32)
250                .unwrap_err()
251                .is_instance_of::<PyKeyError>(py));
252        });
253    }
254
255    #[test]
256    fn test_set_item() {
257        Python::attach(|py| {
258            let mut v = HashMap::new();
259            v.insert(7, 32);
260            let ob = v.into_pyobject(py).unwrap();
261            let mapping = ob.cast::<PyMapping>().unwrap();
262            assert!(mapping.set_item(7i32, 42i32).is_ok()); // change
263            assert!(mapping.set_item(8i32, 123i32).is_ok()); // insert
264            assert_eq!(
265                42i32,
266                mapping.get_item(7i32).unwrap().extract::<i32>().unwrap()
267            );
268            assert_eq!(
269                123i32,
270                mapping.get_item(8i32).unwrap().extract::<i32>().unwrap()
271            );
272        });
273    }
274
275    #[test]
276    fn test_del_item() {
277        Python::attach(|py| {
278            let mut v = HashMap::new();
279            v.insert(7, 32);
280            let ob = v.into_pyobject(py).unwrap();
281            let mapping = ob.cast::<PyMapping>().unwrap();
282            assert!(mapping.del_item(7i32).is_ok());
283            assert_eq!(0, mapping.len().unwrap());
284            assert!(mapping
285                .get_item(7i32)
286                .unwrap_err()
287                .is_instance_of::<PyKeyError>(py));
288        });
289    }
290
291    #[test]
292    fn test_items() {
293        Python::attach(|py| {
294            let mut v = HashMap::new();
295            v.insert(7, 32);
296            v.insert(8, 42);
297            v.insert(9, 123);
298            let ob = v.into_pyobject(py).unwrap();
299            let mapping = ob.cast::<PyMapping>().unwrap();
300            // Can't just compare against a vector of tuples since we don't have a guaranteed ordering.
301            let mut key_sum = 0;
302            let mut value_sum = 0;
303            for el in mapping.items().unwrap().try_iter().unwrap() {
304                let tuple = el.unwrap().cast_into::<PyTuple>().unwrap();
305                key_sum += tuple.get_item(0).unwrap().extract::<i32>().unwrap();
306                value_sum += tuple.get_item(1).unwrap().extract::<i32>().unwrap();
307            }
308            assert_eq!(7 + 8 + 9, key_sum);
309            assert_eq!(32 + 42 + 123, value_sum);
310        });
311    }
312
313    #[test]
314    fn test_keys() {
315        Python::attach(|py| {
316            let mut v = HashMap::new();
317            v.insert(7, 32);
318            v.insert(8, 42);
319            v.insert(9, 123);
320            let ob = v.into_pyobject(py).unwrap();
321            let mapping = ob.cast::<PyMapping>().unwrap();
322            // Can't just compare against a vector of tuples since we don't have a guaranteed ordering.
323            let mut key_sum = 0;
324            for el in mapping.keys().unwrap().try_iter().unwrap() {
325                key_sum += el.unwrap().extract::<i32>().unwrap();
326            }
327            assert_eq!(7 + 8 + 9, key_sum);
328        });
329    }
330
331    #[test]
332    fn test_values() {
333        Python::attach(|py| {
334            let mut v = HashMap::new();
335            v.insert(7, 32);
336            v.insert(8, 42);
337            v.insert(9, 123);
338            let ob = v.into_pyobject(py).unwrap();
339            let mapping = ob.cast::<PyMapping>().unwrap();
340            // Can't just compare against a vector of tuples since we don't have a guaranteed ordering.
341            let mut values_sum = 0;
342            for el in mapping.values().unwrap().try_iter().unwrap() {
343                values_sum += el.unwrap().extract::<i32>().unwrap();
344            }
345            assert_eq!(32 + 42 + 123, values_sum);
346        });
347    }
348
349    #[test]
350    fn test_type_object() {
351        Python::attach(|py| {
352            let abc = PyMapping::type_object(py);
353            assert!(PyDict::new(py).is_instance(&abc).unwrap());
354        })
355    }
356}