| 1 | #![cfg (feature = "indexmap" )] |
| 2 | |
| 3 | //! Conversions to and from [indexmap](https://docs.rs/indexmap/)’s |
| 4 | //! `IndexMap`. |
| 5 | //! |
| 6 | //! [`indexmap::IndexMap`] is a hash table that is closely compatible with the standard [`std::collections::HashMap`], |
| 7 | //! with the difference that it preserves the insertion order when iterating over keys. It was inspired |
| 8 | //! by Python's 3.6+ dict implementation. |
| 9 | //! |
| 10 | //! Dictionary order is guaranteed to be insertion order in Python, hence IndexMap is a good candidate |
| 11 | //! for maintaining an equivalent behaviour in Rust. |
| 12 | //! |
| 13 | //! # Setup |
| 14 | //! |
| 15 | //! To use this feature, add this to your **`Cargo.toml`**: |
| 16 | //! |
| 17 | //! ```toml |
| 18 | //! [dependencies] |
| 19 | //! # change * to the latest versions |
| 20 | //! indexmap = "*" |
| 21 | #![doc = concat!("pyo3 = { version = \"" , env!("CARGO_PKG_VERSION" ), " \", features = [ \"indexmap \"] }" )] |
| 22 | //! ``` |
| 23 | //! |
| 24 | //! Note that you must use compatible versions of indexmap and PyO3. |
| 25 | //! The required indexmap version may vary based on the version of PyO3. |
| 26 | //! |
| 27 | //! # Examples |
| 28 | //! |
| 29 | //! Using [indexmap](https://docs.rs/indexmap) to return a dictionary with some statistics |
| 30 | //! about a list of numbers. Because of the insertion order guarantees, the Python code will |
| 31 | //! always print the same result, matching users' expectations about Python's dict. |
| 32 | //! ```rust |
| 33 | //! use indexmap::{indexmap, IndexMap}; |
| 34 | //! use pyo3::prelude::*; |
| 35 | //! |
| 36 | //! fn median(data: &Vec<i32>) -> f32 { |
| 37 | //! let sorted_data = data.clone().sort(); |
| 38 | //! let mid = data.len() / 2; |
| 39 | //! if data.len() % 2 == 0 { |
| 40 | //! data[mid] as f32 |
| 41 | //! } |
| 42 | //! else { |
| 43 | //! (data[mid] + data[mid - 1]) as f32 / 2.0 |
| 44 | //! } |
| 45 | //! } |
| 46 | //! |
| 47 | //! fn mean(data: &Vec<i32>) -> f32 { |
| 48 | //! data.iter().sum::<i32>() as f32 / data.len() as f32 |
| 49 | //! } |
| 50 | //! fn mode(data: &Vec<i32>) -> f32 { |
| 51 | //! let mut frequency = IndexMap::new(); // we can use IndexMap as any hash table |
| 52 | //! |
| 53 | //! for &element in data { |
| 54 | //! *frequency.entry(element).or_insert(0) += 1; |
| 55 | //! } |
| 56 | //! |
| 57 | //! frequency |
| 58 | //! .iter() |
| 59 | //! .max_by(|a, b| a.1.cmp(&b.1)) |
| 60 | //! .map(|(k, _v)| *k) |
| 61 | //! .unwrap() as f32 |
| 62 | //! } |
| 63 | //! |
| 64 | //! #[pyfunction] |
| 65 | //! fn calculate_statistics(data: Vec<i32>) -> IndexMap<&'static str, f32> { |
| 66 | //! indexmap! { |
| 67 | //! "median" => median(&data), |
| 68 | //! "mean" => mean(&data), |
| 69 | //! "mode" => mode(&data), |
| 70 | //! } |
| 71 | //! } |
| 72 | //! |
| 73 | //! #[pymodule] |
| 74 | //! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { |
| 75 | //! m.add_function(wrap_pyfunction!(calculate_statistics, m)?)?; |
| 76 | //! Ok(()) |
| 77 | //! } |
| 78 | //! ``` |
| 79 | //! |
| 80 | //! Python code: |
| 81 | //! ```python |
| 82 | //! from my_module import calculate_statistics |
| 83 | //! |
| 84 | //! data = [1, 1, 1, 3, 4, 5] |
| 85 | //! print(calculate_statistics(data)) |
| 86 | //! # always prints {"median": 2.0, "mean": 2.5, "mode": 1.0} in the same order |
| 87 | //! # if another hash table was used, the order could be random |
| 88 | //! ``` |
| 89 | |
| 90 | use crate::conversion::IntoPyObject; |
| 91 | use crate::types::*; |
| 92 | use crate::{Bound, FromPyObject, PyErr, PyObject, Python}; |
| 93 | #[allow (deprecated)] |
| 94 | use crate::{IntoPy, ToPyObject}; |
| 95 | use std::{cmp, hash}; |
| 96 | |
| 97 | #[allow (deprecated)] |
| 98 | impl<K, V, H> ToPyObject for indexmap::IndexMap<K, V, H> |
| 99 | where |
| 100 | K: hash::Hash + cmp::Eq + ToPyObject, |
| 101 | V: ToPyObject, |
| 102 | H: hash::BuildHasher, |
| 103 | { |
| 104 | fn to_object(&self, py: Python<'_>) -> PyObject { |
| 105 | let dict: Bound<'_, PyDict> = PyDict::new(py); |
| 106 | for (k: &K, v: &V) in self { |
| 107 | dict.set_item(key:k.to_object(py), value:v.to_object(py)).unwrap(); |
| 108 | } |
| 109 | dict.into_any().unbind() |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | #[allow (deprecated)] |
| 114 | impl<K, V, H> IntoPy<PyObject> for indexmap::IndexMap<K, V, H> |
| 115 | where |
| 116 | K: hash::Hash + cmp::Eq + IntoPy<PyObject>, |
| 117 | V: IntoPy<PyObject>, |
| 118 | H: hash::BuildHasher, |
| 119 | { |
| 120 | fn into_py(self, py: Python<'_>) -> PyObject { |
| 121 | let dict: Bound<'_, PyDict> = PyDict::new(py); |
| 122 | for (k: K, v: V) in self { |
| 123 | dict.set_item(key:k.into_py(py), value:v.into_py(py)).unwrap(); |
| 124 | } |
| 125 | dict.into_any().unbind() |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | impl<'py, K, V, H> IntoPyObject<'py> for indexmap::IndexMap<K, V, H> |
| 130 | where |
| 131 | K: IntoPyObject<'py> + cmp::Eq + hash::Hash, |
| 132 | V: IntoPyObject<'py>, |
| 133 | H: hash::BuildHasher, |
| 134 | { |
| 135 | type Target = PyDict; |
| 136 | type Output = Bound<'py, Self::Target>; |
| 137 | type Error = PyErr; |
| 138 | |
| 139 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 140 | let dict: Bound<'_, PyDict> = PyDict::new(py); |
| 141 | for (k: K, v: V) in self { |
| 142 | dict.set_item(key:k, value:v)?; |
| 143 | } |
| 144 | Ok(dict) |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | impl<'a, 'py, K, V, H> IntoPyObject<'py> for &'a indexmap::IndexMap<K, V, H> |
| 149 | where |
| 150 | &'a K: IntoPyObject<'py> + cmp::Eq + hash::Hash, |
| 151 | &'a V: IntoPyObject<'py>, |
| 152 | H: hash::BuildHasher, |
| 153 | { |
| 154 | type Target = PyDict; |
| 155 | type Output = Bound<'py, Self::Target>; |
| 156 | type Error = PyErr; |
| 157 | |
| 158 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 159 | let dict: Bound<'_, PyDict> = PyDict::new(py); |
| 160 | for (k: &'a K, v: &'a V) in self { |
| 161 | dict.set_item(key:k, value:v)?; |
| 162 | } |
| 163 | Ok(dict) |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | impl<'py, K, V, S> FromPyObject<'py> for indexmap::IndexMap<K, V, S> |
| 168 | where |
| 169 | K: FromPyObject<'py> + cmp::Eq + hash::Hash, |
| 170 | V: FromPyObject<'py>, |
| 171 | S: hash::BuildHasher + Default, |
| 172 | { |
| 173 | fn extract_bound(ob: &Bound<'py, PyAny>) -> Result<Self, PyErr> { |
| 174 | let dict: &Bound<'_, PyDict> = ob.downcast::<PyDict>()?; |
| 175 | let mut ret: IndexMap = indexmap::IndexMap::with_capacity_and_hasher(n:dict.len(), S::default()); |
| 176 | for (k: Bound<'py, PyAny>, v: Bound<'py, PyAny>) in dict { |
| 177 | ret.insert(key:k.extract()?, value:v.extract()?); |
| 178 | } |
| 179 | Ok(ret) |
| 180 | } |
| 181 | } |
| 182 | |
| 183 | #[cfg (test)] |
| 184 | mod test_indexmap { |
| 185 | |
| 186 | use crate::types::*; |
| 187 | use crate::{IntoPyObject, Python}; |
| 188 | |
| 189 | #[test ] |
| 190 | fn test_indexmap_indexmap_into_pyobject() { |
| 191 | Python::with_gil(|py| { |
| 192 | let mut map = indexmap::IndexMap::<i32, i32>::new(); |
| 193 | map.insert(1, 1); |
| 194 | |
| 195 | let py_map = (&map).into_pyobject(py).unwrap(); |
| 196 | |
| 197 | assert!(py_map.len() == 1); |
| 198 | assert!( |
| 199 | py_map |
| 200 | .get_item(1) |
| 201 | .unwrap() |
| 202 | .unwrap() |
| 203 | .extract::<i32>() |
| 204 | .unwrap() |
| 205 | == 1 |
| 206 | ); |
| 207 | assert_eq!( |
| 208 | map, |
| 209 | py_map.extract::<indexmap::IndexMap::<i32, i32>>().unwrap() |
| 210 | ); |
| 211 | }); |
| 212 | } |
| 213 | |
| 214 | #[test ] |
| 215 | fn test_indexmap_indexmap_into_dict() { |
| 216 | Python::with_gil(|py| { |
| 217 | let mut map = indexmap::IndexMap::<i32, i32>::new(); |
| 218 | map.insert(1, 1); |
| 219 | |
| 220 | let py_map = map.into_py_dict(py).unwrap(); |
| 221 | |
| 222 | assert_eq!(py_map.len(), 1); |
| 223 | assert_eq!( |
| 224 | py_map |
| 225 | .get_item(1) |
| 226 | .unwrap() |
| 227 | .unwrap() |
| 228 | .extract::<i32>() |
| 229 | .unwrap(), |
| 230 | 1 |
| 231 | ); |
| 232 | }); |
| 233 | } |
| 234 | |
| 235 | #[test ] |
| 236 | fn test_indexmap_indexmap_insertion_order_round_trip() { |
| 237 | Python::with_gil(|py| { |
| 238 | let n = 20; |
| 239 | let mut map = indexmap::IndexMap::<i32, i32>::new(); |
| 240 | |
| 241 | for i in 1..=n { |
| 242 | if i % 2 == 1 { |
| 243 | map.insert(i, i); |
| 244 | } else { |
| 245 | map.insert(n - i, i); |
| 246 | } |
| 247 | } |
| 248 | |
| 249 | let py_map = (&map).into_py_dict(py).unwrap(); |
| 250 | |
| 251 | let trip_map = py_map.extract::<indexmap::IndexMap<i32, i32>>().unwrap(); |
| 252 | |
| 253 | for (((k1, v1), (k2, v2)), (k3, v3)) in |
| 254 | map.iter().zip(py_map.iter()).zip(trip_map.iter()) |
| 255 | { |
| 256 | let k2 = k2.extract::<i32>().unwrap(); |
| 257 | let v2 = v2.extract::<i32>().unwrap(); |
| 258 | assert_eq!((k1, v1), (&k2, &v2)); |
| 259 | assert_eq!((k1, v1), (k3, v3)); |
| 260 | assert_eq!((&k2, &v2), (k3, v3)); |
| 261 | } |
| 262 | }); |
| 263 | } |
| 264 | } |
| 265 | |