Skip to main content

pyo3/types/weakref/
reference.rs

1use crate::err::PyResult;
2use crate::ffi_ptr_ext::FfiPtrExt;
3use crate::py_result_ext::PyResultExt;
4#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))]
5use crate::sync::PyOnceLock;
6use crate::types::any::PyAny;
7#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))]
8use crate::types::typeobject::PyTypeMethods;
9#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))]
10use crate::types::PyType;
11#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))]
12use crate::Py;
13use crate::{ffi, Borrowed, Bound, BoundObject, IntoPyObject, IntoPyObjectExt};
14
15use super::PyWeakrefMethods;
16
17/// Represents a Python `weakref.ReferenceType`.
18///
19/// In Python this is created by calling `weakref.ref`.
20#[repr(transparent)]
21pub struct PyWeakrefReference(PyAny);
22
23#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))]
24pyobject_subclassable_native_type!(PyWeakrefReference, ffi::PyWeakReference);
25
26#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))]
27pyobject_native_type!(
28    PyWeakrefReference,
29    ffi::PyWeakReference,
30    // TODO: should not be depending on a private symbol here!
31    pyobject_native_static_type_object!(ffi::_PyWeakref_RefType),
32    "weakref",
33    "ReferenceType",
34    #module=Some("weakref"),
35    #checkfunction=ffi::PyWeakref_CheckRefExact
36);
37
38// When targeting alternative or multiple interpreters, it is better to not use the internal API.
39#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))]
40pyobject_native_type_core!(
41    PyWeakrefReference,
42    |py| {
43        static TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();
44        TYPE.import(py, "weakref", "ref")
45            .unwrap()
46            .as_type_ptr()
47    },
48    "weakref",
49    "ReferenceType",
50    #module=Some("weakref"),
51    #checkfunction=ffi::PyWeakref_CheckRef
52);
53
54impl PyWeakrefReference {
55    /// Constructs a new Weak Reference (`weakref.ref`/`weakref.ReferenceType`) for the given object.
56    ///
57    /// Returns a `TypeError` if `object` is not weak referenceable (Most native types and PyClasses without `weakref` flag).
58    ///
59    /// # Examples
60    #[cfg_attr(
61        not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))),
62        doc = "```rust,ignore"
63    )]
64    #[cfg_attr(
65        all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))),
66        doc = "```rust"
67    )]
68    /// use pyo3::prelude::*;
69    /// use pyo3::types::PyWeakrefReference;
70    ///
71    /// #[pyclass(weakref)]
72    /// struct Foo { /* fields omitted */ }
73    ///
74    /// # fn main() -> PyResult<()> {
75    /// Python::attach(|py| {
76    ///     let foo = Bound::new(py, Foo {})?;
77    ///     let weakref = PyWeakrefReference::new(&foo)?;
78    ///     assert!(
79    ///         // In normal situations where a direct `Bound<'py, Foo>` is required use `upgrade::<Foo>`
80    ///         weakref.upgrade().is_some_and(|obj| obj.is(&foo))
81    ///     );
82    ///
83    ///     let weakref2 = PyWeakrefReference::new(&foo)?;
84    ///     assert!(weakref.is(&weakref2));
85    ///
86    ///     drop(foo);
87    ///
88    ///     assert!(weakref.upgrade().is_none());
89    ///     Ok(())
90    /// })
91    /// # }
92    /// ```
93    pub fn new<'py>(object: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyWeakrefReference>> {
94        unsafe {
95            Bound::from_owned_ptr_or_err(
96                object.py(),
97                ffi::PyWeakref_NewRef(object.as_ptr(), ffi::Py_None()),
98            )
99            .cast_into_unchecked()
100        }
101    }
102
103    /// Constructs a new Weak Reference (`weakref.ref`/`weakref.ReferenceType`) for the given object with a callback.
104    ///
105    /// Returns a `TypeError` if `object` is not weak referenceable (Most native types and PyClasses without `weakref` flag) or if the `callback` is not callable or None.
106    ///
107    /// # Examples
108    #[cfg_attr(
109        not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))),
110        doc = "```rust,ignore"
111    )]
112    #[cfg_attr(
113        all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))),
114        doc = "```rust"
115    )]
116    /// use pyo3::prelude::*;
117    /// use pyo3::types::PyWeakrefReference;
118    /// use pyo3::ffi::c_str;
119    ///
120    /// #[pyclass(weakref)]
121    /// struct Foo { /* fields omitted */ }
122    ///
123    /// #[pyfunction]
124    /// fn callback(wref: Bound<'_, PyWeakrefReference>) -> PyResult<()> {
125    ///         let py = wref.py();
126    ///         assert!(wref.upgrade_as::<Foo>()?.is_none());
127    ///         py.run(c"counter = 1", None, None)
128    /// }
129    ///
130    /// # fn main() -> PyResult<()> {
131    /// Python::attach(|py| {
132    ///     py.run(c"counter = 0", None, None)?;
133    ///     assert_eq!(py.eval(c"counter", None, None)?.extract::<u32>()?, 0);
134    ///     let foo = Bound::new(py, Foo{})?;
135    ///
136    ///     // This is fine.
137    ///     let weakref = PyWeakrefReference::new_with(&foo, py.None())?;
138    ///     assert!(weakref.upgrade_as::<Foo>()?.is_some());
139    ///     assert!(
140    ///         // In normal situations where a direct `Bound<'py, Foo>` is required use `upgrade::<Foo>`
141    ///         weakref.upgrade().is_some_and(|obj| obj.is(&foo))
142    ///     );
143    ///     assert_eq!(py.eval(c"counter", None, None)?.extract::<u32>()?, 0);
144    ///
145    ///     let weakref2 = PyWeakrefReference::new_with(&foo, wrap_pyfunction!(callback, py)?)?;
146    ///     assert!(!weakref.is(&weakref2)); // Not the same weakref
147    ///     assert!(weakref.eq(&weakref2)?);  // But Equal, since they point to the same object
148    ///
149    ///     drop(foo);
150    ///
151    ///     assert!(weakref.upgrade_as::<Foo>()?.is_none());
152    ///     assert_eq!(py.eval(c"counter", None, None)?.extract::<u32>()?, 1);
153    ///     Ok(())
154    /// })
155    /// # }
156    /// ```
157    pub fn new_with<'py, C>(
158        object: &Bound<'py, PyAny>,
159        callback: C,
160    ) -> PyResult<Bound<'py, PyWeakrefReference>>
161    where
162        C: IntoPyObject<'py>,
163    {
164        fn inner<'py>(
165            object: &Bound<'py, PyAny>,
166            callback: Borrowed<'_, 'py, PyAny>,
167        ) -> PyResult<Bound<'py, PyWeakrefReference>> {
168            unsafe {
169                Bound::from_owned_ptr_or_err(
170                    object.py(),
171                    ffi::PyWeakref_NewRef(object.as_ptr(), callback.as_ptr()),
172                )
173                .cast_into_unchecked()
174            }
175        }
176
177        let py = object.py();
178        inner(
179            object,
180            callback
181                .into_pyobject_or_pyerr(py)?
182                .into_any()
183                .as_borrowed(),
184        )
185    }
186}
187
188impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefReference> {
189    fn upgrade(&self) -> Option<Bound<'py, PyAny>> {
190        let mut obj: *mut ffi::PyObject = std::ptr::null_mut();
191        match unsafe { ffi::compat::PyWeakref_GetRef(self.as_ptr(), &mut obj) } {
192            std::ffi::c_int::MIN..=-1 => panic!("The 'weakref.ReferenceType' instance should be valid (non-null and actually a weakref reference)"),
193            0 => None,
194            1..=std::ffi::c_int::MAX => Some(unsafe { obj.assume_owned_unchecked(self.py()) }),
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use crate::types::any::{PyAny, PyAnyMethods};
202    use crate::types::weakref::{PyWeakrefMethods, PyWeakrefReference};
203    use crate::{Bound, PyResult, Python};
204
205    #[cfg(all(not(Py_LIMITED_API), Py_3_10))]
206    const CLASS_NAME: &str = "<class 'weakref.ReferenceType'>";
207    #[cfg(all(not(Py_LIMITED_API), not(Py_3_10)))]
208    const CLASS_NAME: &str = "<class 'weakref'>";
209
210    fn check_repr(
211        reference: &Bound<'_, PyWeakrefReference>,
212        object: Option<(&Bound<'_, PyAny>, &str)>,
213    ) -> PyResult<()> {
214        let repr = reference.repr()?.to_string();
215        let (first_part, second_part) = repr.split_once("; ").unwrap();
216
217        {
218            let (msg, addr) = first_part.split_once("0x").unwrap();
219
220            assert_eq!(msg, "<weakref at ");
221            assert!(addr
222                .to_lowercase()
223                .contains(format!("{:x?}", reference.as_ptr()).split_at(2).1));
224        }
225
226        match object {
227            Some((object, class)) => {
228                let (msg, addr) = second_part.split_once("0x").unwrap();
229
230                // Avoid testing on reprs directly since they the quoting and full path vs class name tends to be changedi undocumented.
231                assert!(msg.starts_with("to '"));
232                assert!(msg.contains(class));
233                assert!(msg.ends_with("' at "));
234
235                assert!(addr
236                    .to_lowercase()
237                    .contains(format!("{:x?}", object.as_ptr()).split_at(2).1));
238            }
239            None => {
240                assert_eq!(second_part, "dead>")
241            }
242        }
243
244        Ok(())
245    }
246
247    mod python_class {
248        use super::*;
249        use crate::PyTypeInfo;
250        use crate::{py_result_ext::PyResultExt, types::PyType};
251        use std::ptr;
252
253        fn get_type(py: Python<'_>) -> PyResult<Bound<'_, PyType>> {
254            py.run(c"class A:\n    pass\n", None, None)?;
255            py.eval(c"A", None, None).cast_into::<PyType>()
256        }
257
258        #[test]
259        fn test_weakref_reference_behavior() -> PyResult<()> {
260            Python::attach(|py| {
261                let class = get_type(py)?;
262                let object = class.call0()?;
263                let reference = PyWeakrefReference::new(&object)?;
264
265                assert!(!reference.is(&object));
266                assert!(reference.upgrade().unwrap().is(&object));
267
268                #[cfg(not(Py_LIMITED_API))]
269                assert_eq!(reference.get_type().to_string(), CLASS_NAME);
270
271                #[cfg(not(Py_LIMITED_API))]
272                assert_eq!(reference.getattr("__class__")?.to_string(), CLASS_NAME);
273
274                #[cfg(not(Py_LIMITED_API))]
275                check_repr(&reference, Some((object.as_any(), "A")))?;
276
277                assert!(reference
278                    .getattr("__callback__")
279                    .is_ok_and(|result| result.is_none()));
280
281                assert!(reference.call0()?.is(&object));
282
283                drop(object);
284
285                assert!(reference.upgrade().is_none());
286                #[cfg(not(Py_LIMITED_API))]
287                assert_eq!(reference.getattr("__class__")?.to_string(), CLASS_NAME);
288                check_repr(&reference, None)?;
289
290                assert!(reference
291                    .getattr("__callback__")
292                    .is_ok_and(|result| result.is_none()));
293
294                assert!(reference.call0()?.is_none());
295
296                Ok(())
297            })
298        }
299
300        #[test]
301        fn test_weakref_upgrade_as() -> PyResult<()> {
302            Python::attach(|py| {
303                let class = get_type(py)?;
304                let object = class.call0()?;
305                let reference = PyWeakrefReference::new(&object)?;
306
307                {
308                    // This test is a bit weird but ok.
309                    let obj = reference.upgrade_as::<PyAny>();
310
311                    assert!(obj.is_ok());
312                    let obj = obj.unwrap();
313
314                    assert!(obj.is_some());
315                    assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr())
316                        && obj.is_exact_instance(&class)));
317                }
318
319                drop(object);
320
321                {
322                    // This test is a bit weird but ok.
323                    let obj = reference.upgrade_as::<PyAny>();
324
325                    assert!(obj.is_ok());
326                    let obj = obj.unwrap();
327
328                    assert!(obj.is_none());
329                }
330
331                Ok(())
332            })
333        }
334
335        #[test]
336        fn test_weakref_upgrade_as_unchecked() -> PyResult<()> {
337            Python::attach(|py| {
338                let class = get_type(py)?;
339                let object = class.call0()?;
340                let reference = PyWeakrefReference::new(&object)?;
341
342                {
343                    // This test is a bit weird but ok.
344                    let obj = unsafe { reference.upgrade_as_unchecked::<PyAny>() };
345
346                    assert!(obj.is_some());
347                    assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr())
348                        && obj.is_exact_instance(&class)));
349                }
350
351                drop(object);
352
353                {
354                    // This test is a bit weird but ok.
355                    let obj = unsafe { reference.upgrade_as_unchecked::<PyAny>() };
356
357                    assert!(obj.is_none());
358                }
359
360                Ok(())
361            })
362        }
363
364        #[test]
365        fn test_weakref_upgrade() -> PyResult<()> {
366            Python::attach(|py| {
367                let class = get_type(py)?;
368                let object = class.call0()?;
369                let reference = PyWeakrefReference::new(&object)?;
370
371                assert!(reference.call0()?.is(&object));
372                assert!(reference.upgrade().is_some());
373                assert!(reference.upgrade().is_some_and(|obj| obj.is(&object)));
374
375                drop(object);
376
377                assert!(reference.call0()?.is_none());
378                assert!(reference.upgrade().is_none());
379
380                Ok(())
381            })
382        }
383
384        #[test]
385        fn test_type_object() -> PyResult<()> {
386            Python::attach(|py| {
387                let class = get_type(py)?;
388                let object = class.call0()?;
389                let reference = PyWeakrefReference::new(&object)?;
390
391                assert!(reference.is_instance(&PyWeakrefReference::type_object(py))?);
392                Ok(())
393            })
394        }
395    }
396
397    // under 'abi3-py37' and 'abi3-py38' PyClass cannot be weakreferencable.
398    #[cfg(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))))]
399    mod pyo3_pyclass {
400        use super::*;
401        use crate::{pyclass, Py};
402        use std::ptr;
403
404        #[pyclass(weakref, crate = "crate")]
405        struct WeakrefablePyClass {}
406
407        #[test]
408        fn test_weakref_reference_behavior() -> PyResult<()> {
409            Python::attach(|py| {
410                let object: Bound<'_, WeakrefablePyClass> = Bound::new(py, WeakrefablePyClass {})?;
411                let reference = PyWeakrefReference::new(&object)?;
412
413                assert!(!reference.is(&object));
414                assert!(reference.upgrade().unwrap().is(&object));
415                #[cfg(not(Py_LIMITED_API))]
416                assert_eq!(reference.get_type().to_string(), CLASS_NAME);
417
418                #[cfg(not(Py_LIMITED_API))]
419                assert_eq!(reference.getattr("__class__")?.to_string(), CLASS_NAME);
420                #[cfg(not(Py_LIMITED_API))]
421                check_repr(&reference, Some((object.as_any(), "WeakrefablePyClass")))?;
422
423                assert!(reference
424                    .getattr("__callback__")
425                    .is_ok_and(|result| result.is_none()));
426
427                assert!(reference.call0()?.is(&object));
428
429                drop(object);
430
431                assert!(reference.upgrade().is_none());
432                #[cfg(not(Py_LIMITED_API))]
433                assert_eq!(reference.getattr("__class__")?.to_string(), CLASS_NAME);
434                check_repr(&reference, None)?;
435
436                assert!(reference
437                    .getattr("__callback__")
438                    .is_ok_and(|result| result.is_none()));
439
440                assert!(reference.call0()?.is_none());
441
442                Ok(())
443            })
444        }
445
446        #[test]
447        fn test_weakref_upgrade_as() -> PyResult<()> {
448            Python::attach(|py| {
449                let object = Py::new(py, WeakrefablePyClass {})?;
450                let reference = PyWeakrefReference::new(object.bind(py))?;
451
452                {
453                    let obj = reference.upgrade_as::<WeakrefablePyClass>();
454
455                    assert!(obj.is_ok());
456                    let obj = obj.unwrap();
457
458                    assert!(obj.is_some());
459                    assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr())));
460                }
461
462                drop(object);
463
464                {
465                    let obj = reference.upgrade_as::<WeakrefablePyClass>();
466
467                    assert!(obj.is_ok());
468                    let obj = obj.unwrap();
469
470                    assert!(obj.is_none());
471                }
472
473                Ok(())
474            })
475        }
476
477        #[test]
478        fn test_weakref_upgrade_as_unchecked() -> PyResult<()> {
479            Python::attach(|py| {
480                let object = Py::new(py, WeakrefablePyClass {})?;
481                let reference = PyWeakrefReference::new(object.bind(py))?;
482
483                {
484                    let obj = unsafe { reference.upgrade_as_unchecked::<WeakrefablePyClass>() };
485
486                    assert!(obj.is_some());
487                    assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr())));
488                }
489
490                drop(object);
491
492                {
493                    let obj = unsafe { reference.upgrade_as_unchecked::<WeakrefablePyClass>() };
494
495                    assert!(obj.is_none());
496                }
497
498                Ok(())
499            })
500        }
501
502        #[test]
503        fn test_weakref_upgrade() -> PyResult<()> {
504            Python::attach(|py| {
505                let object = Py::new(py, WeakrefablePyClass {})?;
506                let reference = PyWeakrefReference::new(object.bind(py))?;
507
508                assert!(reference.call0()?.is(&object));
509                assert!(reference.upgrade().is_some());
510                assert!(reference.upgrade().is_some_and(|obj| obj.is(&object)));
511
512                drop(object);
513
514                assert!(reference.call0()?.is_none());
515                assert!(reference.upgrade().is_none());
516
517                Ok(())
518            })
519        }
520    }
521}