Skip to main content

pyo3/types/
frozenset.rs

1//! Python frozen sets and related types.
2
3use crate::types::PyIterator;
4use crate::{
5    Bound, PyAny, Python,
6    err::{self, PyErr, PyResult},
7    ffi,
8    ffi_ptr_ext::FfiPtrExt,
9    py_result_ext::PyResultExt,
10};
11use crate::{BoundObject, IntoPyObject, IntoPyObjectExt};
12#[cfg(RustPython)]
13use crate::{
14    Py,
15    sync::PyOnceLock,
16    types::{PyType, PyTypeMethods},
17};
18use core::ptr;
19
20/// Allows building a Python `frozenset` one item at a time
21pub struct PyFrozenSetBuilder<'py> {
22    py_frozen_set: Bound<'py, PyFrozenSet>,
23}
24
25impl<'py> PyFrozenSetBuilder<'py> {
26    /// Create a new `FrozenSetBuilder`.
27    /// Since this allocates a `PyFrozenSet` internally it may
28    /// panic when running out of memory.
29    pub fn new(py: Python<'py>) -> PyResult<PyFrozenSetBuilder<'py>> {
30        Ok(PyFrozenSetBuilder {
31            py_frozen_set: PyFrozenSet::empty(py)?,
32        })
33    }
34
35    /// Adds an element to the set.
36    pub fn add<K>(&mut self, key: K) -> PyResult<()>
37    where
38        K: IntoPyObject<'py>,
39    {
40        let py = self.py_frozen_set.py();
41        let key = key.into_pyobject_or_pyerr(py)?;
42        err::error_on_negative(py, unsafe {
43            ffi::PySet_Add(self.py_frozen_set.as_ptr(), key.as_ptr())
44        })
45    }
46
47    /// Finish building the set and take ownership of its current value
48    pub fn finalize(self) -> Bound<'py, PyFrozenSet> {
49        self.py_frozen_set
50    }
51}
52
53/// Represents a  Python `frozenset`.
54///
55/// Values of this type are accessed via PyO3's smart pointers, e.g. as
56/// [`Py<PyFrozenSet>`][crate::Py] or [`Bound<'py, PyFrozenSet>`][Bound].
57///
58/// For APIs available on `frozenset` objects, see the [`PyFrozenSetMethods`] trait which is implemented for
59/// [`Bound<'py, PyFrozenSet>`][Bound].
60#[repr(transparent)]
61pub struct PyFrozenSet(PyAny);
62
63#[cfg(not(any(PyPy, GraalPy)))]
64pyobject_subclassable_native_type!(PyFrozenSet, crate::ffi::PySetObject);
65#[cfg(all(not(any(PyPy, GraalPy)), not(RustPython)))]
66pyobject_native_type!(
67    PyFrozenSet,
68    ffi::PySetObject,
69    pyobject_native_static_type_object!(ffi::PyFrozenSet_Type),
70    "builtins",
71    "frozenset",
72    #checkfunction=ffi::PyFrozenSet_Check
73);
74
75#[cfg(all(not(any(PyPy, GraalPy)), RustPython))]
76pyobject_native_type!(
77    PyFrozenSet,
78    ffi::PySetObject,
79    |py| {
80        static TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();
81        TYPE.import(py, "builtins", "frozenset").unwrap().as_type_ptr()
82    },
83    "builtins",
84    "frozenset",
85    #checkfunction=ffi::PyFrozenSet_Check
86);
87
88#[cfg(any(PyPy, GraalPy))]
89pyobject_native_type_core!(
90    PyFrozenSet,
91    pyobject_native_static_type_object!(ffi::PyFrozenSet_Type),
92    "builtins",
93    "frozenset",
94    #checkfunction=ffi::PyFrozenSet_Check
95);
96
97impl PyFrozenSet {
98    /// Creates a new frozenset.
99    ///
100    /// May panic when running out of memory.
101    #[inline]
102    pub fn new<'py, T>(
103        py: Python<'py>,
104        elements: impl IntoIterator<Item = T>,
105    ) -> PyResult<Bound<'py, PyFrozenSet>>
106    where
107        T: IntoPyObject<'py>,
108    {
109        let mut builder = PyFrozenSetBuilder::new(py)?;
110        for e in elements {
111            builder.add(e)?;
112        }
113        Ok(builder.finalize())
114    }
115
116    /// Creates a new empty frozen set
117    pub fn empty(py: Python<'_>) -> PyResult<Bound<'_, PyFrozenSet>> {
118        unsafe {
119            ffi::PyFrozenSet_New(ptr::null_mut())
120                .assume_owned_or_err(py)
121                .cast_into_unchecked()
122        }
123    }
124}
125
126/// Implementation of functionality for [`PyFrozenSet`].
127///
128/// These methods are defined for the `Bound<'py, PyFrozenSet>` smart pointer, so to use method call
129/// syntax these methods are separated into a trait, because stable Rust does not yet support
130/// `arbitrary_self_types`.
131#[doc(alias = "PyFrozenSet")]
132pub trait PyFrozenSetMethods<'py>: crate::sealed::Sealed {
133    /// Returns the number of items in the set.
134    ///
135    /// This is equivalent to the Python expression `len(self)`.
136    fn len(&self) -> usize;
137
138    /// Checks if set is empty.
139    fn is_empty(&self) -> bool {
140        self.len() == 0
141    }
142
143    /// Determines if the set contains the specified key.
144    ///
145    /// This is equivalent to the Python expression `key in self`.
146    fn contains<K>(&self, key: K) -> PyResult<bool>
147    where
148        K: IntoPyObject<'py>;
149
150    /// Returns an iterator of values in this set.
151    fn iter(&self) -> BoundFrozenSetIterator<'py>;
152}
153
154impl<'py> PyFrozenSetMethods<'py> for Bound<'py, PyFrozenSet> {
155    #[inline]
156    fn len(&self) -> usize {
157        let size = cfg_select! {
158            // SAFETY: self is a valid frozenset object.
159            not(any(Py_LIMITED_API, PyPy, GraalPy)) => unsafe {
160                ffi::PySet_GET_SIZE(self.as_ptr())
161            },
162            // SAFETY: self is a valid frozenset object.
163            _ => unsafe { ffi::PySet_Size(self.as_ptr()) },
164        };
165        size as usize
166    }
167
168    fn contains<K>(&self, key: K) -> PyResult<bool>
169    where
170        K: IntoPyObject<'py>,
171    {
172        let py = self.py();
173        let key = key.into_pyobject_or_pyerr(py)?;
174        match unsafe { ffi::PySet_Contains(self.as_ptr(), key.as_ptr()) } {
175            1 => Ok(true),
176            0 => Ok(false),
177            _ => Err(PyErr::fetch(py)),
178        }
179    }
180
181    fn iter(&self) -> BoundFrozenSetIterator<'py> {
182        BoundFrozenSetIterator::new(self.clone())
183    }
184}
185
186impl<'py> IntoIterator for Bound<'py, PyFrozenSet> {
187    type Item = Bound<'py, PyAny>;
188    type IntoIter = BoundFrozenSetIterator<'py>;
189
190    /// Returns an iterator of values in this set.
191    fn into_iter(self) -> Self::IntoIter {
192        BoundFrozenSetIterator::new(self)
193    }
194}
195
196impl<'py> IntoIterator for &Bound<'py, PyFrozenSet> {
197    type Item = Bound<'py, PyAny>;
198    type IntoIter = BoundFrozenSetIterator<'py>;
199
200    /// Returns an iterator of values in this set.
201    fn into_iter(self) -> Self::IntoIter {
202        self.iter()
203    }
204}
205
206/// PyO3 implementation of an iterator for a Python `frozenset` object.
207pub struct BoundFrozenSetIterator<'py>(Bound<'py, PyIterator>);
208
209impl<'py> BoundFrozenSetIterator<'py> {
210    pub(super) fn new(set: Bound<'py, PyFrozenSet>) -> Self {
211        Self(PyIterator::from_object(&set).expect("frozenset should always be iterable"))
212    }
213}
214
215impl<'py> Iterator for BoundFrozenSetIterator<'py> {
216    type Item = Bound<'py, super::PyAny>;
217
218    /// Advances the iterator and returns the next value.
219    fn next(&mut self) -> Option<Self::Item> {
220        self.0
221            .next()
222            .map(|result| result.expect("frozenset iteration should be infallible"))
223    }
224
225    fn size_hint(&self) -> (usize, Option<usize>) {
226        let len = ExactSizeIterator::len(self);
227        (len, Some(len))
228    }
229
230    #[inline]
231    fn count(self) -> usize
232    where
233        Self: Sized,
234    {
235        self.len()
236    }
237}
238
239impl ExactSizeIterator for BoundFrozenSetIterator<'_> {
240    fn len(&self) -> usize {
241        self.0.size_hint().0
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use crate::types::PyAnyMethods as _;
249
250    #[test]
251    fn test_frozenset_new_and_len() {
252        Python::attach(|py| {
253            let set = PyFrozenSet::new(py, [1]).unwrap();
254            assert_eq!(1, set.len());
255
256            let v = vec![1];
257            assert!(PyFrozenSet::new(py, &[v]).is_err());
258        });
259    }
260
261    #[test]
262    fn test_frozenset_empty() {
263        Python::attach(|py| {
264            let set = PyFrozenSet::empty(py).unwrap();
265            assert_eq!(0, set.len());
266            assert!(set.is_empty());
267        });
268    }
269
270    #[test]
271    fn test_frozenset_contains() {
272        Python::attach(|py| {
273            let set = PyFrozenSet::new(py, [1]).unwrap();
274            assert!(set.contains(1).unwrap());
275        });
276    }
277
278    #[test]
279    fn test_frozenset_iter() {
280        Python::attach(|py| {
281            let set = PyFrozenSet::new(py, [1]).unwrap();
282
283            for el in set {
284                assert_eq!(1i32, el.extract::<i32>().unwrap());
285            }
286        });
287    }
288
289    #[test]
290    fn test_frozenset_iter_bound() {
291        Python::attach(|py| {
292            let set = PyFrozenSet::new(py, [1]).unwrap();
293
294            for el in &set {
295                assert_eq!(1i32, el.extract::<i32>().unwrap());
296            }
297        });
298    }
299
300    #[test]
301    fn test_frozenset_iter_size_hint() {
302        Python::attach(|py| {
303            let set = PyFrozenSet::new(py, [1]).unwrap();
304            let mut iter = set.iter();
305
306            // Exact size
307            assert_eq!(iter.len(), 1);
308            assert_eq!(iter.size_hint(), (1, Some(1)));
309            iter.next();
310            assert_eq!(iter.len(), 0);
311            assert_eq!(iter.size_hint(), (0, Some(0)));
312        });
313    }
314
315    #[test]
316    fn test_frozenset_builder() {
317        use super::PyFrozenSetBuilder;
318
319        Python::attach(|py| {
320            let mut builder = PyFrozenSetBuilder::new(py).unwrap();
321
322            // add an item
323            builder.add(1).unwrap();
324            builder.add(2).unwrap();
325            builder.add(2).unwrap();
326
327            // finalize it
328            let set = builder.finalize();
329
330            assert!(set.contains(1).unwrap());
331            assert!(set.contains(2).unwrap());
332            assert!(!set.contains(3).unwrap());
333        });
334    }
335
336    #[test]
337    fn test_iter_count() {
338        Python::attach(|py| {
339            let set = PyFrozenSet::new(py, vec![1, 2, 3]).unwrap();
340            assert_eq!(set.iter().count(), 3);
341        })
342    }
343}