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 | |