Skip to content

Commit

Permalink
Merge pull request #1844 from davidhewitt/mapping-type
Browse files Browse the repository at this point in the history
types: add PyMapping
  • Loading branch information
davidhewitt authored Sep 26, 2021
2 parents 4a34cc6 + d929916 commit 16ac7d4
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add `PyAny::py` as a convenience for `PyNativeType::py`. [#1751](https://github.com/PyO3/pyo3/pull/1751)
- Add implementation of `std::ops::Index<usize>` for `PyList`, `PyTuple` and `PySequence`. [#1825](https://github.com/PyO3/pyo3/pull/1825)
- Add range indexing implementations of `std::ops::Index` for `PyList`, `PyTuple` and `PySequence`. [#1829](https://github.com/PyO3/pyo3/pull/1829)
- Add `PyMapping` type to represent the Python mapping protocol. [#1844](https://github.com/PyO3/pyo3/pull/1844)
- Add commonly-used sequence methods to `PyList` and `PyTuple`. [#1849](https://github.com/PyO3/pyo3/pull/1849)
- Add `as_sequence` methods to `PyList` and `PyTuple`. [#1860](https://github.com/PyO3/pyo3/pull/1860)

Expand Down
1 change: 1 addition & 0 deletions guide/src/conversions/tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ The table below contains the Python type and the corresponding function argument
| `datetime.timedelta` | - | `&PyDelta` |
| `typing.Optional[T]` | `Option<T>` | - |
| `typing.Sequence[T]` | `Vec<T>` | `&PySequence` |
| `typing.Mapping[K, V]` | `HashMap<K, V>`, `BTreeMap<K, V>`, `hashbrown::HashMap<K, V>`[^2], `indexmap::IndexMap<K, V>`[^3] | `&PyMapping` |
| `typing.Iterator[Any]` | - | `&PyIterator` |
| `typing.Union[...]` | See [`#[derive(FromPyObject)]`](traits.html#deriving-a-hrefhttpsdocsrspyo3latestpyo3conversiontraitfrompyobjecthtmlfrompyobjecta-for-enums) | - |

Expand Down
3 changes: 3 additions & 0 deletions src/ffi/abstract_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ extern "C" {
pub fn PyObject_GetItem(o: *mut PyObject, key: *mut PyObject) -> *mut PyObject;
#[cfg_attr(PyPy, link_name = "PyPyObject_SetItem")]
pub fn PyObject_SetItem(o: *mut PyObject, key: *mut PyObject, v: *mut PyObject) -> c_int;
#[cfg_attr(PyPy, link_name = "PyPyObject_DelItemString")]
pub fn PyObject_DelItemString(o: *mut PyObject, key: *const c_char) -> c_int;
#[cfg_attr(PyPy, link_name = "PyPyObject_DelItem")]
pub fn PyObject_DelItem(o: *mut PyObject, key: *mut PyObject) -> c_int;
}

Expand Down Expand Up @@ -300,6 +302,7 @@ pub unsafe fn PyMapping_DelItem(o: *mut PyObject, key: *mut PyObject) -> c_int {
extern "C" {
#[cfg_attr(PyPy, link_name = "PyPyMapping_HasKeyString")]
pub fn PyMapping_HasKeyString(o: *mut PyObject, key: *const c_char) -> c_int;
#[cfg_attr(PyPy, link_name = "PyPyMapping_HasKey")]
pub fn PyMapping_HasKey(o: *mut PyObject, key: *mut PyObject) -> c_int;
#[cfg_attr(PyPy, link_name = "PyPyMapping_Keys")]
pub fn PyMapping_Keys(o: *mut PyObject) -> *mut PyObject;
Expand Down
28 changes: 28 additions & 0 deletions src/types/dict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use std::collections::{BTreeMap, HashMap};
use std::ptr::NonNull;
use std::{cmp, collections, hash};

use super::PyMapping;

/// Represents a Python `dict`.
#[repr(transparent)]
pub struct PyDict(PyAny);
Expand Down Expand Up @@ -178,6 +180,11 @@ impl PyDict {
pos: 0,
}
}

/// Returns `self` cast as a `PyMapping`.
pub fn as_mapping(&self) -> &PyMapping {
unsafe { PyMapping::try_from_unchecked(self) }
}
}

pub struct PyDictIterator<'py> {
Expand Down Expand Up @@ -762,4 +769,25 @@ mod tests {
assert_eq!(py_map.get_item("b").unwrap().extract::<i32>().unwrap(), 2);
});
}

#[test]
fn dict_as_mapping() {
Python::with_gil(|py| {
let mut map = HashMap::<i32, i32>::new();
map.insert(1, 1);

let py_map = map.into_py_dict(py);

assert_eq!(py_map.as_mapping().len().unwrap(), 1);
assert_eq!(
py_map
.as_mapping()
.get_item(1)
.unwrap()
.extract::<i32>()
.unwrap(),
1
);
});
}
}
259 changes: 259 additions & 0 deletions src/types/mapping.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
// Copyright (c) 2017-present PyO3 Project and Contributors

use crate::err::{PyDowncastError, PyErr, PyResult};
use crate::types::{PyAny, PySequence};
use crate::AsPyPointer;
use crate::{ffi, ToPyObject};
use crate::{PyTryFrom, ToBorrowedObject};

/// Represents a reference to a Python object supporting the mapping protocol.
#[repr(transparent)]
pub struct PyMapping(PyAny);
pyobject_native_type_named!(PyMapping);
pyobject_native_type_extract!(PyMapping);

impl PyMapping {
/// Returns the number of objects in the mapping.
///
/// This is equivalent to the Python expression `len(self)`.
#[inline]
pub fn len(&self) -> PyResult<usize> {
let v = unsafe { ffi::PyMapping_Size(self.as_ptr()) };
if v == -1 {
Err(PyErr::api_call_failed(self.py()))
} else {
Ok(v as usize)
}
}

/// Returns whether the mapping is empty.
#[inline]
pub fn is_empty(&self) -> PyResult<bool> {
self.len().map(|l| l == 0)
}

/// Gets the item in self with key `key`.
///
/// Returns an `Err` if the item with specified key is not found, usually `KeyError`.
///
/// This is equivalent to the Python expression `self[key]`.
#[inline]
pub fn get_item<K>(&self, key: K) -> PyResult<&PyAny>
where
K: ToBorrowedObject,
{
PyAny::get_item(self, key)
}

/// Sets the item in self with key `key`.
///
/// This is equivalent to the Python expression `self[key] = value`.
#[inline]
pub fn set_item<K, V>(&self, key: K, value: V) -> PyResult<()>
where
K: ToPyObject,
V: ToPyObject,
{
PyAny::set_item(self, key, value)
}

/// Deletes the item with key `key`.
///
/// This is equivalent to the Python statement `del self[key]`.
#[inline]
pub fn del_item<K>(&self, key: K) -> PyResult<()>
where
K: ToBorrowedObject,
{
PyAny::del_item(self, key)
}

/// Returns a sequence containing all keys in the mapping.
#[inline]
pub fn keys(&self) -> PyResult<&PySequence> {
unsafe {
self.py()
.from_owned_ptr_or_err(ffi::PyMapping_Keys(self.as_ptr()))
}
}

/// Returns a sequence containing all values in the mapping.
#[inline]
pub fn values(&self) -> PyResult<&PySequence> {
unsafe {
self.py()
.from_owned_ptr_or_err(ffi::PyMapping_Values(self.as_ptr()))
}
}

/// Returns a sequence of tuples of all (key, value) pairs in the mapping.
#[inline]
pub fn items(&self) -> PyResult<&PySequence> {
unsafe {
self.py()
.from_owned_ptr_or_err(ffi::PyMapping_Items(self.as_ptr()))
}
}
}

impl<'v> PyTryFrom<'v> for PyMapping {
fn try_from<V: Into<&'v PyAny>>(value: V) -> Result<&'v PyMapping, PyDowncastError<'v>> {
let value = value.into();
unsafe {
if ffi::PyMapping_Check(value.as_ptr()) != 0 {
Ok(<PyMapping as PyTryFrom>::try_from_unchecked(value))
} else {
Err(PyDowncastError::new(value, "Mapping"))
}
}
}

#[inline]
fn try_from_exact<V: Into<&'v PyAny>>(value: V) -> Result<&'v PyMapping, PyDowncastError<'v>> {
<PyMapping as PyTryFrom>::try_from(value)
}

#[inline]
unsafe fn try_from_unchecked<V: Into<&'v PyAny>>(value: V) -> &'v PyMapping {
let ptr = value.into() as *const _ as *const PyMapping;
&*ptr
}
}

#[cfg(test)]
mod tests {
use std::collections::HashMap;

use crate::{exceptions::PyKeyError, types::PyTuple, Python};

use super::*;

#[test]
fn test_len() {
Python::with_gil(|py| {
let mut v = HashMap::new();
let ob = v.to_object(py);
let mapping = <PyMapping as PyTryFrom>::try_from(ob.as_ref(py)).unwrap();
assert_eq!(0, mapping.len().unwrap());
assert!(mapping.is_empty().unwrap());

v.insert(7, 32);
let ob = v.to_object(py);
let mapping2 = <PyMapping as PyTryFrom>::try_from(ob.as_ref(py)).unwrap();
assert_eq!(1, mapping2.len().unwrap());
assert!(!mapping2.is_empty().unwrap());
});
}

#[test]
fn test_get_item() {
Python::with_gil(|py| {
let mut v = HashMap::new();
v.insert(7, 32);
let ob = v.to_object(py);
let mapping = <PyMapping as PyTryFrom>::try_from(ob.as_ref(py)).unwrap();
assert_eq!(
32,
mapping.get_item(7i32).unwrap().extract::<i32>().unwrap()
);
assert!(mapping
.get_item(8i32)
.unwrap_err()
.is_instance::<PyKeyError>(py));
});
}

#[test]
fn test_set_item() {
Python::with_gil(|py| {
let mut v = HashMap::new();
v.insert(7, 32);
let ob = v.to_object(py);
let mapping = <PyMapping as PyTryFrom>::try_from(ob.as_ref(py)).unwrap();
assert!(mapping.set_item(7i32, 42i32).is_ok()); // change
assert!(mapping.set_item(8i32, 123i32).is_ok()); // insert
assert_eq!(
42i32,
mapping.get_item(7i32).unwrap().extract::<i32>().unwrap()
);
assert_eq!(
123i32,
mapping.get_item(8i32).unwrap().extract::<i32>().unwrap()
);
});
}

#[test]
fn test_del_item() {
Python::with_gil(|py| {
let mut v = HashMap::new();
v.insert(7, 32);
let ob = v.to_object(py);
let mapping = <PyMapping as PyTryFrom>::try_from(ob.as_ref(py)).unwrap();
assert!(mapping.del_item(7i32).is_ok());
assert_eq!(0, mapping.len().unwrap());
assert!(mapping
.get_item(7i32)
.unwrap_err()
.is_instance::<PyKeyError>(py));
});
}

#[test]
fn test_items() {
Python::with_gil(|py| {
let mut v = HashMap::new();
v.insert(7, 32);
v.insert(8, 42);
v.insert(9, 123);
let ob = v.to_object(py);
let mapping = <PyMapping as PyTryFrom>::try_from(ob.as_ref(py)).unwrap();
// Can't just compare against a vector of tuples since we don't have a guaranteed ordering.
let mut key_sum = 0;
let mut value_sum = 0;
for el in mapping.items().unwrap().iter().unwrap() {
let tuple = el.unwrap().cast_as::<PyTuple>().unwrap();
key_sum += tuple.get_item(0).unwrap().extract::<i32>().unwrap();
value_sum += tuple.get_item(1).unwrap().extract::<i32>().unwrap();
}
assert_eq!(7 + 8 + 9, key_sum);
assert_eq!(32 + 42 + 123, value_sum);
});
}

#[test]
fn test_keys() {
Python::with_gil(|py| {
let mut v = HashMap::new();
v.insert(7, 32);
v.insert(8, 42);
v.insert(9, 123);
let ob = v.to_object(py);
let mapping = <PyMapping as PyTryFrom>::try_from(ob.as_ref(py)).unwrap();
// Can't just compare against a vector of tuples since we don't have a guaranteed ordering.
let mut key_sum = 0;
for el in mapping.keys().unwrap().iter().unwrap() {
key_sum += el.unwrap().extract::<i32>().unwrap();
}
assert_eq!(7 + 8 + 9, key_sum);
});
}

#[test]
fn test_values() {
Python::with_gil(|py| {
let mut v = HashMap::new();
v.insert(7, 32);
v.insert(8, 42);
v.insert(9, 123);
let ob = v.to_object(py);
let mapping = <PyMapping as PyTryFrom>::try_from(ob.as_ref(py)).unwrap();
// Can't just compare against a vector of tuples since we don't have a guaranteed ordering.
let mut values_sum = 0;
for el in mapping.values().unwrap().iter().unwrap() {
values_sum += el.unwrap().extract::<i32>().unwrap();
}
assert_eq!(32 + 42 + 123, values_sum);
});
}
}
2 changes: 2 additions & 0 deletions src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub use self::floatob::PyFloat;
pub use self::function::{PyCFunction, PyFunction};
pub use self::iterator::PyIterator;
pub use self::list::PyList;
pub use self::mapping::PyMapping;
pub use self::module::PyModule;
pub use self::num::PyLong;
pub use self::num::PyLong as PyInt;
Expand Down Expand Up @@ -231,6 +232,7 @@ mod floatob;
mod function;
mod iterator;
mod list;
mod mapping;
mod module;
mod num;
mod sequence;
Expand Down

0 comments on commit 16ac7d4

Please sign in to comment.