From f221942b44db875a3654f3307d8ebb777a8e73d4 Mon Sep 17 00:00:00 2001 From: Dmitry Agafonov <42949186+Malkiz223@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:51:20 +0300 Subject: [PATCH 1/2] Make iterators hold a reference to the cache --- src/pyclasses/cache.rs | 30 +++++++++++++-------- src/pyclasses/fifocache.rs | 30 +++++++++++++-------- src/pyclasses/lfucache.rs | 39 ++++++++++++++++++---------- src/pyclasses/lrucache.rs | 30 +++++++++++++-------- src/pyclasses/rrcache.rs | 30 +++++++++++++-------- src/pyclasses/ttlcache.rs | 39 ++++++++++++++++++---------- src/pyclasses/vttlcache.rs | 39 ++++++++++++++++++---------- tests/mixins.py | 53 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 204 insertions(+), 86 deletions(-) diff --git a/src/pyclasses/cache.rs b/src/pyclasses/cache.rs index de1ef9d..f2a6ff4 100644 --- a/src/pyclasses/cache.rs +++ b/src/pyclasses/cache.rs @@ -531,52 +531,55 @@ impl PyCache { .map(|x| !x) } - fn items(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn items(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let gv = inner.shared().generation_version().clone(); let initial_gv = gv.get(); let result = PyCacheItems { + cache: slf.as_any().clone().unbind(), // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] iter: parking_lot::Mutex::new(unsafe { inner.policy().table().iter() }), gv, initial_gv, }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } - fn values(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn values(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let gv = inner.shared().generation_version().clone(); let initial_gv = gv.get(); let result = PyCacheValues { + cache: slf.as_any().clone().unbind(), // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] iter: parking_lot::Mutex::new(unsafe { inner.policy().table().iter() }), gv, initial_gv, }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } - fn keys(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn keys(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let gv = inner.shared().generation_version().clone(); let initial_gv = gv.get(); let result = PyCacheKeys { + cache: slf.as_any().clone().unbind(), // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] iter: parking_lot::Mutex::new(unsafe { inner.policy().table().iter() }), gv, initial_gv, }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } #[inline] - fn __iter__(&self) -> pyo3::PyResult> { - self.keys() + fn __iter__(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + Self::keys(slf) } fn copy(&self, py: pyo3::Python) -> pyo3::PyResult> { @@ -670,6 +673,7 @@ macro_rules! implement_iterator { $( implement_pyclass! { [generic, frozen] $name as $pyname { + cache: pyo3::Py, initial_gv: u32, gv: utils::GenerationVersion, iter: parking_lot::Mutex>, @@ -683,6 +687,10 @@ macro_rules! implement_iterator { slf } + fn __traverse__(&self, visit: pyo3::PyVisit<'_>) -> Result<(), pyo3::PyTraverseError> { + visit.call(&self.cache) + } + fn __next__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<$rt_type> { if slf.initial_gv != slf.gv.get() { return Err(new_py_error!( diff --git a/src/pyclasses/fifocache.rs b/src/pyclasses/fifocache.rs index ffdb360..7cbb726 100644 --- a/src/pyclasses/fifocache.rs +++ b/src/pyclasses/fifocache.rs @@ -535,51 +535,54 @@ impl PyFIFOCache { .map(|x| !x) } - fn items(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn items(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let gv = inner.shared().generation_version().clone(); let initial_gv = gv.get(); // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyFIFOCacheItems { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(inner.policy().iter()), gv, initial_gv, }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } - fn values(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn values(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let gv = inner.shared().generation_version().clone(); let initial_gv = gv.get(); // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyFIFOCacheValues { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(inner.policy().iter()), gv, initial_gv, }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } - fn keys(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn keys(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let gv = inner.shared().generation_version().clone(); let initial_gv = gv.get(); // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyFIFOCacheKeys { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(inner.policy().iter()), gv, initial_gv, }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } #[inline] - fn __iter__(&self) -> pyo3::PyResult> { - self.keys() + fn __iter__(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + Self::keys(slf) } fn copy(&self, py: pyo3::Python) -> pyo3::PyResult> { @@ -696,6 +699,7 @@ macro_rules! implement_iterator { $( implement_pyclass! { [generic, frozen] $name as $pyname { + cache: pyo3::Py, initial_gv: u32, gv: utils::GenerationVersion, iter: parking_lot::Mutex>, @@ -709,6 +713,10 @@ macro_rules! implement_iterator { slf } + fn __traverse__(&self, visit: pyo3::PyVisit<'_>) -> Result<(), pyo3::PyTraverseError> { + visit.call(&self.cache) + } + fn __next__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<$rt_type> { if slf.initial_gv != slf.gv.get() { return Err(new_py_error!( diff --git a/src/pyclasses/lfucache.rs b/src/pyclasses/lfucache.rs index 369e984..dff01f0 100644 --- a/src/pyclasses/lfucache.rs +++ b/src/pyclasses/lfucache.rs @@ -554,69 +554,75 @@ impl PyLFUCache { .map(|x| !x) } - fn items(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn items(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let mut policy = inner.policy(); let gv = inner.shared().generation_version(); let iter = policy.iter(gv); let result = PyLFUCacheItems { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(iter), gv: gv.clone(), initial_gv: gv.get(), }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } - fn values(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn values(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let mut policy = inner.policy(); let gv = inner.shared().generation_version(); let iter = policy.iter(gv); let result = PyLFUCacheValues { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(iter), gv: gv.clone(), initial_gv: gv.get(), }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } - fn keys(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn keys(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let mut policy = inner.policy(); let gv = inner.shared().generation_version(); let iter = policy.iter(gv); let result = PyLFUCacheKeys { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(iter), gv: gv.clone(), initial_gv: gv.get(), }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } #[inline] - fn __iter__(&self) -> pyo3::PyResult> { - self.keys() + fn __iter__(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + Self::keys(slf) } - fn items_with_frequency(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn items_with_frequency( + slf: pyo3::Bound<'_, Self>, + ) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let mut policy = inner.policy(); let gv = inner.shared().generation_version(); let iter = policy.iter(gv); let result = PyLFUCacheItemsWithFrequency { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(iter), gv: gv.clone(), initial_gv: gv.get(), }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } fn copy(&self, py: pyo3::Python) -> pyo3::PyResult> { @@ -760,6 +766,7 @@ macro_rules! implement_iterator { $( implement_pyclass! { [generic, frozen] $name as $pyname { + cache: pyo3::Py, initial_gv: u32, gv: utils::GenerationVersion, iter: parking_lot::Mutex>, @@ -773,6 +780,10 @@ macro_rules! implement_iterator { slf } + fn __traverse__(&self, visit: pyo3::PyVisit<'_>) -> Result<(), pyo3::PyTraverseError> { + visit.call(&self.cache) + } + fn __next__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<$rt_type> { if slf.initial_gv != slf.gv.get() { return Err(new_py_error!( diff --git a/src/pyclasses/lrucache.rs b/src/pyclasses/lrucache.rs index 1eb6a5a..e0d419a 100644 --- a/src/pyclasses/lrucache.rs +++ b/src/pyclasses/lrucache.rs @@ -562,51 +562,54 @@ impl PyLRUCache { .map(|x| !x) } - fn items(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn items(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let gv = inner.shared().generation_version().clone(); let initial_gv = gv.get(); // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyLRUCacheItems { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(unsafe { inner.policy().list().iter() }), gv, initial_gv, }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } - fn values(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn values(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let gv = inner.shared().generation_version().clone(); let initial_gv = gv.get(); // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyLRUCacheValues { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(unsafe { inner.policy().list().iter() }), gv, initial_gv, }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } - fn keys(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn keys(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let gv = inner.shared().generation_version().clone(); let initial_gv = gv.get(); // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyLRUCacheKeys { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(unsafe { inner.policy().list().iter() }), gv, initial_gv, }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } #[inline] - fn __iter__(&self) -> pyo3::PyResult> { - self.keys() + fn __iter__(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + Self::keys(slf) } fn copy(&self, py: pyo3::Python) -> pyo3::PyResult> { @@ -744,6 +747,7 @@ macro_rules! implement_iterator { $( implement_pyclass! { [generic, frozen] $name as $pyname { + cache: pyo3::Py, initial_gv: u32, gv: utils::GenerationVersion, iter: parking_lot::Mutex>, @@ -757,6 +761,10 @@ macro_rules! implement_iterator { slf } + fn __traverse__(&self, visit: pyo3::PyVisit<'_>) -> Result<(), pyo3::PyTraverseError> { + visit.call(&self.cache) + } + fn __next__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<$rt_type> { if slf.initial_gv != slf.gv.get() { return Err(new_py_error!( diff --git a/src/pyclasses/rrcache.rs b/src/pyclasses/rrcache.rs index 6d182c7..1b7d5e2 100644 --- a/src/pyclasses/rrcache.rs +++ b/src/pyclasses/rrcache.rs @@ -535,51 +535,54 @@ impl PyRRCache { .map(|x| !x) } - fn items(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn items(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let gv = inner.shared().generation_version().clone(); let initial_gv = gv.get(); // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyRRCacheItems { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(unsafe { inner.policy().table().iter() }), gv, initial_gv, }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } - fn values(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn values(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let gv = inner.shared().generation_version().clone(); let initial_gv = gv.get(); // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyRRCacheValues { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(unsafe { inner.policy().table().iter() }), gv, initial_gv, }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } - fn keys(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn keys(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let gv = inner.shared().generation_version().clone(); let initial_gv = gv.get(); // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyRRCacheKeys { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(unsafe { inner.policy().table().iter() }), gv, initial_gv, }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } #[inline] - fn __iter__(&self) -> pyo3::PyResult> { - self.keys() + fn __iter__(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + Self::keys(slf) } fn copy(&self, py: pyo3::Python) -> pyo3::PyResult> { @@ -690,6 +693,7 @@ macro_rules! implement_iterator { $( implement_pyclass! { [generic, frozen] $name as $pyname { + cache: pyo3::Py, initial_gv: u32, gv: utils::GenerationVersion, iter: parking_lot::Mutex>, @@ -703,6 +707,10 @@ macro_rules! implement_iterator { slf } + fn __traverse__(&self, visit: pyo3::PyVisit<'_>) -> Result<(), pyo3::PyTraverseError> { + visit.call(&self.cache) + } + fn __next__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<$rt_type> { if slf.initial_gv != slf.gv.get() { return Err(new_py_error!( diff --git a/src/pyclasses/ttlcache.rs b/src/pyclasses/ttlcache.rs index 5f8cf08..8545659 100644 --- a/src/pyclasses/ttlcache.rs +++ b/src/pyclasses/ttlcache.rs @@ -540,8 +540,8 @@ impl PyTTLCache { .map(|x| !x) } - fn items(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn items(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let iter = inner.policy().iter(inner.shared()); @@ -550,15 +550,16 @@ impl PyTTLCache { // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyTTLCacheItems { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(iter), gv, initial_gv, }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } - fn values(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn values(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let iter = inner.policy().iter(inner.shared()); @@ -567,15 +568,16 @@ impl PyTTLCache { // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyTTLCacheValues { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(iter), gv, initial_gv, }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } - fn keys(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn keys(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let iter = inner.policy().iter(inner.shared()); @@ -584,16 +586,17 @@ impl PyTTLCache { // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyTTLCacheKeys { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(iter), gv, initial_gv, }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } #[inline] - fn __iter__(&self) -> pyo3::PyResult> { - self.keys() + fn __iter__(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + Self::keys(slf) } fn copy(&self, py: pyo3::Python) -> pyo3::PyResult> { @@ -776,8 +779,10 @@ impl PyTTLCache { Ok((key.into(), val, dur.as_secs_f64())) } - fn items_with_expire(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn items_with_expire( + slf: pyo3::Bound<'_, Self>, + ) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let iter = inner.policy().iter(inner.shared()); @@ -786,11 +791,12 @@ impl PyTTLCache { // SAFETY: We cannot use lifetimes here, but we're tracking changes using [`GenerationVersion`] let result = PyTTLCacheItemsWithExpire { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(iter), gv, initial_gv, }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } fn __traverse__(&self, visit: pyo3::PyVisit<'_>) -> Result<(), pyo3::PyTraverseError> { @@ -830,6 +836,7 @@ macro_rules! implement_iterator { $( implement_pyclass! { [generic, frozen] $name as $pyname { + cache: pyo3::Py, initial_gv: u32, gv: utils::GenerationVersion, iter: parking_lot::Mutex>, @@ -843,6 +850,10 @@ macro_rules! implement_iterator { slf } + fn __traverse__(&self, visit: pyo3::PyVisit<'_>) -> Result<(), pyo3::PyTraverseError> { + visit.call(&self.cache) + } + fn __next__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<$rt_type> { if slf.initial_gv != slf.gv.get() { return Err(new_py_error!( diff --git a/src/pyclasses/vttlcache.rs b/src/pyclasses/vttlcache.rs index 897130c..a89f039 100644 --- a/src/pyclasses/vttlcache.rs +++ b/src/pyclasses/vttlcache.rs @@ -515,54 +515,57 @@ impl PyVTTLCache { .map(|x| !x) } - fn items(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn items(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let mut policy = inner.policy(); let gv = inner.shared().generation_version(); let iter = policy.iter(gv); let result = PyVTTLCacheItems { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(iter), gv: gv.clone(), initial_gv: gv.get(), }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } - fn values(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn values(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let mut policy = inner.policy(); let gv = inner.shared().generation_version(); let iter = policy.iter(gv); let result = PyVTTLCacheValues { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(iter), gv: gv.clone(), initial_gv: gv.get(), }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } - fn keys(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn keys(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let mut policy = inner.policy(); let gv = inner.shared().generation_version(); let iter = policy.iter(gv); let result = PyVTTLCacheKeys { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(iter), gv: gv.clone(), initial_gv: gv.get(), }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } #[inline] - fn __iter__(&self) -> pyo3::PyResult> { - self.keys() + fn __iter__(slf: pyo3::Bound<'_, Self>) -> pyo3::PyResult> { + Self::keys(slf) } fn copy(&self, py: pyo3::Python) -> pyo3::PyResult> { @@ -739,19 +742,22 @@ impl PyVTTLCache { Ok((key.into(), val, dur)) } - fn items_with_expire(&self) -> pyo3::PyResult> { - let inner = self.0.get(); + fn items_with_expire( + slf: pyo3::Bound<'_, Self>, + ) -> pyo3::PyResult> { + let inner = slf.get().0.get(); let mut policy = inner.policy(); let gv = inner.shared().generation_version(); let iter = policy.iter(gv); let result = PyVTTLCacheItemsWithExpire { + cache: slf.as_any().clone().unbind(), iter: parking_lot::Mutex::new(iter), gv: gv.clone(), initial_gv: gv.get(), }; - pyo3::Python::attach(|py| pyo3::Py::new(py, result)) + pyo3::Py::new(slf.py(), result) } fn __traverse__(&self, visit: pyo3::PyVisit<'_>) -> Result<(), pyo3::PyTraverseError> { @@ -793,6 +799,7 @@ macro_rules! implement_iterator { $( implement_pyclass! { [generic, frozen] $name as $pyname { + cache: pyo3::Py, initial_gv: u32, gv: utils::GenerationVersion, iter: parking_lot::Mutex>, @@ -806,6 +813,10 @@ macro_rules! implement_iterator { slf } + fn __traverse__(&self, visit: pyo3::PyVisit<'_>) -> Result<(), pyo3::PyTraverseError> { + visit.call(&self.cache) + } + fn __next__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<$rt_type> { if slf.initial_gv != slf.gv.get() { return Err(new_py_error!( diff --git a/tests/mixins.py b/tests/mixins.py index 63c71d3..d698bb5 100644 --- a/tests/mixins.py +++ b/tests/mixins.py @@ -2,10 +2,12 @@ import dataclasses import gc import pickle +import subprocess import sys import threading import time import typing +import weakref from datetime import timedelta from unittest.mock import Mock, patch @@ -401,6 +403,29 @@ def test_ne_different_contents(self): assert c1 != c2 +WALKING_A_TEMPORARY_CACHE = """ +import sys + +import cachebox + +name = sys.argv[1] +cls = getattr(cachebox, name) + + +def make(): + cache = cls(10, global_ttl=60) if name == "TTLCache" else cls(10) + cache.update({"a": 1, "b": 2}) + return cache + + +# nothing else holds the cache by the time the walk starts +assert set(make().keys()) == {"a", "b"} +assert set(make().items()) == {("a", 1), ("b", 2)} +assert sorted(make().values()) == [1, 2] +print("ok") +""" + + class IterationMixin(BaseMixin): def test_keys_returns_all_keys(self): cache = self.create_cache() @@ -426,6 +451,34 @@ def test_iter_yields_keys(self): cache.update({"x": 10, "y": 20}) assert set(iter(cache)) == {"x", "y"} + def test_walking_a_cache_nothing_else_holds(self): + # reading freed memory faults instead of failing, so it runs in a child process + name = type(self.create_cache()).__name__ + + done = subprocess.run( + [sys.executable, "-c", WALKING_A_TEMPORARY_CACHE, name], + capture_output=True, + text=True, + timeout=60, + ) + + assert done.stdout.strip() == "ok", done.stderr or f"exit code {done.returncode}" + + def test_cache_holding_its_own_iterator_is_collected(self): + class Canary: + pass + + canary = Canary() + ref = weakref.ref(canary) + + cache = self.create_cache() + cache.insert("canary", canary) + cache.insert("self", cache.keys()) # the cycle: cache -> iterator -> cache + del cache, canary + gc.collect() + + assert ref() is None + def test_generation_version_on_remove(self): cache = self.create_cache(10, {i: i for i in range(10)}) From 378267f4277f6654c1003a17e19cd383fad0abc8 Mon Sep 17 00:00:00 2001 From: Dmitry Agafonov <42949186+Malkiz223@users.noreply.github.com> Date: Sun, 9 Aug 2026 14:06:03 +0300 Subject: [PATCH 2/2] Skip the iterator cycle test on PyPy --- tests/mixins.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/mixins.py b/tests/mixins.py index d698bb5..bf5d5c3 100644 --- a/tests/mixins.py +++ b/tests/mixins.py @@ -2,6 +2,7 @@ import dataclasses import gc import pickle +import platform import subprocess import sys import threading @@ -464,6 +465,11 @@ def test_walking_a_cache_nothing_else_holds(self): assert done.stdout.strip() == "ok", done.stderr or f"exit code {done.returncode}" + @pytest.mark.skipif( + platform.python_implementation() == "PyPy", + reason="PyPy's GC does not collect cycles through the cache objects: " + "a cache holding itself is not collected either", + ) def test_cache_holding_its_own_iterator_is_collected(self): class Canary: pass