use crate::err::{PyErr, PyResult};
use crate::ffi_ptr_ext::FfiPtrExt;
use crate::instance::Bound;
#[cfg(Py_LIMITED_API)]
use crate::types::PyAnyMethods;
use crate::types::{PyAny, PyList, PyMapping};
use crate::{BoundObject, IntoPyObject, IntoPyObjectExt, Python, ffi};
#[cfg(Py_LIMITED_API)]
use crate::{
Py,
sync::PyOnceLock,
type_object::PyTypeInfo,
types::{PyType, PyTypeMethods},
};
#[cfg(not(Py_LIMITED_API))]
use core::ptr;
#[repr(transparent)]
pub struct PyFrozenDict(PyAny);
#[cfg(not(Py_LIMITED_API))]
pyobject_native_type_core!(
PyFrozenDict,
pyobject_native_static_type_object!(ffi::PyFrozenDict_Type),
"builtins",
"frozendict",
#checkfunction=ffi::PyFrozenDict_Check
);
#[cfg(Py_LIMITED_API)]
pyobject_native_type_core!(
PyFrozenDict,
|py| {
static TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();
TYPE.import(py, "builtins", "frozendict")
.unwrap()
.as_type_ptr()
},
"builtins",
"frozendict"
);
impl PyFrozenDict {
pub fn new<'py, K, V, I>(py: Python<'py>, iterable: I) -> PyResult<Bound<'py, PyFrozenDict>>
where
I: IntoIterator<Item = (K, V)>,
K: IntoPyObject<'py>,
V: IntoPyObject<'py>,
{
let items = PyList::new(py, iterable)?;
Self::from_sequence(&items)
}
pub fn from_sequence<'py>(seq: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyFrozenDict>> {
let py = seq.py();
#[cfg(Py_LIMITED_API)]
{
PyFrozenDict::type_object(py)
.call1((seq,))
.map(|obj| unsafe { obj.cast_into_unchecked() })
}
#[cfg(not(Py_LIMITED_API))]
{
unsafe {
ffi::PyFrozenDict_New(seq.as_ptr())
.assume_owned_or_err(py)
.map(|obj| obj.cast_into_unchecked())
}
}
}
pub fn empty(py: Python<'_>) -> PyResult<Bound<'_, PyFrozenDict>> {
#[cfg(Py_LIMITED_API)]
{
PyFrozenDict::type_object(py)
.call0()
.map(|obj| unsafe { obj.cast_into_unchecked() })
}
#[cfg(not(Py_LIMITED_API))]
unsafe {
ffi::PyFrozenDict_New(ptr::null_mut())
.assume_owned_or_err(py)
.map(|obj| obj.cast_into_unchecked())
}
}
}
#[doc(alias = "PyFrozenDict")]
pub trait PyFrozenDictMethods<'py>: crate::sealed::Sealed {
fn len(&self) -> usize;
fn is_empty(&self) -> bool;
fn contains<K>(&self, key: K) -> PyResult<bool>
where
K: IntoPyObject<'py>;
fn get_item<K>(&self, key: K) -> PyResult<Option<Bound<'py, PyAny>>>
where
K: IntoPyObject<'py>;
fn keys(&self) -> Bound<'py, PyList>;
fn values(&self) -> Bound<'py, PyList>;
fn items(&self) -> Bound<'py, PyList>;
fn iter(&self) -> BoundFrozenDictIterator<'py>;
fn as_mapping(&self) -> &Bound<'py, PyMapping>;
fn into_mapping(self) -> Bound<'py, PyMapping>;
}
impl<'py> PyFrozenDictMethods<'py> for Bound<'py, PyFrozenDict> {
#[inline]
fn len(&self) -> usize {
unsafe { ffi::PyDict_Size(self.as_ptr()) as usize }
}
#[inline]
fn is_empty(&self) -> bool {
self.len() == 0
}
fn contains<K>(&self, key: K) -> PyResult<bool>
where
K: IntoPyObject<'py>,
{
let py = self.py();
let key = key.into_pyobject_or_pyerr(py)?;
match unsafe { ffi::PyDict_Contains(self.as_ptr(), key.as_ptr()) } {
1 => Ok(true),
0 => Ok(false),
_ => Err(PyErr::fetch(py)),
}
}
fn get_item<K>(&self, key: K) -> PyResult<Option<Bound<'py, PyAny>>>
where
K: IntoPyObject<'py>,
{
let py = self.py();
let key = key.into_pyobject_or_pyerr(py)?;
let mut result: *mut ffi::PyObject = core::ptr::null_mut();
match unsafe { ffi::compat::PyDict_GetItemRef(self.as_ptr(), key.as_ptr(), &mut result) } {
core::ffi::c_int::MIN..=-1 => Err(PyErr::fetch(py)),
0 => Ok(None),
1..=core::ffi::c_int::MAX => {
Ok(Some(unsafe { result.assume_owned_unchecked(py) }))
}
}
}
fn keys(&self) -> Bound<'py, PyList> {
unsafe {
ffi::PyDict_Keys(self.as_ptr())
.assume_owned(self.py())
.cast_into_unchecked()
}
}
fn values(&self) -> Bound<'py, PyList> {
unsafe {
ffi::PyDict_Values(self.as_ptr())
.assume_owned(self.py())
.cast_into_unchecked()
}
}
fn items(&self) -> Bound<'py, PyList> {
unsafe {
ffi::PyDict_Items(self.as_ptr())
.assume_owned(self.py())
.cast_into_unchecked()
}
}
fn iter(&self) -> BoundFrozenDictIterator<'py> {
BoundFrozenDictIterator::new(self.clone())
}
fn as_mapping(&self) -> &Bound<'py, PyMapping> {
unsafe { self.cast_unchecked() }
}
fn into_mapping(self) -> Bound<'py, PyMapping> {
unsafe { self.cast_into_unchecked() }
}
}
pub struct BoundFrozenDictIterator<'py> {
fd: Bound<'py, PyFrozenDict>,
ppos: isize,
remaining: usize,
}
impl<'py> BoundFrozenDictIterator<'py> {
fn new(fd: Bound<'py, PyFrozenDict>) -> Self {
let remaining = fd.len();
BoundFrozenDictIterator {
fd,
ppos: 0,
remaining,
}
}
}
impl<'py> Iterator for BoundFrozenDictIterator<'py> {
type Item = (Bound<'py, PyAny>, Bound<'py, PyAny>);
fn next(&mut self) -> Option<Self::Item> {
let ppos: *mut ffi::Py_ssize_t = &mut self.ppos;
let mut key: *mut ffi::PyObject = core::ptr::null_mut();
let mut value: *mut ffi::PyObject = core::ptr::null_mut();
if unsafe { ffi::PyDict_Next(self.fd.as_ptr(), ppos, &mut key, &mut value) != 0 } {
let py = self.fd.py();
self.remaining -= 1;
Some((
unsafe { key.assume_borrowed_unchecked(py).to_owned() },
unsafe { value.assume_borrowed_unchecked(py).to_owned() },
))
} else {
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.len();
(len, Some(len))
}
#[inline]
fn count(self) -> usize
where
Self: Sized,
{
self.len()
}
}
impl ExactSizeIterator for BoundFrozenDictIterator<'_> {
fn len(&self) -> usize {
self.remaining
}
}
impl<'py> IntoIterator for Bound<'py, PyFrozenDict> {
type Item = (Bound<'py, PyAny>, Bound<'py, PyAny>);
type IntoIter = BoundFrozenDictIterator<'py>;
fn into_iter(self) -> Self::IntoIter {
BoundFrozenDictIterator::new(self)
}
}
impl<'py> IntoIterator for &Bound<'py, PyFrozenDict> {
type Item = (Bound<'py, PyAny>, Bound<'py, PyAny>);
type IntoIter = BoundFrozenDictIterator<'py>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{PyAnyMethods, list::PyListMethods, mapping::PyMappingMethods};
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[test]
fn test_frozendict_new() {
Python::attach(|py| {
let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2)]).unwrap();
assert_eq!(fd.len(), 2);
})
}
#[test]
fn test_frozendict_new_from_iterator() {
Python::attach(|py| {
let mut items = BTreeMap::new();
items.insert("a", 1);
items.insert("b", 2);
let fd = PyFrozenDict::new(py, items).unwrap();
assert_eq!(fd.len(), 2);
assert!(fd.contains("a").unwrap());
assert!(fd.contains("b").unwrap());
})
}
#[test]
fn test_frozendict_new_from_sequence() {
Python::attach(|py| {
let seq = PyList::new(py, vec![("a", 1), ("b", 2)]).unwrap();
let fd = PyFrozenDict::from_sequence(&seq).unwrap();
assert_eq!(fd.len(), 2);
assert!(fd.contains("a").unwrap());
assert!(fd.contains("b").unwrap());
})
}
#[test]
fn test_frozendict_empty() {
Python::attach(|py| {
let fd = PyFrozenDict::empty(py).unwrap();
assert!(fd.is_empty());
assert_eq!(fd.len(), 0);
})
}
#[test]
fn test_frozendict_contains() {
Python::attach(|py| {
let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2)]).unwrap();
assert!(fd.contains("a").unwrap());
assert!(!fd.contains("c").unwrap());
})
}
#[test]
fn test_frozendict_get_item() {
Python::attach(|py| {
let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2)]).unwrap();
let val = fd.get_item("a").unwrap();
assert!(val.is_some_and(|v| v.extract::<i32>().unwrap() == 1));
})
}
#[test]
fn test_frozendict_keys() {
Python::attach(|py| {
let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2)]).unwrap();
let keys = fd.keys();
assert_eq!(keys.len(), 2);
assert!(keys.contains("a").unwrap());
assert!(keys.contains("b").unwrap());
})
}
#[test]
fn test_frozendict_values() {
Python::attach(|py| {
let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2)]).unwrap();
let values = fd.values();
assert_eq!(values.len(), 2);
assert!(values.contains(1).unwrap());
assert!(values.contains(2).unwrap());
})
}
#[test]
fn test_frozendict_items() {
Python::attach(|py| {
let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2)]).unwrap();
let items = fd.items();
assert_eq!(items.len(), 2);
assert!(items.contains(("a", 1)).unwrap());
assert!(items.contains(("b", 2)).unwrap());
})
}
#[test]
fn test_frozendict_iter() {
Python::attach(|py| {
let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2)]).unwrap();
let mut count = 0;
for ((k, v), (expected_k, expected_v)) in fd.iter().zip([("a", 1), ("b", 2)].iter()) {
count += 1;
assert_eq!(
(k.extract::<String>().unwrap(), v.extract::<i32>().unwrap()),
(expected_k.to_string(), *expected_v)
);
}
assert_eq!(count, 2);
})
}
#[test]
fn test_frozendict_iter_size_hint() {
Python::attach(|py| {
let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2)]).unwrap();
let mut iter = fd.iter();
assert_eq!(iter.size_hint(), (2, Some(2)));
iter.next();
assert_eq!(iter.size_hint(), (1, Some(1)));
for _ in &mut iter {}
assert_eq!(iter.size_hint(), (0, Some(0)));
assert!(iter.next().is_none());
})
}
#[test]
fn test_frozendict_into_iter() {
Python::attach(|py| {
let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2)]).unwrap();
let mut items = Vec::new();
for (key, value) in fd {
items.push((
key.extract::<String>().unwrap(),
value.extract::<i32>().unwrap(),
));
}
assert_eq!(items.len(), 2);
assert!(items.contains(&("a".to_string(), 1)));
assert!(items.contains(&("b".to_string(), 2)));
})
}
#[test]
fn test_frozendict_as_mapping() {
Python::attach(|py| {
let fd = PyFrozenDict::new(py, vec![("a", 1)]).unwrap();
let mapping = fd.as_mapping();
assert!(PyMappingMethods::len(mapping).unwrap() == 1);
})
}
}