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(_py: Python<'_>, m: &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::types::*; |
91 | use crate::{FromPyObject, IntoPy, PyErr, PyObject, Python, ToPyObject}; |
92 | use std::{cmp, hash}; |
93 | |
94 | impl<K, V, H> ToPyObject for indexmap::IndexMap<K, V, H> |
95 | where |
96 | K: hash::Hash + cmp::Eq + ToPyObject, |
97 | V: ToPyObject, |
98 | H: hash::BuildHasher, |
99 | { |
100 | fn to_object(&self, py: Python<'_>) -> PyObject { |
101 | IntoPyDict::into_py_dict(self, py).into() |
102 | } |
103 | } |
104 | |
105 | impl<K, V, H> IntoPy<PyObject> for indexmap::IndexMap<K, V, H> |
106 | where |
107 | K: hash::Hash + cmp::Eq + IntoPy<PyObject>, |
108 | V: IntoPy<PyObject>, |
109 | H: hash::BuildHasher, |
110 | { |
111 | fn into_py(self, py: Python<'_>) -> PyObject { |
112 | let iter: impl Iterator- , …)>
= self |
113 | .into_iter() |
114 | .map(|(k: K, v: V)| (k.into_py(py), v.into_py(py))); |
115 | IntoPyDict::into_py_dict(self:iter, py).into() |
116 | } |
117 | } |
118 | |
119 | impl<'source, K, V, S> FromPyObject<'source> for indexmap::IndexMap<K, V, S> |
120 | where |
121 | K: FromPyObject<'source> + cmp::Eq + hash::Hash, |
122 | V: FromPyObject<'source>, |
123 | S: hash::BuildHasher + Default, |
124 | { |
125 | fn extract(ob: &'source PyAny) -> Result<Self, PyErr> { |
126 | let dict: &PyDict = ob.downcast()?; |
127 | let mut ret: IndexMap = indexmap::IndexMap::with_capacity_and_hasher(n:dict.len(), S::default()); |
128 | for (k: &PyAny, v: &PyAny) in dict { |
129 | ret.insert(K::extract(k)?, V::extract(ob:v)?); |
130 | } |
131 | Ok(ret) |
132 | } |
133 | } |
134 | |
135 | #[cfg (test)] |
136 | mod test_indexmap { |
137 | |
138 | use crate::types::*; |
139 | use crate::{IntoPy, PyObject, Python, ToPyObject}; |
140 | |
141 | #[test ] |
142 | fn test_indexmap_indexmap_to_python() { |
143 | Python::with_gil(|py| { |
144 | let mut map = indexmap::IndexMap::<i32, i32>::new(); |
145 | map.insert(1, 1); |
146 | |
147 | let m = map.to_object(py); |
148 | let py_map: &PyDict = m.downcast(py).unwrap(); |
149 | |
150 | assert!(py_map.len() == 1); |
151 | assert!( |
152 | py_map |
153 | .get_item(1) |
154 | .unwrap() |
155 | .unwrap() |
156 | .extract::<i32>() |
157 | .unwrap() |
158 | == 1 |
159 | ); |
160 | assert_eq!( |
161 | map, |
162 | py_map.extract::<indexmap::IndexMap::<i32, i32>>().unwrap() |
163 | ); |
164 | }); |
165 | } |
166 | |
167 | #[test ] |
168 | fn test_indexmap_indexmap_into_python() { |
169 | Python::with_gil(|py| { |
170 | let mut map = indexmap::IndexMap::<i32, i32>::new(); |
171 | map.insert(1, 1); |
172 | |
173 | let m: PyObject = map.into_py(py); |
174 | let py_map: &PyDict = m.downcast(py).unwrap(); |
175 | |
176 | assert!(py_map.len() == 1); |
177 | assert!( |
178 | py_map |
179 | .get_item(1) |
180 | .unwrap() |
181 | .unwrap() |
182 | .extract::<i32>() |
183 | .unwrap() |
184 | == 1 |
185 | ); |
186 | }); |
187 | } |
188 | |
189 | #[test ] |
190 | fn test_indexmap_indexmap_into_dict() { |
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_py_dict(py); |
196 | |
197 | assert_eq!(py_map.len(), 1); |
198 | assert_eq!( |
199 | py_map |
200 | .get_item(1) |
201 | .unwrap() |
202 | .unwrap() |
203 | .extract::<i32>() |
204 | .unwrap(), |
205 | 1 |
206 | ); |
207 | }); |
208 | } |
209 | |
210 | #[test ] |
211 | fn test_indexmap_indexmap_insertion_order_round_trip() { |
212 | Python::with_gil(|py| { |
213 | let n = 20; |
214 | let mut map = indexmap::IndexMap::<i32, i32>::new(); |
215 | |
216 | for i in 1..=n { |
217 | if i % 2 == 1 { |
218 | map.insert(i, i); |
219 | } else { |
220 | map.insert(n - i, i); |
221 | } |
222 | } |
223 | |
224 | let py_map = map.clone().into_py_dict(py); |
225 | |
226 | let trip_map = py_map.extract::<indexmap::IndexMap<i32, i32>>().unwrap(); |
227 | |
228 | for (((k1, v1), (k2, v2)), (k3, v3)) in |
229 | map.iter().zip(py_map.iter()).zip(trip_map.iter()) |
230 | { |
231 | let k2 = k2.extract::<i32>().unwrap(); |
232 | let v2 = v2.extract::<i32>().unwrap(); |
233 | assert_eq!((k1, v1), (&k2, &v2)); |
234 | assert_eq!((k1, v1), (k3, v3)); |
235 | assert_eq!((&k2, &v2), (k3, v3)); |
236 | } |
237 | }); |
238 | } |
239 | } |
240 | |