pyo3/types/
mapping.rs

1use crate::err::PyResult;
2use crate::ffi_ptr_ext::FfiPtrExt;
3use crate::instance::Bound;
4use crate::py_result_ext::PyResultExt;
5use crate::sync::GILOnceCell;
6use crate::type_object::PyTypeInfo;
7use crate::types::any::PyAnyMethods;
8use crate::types::{PyAny, PyDict, PySequence, PyType};
9#[cfg(feature = "gil-refs")]
10use crate::{err::PyDowncastError, PyNativeType};
11use crate::{ffi, Py, PyTypeCheck, Python, ToPyObject};
12
13/// Represents a reference to a Python object supporting the mapping protocol.
14///
15/// Values of this type are accessed via PyO3's smart pointers, e.g. as
16/// [`Py<PyMapping>`][crate::Py] or [`Bound<'py, PyMapping>`][Bound].
17///
18/// For APIs available on mapping objects, see the [`PyMappingMethods`] trait which is implemented for
19/// [`Bound<'py, PyMapping>`][Bound].
20#[repr(transparent)]
21pub struct PyMapping(PyAny);
22pyobject_native_type_named!(PyMapping);
23pyobject_native_type_extract!(PyMapping);
24
25impl PyMapping {
26    /// Register a pyclass as a subclass of `collections.abc.Mapping` (from the Python standard
27    /// library). This is equivalent to `collections.abc.Mapping.register(T)` in Python.
28    /// This registration is required for a pyclass to be downcastable from `PyAny` to `PyMapping`.
29    pub fn register<T: PyTypeInfo>(py: Python<'_>) -> PyResult<()> {
30        let ty = T::type_object_bound(py);
31        get_mapping_abc(py)?.call_method1("register", (ty,))?;
32        Ok(())
33    }
34}
35
36#[cfg(feature = "gil-refs")]
37impl PyMapping {
38    /// Returns the number of objects in the mapping.
39    ///
40    /// This is equivalent to the Python expression `len(self)`.
41    #[inline]
42    pub fn len(&self) -> PyResult<usize> {
43        self.as_borrowed().len()
44    }
45
46    /// Returns whether the mapping is empty.
47    #[inline]
48    pub fn is_empty(&self) -> PyResult<bool> {
49        self.as_borrowed().is_empty()
50    }
51
52    /// Determines if the mapping contains the specified key.
53    ///
54    /// This is equivalent to the Python expression `key in self`.
55    pub fn contains<K>(&self, key: K) -> PyResult<bool>
56    where
57        K: ToPyObject,
58    {
59        self.as_borrowed().contains(key)
60    }
61
62    /// Gets the item in self with key `key`.
63    ///
64    /// Returns an `Err` if the item with specified key is not found, usually `KeyError`.
65    ///
66    /// This is equivalent to the Python expression `self[key]`.
67    #[inline]
68    pub fn get_item<K>(&self, key: K) -> PyResult<&PyAny>
69    where
70        K: ToPyObject,
71    {
72        self.as_borrowed().get_item(key).map(Bound::into_gil_ref)
73    }
74
75    /// Sets the item in self with key `key`.
76    ///
77    /// This is equivalent to the Python expression `self[key] = value`.
78    #[inline]
79    pub fn set_item<K, V>(&self, key: K, value: V) -> PyResult<()>
80    where
81        K: ToPyObject,
82        V: ToPyObject,
83    {
84        self.as_borrowed().set_item(key, value)
85    }
86
87    /// Deletes the item with key `key`.
88    ///
89    /// This is equivalent to the Python statement `del self[key]`.
90    #[inline]
91    pub fn del_item<K>(&self, key: K) -> PyResult<()>
92    where
93        K: ToPyObject,
94    {
95        self.as_borrowed().del_item(key)
96    }
97
98    /// Returns a sequence containing all keys in the mapping.
99    #[inline]
100    pub fn keys(&self) -> PyResult<&PySequence> {
101        self.as_borrowed().keys().map(Bound::into_gil_ref)
102    }
103
104    /// Returns a sequence containing all values in the mapping.
105    #[inline]
106    pub fn values(&self) -> PyResult<&PySequence> {
107        self.as_borrowed().values().map(Bound::into_gil_ref)
108    }
109
110    /// Returns a sequence of tuples of all (key, value) pairs in the mapping.
111    #[inline]
112    pub fn items(&self) -> PyResult<&PySequence> {
113        self.as_borrowed().items().map(Bound::into_gil_ref)
114    }
115}
116
117/// Implementation of functionality for [`PyMapping`].
118///
119/// These methods are defined for the `Bound<'py, PyMapping>` smart pointer, so to use method call
120/// syntax these methods are separated into a trait, because stable Rust does not yet support
121/// `arbitrary_self_types`.
122#[doc(alias = "PyMapping")]
123pub trait PyMappingMethods<'py>: crate::sealed::Sealed {
124    /// Returns the number of objects in the mapping.
125    ///
126    /// This is equivalent to the Python expression `len(self)`.
127    fn len(&self) -> PyResult<usize>;
128
129    /// Returns whether the mapping is empty.
130    fn is_empty(&self) -> PyResult<bool>;
131
132    /// Determines if the mapping contains the specified key.
133    ///
134    /// This is equivalent to the Python expression `key in self`.
135    fn contains<K>(&self, key: K) -> PyResult<bool>
136    where
137        K: ToPyObject;
138
139    /// Gets the item in self with key `key`.
140    ///
141    /// Returns an `Err` if the item with specified key is not found, usually `KeyError`.
142    ///
143    /// This is equivalent to the Python expression `self[key]`.
144    fn get_item<K>(&self, key: K) -> PyResult<Bound<'py, PyAny>>
145    where
146        K: ToPyObject;
147
148    /// Sets the item in self with key `key`.
149    ///
150    /// This is equivalent to the Python expression `self[key] = value`.
151    fn set_item<K, V>(&self, key: K, value: V) -> PyResult<()>
152    where
153        K: ToPyObject,
154        V: ToPyObject;
155
156    /// Deletes the item with key `key`.
157    ///
158    /// This is equivalent to the Python statement `del self[key]`.
159    fn del_item<K>(&self, key: K) -> PyResult<()>
160    where
161        K: ToPyObject;
162
163    /// Returns a sequence containing all keys in the mapping.
164    fn keys(&self) -> PyResult<Bound<'py, PySequence>>;
165
166    /// Returns a sequence containing all values in the mapping.
167    fn values(&self) -> PyResult<Bound<'py, PySequence>>;
168
169    /// Returns a sequence of tuples of all (key, value) pairs in the mapping.
170    fn items(&self) -> PyResult<Bound<'py, PySequence>>;
171}
172
173impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> {
174    #[inline]
175    fn len(&self) -> PyResult<usize> {
176        let v = unsafe { ffi::PyMapping_Size(self.as_ptr()) };
177        crate::err::error_on_minusone(self.py(), v)?;
178        Ok(v as usize)
179    }
180
181    #[inline]
182    fn is_empty(&self) -> PyResult<bool> {
183        self.len().map(|l| l == 0)
184    }
185
186    fn contains<K>(&self, key: K) -> PyResult<bool>
187    where
188        K: ToPyObject,
189    {
190        PyAnyMethods::contains(&**self, key)
191    }
192
193    #[inline]
194    fn get_item<K>(&self, key: K) -> PyResult<Bound<'py, PyAny>>
195    where
196        K: ToPyObject,
197    {
198        PyAnyMethods::get_item(&**self, key)
199    }
200
201    #[inline]
202    fn set_item<K, V>(&self, key: K, value: V) -> PyResult<()>
203    where
204        K: ToPyObject,
205        V: ToPyObject,
206    {
207        PyAnyMethods::set_item(&**self, key, value)
208    }
209
210    #[inline]
211    fn del_item<K>(&self, key: K) -> PyResult<()>
212    where
213        K: ToPyObject,
214    {
215        PyAnyMethods::del_item(&**self, key)
216    }
217
218    #[inline]
219    fn keys(&self) -> PyResult<Bound<'py, PySequence>> {
220        unsafe {
221            ffi::PyMapping_Keys(self.as_ptr())
222                .assume_owned_or_err(self.py())
223                .downcast_into_unchecked()
224        }
225    }
226
227    #[inline]
228    fn values(&self) -> PyResult<Bound<'py, PySequence>> {
229        unsafe {
230            ffi::PyMapping_Values(self.as_ptr())
231                .assume_owned_or_err(self.py())
232                .downcast_into_unchecked()
233        }
234    }
235
236    #[inline]
237    fn items(&self) -> PyResult<Bound<'py, PySequence>> {
238        unsafe {
239            ffi::PyMapping_Items(self.as_ptr())
240                .assume_owned_or_err(self.py())
241                .downcast_into_unchecked()
242        }
243    }
244}
245
246fn get_mapping_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
247    static MAPPING_ABC: GILOnceCell<Py<PyType>> = GILOnceCell::new();
248
249    MAPPING_ABC.get_or_try_init_type_ref(py, "collections.abc", "Mapping")
250}
251
252impl PyTypeCheck for PyMapping {
253    const NAME: &'static str = "Mapping";
254
255    #[inline]
256    fn type_check(object: &Bound<'_, PyAny>) -> bool {
257        // Using `is_instance` for `collections.abc.Mapping` is slow, so provide
258        // optimized case dict as a well-known mapping
259        PyDict::is_type_of_bound(object)
260            || get_mapping_abc(object.py())
261                .and_then(|abc| object.is_instance(abc))
262                .unwrap_or_else(|err| {
263                    err.write_unraisable_bound(object.py(), Some(&object.as_borrowed()));
264                    false
265                })
266    }
267}
268
269#[cfg(feature = "gil-refs")]
270#[allow(deprecated)]
271impl<'v> crate::PyTryFrom<'v> for PyMapping {
272    /// Downcasting to `PyMapping` requires the concrete class to be a subclass (or registered
273    /// subclass) of `collections.abc.Mapping` (from the Python standard library) - i.e.
274    /// `isinstance(<class>, collections.abc.Mapping) == True`.
275    fn try_from<V: Into<&'v PyAny>>(value: V) -> Result<&'v PyMapping, PyDowncastError<'v>> {
276        let value = value.into();
277
278        if PyMapping::type_check(&value.as_borrowed()) {
279            unsafe { return Ok(value.downcast_unchecked()) }
280        }
281
282        Err(PyDowncastError::new(value, "Mapping"))
283    }
284
285    #[inline]
286    fn try_from_exact<V: Into<&'v PyAny>>(value: V) -> Result<&'v PyMapping, PyDowncastError<'v>> {
287        value.into().downcast()
288    }
289
290    #[inline]
291    unsafe fn try_from_unchecked<V: Into<&'v PyAny>>(value: V) -> &'v PyMapping {
292        let ptr = value.into() as *const _ as *const PyMapping;
293        &*ptr
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use std::collections::HashMap;
300
301    use crate::{exceptions::PyKeyError, types::PyTuple};
302
303    use super::*;
304
305    #[test]
306    fn test_len() {
307        Python::with_gil(|py| {
308            let mut v = HashMap::new();
309            let ob = v.to_object(py);
310            let mapping = ob.downcast_bound::<PyMapping>(py).unwrap();
311            assert_eq!(0, mapping.len().unwrap());
312            assert!(mapping.is_empty().unwrap());
313
314            v.insert(7, 32);
315            let ob = v.to_object(py);
316            let mapping2 = ob.downcast_bound::<PyMapping>(py).unwrap();
317            assert_eq!(1, mapping2.len().unwrap());
318            assert!(!mapping2.is_empty().unwrap());
319        });
320    }
321
322    #[test]
323    fn test_contains() {
324        Python::with_gil(|py| {
325            let mut v = HashMap::new();
326            v.insert("key0", 1234);
327            let ob = v.to_object(py);
328            let mapping = ob.downcast_bound::<PyMapping>(py).unwrap();
329            mapping.set_item("key1", "foo").unwrap();
330
331            assert!(mapping.contains("key0").unwrap());
332            assert!(mapping.contains("key1").unwrap());
333            assert!(!mapping.contains("key2").unwrap());
334        });
335    }
336
337    #[test]
338    fn test_get_item() {
339        Python::with_gil(|py| {
340            let mut v = HashMap::new();
341            v.insert(7, 32);
342            let ob = v.to_object(py);
343            let mapping = ob.downcast_bound::<PyMapping>(py).unwrap();
344            assert_eq!(
345                32,
346                mapping.get_item(7i32).unwrap().extract::<i32>().unwrap()
347            );
348            assert!(mapping
349                .get_item(8i32)
350                .unwrap_err()
351                .is_instance_of::<PyKeyError>(py));
352        });
353    }
354
355    #[test]
356    fn test_set_item() {
357        Python::with_gil(|py| {
358            let mut v = HashMap::new();
359            v.insert(7, 32);
360            let ob = v.to_object(py);
361            let mapping = ob.downcast_bound::<PyMapping>(py).unwrap();
362            assert!(mapping.set_item(7i32, 42i32).is_ok()); // change
363            assert!(mapping.set_item(8i32, 123i32).is_ok()); // insert
364            assert_eq!(
365                42i32,
366                mapping.get_item(7i32).unwrap().extract::<i32>().unwrap()
367            );
368            assert_eq!(
369                123i32,
370                mapping.get_item(8i32).unwrap().extract::<i32>().unwrap()
371            );
372        });
373    }
374
375    #[test]
376    fn test_del_item() {
377        Python::with_gil(|py| {
378            let mut v = HashMap::new();
379            v.insert(7, 32);
380            let ob = v.to_object(py);
381            let mapping = ob.downcast_bound::<PyMapping>(py).unwrap();
382            assert!(mapping.del_item(7i32).is_ok());
383            assert_eq!(0, mapping.len().unwrap());
384            assert!(mapping
385                .get_item(7i32)
386                .unwrap_err()
387                .is_instance_of::<PyKeyError>(py));
388        });
389    }
390
391    #[test]
392    fn test_items() {
393        Python::with_gil(|py| {
394            let mut v = HashMap::new();
395            v.insert(7, 32);
396            v.insert(8, 42);
397            v.insert(9, 123);
398            let ob = v.to_object(py);
399            let mapping = ob.downcast_bound::<PyMapping>(py).unwrap();
400            // Can't just compare against a vector of tuples since we don't have a guaranteed ordering.
401            let mut key_sum = 0;
402            let mut value_sum = 0;
403            for el in mapping.items().unwrap().iter().unwrap() {
404                let tuple = el.unwrap().downcast_into::<PyTuple>().unwrap();
405                key_sum += tuple.get_item(0).unwrap().extract::<i32>().unwrap();
406                value_sum += tuple.get_item(1).unwrap().extract::<i32>().unwrap();
407            }
408            assert_eq!(7 + 8 + 9, key_sum);
409            assert_eq!(32 + 42 + 123, value_sum);
410        });
411    }
412
413    #[test]
414    fn test_keys() {
415        Python::with_gil(|py| {
416            let mut v = HashMap::new();
417            v.insert(7, 32);
418            v.insert(8, 42);
419            v.insert(9, 123);
420            let ob = v.to_object(py);
421            let mapping = ob.downcast_bound::<PyMapping>(py).unwrap();
422            // Can't just compare against a vector of tuples since we don't have a guaranteed ordering.
423            let mut key_sum = 0;
424            for el in mapping.keys().unwrap().iter().unwrap() {
425                key_sum += el.unwrap().extract::<i32>().unwrap();
426            }
427            assert_eq!(7 + 8 + 9, key_sum);
428        });
429    }
430
431    #[test]
432    fn test_values() {
433        Python::with_gil(|py| {
434            let mut v = HashMap::new();
435            v.insert(7, 32);
436            v.insert(8, 42);
437            v.insert(9, 123);
438            let ob = v.to_object(py);
439            let mapping = ob.downcast_bound::<PyMapping>(py).unwrap();
440            // Can't just compare against a vector of tuples since we don't have a guaranteed ordering.
441            let mut values_sum = 0;
442            for el in mapping.values().unwrap().iter().unwrap() {
443                values_sum += el.unwrap().extract::<i32>().unwrap();
444            }
445            assert_eq!(32 + 42 + 123, values_sum);
446        });
447    }
448
449    #[test]
450    #[cfg(feature = "gil-refs")]
451    #[allow(deprecated)]
452    fn test_mapping_try_from() {
453        use crate::PyTryFrom;
454        Python::with_gil(|py| {
455            let dict = PyDict::new(py);
456            let _ = <PyMapping as PyTryFrom>::try_from(dict).unwrap();
457            let _ = PyMapping::try_from_exact(dict).unwrap();
458        });
459    }
460}