#![allow(clippy::undocumented_unsafe_blocks)]
#![cfg(feature = "macros")]
use pyo3::class::PyTraverseError;
use pyo3::class::PyVisit;
use pyo3::ffi;
use pyo3::prelude::*;
#[cfg(not(Py_GIL_DISABLED))]
use pyo3::py_run;
#[cfg(not(target_arch = "wasm32"))]
use std::cell::Cell;
use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
use std::sync::Once;
use std::sync::{Arc, Mutex};
mod test_utils;
#[pyclass(freelist = 2)]
struct ClassWithFreelist {}
#[test]
fn class_with_freelist() {
let ptr = Python::attach(|py| {
let inst = Py::new(py, ClassWithFreelist {}).unwrap();
let _inst2 = Py::new(py, ClassWithFreelist {}).unwrap();
let ptr = inst.as_ptr();
drop(inst);
ptr
});
Python::attach(|py| {
let inst3 = Py::new(py, ClassWithFreelist {}).unwrap();
assert_eq!(ptr, inst3.as_ptr());
let inst4 = Py::new(py, ClassWithFreelist {}).unwrap();
assert_ne!(ptr, inst4.as_ptr())
});
}
#[pyclass(freelist = 2)]
#[cfg(not(target_arch = "wasm32"))]
struct ClassWithFreelistAndData {
data: Option<usize>,
}
#[cfg(not(target_arch = "wasm32"))]
fn spin_freelist(py: Python<'_>, data: usize) {
for _ in 0..500 {
let inst1 = Py::new(py, ClassWithFreelistAndData { data: Some(data) }).unwrap();
let inst2 = Py::new(py, ClassWithFreelistAndData { data: Some(data) }).unwrap();
assert_eq!(inst1.borrow(py).data, Some(data));
assert_eq!(inst2.borrow(py).data, Some(data));
}
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn multithreaded_class_with_freelist() {
std::thread::scope(|s| {
s.spawn(|| {
Python::attach(|py| spin_freelist(py, 12));
});
s.spawn(|| {
Python::attach(|py| spin_freelist(py, 0x4d3d3d3));
});
});
}
fn drop_check() -> (DropGuard, DropCheck) {
let flag = Arc::new(Once::new());
(DropGuard(flag.clone()), DropCheck(flag))
}
struct DropGuard(Arc<Once>);
impl Drop for DropGuard {
fn drop(&mut self) {
self.0.call_once(|| ());
}
}
struct DropCheck(Arc<Once>);
impl DropCheck {
#[track_caller]
fn assert_not_dropped(&self) {
assert!(!self.0.is_completed());
}
#[track_caller]
fn assert_dropped(&self) {
assert!(self.0.is_completed());
}
#[track_caller]
fn assert_drops_with_gc(&self, object: *mut pyo3::ffi::PyObject) {
for _ in 0..100 {
if self.0.is_completed() {
return;
}
Python::attach(|py| {
py.run(c"import gc; gc.collect()", None, None).unwrap();
});
#[cfg(Py_GIL_DISABLED)]
{
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
panic!(
"Object was not dropped after 100 GC cycles, refcount is {}",
unsafe { ffi::Py_REFCNT(object) }
);
}
}
#[test]
fn data_is_dropped() {
#[pyclass]
struct DataIsDropped {
_guard1: DropGuard,
_guard2: DropGuard,
}
let (guard1, check1) = drop_check();
let (guard2, check2) = drop_check();
Python::attach(|py| {
let data_is_dropped = DataIsDropped {
_guard1: guard1,
_guard2: guard2,
};
let inst = Py::new(py, data_is_dropped).unwrap();
check1.assert_not_dropped();
check2.assert_not_dropped();
drop(inst);
});
check1.assert_dropped();
check2.assert_dropped();
}
#[pyclass(subclass)]
struct CycleWithClear {
cycle: Option<Py<PyAny>>,
_guard: DropGuard,
}
#[pymethods]
impl CycleWithClear {
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
visit.call(&self.cycle)
}
fn __clear__(slf: &Bound<'_, Self>) {
slf.borrow_mut().cycle = None;
}
}
#[test]
fn test_cycle_clear() {
let (guard, check) = drop_check();
let ptr = Python::attach(|py| {
let inst = Bound::new(
py,
CycleWithClear {
cycle: None,
_guard: guard,
},
)
.unwrap();
inst.borrow_mut().cycle = Some(inst.clone().into_any().unbind());
#[cfg(not(Py_GIL_DISABLED))]
py_run!(py, inst, "import gc; assert inst in gc.get_objects()");
check.assert_not_dropped();
inst.as_ptr()
});
check.assert_drops_with_gc(ptr);
}
#[test]
fn gc_null_traversal() {
#[pyclass]
struct GcNullTraversal {
cycle: Option<Py<Self>>,
null: Option<Py<Self>>,
}
#[pymethods]
impl GcNullTraversal {
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
visit.call(&self.cycle)?;
visit.call(&self.null)?; Ok(())
}
fn __clear__(&mut self) {
self.cycle = None;
self.null = None;
}
}
Python::attach(|py| {
let obj = Py::new(
py,
GcNullTraversal {
cycle: None,
null: None,
},
)
.unwrap();
obj.borrow_mut(py).cycle = Some(obj.clone_ref(py));
py.run(c"import gc; gc.collect()", None, None).unwrap();
});
}
#[test]
fn inheritance_with_new_methods_with_drop() {
#[pyclass(subclass)]
struct BaseClassWithDrop {
guard: Option<DropGuard>,
}
#[pymethods]
impl BaseClassWithDrop {
#[new]
fn new() -> BaseClassWithDrop {
BaseClassWithDrop { guard: None }
}
}
#[pyclass(extends = BaseClassWithDrop)]
struct SubClassWithDrop {
guard: Option<DropGuard>,
}
#[pymethods]
impl SubClassWithDrop {
#[new]
fn new() -> PyClassInitializer<Self> {
PyClassInitializer::from(BaseClassWithDrop { guard: None })
.add_subclass(SubClassWithDrop { guard: None })
}
}
let (guard_base, check_base) = drop_check();
let (guard_sub, check_sub) = drop_check();
Python::attach(|py| {
let typeobj = py.get_type::<SubClassWithDrop>();
let inst = typeobj
.call((), None)
.unwrap()
.cast_into::<SubClassWithDrop>()
.unwrap();
inst.as_super().borrow_mut().guard = Some(guard_base);
inst.borrow_mut().guard = Some(guard_sub);
check_base.assert_not_dropped();
check_sub.assert_not_dropped();
});
check_base.assert_dropped();
check_sub.assert_dropped();
}
#[test]
fn gc_during_borrow() {
#[pyclass]
struct TraversableClass {
traversed: AtomicBool,
}
impl TraversableClass {
fn new() -> Self {
Self {
traversed: AtomicBool::new(false),
}
}
}
#[pymethods]
impl TraversableClass {
fn __clear__(&mut self) {}
#[expect(clippy::unnecessary_wraps)]
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
self.traversed.store(true, Ordering::Relaxed);
Ok(())
}
}
Python::attach(|py| {
let ty = py.get_type::<TraversableClass>();
let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
let cell = Bound::new(py, TraversableClass::new()).unwrap();
assert!(!cell.borrow().traversed.load(Ordering::Relaxed));
unsafe { traverse(cell.as_ptr(), novisit, std::ptr::null_mut()) };
assert!(cell.borrow().traversed.load(Ordering::Relaxed));
let cell2 = Bound::new(py, TraversableClass::new()).unwrap();
let guard = cell2.borrow_mut();
assert!(!guard.traversed.load(Ordering::Relaxed));
unsafe { traverse(cell2.as_ptr(), novisit, std::ptr::null_mut()) };
assert!(!guard.traversed.load(Ordering::Relaxed));
drop(guard);
});
}
#[test]
fn traverse_partial() {
#[pyclass]
struct PartialTraverse {
member: Py<PyAny>,
}
impl PartialTraverse {
fn new(py: Python<'_>) -> Self {
Self { member: py.None() }
}
}
#[pymethods]
impl PartialTraverse {
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
visit.call(&self.member)?;
unreachable!()
}
}
Python::attach(|py| {
let ty = py.get_type::<PartialTraverse>();
let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
let obj = Py::new(py, PartialTraverse::new(py)).unwrap();
assert_eq!(
unsafe { traverse(obj.as_ptr(), visit_error, std::ptr::null_mut()) },
-1
);
})
}
#[test]
#[cfg(panic = "unwind")]
fn traverse_panic() {
#[pyclass]
struct PanickyTraverse {
member: Py<PyAny>,
}
impl PanickyTraverse {
fn new(py: Python<'_>) -> Self {
Self { member: py.None() }
}
}
#[pymethods]
impl PanickyTraverse {
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
visit.call(&self.member)?;
panic!("at the disco");
}
}
Python::attach(|py| {
let ty = py.get_type::<PanickyTraverse>();
let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
let obj = Py::new(py, PanickyTraverse::new(py)).unwrap();
assert_eq!(
unsafe { traverse(obj.as_ptr(), novisit, std::ptr::null_mut()) },
-1
);
})
}
#[test]
#[cfg(panic = "unwind")]
fn tries_gil_in_traverse() {
#[pyclass]
struct TriesGILInTraverse {}
#[pymethods]
impl TriesGILInTraverse {
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
Python::attach(|_py| Ok(()))
}
}
Python::attach(|py| {
let ty = py.get_type::<TriesGILInTraverse>();
let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
let obj = Py::new(py, TriesGILInTraverse {}).unwrap();
assert_eq!(
unsafe { traverse(obj.as_ptr(), novisit, std::ptr::null_mut()) },
-1
);
})
}
#[test]
fn traverse_cannot_be_hijacked() {
#[pyclass]
struct HijackedTraverse {
traversed: AtomicBool,
hijacked: AtomicBool,
}
impl HijackedTraverse {
fn new() -> Self {
Self {
traversed: AtomicBool::new(false),
hijacked: AtomicBool::new(false),
}
}
fn traversed_and_hijacked(&self) -> (bool, bool) {
(
self.traversed.load(Ordering::Acquire),
self.hijacked.load(Ordering::Acquire),
)
}
}
#[pymethods]
impl HijackedTraverse {
#[expect(clippy::unnecessary_wraps)]
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
self.traversed.store(true, Ordering::Release);
Ok(())
}
}
#[allow(dead_code, reason = "used to test the trait method is not used")]
trait Traversable {
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>;
}
impl Traversable for PyRef<'_, HijackedTraverse> {
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
self.hijacked.store(true, Ordering::Release);
Ok(())
}
}
Python::attach(|py| {
let ty = py.get_type::<HijackedTraverse>();
let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
let cell = Bound::new(py, HijackedTraverse::new()).unwrap();
assert_eq!(cell.borrow().traversed_and_hijacked(), (false, false));
unsafe { traverse(cell.as_ptr(), novisit, std::ptr::null_mut()) };
assert_eq!(cell.borrow().traversed_and_hijacked(), (true, false));
})
}
#[pyclass]
struct DropDuringTraversal {
cycle: Mutex<Option<Py<Self>>>,
_guard: DropGuard,
}
#[pymethods]
impl DropDuringTraversal {
#[expect(clippy::unnecessary_wraps)]
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
let mut cycle_ref = self.cycle.lock().unwrap();
*cycle_ref = None;
Ok(())
}
}
#[cfg(not(pyo3_disable_reference_pool))]
#[test]
fn drop_during_traversal_with_gil() {
let (guard, check) = drop_check();
let ptr = Python::attach(|py| {
let cycle = Mutex::new(None);
let inst = Py::new(
py,
DropDuringTraversal {
cycle,
_guard: guard,
},
)
.unwrap();
*inst.borrow_mut(py).cycle.lock().unwrap() = Some(inst.clone_ref(py));
check.assert_not_dropped();
let ptr = inst.as_ptr();
drop(inst);
#[cfg(not(Py_GIL_DISABLED))]
{
check.assert_not_dropped();
}
ptr
});
check.assert_drops_with_gc(ptr);
}
#[cfg(not(pyo3_disable_reference_pool))]
#[test]
fn drop_during_traversal_without_gil() {
let (guard, check) = drop_check();
let inst = Python::attach(|py| {
let cycle = Mutex::new(None);
let inst = Py::new(
py,
DropDuringTraversal {
cycle,
_guard: guard,
},
)
.unwrap();
*inst.borrow_mut(py).cycle.lock().unwrap() = Some(inst.clone_ref(py));
check.assert_not_dropped();
inst
});
let ptr = inst.as_ptr();
drop(inst);
check.assert_drops_with_gc(ptr);
}
#[test]
#[cfg(not(target_arch = "wasm32"))] fn unsendable_are_not_traversed_on_foreign_thread() {
#[pyclass(unsendable)]
struct UnsendableTraversal {
traversed: Cell<bool>,
}
#[pymethods]
impl UnsendableTraversal {
fn __clear__(&mut self) {}
#[expect(clippy::unnecessary_wraps)]
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
self.traversed.set(true);
Ok(())
}
}
#[derive(Clone, Copy)]
struct SendablePtr(*mut pyo3::ffi::PyObject);
unsafe impl Send for SendablePtr {}
Python::attach(|py| {
let ty = py.get_type::<UnsendableTraversal>();
let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
let obj = Bound::new(
py,
UnsendableTraversal {
traversed: Cell::new(false),
},
)
.unwrap();
let ptr = SendablePtr(obj.as_ptr());
std::thread::spawn(move || {
assert_eq!(
unsafe { traverse({ ptr }.0, novisit, std::ptr::null_mut()) },
0
);
})
.join()
.unwrap();
assert!(!obj.borrow().traversed.get());
assert_eq!(
unsafe { traverse({ ptr }.0, novisit, std::ptr::null_mut()) },
0
);
assert!(obj.borrow().traversed.get());
});
}
#[test]
fn test_traverse_subclass() {
#[pyclass(extends = CycleWithClear)]
struct SubOverrideTraverse {}
#[pymethods]
impl SubOverrideTraverse {
#[expect(clippy::unnecessary_wraps)]
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
Ok(())
}
}
let (guard, check) = drop_check();
Python::attach(|py| {
let base = CycleWithClear {
cycle: None,
_guard: guard,
};
let obj = Bound::new(
py,
PyClassInitializer::from(base).add_subclass(SubOverrideTraverse {}),
)
.unwrap();
obj.borrow_mut().as_super().cycle = Some(obj.clone().into_any().unbind());
check.assert_not_dropped();
let ptr = obj.as_ptr();
drop(obj);
#[cfg(not(Py_GIL_DISABLED))]
{
check.assert_not_dropped();
}
check.assert_drops_with_gc(ptr);
});
}
#[test]
fn test_traverse_subclass_override_clear() {
#[pyclass(extends = CycleWithClear)]
struct SubOverrideClear {}
#[pymethods]
impl SubOverrideClear {
#[expect(clippy::unnecessary_wraps)]
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
Ok(())
}
fn __clear__(&self) {
}
}
let (guard, check) = drop_check();
Python::attach(|py| {
let base = CycleWithClear {
cycle: None,
_guard: guard,
};
let obj = Bound::new(
py,
PyClassInitializer::from(base).add_subclass(SubOverrideClear {}),
)
.unwrap();
obj.borrow_mut().as_super().cycle = Some(obj.clone().into_any().unbind());
check.assert_not_dropped();
let ptr = obj.as_ptr();
drop(obj);
#[cfg(not(Py_GIL_DISABLED))]
{
check.assert_not_dropped();
}
check.assert_drops_with_gc(ptr);
});
}
unsafe fn get_type_traverse(tp: *mut pyo3::ffi::PyTypeObject) -> Option<pyo3::ffi::traverseproc> {
unsafe { std::mem::transmute(pyo3::ffi::PyType_GetSlot(tp, pyo3::ffi::Py_tp_traverse)) }
}
extern "C" fn novisit(
_object: *mut pyo3::ffi::PyObject,
_arg: *mut core::ffi::c_void,
) -> std::ffi::c_int {
0
}
extern "C" fn visit_error(
_object: *mut pyo3::ffi::PyObject,
_arg: *mut core::ffi::c_void,
) -> std::ffi::c_int {
-1
}
static BASE_FIELD: AtomicPtr<pyo3::ffi::PyObject> = AtomicPtr::new(std::ptr::null_mut());
static CHILD_FIELD: AtomicPtr<pyo3::ffi::PyObject> = AtomicPtr::new(std::ptr::null_mut());
static BASE_VISITED: AtomicBool = AtomicBool::new(false);
static CHILD_VISITED: AtomicBool = AtomicBool::new(false);
extern "C" fn visit_error_on_base_field(
object: *mut pyo3::ffi::PyObject,
_arg: *mut core::ffi::c_void,
) -> std::ffi::c_int {
if object == CHILD_FIELD.load(Ordering::SeqCst) {
CHILD_VISITED.store(true, Ordering::SeqCst);
}
if object == BASE_FIELD.load(Ordering::SeqCst) {
BASE_VISITED.store(true, Ordering::SeqCst);
return -1;
}
0
}
#[test]
#[cfg(any(not(Py_LIMITED_API), Py_3_11))] fn test_drop_buffer_during_traversal_without_gil() {
use pyo3::buffer::PyBuffer;
use pyo3::types::PyBytes;
#[pyclass]
struct BufferDropDuringTraversal {
inner: Mutex<Option<(DropGuard, PyBuffer<u8>)>>,
cycle: Option<Py<PyAny>>,
}
#[pymethods]
impl BufferDropDuringTraversal {
#[expect(clippy::unnecessary_wraps)]
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
self.inner.lock().unwrap().take();
Ok(())
}
fn __clear__(&mut self) {
self.cycle = None;
}
}
let (guard, check) = drop_check();
Python::attach(|py| {
let obj = Py::new(
py,
BufferDropDuringTraversal {
inner: Mutex::new(Some((
guard,
PyBuffer::get(&PyBytes::new(py, b"test")).unwrap(),
))),
cycle: None,
},
)
.unwrap();
obj.borrow_mut(py).cycle = Some(obj.clone_ref(py).into_any());
let ptr = obj.as_ptr();
drop(obj);
check.assert_drops_with_gc(ptr);
});
}
#[test]
fn test_super_traverse_early_return_does_not_abort() {
#[pyclass(subclass)]
struct TraverseBase {
field: Py<PyAny>,
}
#[pymethods]
impl TraverseBase {
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
visit.call(&self.field)
}
}
#[pyclass(extends=TraverseBase)]
struct TraverseChild {
field: Py<PyAny>,
}
#[pymethods]
impl TraverseChild {
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
visit.call(&self.field)
}
}
Python::attach(|py| {
let base_field = pyo3::types::PyList::empty(py).into_any().unbind();
let child_field = pyo3::types::PyList::empty(py).into_any().unbind();
let initializer = PyClassInitializer::from(TraverseBase {
field: base_field.clone_ref(py),
})
.add_subclass(TraverseChild {
field: child_field.clone_ref(py),
});
let child = Bound::new(py, initializer).unwrap();
BASE_FIELD.store(base_field.as_ptr(), Ordering::SeqCst);
CHILD_FIELD.store(child_field.as_ptr(), Ordering::SeqCst);
let traverse =
unsafe { get_type_traverse(py.get_type::<TraverseChild>().as_type_ptr()).unwrap() };
let retval = unsafe {
traverse(
child.as_ptr(),
visit_error_on_base_field,
std::ptr::null_mut(),
)
};
assert!(
BASE_VISITED.load(Ordering::SeqCst),
"super-type traverse never visited its field, so the early return was not exercised"
);
assert_eq!(
retval, -1,
"traverse did not propagate the non-zero return of the super-type traverse"
);
assert!(
!CHILD_VISITED.load(Ordering::SeqCst),
"child __traverse__ ran despite the super-type traverse returning non-zero"
);
});
}
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
#[test]
fn dict_class_is_a_gc_type() {
Python::attach(|py| {
let ty = py.get_type::<DictCycleNoTraverse>();
let flags = unsafe { ffi::PyType_GetFlags(ty.as_type_ptr()) };
assert_ne!(flags & ffi::Py_TPFLAGS_HAVE_GC, 0);
});
}
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
#[pyclass(dict)]
struct DictCycleNoTraverse {
_guard: DropGuard,
}
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
#[test]
fn dict_cycle_collected_without_traverse() {
let (guard, check) = drop_check();
let ptr = Python::attach(|py| {
let inst = Bound::new(py, DictCycleNoTraverse { _guard: guard }).unwrap();
inst.setattr("cycle", &inst).unwrap();
check.assert_not_dropped();
inst.as_ptr()
});
check.assert_drops_with_gc(ptr);
}
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
#[pyclass(dict)]
struct DictCycleTraverseOnly {
_guard: DropGuard,
}
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
#[pymethods]
impl DictCycleTraverseOnly {
#[expect(clippy::unnecessary_wraps)]
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
Ok(())
}
}
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
#[test]
fn dict_cycle_collected_with_traverse_only() {
let (guard, check) = drop_check();
let ptr = Python::attach(|py| {
let inst = Bound::new(py, DictCycleTraverseOnly { _guard: guard }).unwrap();
inst.setattr("cycle", &inst).unwrap();
check.assert_not_dropped();
inst.as_ptr()
});
check.assert_drops_with_gc(ptr);
}
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
#[pyclass(dict)]
struct DictCycleTraverseAndClear {
_guard: DropGuard,
field: Option<Py<PyAny>>,
}
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
#[pymethods]
impl DictCycleTraverseAndClear {
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
if let Some(field) = &self.field {
visit.call(field)?;
}
Ok(())
}
fn __clear__(&mut self) {
self.field = None;
}
}
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
#[test]
fn dict_cycle_collected_with_traverse_and_clear() {
let (guard, check) = drop_check();
let ptr = Python::attach(|py| {
let inst = Bound::new(
py,
DictCycleTraverseAndClear {
_guard: guard,
field: None,
},
)
.unwrap();
inst.setattr("cycle", &inst).unwrap();
check.assert_not_dropped();
inst.as_ptr()
});
check.assert_drops_with_gc(ptr);
}