From fc684b5b376929fed5f3a29de7224562b9e6ddff Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 9 Aug 2026 18:48:44 +0200 Subject: [PATCH 1/6] ci: avoid measuring refcounts on shared globals --- tests/test_inheritance.rs | 148 +++++++++++++++----------------------- 1 file changed, 57 insertions(+), 91 deletions(-) diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index f8e52430f4e..0fed52e86d4 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -1,5 +1,6 @@ #![cfg(feature = "macros")] +use pyo3::ffi; use pyo3::prelude::*; use pyo3::py_run; use pyo3::types::IntoPyDict; @@ -9,39 +10,31 @@ mod test_utils; /// Macro to generate refcount leak tests for types. /// Ensures that creating and destroying instances doesn't leak references to the type. /// Regression test for issues #1363 and #6223. -#[cfg(not(Py_GIL_DISABLED))] macro_rules! assert_type_refcount_stable { // Simple case: type with parameterless constructor ($type_name:ty) => { - assert_type_refcount_stable!($type_name, stringify!($type_name), "Type()"); + assert_type_refcount_stable!($type_name, stringify!($type_name), |py| Py::new( + py, + <$type_name>::new() + ) + .unwrap()); }; // With custom constructor ($type_name:ty, $test_name:expr, $ctor:expr) => {{ Python::attach(|py| { - #[expect(non_snake_case)] - let Type = py.get_type::<$type_name>(); - let ctor_code = $ctor; - py_run!( - py, - Type, - &format!( - r#" - import gc - import sys - - gc.collect() - count = sys.getrefcount(Type) - - for i in range(1000): - obj = {} - del obj - - gc.collect() - after = sys.getrefcount(Type) - assert after == count, f"Type ref count leaked: {{after}} vs {{count}}" - "#, - ctor_code - ) + let ty = py.get_type::<$type_name>(); + + let before = unsafe { ffi::Py_REFCNT(ty.as_ptr()) }; + + for _ in 0..1000 { + let _ = ($ctor)(py); + } + + let after = unsafe { ffi::Py_REFCNT(ty.as_ptr()) }; + assert_eq!( + before, after, + "Type ref count leaked: {} vs {}", + after, before ); }); }}; @@ -459,83 +452,56 @@ mod inheriting_native_type { ); }); } +} - // Refcount tests for native type classes - #[cfg(not(any(PyPy, GraalPy, Py_GIL_DISABLED)))] - #[test] - fn test_setwitname_ref_counts() { - assert_type_refcount_stable!(SetWithName); - } - - #[cfg(not(any(GraalPy, Py_GIL_DISABLED)))] - #[test] - fn test_dictwithname_ref_counts() { - assert_type_refcount_stable!(DictWithName); - } - - #[cfg(not(Py_GIL_DISABLED))] - #[test] - fn test_customexception_ref_counts() { - assert_type_refcount_stable!(CustomException, "custom_exception", r#"Type('test')"#); - } - - #[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))] - #[test] - fn test_tzinfowithname_ref_counts() { - assert_type_refcount_stable!(TzInfoWithName); - } +#[test] +fn test_inherit_object_refcount() { + #[pyclass] // no extends is equivalent to inheriting from `object` + struct InheritObject {} - #[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))] - #[test] - fn test_listwithname_ref_counts() { - assert_type_refcount_stable!(ListWithName); + #[pymethods] + impl InheritObject { + #[new] + fn new() -> Self { + Self {} + } } - #[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))] - #[test] - fn test_sublistwithname_ref_counts() { - assert_type_refcount_stable!(SubListWithName); - } + assert_type_refcount_stable!(InheritObject); } -#[pyclass(subclass)] -struct SimpleClass {} +#[test] +fn test_inherit_pyclass_refcount() { + #[pyclass(subclass)] + struct Base {} -#[pymethods] -impl SimpleClass { - #[new] - fn new() -> Self { - Self {} - } -} + #[pyclass(extends=Base)] + struct InheritPyClass {} -// Generate refcount tests for all top-level types -#[cfg(not(Py_GIL_DISABLED))] -#[test] -fn test_baseclass_ref_counts() { - assert_type_refcount_stable!(BaseClass); -} + #[pymethods] + impl InheritPyClass { + #[new] + fn new() -> PyClassInitializer { + PyClassInitializer::from(Base {}).add_subclass(Self {}) + } + } -#[cfg(not(Py_GIL_DISABLED))] -#[test] -fn test_subclass_ref_counts() { - assert_type_refcount_stable!(SubClass); + assert_type_refcount_stable!(InheritPyClass); } -#[cfg(not(Py_GIL_DISABLED))] +#[cfg(any(Py_3_12, not(Py_LIMITED_API)))] #[test] -fn test_base_class_with_result_ref_counts() { - assert_type_refcount_stable!(BaseClassWithResult, "base_class_with_result", "Type(10)"); -} +fn test_inherit_native_type_refcount() { + #[pyclass(extends=pyo3::types::PyList)] + struct InheritList {} -#[cfg(not(Py_GIL_DISABLED))] -#[test] -fn test_subclass2_ref_counts() { - assert_type_refcount_stable!(SubClass2, "subclass2", "Type(10)"); -} + #[pymethods] + impl InheritList { + #[new] + fn new() -> PyClassInitializer { + PyClassInitializer::from(pyo3::types::PyList::empty()).add_subclass(Self {}) + } + } -#[cfg(not(Py_GIL_DISABLED))] -#[test] -fn test_simpleclass_ref_counts() { - assert_type_refcount_stable!(SimpleClass); + assert_type_refcount_stable!(InheritList); } From 347302f611287665809a9b6d00dc447b00cb3c47 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 9 Aug 2026 19:38:12 +0200 Subject: [PATCH 2/6] fix native inheritance test --- tests/test_inheritance.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 0fed52e86d4..588e0cd3fb8 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -24,12 +24,14 @@ macro_rules! assert_type_refcount_stable { Python::attach(|py| { let ty = py.get_type::<$type_name>(); + // SAFETY: ty is known to be a valid object let before = unsafe { ffi::Py_REFCNT(ty.as_ptr()) }; for _ in 0..1000 { let _ = ($ctor)(py); } + // SAFETY: ty is known to be a valid object let after = unsafe { ffi::Py_REFCNT(ty.as_ptr()) }; assert_eq!( before, after, @@ -492,16 +494,16 @@ fn test_inherit_pyclass_refcount() { #[cfg(any(Py_3_12, not(Py_LIMITED_API)))] #[test] fn test_inherit_native_type_refcount() { - #[pyclass(extends=pyo3::types::PyList)] - struct InheritList {} + #[pyclass(extends=pyo3::types::PyDict)] + struct InheritDict {} #[pymethods] - impl InheritList { + impl InheritDict { #[new] fn new() -> PyClassInitializer { - PyClassInitializer::from(pyo3::types::PyList::empty()).add_subclass(Self {}) + PyClassInitializer::from(Self {}) } } - assert_type_refcount_stable!(InheritList); + assert_type_refcount_stable!(InheritDict); } From 3a7b2b50abf6f44ad3bab52d7b2cea9a70470485 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 9 Aug 2026 20:01:56 +0200 Subject: [PATCH 3/6] work around complexity in free-threaded Python --- tests/test_inheritance.rs | 42 ++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 588e0cd3fb8..7463c77bd92 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -21,24 +21,34 @@ macro_rules! assert_type_refcount_stable { }; // With custom constructor ($type_name:ty, $test_name:expr, $ctor:expr) => {{ - Python::attach(|py| { - let ty = py.get_type::<$type_name>(); - - // SAFETY: ty is known to be a valid object - let before = unsafe { ffi::Py_REFCNT(ty.as_ptr()) }; + let ty = Python::attach(|py| py.get_type::<$type_name>().unbind()); + + // SAFETY: ty is known to be a valid object + let before = Python::attach(|_| unsafe { ffi::Py_REFCNT(ty.as_ptr()) }); + + // Using a separate thread works around tricks in free-threaded Python to + // make type object reference counting fast. We spawn a new thread and + // using a temporary thread state for the loop. When that thread is destructed, + // free-threaded Python merges the temporary thread state refcounts + // back onto the main object. + std::thread::scope(|s| { + s.spawn(|| { + Python::attach(|py| { + for _ in 0..1000 { + let _ = ($ctor)(py); + } + }) + }); + }); - for _ in 0..1000 { - let _ = ($ctor)(py); - } + // SAFETY: ty is known to be a valid object + let after = Python::attach(|_| unsafe { ffi::Py_REFCNT(ty.as_ptr()) }); - // SAFETY: ty is known to be a valid object - let after = unsafe { ffi::Py_REFCNT(ty.as_ptr()) }; - assert_eq!( - before, after, - "Type ref count leaked: {} vs {}", - after, before - ); - }); + assert_eq!( + before, after, + "Type ref count leaked: {} vs {}", + after, before + ); }}; } From e86c46f51a563839545a286b0aa492ee8b8320eb Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 9 Aug 2026 20:10:17 +0200 Subject: [PATCH 4/6] fixup wasm & threads --- tests/test_inheritance.rs | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 7463c77bd92..56f1ef8bd8f 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -5,11 +5,16 @@ use pyo3::prelude::*; use pyo3::py_run; use pyo3::types::IntoPyDict; +#[path = "../src/internal/macros.rs"] +#[macro_use] +mod macros; // for cfg_select polyfill on MSRV < 1.95 + mod test_utils; /// Macro to generate refcount leak tests for types. /// Ensures that creating and destroying instances doesn't leak references to the type. /// Regression test for issues #1363 and #6223. +#[cfg(not(all(target_arch = "wasm32", Py_GIL_DISABLED)))] macro_rules! assert_type_refcount_stable { // Simple case: type with parameterless constructor ($type_name:ty) => { @@ -26,20 +31,31 @@ macro_rules! assert_type_refcount_stable { // SAFETY: ty is known to be a valid object let before = Python::attach(|_| unsafe { ffi::Py_REFCNT(ty.as_ptr()) }); + let drive_refcounts = || { + Python::attach(|py| { + for _ in 0..1000 { + let _ = ($ctor)(py); + } + }) + }; + // Using a separate thread works around tricks in free-threaded Python to // make type object reference counting fast. We spawn a new thread and // using a temporary thread state for the loop. When that thread is destructed, // free-threaded Python merges the temporary thread state refcounts // back onto the main object. - std::thread::scope(|s| { - s.spawn(|| { - Python::attach(|py| { - for _ in 0..1000 { - let _ = ($ctor)(py); - } - }) - }); - }); + cfg_select! { + Py_GIL_DISABLED => { + std::thread::scope(|s| { + s.spawn(|| { + drive_refcounts(); + }); + }); + } + not(Py_GIL_DISABLED) => { + drive_refcounts(); + } + } // SAFETY: ty is known to be a valid object let after = Python::attach(|_| unsafe { ffi::Py_REFCNT(ty.as_ptr()) }); @@ -467,6 +483,7 @@ mod inheriting_native_type { } #[test] +#[cfg(not(all(target_arch = "wasm32", Py_GIL_DISABLED)))] fn test_inherit_object_refcount() { #[pyclass] // no extends is equivalent to inheriting from `object` struct InheritObject {} @@ -483,6 +500,7 @@ fn test_inherit_object_refcount() { } #[test] +#[cfg(not(all(target_arch = "wasm32", Py_GIL_DISABLED)))] fn test_inherit_pyclass_refcount() { #[pyclass(subclass)] struct Base {} @@ -502,6 +520,7 @@ fn test_inherit_pyclass_refcount() { } #[cfg(any(Py_3_12, not(Py_LIMITED_API)))] +#[cfg(not(all(target_arch = "wasm32", Py_GIL_DISABLED)))] #[test] fn test_inherit_native_type_refcount() { #[pyclass(extends=pyo3::types::PyDict)] From 5145882918809c52188ab3815d9500bb1a136383 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 10 Aug 2026 11:00:41 +0200 Subject: [PATCH 5/6] skip dict inheritance test on GraalPy --- tests/test_inheritance.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 56f1ef8bd8f..0b2d258dd29 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -521,6 +521,7 @@ fn test_inherit_pyclass_refcount() { #[cfg(any(Py_3_12, not(Py_LIMITED_API)))] #[cfg(not(all(target_arch = "wasm32", Py_GIL_DISABLED)))] +#[cfg(not(GraalPy))] // FIXME: it should be possible to use variable layout to inherit dict on graalpy #[test] fn test_inherit_native_type_refcount() { #[pyclass(extends=pyo3::types::PyDict)] From c4b42eaa283168164a8fe81c6b2bb3b249b37dcc Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 10 Aug 2026 13:34:34 +0200 Subject: [PATCH 6/6] fix unused import on wasm --- tests/test_inheritance.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 0b2d258dd29..197145c0936 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -1,6 +1,5 @@ #![cfg(feature = "macros")] -use pyo3::ffi; use pyo3::prelude::*; use pyo3::py_run; use pyo3::types::IntoPyDict; @@ -29,7 +28,7 @@ macro_rules! assert_type_refcount_stable { let ty = Python::attach(|py| py.get_type::<$type_name>().unbind()); // SAFETY: ty is known to be a valid object - let before = Python::attach(|_| unsafe { ffi::Py_REFCNT(ty.as_ptr()) }); + let before = Python::attach(|_| unsafe { pyo3::ffi::Py_REFCNT(ty.as_ptr()) }); let drive_refcounts = || { Python::attach(|py| { @@ -58,7 +57,7 @@ macro_rules! assert_type_refcount_stable { } // SAFETY: ty is known to be a valid object - let after = Python::attach(|_| unsafe { ffi::Py_REFCNT(ty.as_ptr()) }); + let after = Python::attach(|_| unsafe { pyo3::ffi::Py_REFCNT(ty.as_ptr()) }); assert_eq!( before, after,