Skip to main content

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::{PyStaticExpr, type_hint_identifier};
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::{Py, Python, err, ffi};
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    #[cfg(feature = "experimental-inspect")]
28    const TYPE_HINT: PyStaticExpr = type_hint_identifier!("collections.abc", "Mapping");
29
30    #[inline]
31    #[allow(clippy::redundant_closure_call)]
32    fn type_object_raw(py: Python<'_>) -> *mut ffi::PyTypeObject {
33        static TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();
34        TYPE.import(py, "collections.abc", "Mapping")
35            .unwrap()
36            .as_type_ptr()
37    }
38
39    #[inline]
40    fn is_type_of(object: &Bound<'_, PyAny>) -> bool {
41        // Using `is_instance` for `collections.abc.Mapping` is slow, so provide
42        // optimized case dict as a well-known mapping
43        PyDict::is_type_of(object)
44            || object
45                .is_instance(&Self::type_object(object.py()).into_any())
46                .unwrap_or_else(|err| {
47                    err.write_unraisable(object.py(), Some(object));
48                    false
49                })
50    }
51}
52
53impl PyMapping {
54    /// Register a pyclass as a subclass of `collections.abc.Mapping` (from the Python standard
55    /// library). This is equivalent to `collections.abc.Mapping.register(T)` in Python.
56    /// This registration is required for a pyclass to be castable from `PyAny` to `PyMapping`.
57    pub fn register<T: PyTypeInfo>(py: Python<'_>) -> PyResult<()> {
58        let ty = T::type_object(py);
59        Self::type_object(py).call_method1("register", (ty,))?;
60        Ok(())
61    }
62}
63
64/// Implementation of functionality for [`PyMapping`].
65///
66/// These methods are defined for the `Bound<'py, PyMapping>` smart pointer, so to use method call
67/// syntax these methods are separated into a trait, because stable Rust does not yet support
68/// `arbitrary_self_types`.
69#[doc(alias = "PyMapping")]
70pub trait PyMappingMethods<'py>: crate::sealed::Sealed {
71    /// Returns the number of objects in the mapping.
72    ///
73    /// This is equivalent to the Python expression `len(self)`.
74    fn len(&self) -> PyResult<usize>;
75
76    /// Returns whether the mapping is empty.
77    fn is_empty(&self) -> PyResult<bool>;
78
79    /// Determines if the mapping contains the specified key.
80    ///
81    /// This is equivalent to the Python expression `key in self`.
82    fn contains<K>(&self, key: K) -> PyResult<bool>
83    where
84        K: IntoPyObject<'py>;
85
86    /// Gets the item in self with key `key`.
87    ///
88    /// Returns an `Err` if the item with specified key is not found, usually `KeyError`.
89    ///
90    /// This is equivalent to the Python expression `self[key]`.
91    fn get_item<K>(&self, key: K) -> PyResult<Bound<'py, PyAny>>
92    where
93        K: IntoPyObject<'py>;
94
95    /// Sets the item in self with key `key`.
96    ///
97    /// This is equivalent to the Python expression `self[key] = value`.
98    fn set_item<K, V>(&self, key: K, value: V) -> PyResult<()>
99    where
100        K: IntoPyObject<'py>,
101        V: IntoPyObject<'py>;
102
103    /// Deletes the item with key `key`.
104    ///
105    /// This is equivalent to the Python statement `del self[key]`.
106    fn del_item<K>(&self, key: K) -> PyResult<()>
107    where
108        K: IntoPyObject<'py>;
109
110    /// Returns a list containing all keys in the mapping.
111    fn keys(&self) -> PyResult<Bound<'py, PyList>>;
112
113    /// Returns a list containing all values in the mapping.
114    fn values(&self) -> PyResult<Bound<'py, PyList>>;
115
116    /// Returns a list of all (key, value) pairs in the mapping.
117    fn items(&self) -> PyResult<Bound<'py, PyList>>;
118}
119
120impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> {
121    #[inline]
122    fn len(&self) -> PyResult<usize> {
123        let v = unsafe { ffi::PyMapping_Size(self.as_ptr()) };
124        err::error_on_negative(self.py(), v)?;
125        Ok(v as usize)
126    }
127
128    #[inline]
129    fn is_empty(&self) -> PyResult<bool> {
130        self.len().map(|l| l == 0)
131    }
132
133    fn contains<K>(&self, key: K) -> PyResult<bool>
134    where
135        K: IntoPyObject<'py>,
136    {
137        PyAnyMethods::contains(&**self, key)
138    }
139
140    #[inline]
141    fn get_item<K>(&self, key: K) -> PyResult<Bound<'py, PyAny>>
142    where
143        K: IntoPyObject<'py>,
144    {
145        PyAnyMethods::get_item(&**self, key)
146    }
147
148    #[inline]
149    fn set_item<K, V>(&self, key: K, value: V) -> PyResult<()>
150    where
151        K: IntoPyObject<'py>,
152        V: IntoPyObject<'py>,
153    {
154        PyAnyMethods::set_item(&**self, key, value)
155    }
156
157    #[inline]
158    fn del_item<K>(&self, key: K) -> PyResult<()>
159    where
160        K: IntoPyObject<'py>,
161    {
162        PyAnyMethods::del_item(&**self, key)
163    }
164
165    #[inline]
166    fn keys(&self) -> PyResult<Bound<'py, PyList>> {
167        unsafe {
168            ffi::PyMapping_Keys(self.as_ptr())
169                .assume_owned_or_err(self.py())
170                .cast_into_unchecked()
171        }
172    }
173
174    #[inline]
175    fn values(&self) -> PyResult<Bound<'py, PyList>> {
176        unsafe {
177            ffi::PyMapping_Values(self.as_ptr())
178                .assume_owned_or_err(self.py())
179                .cast_into_unchecked()
180        }
181    }
182
183    #[inline]
184    fn items(&self) -> PyResult<Bound<'py, PyList>> {
185        unsafe {
186            ffi::PyMapping_Items(self.as_ptr())
187                .assume_owned_or_err(self.py())
188                .cast_into_unchecked()
189        }
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use crate::platform::collections::HashMap;
196    use crate::{exceptions::PyKeyError, types::PyTuple};
197
198    use super::*;
199    use crate::conversion::IntoPyObject;
200
201    #[test]
202    fn test_len() {
203        Python::attach(|py| {
204            let mut v = HashMap::<i32, i32>::new();
205            let ob = (&v).into_pyobject(py).unwrap();
206            let mapping = ob.cast::<PyMapping>().unwrap();
207            assert_eq!(0, mapping.len().unwrap());
208            assert!(mapping.is_empty().unwrap());
209
210            v.insert(7, 32);
211            let ob = v.into_pyobject(py).unwrap();
212            let mapping2 = ob.cast::<PyMapping>().unwrap();
213            assert_eq!(1, mapping2.len().unwrap());
214            assert!(!mapping2.is_empty().unwrap());
215        });
216    }
217
218    #[test]
219    fn test_contains() {
220        Python::attach(|py| {
221            let mut v = HashMap::new();
222            v.insert("key0", 1234);
223            let ob = v.into_pyobject(py).unwrap();
224            let mapping = ob.cast::<PyMapping>().unwrap();
225            mapping.set_item("key1", "foo").unwrap();
226
227            assert!(mapping.contains("key0").unwrap());
228            assert!(mapping.contains("key1").unwrap());
229            assert!(!mapping.contains("key2").unwrap());
230        });
231    }
232
233    #[test]
234    fn test_get_item() {
235        Python::attach(|py| {
236            let mut v = HashMap::new();
237            v.insert(7, 32);
238            let ob = v.into_pyobject(py).unwrap();
239            let mapping = ob.cast::<PyMapping>().unwrap();
240            assert_eq!(
241                32,
242                mapping.get_item(7i32).unwrap().extract::<i32>().unwrap()
243            );
244            assert!(
245                mapping
246                    .get_item(8i32)
247                    .unwrap_err()
248                    .is_instance_of::<PyKeyError>(py)
249            );
250        });
251    }
252
253    #[test]
254    fn test_set_item() {
255        Python::attach(|py| {
256            let mut v = HashMap::new();
257            v.insert(7, 32);
258            let ob = v.into_pyobject(py).unwrap();
259            let mapping = ob.cast::<PyMapping>().unwrap();
260            assert!(mapping.set_item(7i32, 42i32).is_ok()); // change
261            assert!(mapping.set_item(8i32, 123i32).is_ok()); // insert
262            assert_eq!(
263                42i32,
264                mapping.get_item(7i32).unwrap().extract::<i32>().unwrap()
265            );
266            assert_eq!(
267                123i32,
268                mapping.get_item(8i32).unwrap().extract::<i32>().unwrap()
269            );
270        });
271    }
272
273    #[test]
274    fn test_del_item() {
275        Python::attach(|py| {
276            let mut v = HashMap::new();
277            v.insert(7, 32);
278            let ob = v.into_pyobject(py).unwrap();
279            let mapping = ob.cast::<PyMapping>().unwrap();
280            assert!(mapping.del_item(7i32).is_ok());
281            assert_eq!(0, mapping.len().unwrap());
282            assert!(
283                mapping
284                    .get_item(7i32)
285                    .unwrap_err()
286                    .is_instance_of::<PyKeyError>(py)
287            );
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}