-
Notifications
You must be signed in to change notification settings - Fork 760
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add optional support for conversion from indexmap::IndexMap
#1728
Merged
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
71ddd50
Add support to IndexMap
IvanIsCoding 2056c94
Fix indexmap version to 1.6.2
IvanIsCoding 42454bb
Remove code duplication by mistake
IvanIsCoding 0e767be
Fix ambiguity in test
IvanIsCoding 0bcca6f
Minor change for doc.rs
IvanIsCoding 5dc9da8
Add to lib.rs docstring
IvanIsCoding 6698fa5
Add indexmap to conversion table
IvanIsCoding f5b9dbd
Add indexmap flag in docs.rs action
IvanIsCoding e44f3de
Add indexmap feature to CI
IvanIsCoding d53d253
Add note in changelog
IvanIsCoding 8b24caf
Use with_gil in tests
IvanIsCoding 4bc1fcf
Move code to src/conversions/indexmap.rs
IvanIsCoding 6a4fb72
Add PR number to CHANGELOG
IvanIsCoding c92e77f
Add round trip test
IvanIsCoding 2b2ce3c
Fix issue in MSRV Ubuntu build
IvanIsCoding 834fe48
Fix Github workflow syntax
IvanIsCoding 6ec680d
Yet Another Attempt to Fix MSRV Ubuntu build
IvanIsCoding 685a3a6
Specify hashbrown to avoid ambiguity in CI
IvanIsCoding 474a5e8
Add suggestions
IvanIsCoding d4b5264
More flexible version for indexmap
IvanIsCoding cdfd3ca
Add documentation
IvanIsCoding 2dbfef5
Address PR comments
IvanIsCoding bc6c6fb
Export indexmap for docs
IvanIsCoding File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
//! Conversions to and from [indexmap](https://docs.rs/indexmap/)’s | ||
//! `IndexMap`. | ||
//! | ||
//! [`indexmap::IndexMap`] is a hash table that is closely compatible with the standard [`std::collections::HashMap`], | ||
//! with the difference that it preserves the insertion order when iterating over keys. It was inspired | ||
//! by Python's 3.6+ dict implementation. | ||
//! | ||
//! Dictionary order is guaranteed to be insertion order in Python, hence IndexMap is a good candidate | ||
//! for maintaining an equivalent behaviour in Rust. | ||
//! | ||
//! # Setup | ||
//! | ||
//! To use this feature, add this to your **`Cargo.toml`**: | ||
//! | ||
//! ```toml | ||
//! [dependencies] | ||
//! # change * to the latest versions | ||
//! indexmap = "*" | ||
// workaround for `extended_key_value_attributes`: https://github.com/rust-lang/rust/issues/82768#issuecomment-803935643 | ||
#![cfg_attr(docsrs, cfg_attr(docsrs, doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"indexmap\"] }")))] | ||
#![cfg_attr( | ||
not(docsrs), | ||
doc = "pyo3 = { version = \"*\", features = [\"indexmap\"] }" | ||
)] | ||
//! ``` | ||
//! | ||
//! Note that you must use compatible versions of indexmap and PyO3. | ||
//! The required indexmap version may vary based on the version of PyO3. | ||
//! | ||
//! # Examples | ||
//! | ||
//! Using [indexmap](https://docs.rs/indexmap) to return a dictionary with some statistics | ||
//! about a list of numbers. Because of the insertion order guarantees, the Python code will | ||
//! always print the same result, matching users' expectations about Python's dict. | ||
//! | ||
//! ```rust | ||
//! use indexmap::{indexmap, IndexMap}; | ||
//! use pyo3::prelude::*; | ||
//! | ||
//! fn median(data: &Vec<i32>) -> f32 { | ||
//! let sorted_data = data.clone().sort(); | ||
//! let mid = data.len() / 2; | ||
//! if (data.len() % 2 == 0) { | ||
//! data[mid] as f32 | ||
//! } | ||
//! else { | ||
//! (data[mid] + data[mid - 1]) as f32 / 2.0 | ||
//! } | ||
//! } | ||
//! | ||
//! fn mean(data: &Vec<i32>) -> f32 { | ||
//! data.iter().sum::<i32>() as f32 / data.len() as f32 | ||
//! } | ||
//! fn mode(data: &Vec<i32>) -> f32 { | ||
//! let mut frequency = IndexMap::new(); // we can use IndexMap as any hash table | ||
//! | ||
//! for &element in data { | ||
//! *frequency.entry(element).or_insert(0) += 1; | ||
//! } | ||
//! | ||
//! frequency | ||
//! .iter() | ||
//! .max_by(|a, b| a.1.cmp(&b.1)) | ||
//! .map(|(k, _v)| *k) | ||
//! .unwrap() as f32 | ||
//! } | ||
//! | ||
//! #[pyfunction] | ||
//! fn calculate_statistics(data: Vec<i32>) -> IndexMap<&'static str, f32> { | ||
//! indexmap!{ | ||
//! "median" => median(&data), | ||
//! "mean" => mean(&data), | ||
//! "mode" => mode(&data), | ||
//! } | ||
//! } | ||
//! | ||
//! #[pymodule] | ||
//! fn my_module(_py: Python, m: &PyModule) -> PyResult<()> { | ||
//! m.add_function(wrap_pyfunction!(calculate_statistics, m)?)?; | ||
//! Ok(()) | ||
//! } | ||
//! ``` | ||
//! | ||
//! Python code: | ||
//! ```python | ||
//! from my_module import calculate_statistics | ||
//! | ||
//! data = [1, 1, 1, 3, 4, 5] | ||
//! print(calculate_statistics(data)) | ||
//! # always prints {"median": 2.0, "mean": 2.5, "mode": 1.0} in the same order | ||
//! # if another hash table was used, the order could be random | ||
//! ``` | ||
|
||
use crate::types::*; | ||
use crate::{FromPyObject, IntoPy, PyErr, PyObject, PyTryFrom, Python, ToPyObject}; | ||
use std::{cmp, hash}; | ||
|
||
impl<K, V, H> ToPyObject for indexmap::IndexMap<K, V, H> | ||
where | ||
K: hash::Hash + cmp::Eq + ToPyObject, | ||
V: ToPyObject, | ||
H: hash::BuildHasher, | ||
{ | ||
fn to_object(&self, py: Python) -> PyObject { | ||
IntoPyDict::into_py_dict(self, py).into() | ||
} | ||
} | ||
|
||
impl<K, V, H> IntoPy<PyObject> for indexmap::IndexMap<K, V, H> | ||
where | ||
K: hash::Hash + cmp::Eq + IntoPy<PyObject>, | ||
V: IntoPy<PyObject>, | ||
H: hash::BuildHasher, | ||
{ | ||
fn into_py(self, py: Python) -> PyObject { | ||
let iter = self | ||
.into_iter() | ||
.map(|(k, v)| (k.into_py(py), v.into_py(py))); | ||
IntoPyDict::into_py_dict(iter, py).into() | ||
} | ||
} | ||
|
||
impl<'source, K, V, S> FromPyObject<'source> for indexmap::IndexMap<K, V, S> | ||
where | ||
K: FromPyObject<'source> + cmp::Eq + hash::Hash, | ||
V: FromPyObject<'source>, | ||
S: hash::BuildHasher + Default, | ||
{ | ||
fn extract(ob: &'source PyAny) -> Result<Self, PyErr> { | ||
let dict = <PyDict as PyTryFrom>::try_from(ob)?; | ||
let mut ret = indexmap::IndexMap::with_capacity_and_hasher(dict.len(), S::default()); | ||
for (k, v) in dict.iter() { | ||
ret.insert(K::extract(k)?, V::extract(v)?); | ||
} | ||
Ok(ret) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test_indexmap { | ||
|
||
use crate::types::*; | ||
use crate::{IntoPy, PyObject, PyTryFrom, Python, ToPyObject}; | ||
|
||
#[test] | ||
fn test_indexmap_indexmap_to_python() { | ||
Python::with_gil(|py| { | ||
let mut map = indexmap::IndexMap::<i32, i32>::new(); | ||
map.insert(1, 1); | ||
|
||
let m = map.to_object(py); | ||
let py_map = <PyDict as PyTryFrom>::try_from(m.as_ref(py)).unwrap(); | ||
|
||
assert!(py_map.len() == 1); | ||
assert!(py_map.get_item(1).unwrap().extract::<i32>().unwrap() == 1); | ||
assert_eq!( | ||
map, | ||
py_map.extract::<indexmap::IndexMap::<i32, i32>>().unwrap() | ||
); | ||
}); | ||
} | ||
|
||
#[test] | ||
fn test_indexmap_indexmap_into_python() { | ||
Python::with_gil(|py| { | ||
let mut map = indexmap::IndexMap::<i32, i32>::new(); | ||
map.insert(1, 1); | ||
|
||
let m: PyObject = map.into_py(py); | ||
let py_map = <PyDict as PyTryFrom>::try_from(m.as_ref(py)).unwrap(); | ||
|
||
assert!(py_map.len() == 1); | ||
assert!(py_map.get_item(1).unwrap().extract::<i32>().unwrap() == 1); | ||
}); | ||
} | ||
|
||
#[test] | ||
fn test_indexmap_indexmap_into_dict() { | ||
Python::with_gil(|py| { | ||
let mut map = indexmap::IndexMap::<i32, i32>::new(); | ||
map.insert(1, 1); | ||
|
||
let py_map = map.into_py_dict(py); | ||
|
||
assert_eq!(py_map.len(), 1); | ||
assert_eq!(py_map.get_item(1).unwrap().extract::<i32>().unwrap(), 1); | ||
}); | ||
} | ||
|
||
#[test] | ||
fn test_indexmap_indexmap_insertion_order_round_trip() { | ||
Python::with_gil(|py| { | ||
let n = 20; | ||
let mut map = indexmap::IndexMap::<i32, i32>::new(); | ||
|
||
for i in 1..=n { | ||
if i % 2 == 1 { | ||
map.insert(i, i); | ||
} else { | ||
map.insert(n - i, i); | ||
} | ||
} | ||
|
||
let py_map = map.clone().into_py_dict(py); | ||
|
||
let trip_map = py_map.extract::<indexmap::IndexMap<i32, i32>>().unwrap(); | ||
|
||
for (((k1, v1), (k2, v2)), (k3, v3)) in | ||
map.iter().zip(py_map.iter()).zip(trip_map.iter()) | ||
{ | ||
let k2 = k2.extract::<i32>().unwrap(); | ||
let v2 = v2.extract::<i32>().unwrap(); | ||
assert_eq!((k1, v1), (&k2, &v2)); | ||
assert_eq!((k1, v1), (k3, v3)); | ||
assert_eq!((&k2, &v2), (k3, v3)); | ||
} | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
//! This module contains conversions between various Rust object and their representation in Python. | ||
|
||
mod array; | ||
#[cfg(feature = "indexmap")] | ||
#[cfg_attr(docsrs, doc(cfg(feature = "indexmap")))] | ||
pub mod indexmap; | ||
mod osstr; | ||
mod path; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 This is great!