diff --git a/newsfragments/6174.added.md b/newsfragments/6174.added.md new file mode 100644 index 00000000000..2570cc8b00f --- /dev/null +++ b/newsfragments/6174.added.md @@ -0,0 +1 @@ +Add support for `PyFrozenDict` on 3.15+ \ No newline at end of file diff --git a/pyo3-ffi-check/definitions/wrapper.h b/pyo3-ffi-check/definitions/wrapper.h index 2c822162b6b..db81a913ecf 100644 --- a/pyo3-ffi-check/definitions/wrapper.h +++ b/pyo3-ffi-check/definitions/wrapper.h @@ -1,5 +1,6 @@ #include "Python.h" #include "datetime.h" +#include "dictobject.h" #include "frameobject.h" #include "structmember.h" #include "marshal.h" diff --git a/pyo3-ffi-check/macro/src/lib.rs b/pyo3-ffi-check/macro/src/lib.rs index 68797771406..4aab8908afb 100644 --- a/pyo3-ffi-check/macro/src/lib.rs +++ b/pyo3-ffi-check/macro/src/lib.rs @@ -45,9 +45,9 @@ pub fn for_all_structs(input: proc_macro::TokenStream) -> proc_macro::TokenStrea .unwrap(); if pyo3_build_config::get().target_abi().version() < PY_3_15 - && struct_name == "PyBytesWriter" + && (struct_name == "PyBytesWriter" || struct_name == "PyFrozenDictObject") { - // PyBytesWriter was added in Python 3.15 + // PyBytesWriter and PyFrozenDictObject were added in Python 3.15 continue; } @@ -247,6 +247,8 @@ const MACRO_EXCLUSIONS: &[(&str, &str)] = &[ // FIXME: for many of these `not(PyPy)` cases, // it seems that PyPy might actually offer symbols which PyO3 // should be using rather than implementing inline functions + ("PyAnyDict_Check", "not(PyPy)"), + ("PyAnyDict_CheckExact", "not(PyPy)"), ("PyAnySet_Check", "not(PyPy)"), ("PyAnySet_CheckExact", "not(PyPy)"), ("PyAsyncGen_CheckExact", ""), @@ -324,6 +326,8 @@ const MACRO_EXCLUSIONS: &[(&str, &str)] = &[ ("PyFloat_CheckExact", "not(PyPy)"), ("PyFrame_Check", ""), ("PyFrameLocalsProxy_Check", ""), + ("PyFrozenDict_Check", "not(PyPy)"), + ("PyFrozenDict_CheckExact", "not(PyPy)"), ("PyFrozenSet_Check", "not(PyPy)"), ("PyFrozenSet_CheckExact", "not(PyPy)"), ("PyFunction_Check", "not(PyPy)"), diff --git a/pyo3-ffi/src/cpython/dictobject.rs b/pyo3-ffi/src/cpython/dictobject.rs index 1bd75d8c6c5..892c0737b48 100644 --- a/pyo3-ffi/src/cpython/dictobject.rs +++ b/pyo3-ffi/src/cpython/dictobject.rs @@ -44,10 +44,11 @@ pub struct PyDictObject { _tmpkeys: *mut PyObject, } -extern_libpython! { - #[cfg(Py_3_15)] - pub fn PyFrozenDict_New(iterable: *mut PyObject) -> *mut PyObject; -} +#[cfg(Py_3_15)] +#[cfg(not(GraalPy))] +opaque_struct!(pub PyFrozenDictObject); + +// PyFrozenDict_Type and PyFrozenDict_New are defined in the public dictobject.rs module // skipped private _PyDict_GetItem_KnownHash // skipped private _PyDict_GetItemStringWithError diff --git a/pyo3-ffi/src/dictobject.rs b/pyo3-ffi/src/dictobject.rs index fec8f459e75..dd5604d700c 100644 --- a/pyo3-ffi/src/dictobject.rs +++ b/pyo3-ffi/src/dictobject.rs @@ -20,6 +20,41 @@ pub unsafe fn PyDict_CheckExact(op: *mut PyObject) -> c_int { Py_IS_TYPE(op, &raw mut PyDict_Type) } +#[cfg(Py_3_15)] +#[cfg(not(RustPython))] +extern_libpython! { + pub static mut PyFrozenDict_Type: PyTypeObject; +} + +#[inline] +#[cfg(Py_3_15)] +#[cfg(not(RustPython))] +pub unsafe fn PyFrozenDict_CheckExact(op: *mut PyObject) -> c_int { + (Py_TYPE(op) == &raw mut PyFrozenDict_Type) as c_int +} + +#[inline] +#[cfg(Py_3_15)] +#[cfg(not(RustPython))] +pub unsafe fn PyFrozenDict_Check(op: *mut PyObject) -> c_int { + (Py_TYPE(op) == &raw mut PyFrozenDict_Type + || PyType_IsSubtype(Py_TYPE(op), &raw mut PyFrozenDict_Type) != 0) as c_int +} + +#[inline] +#[cfg(Py_3_15)] +#[cfg(not(RustPython))] +pub unsafe fn PyAnyDict_Check(op: *mut PyObject) -> c_int { + (PyDict_Check(op) != 0 || PyFrozenDict_Check(op) != 0) as c_int +} + +#[inline] +#[cfg(Py_3_15)] +#[cfg(not(RustPython))] +pub unsafe fn PyAnyDict_CheckExact(op: *mut PyObject) -> c_int { + (Py_TYPE(op) == &raw mut PyDict_Type || Py_TYPE(op) == &raw mut PyFrozenDict_Type) as c_int +} + extern_libpython! { #[cfg(RustPython)] pub fn PyDict_Check(op: *mut PyObject) -> c_int; @@ -28,6 +63,8 @@ extern_libpython! { #[cfg_attr(PyPy, link_name = "PyPyDict_New")] pub fn PyDict_New() -> *mut PyObject; + #[cfg(Py_3_15)] + pub fn PyFrozenDict_New(iterable: *mut PyObject) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyDict_GetItem")] pub fn PyDict_GetItem(mp: *mut PyObject, key: *mut PyObject) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyDict_GetItemWithError")] diff --git a/src/prelude.rs b/src/prelude.rs index 33a6f23534e..2632d98545e 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -33,6 +33,8 @@ pub use crate::types::capsule::PyCapsuleMethods; pub use crate::types::complex::PyComplexMethods; pub use crate::types::dict::PyDictMethods; pub use crate::types::float::PyFloatMethods; +#[cfg(Py_3_15)] +pub use crate::types::frozendict::PyFrozenDictMethods; pub use crate::types::frozenset::PyFrozenSetMethods; pub use crate::types::list::PyListMethods; pub use crate::types::mapping::PyMappingMethods; diff --git a/src/sealed.rs b/src/sealed.rs index 0155aa93349..2f909c7451a 100644 --- a/src/sealed.rs +++ b/src/sealed.rs @@ -2,8 +2,8 @@ use crate::impl_::pyfunction::PyFunctionDef; #[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] use crate::types::PyFrame; use crate::types::{ - PyBool, PyByteArray, PyBytes, PyCapsule, PyComplex, PyDict, PyFloat, PyFrozenSet, PyList, - PyMapping, PyMappingProxy, PyModule, PyRange, PySequence, PySet, PySlice, PyString, + PyBool, PyByteArray, PyBytes, PyCapsule, PyComplex, PyDict, PyFloat, PyFrozenDict, PyFrozenSet, + PyList, PyMapping, PyMappingProxy, PyModule, PyRange, PySequence, PySet, PySlice, PyString, PyTraceback, PyTuple, PyType, PyWeakref, PyWeakrefProxy, PyWeakrefReference, }; use crate::{ffi, Bound, PyAny, PyResult}; @@ -33,6 +33,7 @@ impl Sealed for Bound<'_, PyCapsule> {} impl Sealed for Bound<'_, PyComplex> {} impl Sealed for Bound<'_, PyDict> {} impl Sealed for Bound<'_, PyFloat> {} +impl Sealed for Bound<'_, PyFrozenDict> {} impl Sealed for Bound<'_, PyFrozenSet> {} impl Sealed for Bound<'_, PyList> {} impl Sealed for Bound<'_, PyMapping> {} diff --git a/src/types/frozendict.rs b/src/types/frozendict.rs new file mode 100644 index 00000000000..fe052e48e45 --- /dev/null +++ b/src/types/frozendict.rs @@ -0,0 +1,470 @@ +use crate::err::{self, PyErr, PyResult}; +use crate::ffi::Py_ssize_t; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::instance::Bound; +use crate::py_result_ext::PyResultExt; +use crate::types::{PyAny, PyList, PyListMethods, PyMapping}; +use crate::{ffi, Borrowed, BoundObject, IntoPyObject, IntoPyObjectExt, Python}; +#[cfg(RustPython)] +use crate::{ + sync::PyOnceLock, + types::{PyType, PyTypeMethods}, + Py, +}; +use core::ptr; + +/// Represents a Python `frozendict`. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyFrozenDict>`][Bound]. +/// +/// For APIs available on `frozendict` objects, see the [`PyFrozenDictMethods`] trait which is implemented for +/// [`Bound<'py, PyFrozenDict>`][Bound]. +/// +/// This type is only available on Python 3.15+. +#[cfg(Py_3_15)] +#[repr(transparent)] +pub struct PyFrozenDict(PyAny); + +#[cfg(all(Py_3_15, not(GraalPy)))] +pyobject_subclassable_native_type!(PyFrozenDict, crate::ffi::PyFrozenDictObject); + +#[cfg(all(Py_3_15, not(any(GraalPy, PyPy, RustPython))))] +pyobject_native_type_core!( + PyFrozenDict, + pyobject_native_static_type_object!(ffi::PyFrozenDict_Type), + "builtins", + "frozendict", + #checkfunction=ffi::PyFrozenDict_Check +); + +#[cfg(all(Py_3_15, RustPython))] +pyobject_native_type_core!( + PyFrozenDict, + |py| { + static TYPE: PyOnceLock> = PyOnceLock::new(); + TYPE.import(py, "builtins", "frozendict").unwrap().as_type_ptr() + }, + "builtins", + "frozendict", + #checkfunction=ffi::PyFrozenDict_Check +); + +#[cfg(all(Py_3_15, any(GraalPy, PyPy)))] +pyobject_native_type_core!( + PyFrozenDict, + |_py| unsafe { &raw mut ffi::PyFrozenDict_Type }, + "builtins", + "frozendict", + #checkfunction=ffi::PyFrozenDict_Check +); + +#[cfg(Py_3_15)] +impl PyFrozenDict { + /// Creates a new frozendict from an iterable of key-value pairs. + /// + /// The iterable can be any Python object that yields (key, value) pairs, + /// such as another dict, a list of tuples, or any mapping-like object. + /// + /// # Examples + /// + /// ``` + /// # use pyo3::prelude::*; + /// # use pyo3::types::PyFrozenDict; + /// # #[cfg(Py_3_15)] + /// # fn example() -> PyResult<()> { + /// # Python::with_gil(|py| -> PyResult<()> { + /// let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2)])?; + /// assert_eq!(fd.len(), 2); + /// # Ok(()) + /// # }) + /// # } + /// ``` + pub fn new<'py, T>(py: Python<'py>, iterable: T) -> PyResult> + where + T: IntoPyObject<'py>, + { + let obj = iterable.into_pyobject(py)?; + unsafe { + ffi::PyFrozenDict_New(obj.as_ptr()) + .assume_owned_or_err(py) + .cast_into_unchecked() + } + } + + /// Creates a new empty frozendict. + /// + /// # Examples + /// + /// ``` + /// # use pyo3::prelude::*; + /// # use pyo3::types::PyFrozenDict; + /// # #[cfg(Py_3_15)] + /// # fn example() -> PyResult<()> { + /// # Python::with_gil(|py| -> PyResult<()> { + /// let fd = PyFrozenDict::empty(py)?; + /// assert!(fd.is_empty()); + /// # Ok(()) + /// # }) + /// # } + /// ``` + pub fn empty(py: Python<'_>) -> PyResult> { + unsafe { + ffi::PyFrozenDict_New(ptr::null_mut()) + .assume_owned_or_err(py) + .cast_into_unchecked() + } + } +} + +/// Implementation of functionality for [`PyFrozenDict`]. +/// +/// These methods are defined for the `Bound<'py, PyFrozenDict>` smart pointer, so to use method call +/// syntax these methods are separated into a trait, because stable Rust does not yet support +/// `arbitrary_self_types`. +#[cfg(Py_3_15)] +#[doc(alias = "PyFrozenDict")] +pub trait PyFrozenDictMethods<'py>: crate::sealed::Sealed { + /// Return the number of items in the frozendict. + /// + /// This is equivalent to the Python expression `len(self)`. + fn len(&self) -> usize; + + /// Checks if the frozendict is empty, i.e. `len(self) == 0`. + fn is_empty(&self) -> bool; + + /// Determines if the frozendict contains the specified key. + /// + /// This is equivalent to the Python expression `key in self`. + fn contains(&self, key: K) -> PyResult + where + K: IntoPyObject<'py>; + + /// Gets an item from the frozendict. + /// + /// Returns `None` if the item is not present, or if an error occurs. + /// + /// To get a `KeyError` for non-existing keys, use `PyAny::get_item`. + fn get_item(&self, key: K) -> PyResult>> + where + K: IntoPyObject<'py>; + + /// Returns a list of all keys in the frozendict. + /// + /// This is equivalent to the Python expression `list(self.keys())`. + fn keys(&self) -> Bound<'py, PyList>; + + /// Returns a list of all values in the frozendict. + /// + /// This is equivalent to the Python expression `list(self.values())`. + fn values(&self) -> Bound<'py, PyList>; + + /// Returns a list of all (key, value) tuples in the frozendict. + /// + /// This is equivalent to the Python expression `list(self.items())`. + fn items(&self) -> Bound<'py, PyList>; + + /// Returns an iterator of `(key, value)` tuples in this frozendict. + fn iter(&self) -> BoundFrozenDictIterator<'py>; + + /// Returns `self` cast as a `PyMapping`. + /// + /// This is a zero-cost conversion that allows using the frozendict + /// with methods that accept a mapping protocol object. + fn as_mapping(&self) -> &Bound<'py, PyMapping>; + + /// Returns `self` cast as a `PyMapping`. + /// + /// This is a zero-cost conversion that allows using the frozendict + /// with methods that accept a mapping protocol object. + fn into_mapping(self) -> Bound<'py, PyMapping>; + + /// Returns the hash value of this frozendict. + /// + /// Frozendicts are hashable and can be used as dictionary keys. + /// + /// This is equivalent to calling `hash(self)` in Python. + fn hash(&self) -> PyResult; +} + +#[cfg(Py_3_15)] +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(&self, key: K) -> PyResult + where + K: IntoPyObject<'py>, + { + fn inner(fd: &Bound<'_, PyFrozenDict>, key: Borrowed<'_, '_, PyAny>) -> PyResult { + match unsafe { ffi::PyDict_Contains(fd.as_ptr(), key.as_ptr()) } { + 1 => Ok(true), + 0 => Ok(false), + _ => Err(PyErr::fetch(fd.py())), + } + } + + let py = self.py(); + inner( + self, + key.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), + ) + } + + fn get_item(&self, key: K) -> PyResult>> + where + K: IntoPyObject<'py>, + { + fn inner<'py>( + fd: &Bound<'py, PyFrozenDict>, + key: Borrowed<'_, '_, PyAny>, + ) -> PyResult>> { + let py = fd.py(); + let mut result: *mut ffi::PyObject = core::ptr::null_mut(); + match unsafe { ffi::compat::PyDict_GetItemRef(fd.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 => { + // Safety: PyDict_GetItemRef positive return value means the result is a valid + // owned reference + Ok(Some(unsafe { result.assume_owned_unchecked(py) })) + } + } + } + + let py = self.py(); + inner( + self, + key.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), + ) + } + + 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() } + } + + fn hash(&self) -> PyResult { + unsafe { + let hash_val = ffi::PyObject_Hash(self.as_ptr()); + err::error_on_minusone(self.py(), hash_val)?; + Ok(hash_val) + } + } +} + +/// An iterator over the items in a frozendict. +/// +/// Created by the `iter()` method on `Bound<'py, PyFrozenDict>`. +#[cfg(Py_3_15)] +pub struct BoundFrozenDictIterator<'py> { + fd: Bound<'py, PyFrozenDict>, + ppos: isize, +} + +#[cfg(Py_3_15)] +impl<'py> BoundFrozenDictIterator<'py> { + fn new(fd: Bound<'py, PyFrozenDict>) -> Self { + BoundFrozenDictIterator { fd, ppos: 0 } + } +} + +#[cfg(Py_3_15)] +impl<'py> Iterator for BoundFrozenDictIterator<'py> { + type Item = (Bound<'py, PyAny>, Bound<'py, PyAny>); + + fn next(&mut self) -> Option { + unsafe { + let mut 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.py(); + // Safety: + // - PyDict_Next returns borrowed values + // - we have already checked that `PyDict_Next` succeeded, so we can assume these to be non-null + 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) { + let len = ExactSizeIterator::len(self); + (len, Some(len)) + } + + #[inline] + fn count(self) -> usize + where + Self: Sized, + { + self.fd.len() + } +} + +impl ExactSizeIterator for BoundFrozenDictIterator<'_> { + fn len(&self) -> usize { + self.fd.len() + } +} + +#[cfg(Py_3_15)] +impl<'py> IntoIterator for Bound<'py, PyFrozenDict> { + type Item = (Bound<'py, PyAny>, Bound<'py, PyAny>); + type IntoIter = BoundFrozenDictIterator<'py>; + + /// Returns an iterator over the `(key, value)` pairs in this frozendict. + 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(all(Py_3_15, test))] +mod tests { + use super::*; + + #[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_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")?); + assert!(!fd.contains("c")?); + }) + } + + #[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")?; + assert!(val.is_some()); + }) + } + + #[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); + }) + } + + #[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); + }) + } + + #[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); + }) + } + + #[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) in &fd { + count += 1; + } + assert_eq!(count, 2); + }) + } + + #[test] + fn test_frozendict_as_mapping() { + Python::attach(|py| { + let fd = PyFrozenDict::new(py, vec![("a", 1)]).unwrap(); + let _mapping = fd.as_mapping(); + }) + } + + #[test] + fn test_frozendict_hash() { + Python::attach(|py| { + let fd = PyFrozenDict::new(py, vec![("a", 1)]).unwrap(); + let h = fd.hash()?; + assert!(h != 0 || fd.is_empty()); + }) + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 09d05aaa998..970dc80fa8e 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -20,6 +20,8 @@ pub use self::ellipsis::PyEllipsis; pub use self::float::{PyFloat, PyFloatMethods}; #[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] pub use self::frame::{PyFrame, PyFrameMethods}; +#[cfg(Py_3_15)] +pub use self::frozendict::{PyFrozenDict, PyFrozenDictMethods}; pub use self::frozenset::{PyFrozenSet, PyFrozenSetBuilder, PyFrozenSetMethods}; pub use self::function::PyCFunction; pub use self::function::PyFunction; @@ -82,6 +84,8 @@ pub use self::weakref::{PyWeakref, PyWeakrefMethods, PyWeakrefProxy, PyWeakrefRe /// In these cases the iterators are implemented by forwarding to [`PyIterator`]. pub mod iter { pub use super::dict::BoundDictIterator; + #[cfg(Py_3_15)] + pub use super::frozendict::BoundFrozenDictIterator; pub use super::frozenset::BoundFrozenSetIterator; pub use super::list::BoundListIterator; pub use super::set::BoundSetIterator; @@ -273,6 +277,8 @@ mod ellipsis; pub(crate) mod float; #[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] mod frame; +#[cfg(Py_3_15)] +pub(crate) mod frozendict; pub(crate) mod frozenset; mod function; pub(crate) mod genericalias; diff --git a/tests/test_frozendict.rs b/tests/test_frozendict.rs new file mode 100644 index 00000000000..7428344bae2 --- /dev/null +++ b/tests/test_frozendict.rs @@ -0,0 +1,290 @@ +#![cfg(Py_3_15)] + +use pyo3::prelude::*; +use pyo3::types::{IntoPyDict, PyDict, PyFrozenDict, PyList}; + +#[test] +fn test_frozendict_new_from_list_of_tuples() { + Python::with_gil(|py| { + let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2), ("c", 3)]).unwrap(); + assert_eq!(fd.len(), 3); + assert!(!fd.is_empty()); + }) +} + +#[test] +fn test_frozendict_new_from_dict() { + Python::with_gil(|py| { + let dict = PyDict::new(py); + dict.set_item("key1", "value1").unwrap(); + dict.set_item("key2", "value2").unwrap(); + + let fd = PyFrozenDict::new(py, &dict).unwrap(); + assert_eq!(fd.len(), 2); + }) +} + +#[test] +fn test_frozendict_empty() { + Python::with_gil(|py| { + let fd = PyFrozenDict::empty(py).unwrap(); + assert!(fd.is_empty()); + assert_eq!(fd.len(), 0); + }) +} + +#[test] +fn test_frozendict_len() { + Python::with_gil(|py| { + let fd1 = PyFrozenDict::empty(py).unwrap(); + assert_eq!(fd1.len(), 0); + + let fd2 = PyFrozenDict::new(py, vec![("x", 1), ("y", 2)]).unwrap(); + assert_eq!(fd2.len(), 2); + + let fd3 = PyFrozenDict::new(py, vec![("a", 1)]).unwrap(); + assert_eq!(fd3.len(), 1); + }) +} + +#[test] +fn test_frozendict_contains() { + Python::with_gil(|py| { + let fd = PyFrozenDict::new(py, vec![("key1", 1), ("key2", 2)]).unwrap(); + + assert!(fd.contains("key1").unwrap()); + assert!(fd.contains("key2").unwrap()); + assert!(!fd.contains("key3").unwrap()); + }) +} + +#[test] +fn test_frozendict_get_item() { + Python::with_gil(|py| { + let fd = PyFrozenDict::new(py, vec![("key1", 1), ("key2", 2)]).unwrap(); + + let val1 = fd.get_item("key1").unwrap(); + assert!(val1.is_some()); + + let val_missing = fd.get_item("missing").unwrap(); + assert!(val_missing.is_none()); + }) +} + +#[test] +fn test_frozendict_keys() { + Python::with_gil(|py| { + let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2), ("c", 3)]).unwrap(); + let keys = fd.keys(); + + assert_eq!(keys.len(), 3); + }) +} + +#[test] +fn test_frozendict_values() { + Python::with_gil(|py| { + let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2), ("c", 3)]).unwrap(); + let values = fd.values(); + + assert_eq!(values.len(), 3); + }) +} + +#[test] +fn test_frozendict_items() { + Python::with_gil(|py| { + let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2), ("c", 3)]).unwrap(); + let items = fd.items(); + + assert_eq!(items.len(), 3); + }) +} + +#[test] +fn test_frozendict_iter() { + Python::with_gil(|py| { + let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2), ("c", 3)]).unwrap(); + + let mut count = 0; + for (_k, _v) in &fd { + count += 1; + } + assert_eq!(count, 3); + }) +} + +#[test] +fn test_frozendict_iter_into() { + Python::with_gil(|py| { + let fd = PyFrozenDict::new(py, vec![("x", 10), ("y", 20)]).unwrap(); + + let mut items = Vec::new(); + for (k, v) in fd { + items.push((k, v)); + } + assert_eq!(items.len(), 2); + }) +} + +#[test] +fn test_frozendict_hash() { + Python::with_gil(|py| { + let fd1 = PyFrozenDict::new(py, vec![("a", 1)]).unwrap(); + let h1 = fd1.hash().unwrap(); + assert!(h1 != 0 || fd1.is_empty()); // Hash should be non-zero unless empty + + let fd2 = PyFrozenDict::empty(py).unwrap(); + let h2 = fd2.hash().unwrap(); + // Empty frozendict might have a specific hash value + let _ = h2; // Prevent unused variable warning + }) +} + +#[test] +fn test_frozendict_hash_consistency() { + Python::with_gil(|py| { + let fd = PyFrozenDict::new(py, vec![("key", "value")]).unwrap(); + let h1 = fd.hash().unwrap(); + let h2 = fd.hash().unwrap(); + + // Hash should be consistent across multiple calls + assert_eq!(h1, h2); + }) +} + +#[test] +fn test_frozendict_as_mapping() { + Python::with_gil(|py| { + let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2)]).unwrap(); + let mapping = fd.as_mapping(); + + // Verify we can get the len through the mapping + assert_eq!(mapping.len().unwrap(), 2); + }) +} + +#[test] +fn test_frozendict_into_mapping() { + Python::with_gil(|py| { + let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2)]).unwrap(); + let mapping = fd.into_mapping(); + + // Verify we can get the len through the mapping + assert_eq!(mapping.len().unwrap(), 2); + }) +} + +#[test] +fn test_frozendict_use_as_dict_key() { + Python::with_gil(|py| { + let fd = PyFrozenDict::new(py, vec![("inner_key", "inner_value")]).unwrap(); + + // Create a dict and try to use frozendict as a key + let outer_dict = PyDict::new(py); + outer_dict.set_item(&fd, "some_value").unwrap(); + + // Verify we can retrieve the value + let retrieved = outer_dict.get_item(&fd).unwrap(); + assert!(retrieved.is_some()); + }) +} + +#[test] +fn test_frozendict_nested() { + Python::with_gil(|py| { + let inner_fd = PyFrozenDict::new(py, vec![("inner", 42)]).unwrap(); + let outer_fd = PyFrozenDict::new(py, vec![("outer", &inner_fd)]).unwrap(); + + assert_eq!(outer_fd.len(), 1); + let inner_val = outer_fd.get_item("outer").unwrap(); + assert!(inner_val.is_some()); + }) +} + +#[test] +fn test_frozendict_from_python() { + Python::with_gil(|py| { + let code = r#" +fd = frozendict({"x": 10, "y": 20}) +"#; + let globals = py.eval(code, None, None).unwrap(); + let fd: &PyFrozenDict = globals.downcast().unwrap(); + + assert_eq!(fd.len(), 2); + assert!(fd.contains("x").unwrap()); + assert!(fd.contains("y").unwrap()); + }) +} + +#[test] +fn test_frozendict_type_check() { + Python::with_gil(|py| { + let fd = PyFrozenDict::new(py, vec![("a", 1)]).unwrap(); + let any = fd.as_any(); + + // Verify the type + let type_name = any.get_type().name().unwrap(); + assert_eq!(type_name, "frozendict"); + }) +} + +#[test] +fn test_frozendict_not_mutable() { + // This test verifies that frozendict is immutable + // by checking that mutation operations are not available + // (i.e., there are no set_item, del_item, clear methods in the trait) + Python::with_gil(|py| { + let fd = PyFrozenDict::new(py, vec![("a", 1)]).unwrap(); + + // Simply access a method to verify it compiles + let _len = fd.len(); + assert_eq!(_len, 1); + }) +} + +#[test] +fn test_frozendict_repr() { + Python::with_gil(|py| { + let fd = PyFrozenDict::new(py, vec![("key", "value")]).unwrap(); + let repr = fd.repr().unwrap(); + let repr_str = repr.to_string(); + + // Verify it contains frozendict in the representation + assert!(repr_str.contains("frozendict") || repr_str.contains("frozendict({")); + }) +} + +#[test] +fn test_frozendict_equality() { + Python::with_gil(|py| { + let fd1 = PyFrozenDict::new(py, vec![("a", 1), ("b", 2)]).unwrap(); + let fd2 = PyFrozenDict::new(py, vec![("a", 1), ("b", 2)]).unwrap(); + let fd3 = PyFrozenDict::new(py, vec![("a", 1), ("c", 3)]).unwrap(); + + // Two frozendicts with the same content should be equal + assert!(fd1.eq(&fd2).unwrap()); + + // Two frozendicts with different content should not be equal + assert!(!fd1.eq(&fd3).unwrap()); + }) +} + +#[test] +fn test_frozendict_conversion_roundtrip() { + Python::with_gil(|py| { + // Create a frozendict from Rust + let fd = PyFrozenDict::new(py, vec![("a", 1), ("b", 2)]).unwrap(); + + // Convert to a regular dict (via Python) + let code = "def to_dict(fd): return dict(fd)"; + py.run(code, None, None).unwrap(); + + let to_dict_fn = py.eval("to_dict", None, None).unwrap(); + let as_dict = to_dict_fn.call1((&fd,)).unwrap(); + + // Verify it's a dict and has the same content + let dict: &PyDict = as_dict.downcast().unwrap(); + assert_eq!(dict.len(), 2); + }) +}