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::{type_hint_identifier, PyStaticExpr};
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: PyStaticExpr = type_hint_identifier!("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 crate::platform::HashMap;
199    use crate::{exceptions::PyKeyError, types::PyTuple};
200
201    use super::*;
202    use crate::conversion::IntoPyObject;
203
204    #[test]
205    fn test_len() {
206        Python::attach(|py| {
207            let mut v = HashMap::<i32, i32>::new();
208            let ob = (&v).into_pyobject(py).unwrap();
209            let mapping = ob.cast::<PyMapping>().unwrap();
210            assert_eq!(0, mapping.len().unwrap());
211            assert!(mapping.is_empty().unwrap());
212
213            v.insert(7, 32);
214            let ob = v.into_pyobject(py).unwrap();
215            let mapping2 = ob.cast::<PyMapping>().unwrap();
216            assert_eq!(1, mapping2.len().unwrap());
217            assert!(!mapping2.is_empty().unwrap());
218        });
219    }
220
221    #[test]
222    fn test_contains() {
223        Python::attach(|py| {
224            let mut v = HashMap::new();
225            v.insert("key0", 1234);
226            let ob = v.into_pyobject(py).unwrap();
227            let mapping = ob.cast::<PyMapping>().unwrap();
228            mapping.set_item("key1", "foo").unwrap();
229
230            assert!(mapping.contains("key0").unwrap());
231            assert!(mapping.contains("key1").unwrap());
232            assert!(!mapping.contains("key2").unwrap());
233        });
234    }
235
236    #[test]
237    fn test_get_item() {
238        Python::attach(|py| {
239            let mut v = HashMap::new();
240            v.insert(7, 32);
241            let ob = v.into_pyobject(py).unwrap();
242            let mapping = ob.cast::<PyMapping>().unwrap();
243            assert_eq!(
244                32,
245                mapping.get_item(7i32).unwrap().extract::<i32>().unwrap()
246            );
247            assert!(mapping
248                .get_item(8i32)
249                .unwrap_err()
250                .is_instance_of::<PyKeyError>(py));
251        });
252    }
253
254    #[test]
255    fn test_set_item() {
256        Python::attach(|py| {
257            let mut v = HashMap::new();
258            v.insert(7, 32);
259            let ob = v.into_pyobject(py).unwrap();
260            let mapping = ob.cast::<PyMapping>().unwrap();
261            assert!(mapping.set_item(7i32, 42i32).is_ok()); // change
262            assert!(mapping.set_item(8i32, 123i32).is_ok()); // insert
263            assert_eq!(
264                42i32,
265                mapping.get_item(7i32).unwrap().extract::<i32>().unwrap()
266            );
267            assert_eq!(
268                123i32,
269                mapping.get_item(8i32).unwrap().extract::<i32>().unwrap()
270            );
271        });
272    }
273
274    #[test]
275    fn test_del_item() {
276        Python::attach(|py| {
277            let mut v = HashMap::new();
278            v.insert(7, 32);
279            let ob = v.into_pyobject(py).unwrap();
280            let mapping = ob.cast::<PyMapping>().unwrap();
281            assert!(mapping.del_item(7i32).is_ok());
282            assert_eq!(0, mapping.len().unwrap());
283            assert!(mapping
284                .get_item(7i32)
285                .unwrap_err()
286                .is_instance_of::<PyKeyError>(py));
287        });
288    }
289
290    #[test]
291    fn test_items() {
292        Python::attach(|py| {
293            let mut v = HashMap::new();
294            v.insert(7, 32);
295            v.insert(8, 42);
296            v.insert(9, 123);
297            let ob = v.into_pyobject(py).unwrap();
298            let mapping = ob.cast::<PyMapping>().unwrap();
299            // Can't just compare against a vector of tuples since we don't have a guaranteed ordering.
300            let mut key_sum = 0;
301            let mut value_sum = 0;
302            for el in mapping.items().unwrap().try_iter().unwrap() {
303                let tuple = el.unwrap().cast_into::<PyTuple>().unwrap();
304                key_sum += tuple.get_item(0).unwrap().extract::<i32>().unwrap();
305                value_sum += tuple.get_item(1).unwrap().extract::<i32>().unwrap();
306            }
307            assert_eq!(7 + 8 + 9, key_sum);
308            assert_eq!(32 + 42 + 123, value_sum);
309        });
310    }
311
312    #[test]
313    fn test_keys() {
314        Python::attach(|py| {
315            let mut v = HashMap::new();
316            v.insert(7, 32);
317            v.insert(8, 42);
318            v.insert(9, 123);
319            let ob = v.into_pyobject(py).unwrap();
320            let mapping = ob.cast::<PyMapping>().unwrap();
321            // Can't just compare against a vector of tuples since we don't have a guaranteed ordering.
322            let mut key_sum = 0;
323            for el in mapping.keys().unwrap().try_iter().unwrap() {
324                key_sum += el.unwrap().extract::<i32>().unwrap();
325            }
326            assert_eq!(7 + 8 + 9, key_sum);
327        });
328    }
329
330    #[test]
331    fn test_values() {
332        Python::attach(|py| {
333            let mut v = HashMap::new();
334            v.insert(7, 32);
335            v.insert(8, 42);
336            v.insert(9, 123);
337            let ob = v.into_pyobject(py).unwrap();
338            let mapping = ob.cast::<PyMapping>().unwrap();
339            // Can't just compare against a vector of tuples since we don't have a guaranteed ordering.
340            let mut values_sum = 0;
341            for el in mapping.values().unwrap().try_iter().unwrap() {
342                values_sum += el.unwrap().extract::<i32>().unwrap();
343            }
344            assert_eq!(32 + 42 + 123, values_sum);
345        });
346    }
347
348    #[test]
349    fn test_type_object() {
350        Python::attach(|py| {
351            let abc = PyMapping::type_object(py);
352            assert!(PyDict::new(py).is_instance(&abc).unwrap());
353        })
354    }
355}