Add optional support for conversion from `indexmap::IndexMap` (#1728)
* Add support to IndexMap * Fix indexmap version to 1.6.2 * Remove code duplication by mistake * Fix ambiguity in test * Minor change for doc.rs * Add to lib.rs docstring * Add indexmap to conversion table * Add indexmap flag in docs.rs action * Add indexmap feature to CI * Add note in changelog * Use with_gil in tests * Move code to src/conversions/indexmap.rs * Add PR number to CHANGELOG Co-authored-by: David Hewitt <1939362+davidhewitt@users.noreply.github.com> * Add round trip test * Fix issue in MSRV Ubuntu build * Fix Github workflow syntax * Yet Another Attempt to Fix MSRV Ubuntu build * Specify hashbrown to avoid ambiguity in CI * Add suggestions * More flexible version for indexmap * Add documentation * Address PR comments * Export indexmap for docs Co-authored-by: David Hewitt <1939362+davidhewitt@users.noreply.github.com>
This commit is contained in:
parent
9ab7b1fad1
commit
bd0e0d808f
|
@ -130,11 +130,13 @@ jobs:
|
|||
id: settings
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::set-output name=all_additive_features::macros num-bigint num-complex hashbrown serde multiple-pymethods"
|
||||
echo "::set-output name=all_additive_features::macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods"
|
||||
|
||||
- if: matrix.msrv == 'MSRV'
|
||||
name: Prepare minimal package versions (MSRV only)
|
||||
run: cargo update -p hashbrown --precise 0.9.1
|
||||
run: |
|
||||
cargo update -p indexmap --precise 1.6.2
|
||||
cargo update -p hashbrown:0.11.2 --precise 0.9.1
|
||||
|
||||
- name: Build docs
|
||||
run: cargo doc --no-deps --no-default-features --features "${{ steps.settings.outputs.all_additive_features }}"
|
||||
|
@ -229,7 +231,7 @@ jobs:
|
|||
profile: minimal
|
||||
components: llvm-tools-preview
|
||||
- run: cargo test --no-default-features --no-fail-fast
|
||||
- run: cargo test --no-default-features --no-fail-fast --features "macros num-bigint num-complex hashbrown serde multiple-pymethods"
|
||||
- run: cargo test --no-default-features --no-fail-fast --features "macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods"
|
||||
- run: cargo test --manifest-path=pyo3-macros-backend/Cargo.toml
|
||||
- run: cargo test --manifest-path=pyo3-build-config/Cargo.toml
|
||||
# can't yet use actions-rs/grcov with source-based coverage: https://github.com/actions-rs/grcov/issues/105
|
||||
|
|
|
@ -44,7 +44,7 @@ jobs:
|
|||
# This adds the docs to gh-pages-build/doc
|
||||
- name: Build the doc
|
||||
run: |
|
||||
cargo +nightly rustdoc --lib --no-default-features --features="macros num-bigint num-complex hashbrown serde multiple-pymethods" -- --cfg docsrs
|
||||
cargo +nightly rustdoc --lib --no-default-features --features="macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods" -- --cfg docsrs
|
||||
cp -r target/doc gh-pages-build/doc
|
||||
echo "<meta http-equiv=refresh content=0;url=pyo3/index.html>" > gh-pages-build/doc/index.html
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Add `indexmap` feature to add `ToPyObject`, `IntoPy` and `FromPyObject` implementations for `indexmap::IndexMap`. [#1728](https://github.com/PyO3/pyo3/pull/1728)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix regression in 0.14.0 rejecting usage of `#[doc(hidden)]` on structs and functions annotated with PyO3 macros. [#1722](https://github.com/PyO3/pyo3/pull/1722)
|
||||
|
|
|
@ -27,6 +27,7 @@ paste = { version = "0.1.18", optional = true }
|
|||
pyo3-macros = { path = "pyo3-macros", version = "=0.14.1", optional = true }
|
||||
unindent = { version = "0.1.4", optional = true }
|
||||
hashbrown = { version = ">= 0.9, < 0.12", optional = true }
|
||||
indexmap = { version = ">= 1.6, < 1.8", optional = true }
|
||||
serde = {version = "1.0", optional = true}
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -117,5 +118,5 @@ members = [
|
|||
|
||||
[package.metadata.docs.rs]
|
||||
no-default-features = true
|
||||
features = ["macros", "num-bigint", "num-complex", "hashbrown", "serde", "multiple-pymethods"]
|
||||
features = ["macros", "num-bigint", "num-complex", "hashbrown", "serde", "multiple-pymethods", "indexmap"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
|
|
@ -20,7 +20,7 @@ The table below contains the Python type and the corresponding function argument
|
|||
| `float` | `f32`, `f64` | `&PyFloat` |
|
||||
| `complex` | `num_complex::Complex`[^1] | `&PyComplex` |
|
||||
| `list[T]` | `Vec<T>` | `&PyList` |
|
||||
| `dict[K, V]` | `HashMap<K, V>`, `BTreeMap<K, V>`, `hashbrown::HashMap<K, V>`[^2] | `&PyDict` |
|
||||
| `dict[K, V]` | `HashMap<K, V>`, `BTreeMap<K, V>`, `hashbrown::HashMap<K, V>`[^2], `indexmap::IndexMap<K, V>`[^3] | `&PyDict` |
|
||||
| `tuple[T, U]` | `(T, U)`, `Vec<T>` | `&PyTuple` |
|
||||
| `set[T]` | `HashSet<T>`, `BTreeSet<T>`, `hashbrown::HashSet<T>`[^2] | `&PySet` |
|
||||
| `frozenset[T]` | `HashSet<T>`, `BTreeSet<T>`, `hashbrown::HashSet<T>`[^2] | `&PyFrozenSet` |
|
||||
|
@ -94,3 +94,5 @@ Finally, the following Rust types are also able to convert to Python as return v
|
|||
[^1]: Requires the `num-complex` optional feature.
|
||||
|
||||
[^2]: Requires the `hashbrown` optional feature.
|
||||
|
||||
[^3]: Requires the `indexmap` optional feature.
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -74,6 +74,10 @@
|
|||
//! [`HashMap`](https://docs.rs/hashbrown/latest/hashbrown/struct.HashMap.html) and
|
||||
//! [`HashSet`](https://docs.rs/hashbrown/latest/hashbrown/struct.HashSet.html) types.
|
||||
//
|
||||
//! - [`indexmap`](crate::indexmap): Enables conversions between Python dictionary and
|
||||
//! [indexmap](https://docs.rs/indexmap)'s
|
||||
//! [`IndexMap`](https://docs.rs/indexmap/latest/indexmap/map/struct.IndexMap.html).
|
||||
//
|
||||
//! - `multiple-pymethods`: Enables the use of multiple
|
||||
//! [`#[pymethods]`](crate::proc_macro::pymethods) blocks per
|
||||
//! [`#[pyclass]`](crate::proc_macro::pyclass). This adds a dependency on the
|
||||
|
@ -303,6 +307,10 @@ pub mod num_bigint;
|
|||
|
||||
pub mod num_complex;
|
||||
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "indexmap")))]
|
||||
#[cfg(feature = "indexmap")]
|
||||
pub use crate::conversions::indexmap;
|
||||
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
|
||||
#[cfg(feature = "serde")]
|
||||
pub mod serde;
|
||||
|
|
Loading…
Reference in New Issue