From 543a4e85dc974c61cb9f5f202985dacdf68db897 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Sat, 6 Jun 2026 20:51:01 +0530 Subject: [PATCH 01/13] fix various memory leaks in greenlet --- src/greenlet/TGreenlet.cpp | 4 ++++ src/greenlet/TPythonState.cpp | 29 +++++++++++++++++++++++++++++ src/greenlet/TUserGreenlet.cpp | 13 +++++++------ src/greenlet/tests/test_gc.py | 1 - src/greenlet/tests/test_greenlet.py | 2 -- 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/greenlet/TGreenlet.cpp b/src/greenlet/TGreenlet.cpp index 7ec86f98..6f8139d9 100644 --- a/src/greenlet/TGreenlet.cpp +++ b/src/greenlet/TGreenlet.cpp @@ -607,6 +607,10 @@ Greenlet::tp_clear() bool own_top_frame = this->was_running_in_dead_thread(); this->exception_state.tp_clear(); this->python_state.tp_clear(own_top_frame); + if (own_top_frame) { + // Throw away any save stack since the owned frame is cleared. + this->stack_state.set_inactive(); + } return 0; } diff --git a/src/greenlet/TPythonState.cpp b/src/greenlet/TPythonState.cpp index cf105fcb..9c98d14b 100644 --- a/src/greenlet/TPythonState.cpp +++ b/src/greenlet/TPythonState.cpp @@ -397,7 +397,36 @@ void PythonState::tp_clear(bool own_top_frame) noexcept // we got dealloc'd without being finished. We may or may not be // in the same thread. if (own_top_frame) { +#if GREENLET_PY315 + // Release the references held by our suspended frames. + // this->top_frame gets implicitly cleared by the Py_CLEAR(iframe->frame_obj) + // of the first complete frame, so in the end we relinquish ownership of it. + if (this->_top_frame) { + for (_PyInterpreterFrame* iframe = this->_top_frame->f_frame; + iframe != nullptr; iframe = iframe->previous) { + if (iframe->owner != FRAME_OWNED_BY_THREAD) { + continue; + } + // Clear the references held by this frame's evaluation stack. + _PyStackRef* locals = iframe->localsplus; + _PyStackRef* sp = iframe->stackpointer; + if (sp) { + while (sp > locals) { + sp--; + PyStackRef_CLEAR(*sp); + } + iframe->stackpointer = locals; + } + Py_CLEAR(iframe->f_locals); + Py_CLEAR(iframe->frame_obj); + PyStackRef_CLEAR(iframe->f_funcobj); + PyStackRef_CLEAR(iframe->f_executable); + } + } + this->_top_frame.relinquish_ownership(); +#else this->_top_frame.CLEAR(); +#endif } } diff --git a/src/greenlet/TUserGreenlet.cpp b/src/greenlet/TUserGreenlet.cpp index a8eeabb3..896ccc62 100644 --- a/src/greenlet/TUserGreenlet.cpp +++ b/src/greenlet/TUserGreenlet.cpp @@ -422,11 +422,11 @@ UserGreenlet::inner_bootstrap(PyGreenlet* origin_greenlet, PyObject* run) args <<= this->args(); assert(!this->args()); - // XXX: We could clear this much earlier, right? - // Or would that introduce the possibility of running Python - // code when we don't want to? - // CAUTION: This may run arbitrary Python code. this->_run_callable.CLEAR(); + // stash the run callable in this->_run_callable to ensure that GC will be + // able to find the object later. + this->_run_callable.steal(run); + run = nullptr; // The first switch we need to manually call the trace @@ -467,7 +467,7 @@ UserGreenlet::inner_bootstrap(PyGreenlet* origin_greenlet, PyObject* run) // CAUTION: Just invoking this, before the function even // runs, may cause memory allocations, which may trigger // GC, which may run arbitrary Python code. - result = OwnedObject::consuming(PyObject_Call(run, args.args().borrow(), args.kwargs().borrow())); + result = OwnedObject::consuming(PyObject_Call(this->_run_callable.borrow(), args.args().borrow(), args.kwargs().borrow())); } catch (...) { // Unhandled C++ exception! @@ -517,7 +517,8 @@ UserGreenlet::inner_bootstrap(PyGreenlet* origin_greenlet, PyObject* run) } // These lines may run arbitrary code args.CLEAR(); - Py_CLEAR(run); + assert(run == nullptr); + this->_run_callable.CLEAR(); if (!result && mod_globs->PyExc_GreenletExit.PyExceptionMatches() diff --git a/src/greenlet/tests/test_gc.py b/src/greenlet/tests/test_gc.py index b981bc73..c9f66703 100644 --- a/src/greenlet/tests/test_gc.py +++ b/src/greenlet/tests/test_gc.py @@ -43,7 +43,6 @@ def run(self): self.assertIsNone(o()) self.assertFalse(gc.garbage, gc.garbage) - @fails_leakcheck def test_finalizer_crash(self): # This test is designed to crash when active greenlets # are made garbage collectable, until the underlying diff --git a/src/greenlet/tests/test_greenlet.py b/src/greenlet/tests/test_greenlet.py index 5e184518..a83a6153 100644 --- a/src/greenlet/tests/test_greenlet.py +++ b/src/greenlet/tests/test_greenlet.py @@ -506,7 +506,6 @@ def __getattribute__(self, name): g = mygreenlet(lambda: None) self.assertRaises(SomeError, g.throw, SomeError()) - @fails_leakcheck def _do_test_throw_to_dead_thread_doesnt_crash(self, wait_for_cleanup=False): result = [] def worker(): @@ -564,7 +563,6 @@ def creator(): # See issue 252 self.expect_greenlet_leak = True # direct us not to wait for it to go away - @fails_leakcheck def test_throw_to_dead_thread_doesnt_crash(self): self._do_test_throw_to_dead_thread_doesnt_crash() From 2c6966799bc4d683dab71283b84b9af121baa87c Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 17 Jun 2026 00:16:09 +0530 Subject: [PATCH 02/13] fix tests --- src/greenlet/tests/leakcheck.py | 11 +++++++++++ src/greenlet/tests/test_gc.py | 3 ++- src/greenlet/tests/test_greenlet.py | 3 +++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/greenlet/tests/leakcheck.py b/src/greenlet/tests/leakcheck.py index 993e3fa8..6141c584 100644 --- a/src/greenlet/tests/leakcheck.py +++ b/src/greenlet/tests/leakcheck.py @@ -95,6 +95,17 @@ def fails_leakcheck(func): func = unittest.skip("Skipping known failures")(func) return func +def fails_leakcheck_on_py314_or_less(func): + """ + Mark that the function is known to leak on Python 3.14 and earlier. + + The underlying leak is fixed on newer versions (3.15+), where the + function is expected to pass the leakcheck. + """ + if sys.version_info[:2] <= (3, 14): + return fails_leakcheck(func) + return func + class LeakCheckError(AssertionError): pass diff --git a/src/greenlet/tests/test_gc.py b/src/greenlet/tests/test_gc.py index c9f66703..340fdbe3 100644 --- a/src/greenlet/tests/test_gc.py +++ b/src/greenlet/tests/test_gc.py @@ -6,7 +6,7 @@ from . import TestCase -from .leakcheck import fails_leakcheck +from .leakcheck import fails_leakcheck_on_py314_or_less # These only work with greenlet gc support # which is no longer optional. assert greenlet.GREENLET_USE_GC @@ -43,6 +43,7 @@ def run(self): self.assertIsNone(o()) self.assertFalse(gc.garbage, gc.garbage) + @fails_leakcheck_on_py314_or_less def test_finalizer_crash(self): # This test is designed to crash when active greenlets # are made garbage collectable, until the underlying diff --git a/src/greenlet/tests/test_greenlet.py b/src/greenlet/tests/test_greenlet.py index a83a6153..2ee18a2e 100644 --- a/src/greenlet/tests/test_greenlet.py +++ b/src/greenlet/tests/test_greenlet.py @@ -15,6 +15,7 @@ from . import PY314 from . import RUNNING_ON_FREETHREAD_BUILD from .leakcheck import fails_leakcheck +from .leakcheck import fails_leakcheck_on_py314_or_less from .leakcheck import ignores_leakcheck @@ -506,6 +507,7 @@ def __getattribute__(self, name): g = mygreenlet(lambda: None) self.assertRaises(SomeError, g.throw, SomeError()) + @fails_leakcheck_on_py314_or_less def _do_test_throw_to_dead_thread_doesnt_crash(self, wait_for_cleanup=False): result = [] def worker(): @@ -563,6 +565,7 @@ def creator(): # See issue 252 self.expect_greenlet_leak = True # direct us not to wait for it to go away + @fails_leakcheck_on_py314_or_less def test_throw_to_dead_thread_doesnt_crash(self): self._do_test_throw_to_dead_thread_doesnt_crash() From 836f7ecd09539f20e1739c5241262be0437e5e25 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 17 Jun 2026 00:34:48 +0530 Subject: [PATCH 03/13] try to fix windows --- src/greenlet/greenlet_msvc_compat.hpp | 21 +++++++++++++++++++++ src/greenlet/tests/test_gc.py | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/greenlet/greenlet_msvc_compat.hpp b/src/greenlet/greenlet_msvc_compat.hpp index fa25d715..08fca474 100644 --- a/src/greenlet/greenlet_msvc_compat.hpp +++ b/src/greenlet/greenlet_msvc_compat.hpp @@ -85,6 +85,27 @@ PyStackRef_AsPyObjectBorrow(_PyStackRef stackref) return cleared; } +#define Py_TAG_REFCNT 1 +#define BITS_TO_PTR(ref) ((PyObject *)((ref).bits)) + +#define PyStackRef_RefcountOnObject(ref) (((ref).bits & Py_TAG_REFCNT) == 0) + +#define PyStackRef_CLOSE(REF) \ + do { \ + _PyStackRef _close_tmp = (REF); \ + if (PyStackRef_RefcountOnObject(_close_tmp)) { \ + Py_DECREF(BITS_TO_PTR(_close_tmp)); \ + } \ + } while (0) + +#define PyStackRef_CLEAR(REF) \ + do { \ + _PyStackRef* _clear_ptr = &(REF); \ + _PyStackRef _clear_old = (*_clear_ptr); \ + *_clear_ptr = PyStackRef_NULL; \ + PyStackRef_CLOSE(_clear_old); \ + } while (0) + static inline PyCodeObject *_PyFrame_GetCode(_PyInterpreterFrame *f) { assert(!PyStackRef_IsNullOrInt(f->f_executable)); PyObject *executable = PyStackRef_AsPyObjectBorrow(f->f_executable); diff --git a/src/greenlet/tests/test_gc.py b/src/greenlet/tests/test_gc.py index 340fdbe3..b981bc73 100644 --- a/src/greenlet/tests/test_gc.py +++ b/src/greenlet/tests/test_gc.py @@ -6,7 +6,7 @@ from . import TestCase -from .leakcheck import fails_leakcheck_on_py314_or_less +from .leakcheck import fails_leakcheck # These only work with greenlet gc support # which is no longer optional. assert greenlet.GREENLET_USE_GC @@ -43,7 +43,7 @@ def run(self): self.assertIsNone(o()) self.assertFalse(gc.garbage, gc.garbage) - @fails_leakcheck_on_py314_or_less + @fails_leakcheck def test_finalizer_crash(self): # This test is designed to crash when active greenlets # are made garbage collectable, until the underlying From 8fef84f6afed94a5fddd02dc2761a60a22e9d725 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 17 Jun 2026 15:06:49 +0530 Subject: [PATCH 04/13] add tests --- src/greenlet/tests/test_gc.py | 4 ++-- src/greenlet/tests/test_greenlet.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/greenlet/tests/test_gc.py b/src/greenlet/tests/test_gc.py index b981bc73..340fdbe3 100644 --- a/src/greenlet/tests/test_gc.py +++ b/src/greenlet/tests/test_gc.py @@ -6,7 +6,7 @@ from . import TestCase -from .leakcheck import fails_leakcheck +from .leakcheck import fails_leakcheck_on_py314_or_less # These only work with greenlet gc support # which is no longer optional. assert greenlet.GREENLET_USE_GC @@ -43,7 +43,7 @@ def run(self): self.assertIsNone(o()) self.assertFalse(gc.garbage, gc.garbage) - @fails_leakcheck + @fails_leakcheck_on_py314_or_less def test_finalizer_crash(self): # This test is designed to crash when active greenlets # are made garbage collectable, until the underlying diff --git a/src/greenlet/tests/test_greenlet.py b/src/greenlet/tests/test_greenlet.py index 2ee18a2e..d8ebddb7 100644 --- a/src/greenlet/tests/test_greenlet.py +++ b/src/greenlet/tests/test_greenlet.py @@ -1302,6 +1302,14 @@ def test_main_greenlet_is_greenlet(self): self._check_current_is_main() self.assertIsInstance(greenlet.getcurrent(), RawGreenlet) + def test_greenlet_run(self): + def do_it(): + return 42 + + g = greenlet.greenlet(do_it) + self.assertEqual(g.switch(), 42) + with self.assertRaises(AttributeError): + g.run class TestBrokenGreenlets(TestCase): From f3a5b32e766f25a8938ec3973d0421a8f3046641 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 17 Jun 2026 15:13:52 +0530 Subject: [PATCH 05/13] ignore refleaks instead --- src/greenlet/tests/leakcheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/greenlet/tests/leakcheck.py b/src/greenlet/tests/leakcheck.py index 6141c584..84a52493 100644 --- a/src/greenlet/tests/leakcheck.py +++ b/src/greenlet/tests/leakcheck.py @@ -103,7 +103,7 @@ def fails_leakcheck_on_py314_or_less(func): function is expected to pass the leakcheck. """ if sys.version_info[:2] <= (3, 14): - return fails_leakcheck(func) + return ignores_leakcheck(func) return func class LeakCheckError(AssertionError): From b6a502ef61e5d73ec83d8555c25d95a194273c08 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 17 Jun 2026 15:21:09 +0530 Subject: [PATCH 06/13] fix lint --- src/greenlet/tests/test_greenlet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/greenlet/tests/test_greenlet.py b/src/greenlet/tests/test_greenlet.py index d8ebddb7..59391de6 100644 --- a/src/greenlet/tests/test_greenlet.py +++ b/src/greenlet/tests/test_greenlet.py @@ -1309,7 +1309,7 @@ def do_it(): g = greenlet.greenlet(do_it) self.assertEqual(g.switch(), 42) with self.assertRaises(AttributeError): - g.run + getattr(g, 'run') class TestBrokenGreenlets(TestCase): From abdbab55b03334700fd264e523ac75926daba043 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 17 Jun 2026 15:41:33 +0530 Subject: [PATCH 07/13] fix gil enabled --- src/greenlet/PyGreenlet.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/greenlet/PyGreenlet.cpp b/src/greenlet/PyGreenlet.cpp index b022d5c0..7450ae52 100644 --- a/src/greenlet/PyGreenlet.cpp +++ b/src/greenlet/PyGreenlet.cpp @@ -151,6 +151,12 @@ green_is_gc(PyObject* _self) if (self->main() || !self->active()) { result = 1; } +#if GREENLET_PY315 + // On 3.15+ we have full support for traversing and clearing the + // references held by a suspended greenlet's frames, so even active + // (suspended) greenlets can participate in cycle collection. + result = 1; +#endif // The main greenlet pointer will eventually go away after the thread dies. if (self->was_running_in_dead_thread()) { // Our thread is dead! We can never run again. Might as well @@ -808,7 +814,7 @@ PyTypeObject PyGreenlet_Type = { .tp_alloc=PyType_GenericAlloc, /* tp_alloc */ .tp_new=(newfunc)green_new, /* tp_new */ .tp_free=PyObject_GC_Del, /* tp_free */ -#ifndef Py_GIL_DISABLED +#if 0 /* We may have been handling this wrong all along. From 35206b8dc536cfe92c09509432b4a301b5fede5f Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 17 Jun 2026 17:58:45 +0530 Subject: [PATCH 08/13] fix test and restrict tp_is_gc < 3.15 --- src/greenlet/PyGreenlet.cpp | 2 +- src/greenlet/tests/test_greenlet.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/greenlet/PyGreenlet.cpp b/src/greenlet/PyGreenlet.cpp index 7450ae52..ac92f53a 100644 --- a/src/greenlet/PyGreenlet.cpp +++ b/src/greenlet/PyGreenlet.cpp @@ -814,7 +814,7 @@ PyTypeObject PyGreenlet_Type = { .tp_alloc=PyType_GenericAlloc, /* tp_alloc */ .tp_new=(newfunc)green_new, /* tp_new */ .tp_free=PyObject_GC_Del, /* tp_free */ -#if 0 +#if !GREENLET_PY315 /* We may have been handling this wrong all along. diff --git a/src/greenlet/tests/test_greenlet.py b/src/greenlet/tests/test_greenlet.py index 59391de6..5d5f528d 100644 --- a/src/greenlet/tests/test_greenlet.py +++ b/src/greenlet/tests/test_greenlet.py @@ -1304,10 +1304,11 @@ def test_main_greenlet_is_greenlet(self): def test_greenlet_run(self): def do_it(): - return 42 + with self.assertRaises(AttributeError): + getattr(g, 'run') g = greenlet.greenlet(do_it) - self.assertEqual(g.switch(), 42) + g.switch() with self.assertRaises(AttributeError): getattr(g, 'run') From 28bbde3de939acefe8e9d72fb179115b81392a87 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 17 Jun 2026 18:06:02 +0530 Subject: [PATCH 09/13] add comments --- src/greenlet/PyGreenlet.cpp | 6 ------ src/greenlet/TGreenlet.cpp | 2 +- src/greenlet/TUserGreenlet.cpp | 2 ++ 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/greenlet/PyGreenlet.cpp b/src/greenlet/PyGreenlet.cpp index ac92f53a..f84aab88 100644 --- a/src/greenlet/PyGreenlet.cpp +++ b/src/greenlet/PyGreenlet.cpp @@ -151,12 +151,6 @@ green_is_gc(PyObject* _self) if (self->main() || !self->active()) { result = 1; } -#if GREENLET_PY315 - // On 3.15+ we have full support for traversing and clearing the - // references held by a suspended greenlet's frames, so even active - // (suspended) greenlets can participate in cycle collection. - result = 1; -#endif // The main greenlet pointer will eventually go away after the thread dies. if (self->was_running_in_dead_thread()) { // Our thread is dead! We can never run again. Might as well diff --git a/src/greenlet/TGreenlet.cpp b/src/greenlet/TGreenlet.cpp index 6f8139d9..dcbdc001 100644 --- a/src/greenlet/TGreenlet.cpp +++ b/src/greenlet/TGreenlet.cpp @@ -608,7 +608,7 @@ Greenlet::tp_clear() this->exception_state.tp_clear(); this->python_state.tp_clear(own_top_frame); if (own_top_frame) { - // Throw away any save stack since the owned frame is cleared. + // Throw away any saved stack state since the owned frame is cleared. this->stack_state.set_inactive(); } return 0; diff --git a/src/greenlet/TUserGreenlet.cpp b/src/greenlet/TUserGreenlet.cpp index 896ccc62..ce011fd3 100644 --- a/src/greenlet/TUserGreenlet.cpp +++ b/src/greenlet/TUserGreenlet.cpp @@ -425,6 +425,8 @@ UserGreenlet::inner_bootstrap(PyGreenlet* origin_greenlet, PyObject* run) this->_run_callable.CLEAR(); // stash the run callable in this->_run_callable to ensure that GC will be // able to find the object later. + // This is needed for the case of a permanently suspended greenlet + // so that the run callable is not leaked. this->_run_callable.steal(run); run = nullptr; From 2f87f316613d401bbf1b1653595654654e572550 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 17 Jun 2026 19:11:17 +0530 Subject: [PATCH 10/13] rename to ignores_leakcheck_on_py314_or_less --- src/greenlet/tests/leakcheck.py | 7 ++----- src/greenlet/tests/test_gc.py | 4 ++-- src/greenlet/tests/test_greenlet.py | 6 +++--- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/greenlet/tests/leakcheck.py b/src/greenlet/tests/leakcheck.py index 84a52493..87d36f79 100644 --- a/src/greenlet/tests/leakcheck.py +++ b/src/greenlet/tests/leakcheck.py @@ -95,12 +95,9 @@ def fails_leakcheck(func): func = unittest.skip("Skipping known failures")(func) return func -def fails_leakcheck_on_py314_or_less(func): +def ignores_leakcheck_on_py314_or_less(func): """ - Mark that the function is known to leak on Python 3.14 and earlier. - - The underlying leak is fixed on newer versions (3.15+), where the - function is expected to pass the leakcheck. + Mark the function to ignore refcount leakchecks on Python 3.14 or less. """ if sys.version_info[:2] <= (3, 14): return ignores_leakcheck(func) diff --git a/src/greenlet/tests/test_gc.py b/src/greenlet/tests/test_gc.py index 340fdbe3..679a765f 100644 --- a/src/greenlet/tests/test_gc.py +++ b/src/greenlet/tests/test_gc.py @@ -6,7 +6,7 @@ from . import TestCase -from .leakcheck import fails_leakcheck_on_py314_or_less +from .leakcheck import ignores_leakcheck_on_py314_or_less # These only work with greenlet gc support # which is no longer optional. assert greenlet.GREENLET_USE_GC @@ -43,7 +43,7 @@ def run(self): self.assertIsNone(o()) self.assertFalse(gc.garbage, gc.garbage) - @fails_leakcheck_on_py314_or_less + @ignores_leakcheck_on_py314_or_less def test_finalizer_crash(self): # This test is designed to crash when active greenlets # are made garbage collectable, until the underlying diff --git a/src/greenlet/tests/test_greenlet.py b/src/greenlet/tests/test_greenlet.py index 5d5f528d..59060817 100644 --- a/src/greenlet/tests/test_greenlet.py +++ b/src/greenlet/tests/test_greenlet.py @@ -15,7 +15,7 @@ from . import PY314 from . import RUNNING_ON_FREETHREAD_BUILD from .leakcheck import fails_leakcheck -from .leakcheck import fails_leakcheck_on_py314_or_less +from .leakcheck import ignores_leakcheck_on_py314_or_less from .leakcheck import ignores_leakcheck @@ -507,7 +507,7 @@ def __getattribute__(self, name): g = mygreenlet(lambda: None) self.assertRaises(SomeError, g.throw, SomeError()) - @fails_leakcheck_on_py314_or_less + @ignores_leakcheck_on_py314_or_less def _do_test_throw_to_dead_thread_doesnt_crash(self, wait_for_cleanup=False): result = [] def worker(): @@ -565,7 +565,7 @@ def creator(): # See issue 252 self.expect_greenlet_leak = True # direct us not to wait for it to go away - @fails_leakcheck_on_py314_or_less + @ignores_leakcheck_on_py314_or_less def test_throw_to_dead_thread_doesnt_crash(self): self._do_test_throw_to_dead_thread_doesnt_crash() From b0aac05734e9e41d4a592e0c913846990a9c7c24 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 17 Jun 2026 20:47:13 +0530 Subject: [PATCH 11/13] set fail-fast=false and if condition correctly --- .github/workflows/tests.yml | 2 ++ src/greenlet/PyGreenlet.cpp | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 71afd61b..759dd30f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,6 +21,7 @@ jobs: test: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: python-version: - "3.10" @@ -187,6 +188,7 @@ jobs: runs-on: ubuntu-latest # We use a regular Python matrix entry to share as much code as possible. strategy: + fail-fast: false matrix: python-version: - "3.14" diff --git a/src/greenlet/PyGreenlet.cpp b/src/greenlet/PyGreenlet.cpp index f84aab88..b54b742c 100644 --- a/src/greenlet/PyGreenlet.cpp +++ b/src/greenlet/PyGreenlet.cpp @@ -808,7 +808,7 @@ PyTypeObject PyGreenlet_Type = { .tp_alloc=PyType_GenericAlloc, /* tp_alloc */ .tp_new=(newfunc)green_new, /* tp_new */ .tp_free=PyObject_GC_Del, /* tp_free */ -#if !GREENLET_PY315 +#if !GREENLET_PY315 && !(GREENLET_PY314 && defined(Py_GIL_DISABLED)) /* We may have been handling this wrong all along. From 41f5349a0f1b69ce180c90acb9a4cf2c84d612e7 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 17 Jun 2026 20:48:37 +0530 Subject: [PATCH 12/13] revert back to fails_leakcheck_on_py314_or_less --- src/greenlet/tests/leakcheck.py | 6 +++--- src/greenlet/tests/test_gc.py | 4 ++-- src/greenlet/tests/test_greenlet.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/greenlet/tests/leakcheck.py b/src/greenlet/tests/leakcheck.py index 87d36f79..5f846bfd 100644 --- a/src/greenlet/tests/leakcheck.py +++ b/src/greenlet/tests/leakcheck.py @@ -95,12 +95,12 @@ def fails_leakcheck(func): func = unittest.skip("Skipping known failures")(func) return func -def ignores_leakcheck_on_py314_or_less(func): +def fails_leakcheck_on_py314_or_less(func): """ - Mark the function to ignore refcount leakchecks on Python 3.14 or less. + Mark the function as known to leak (fails refcount leakchecks) on Python 3.14 or less. """ if sys.version_info[:2] <= (3, 14): - return ignores_leakcheck(func) + return fails_leakcheck(func) return func class LeakCheckError(AssertionError): diff --git a/src/greenlet/tests/test_gc.py b/src/greenlet/tests/test_gc.py index 679a765f..340fdbe3 100644 --- a/src/greenlet/tests/test_gc.py +++ b/src/greenlet/tests/test_gc.py @@ -6,7 +6,7 @@ from . import TestCase -from .leakcheck import ignores_leakcheck_on_py314_or_less +from .leakcheck import fails_leakcheck_on_py314_or_less # These only work with greenlet gc support # which is no longer optional. assert greenlet.GREENLET_USE_GC @@ -43,7 +43,7 @@ def run(self): self.assertIsNone(o()) self.assertFalse(gc.garbage, gc.garbage) - @ignores_leakcheck_on_py314_or_less + @fails_leakcheck_on_py314_or_less def test_finalizer_crash(self): # This test is designed to crash when active greenlets # are made garbage collectable, until the underlying diff --git a/src/greenlet/tests/test_greenlet.py b/src/greenlet/tests/test_greenlet.py index 59060817..5d5f528d 100644 --- a/src/greenlet/tests/test_greenlet.py +++ b/src/greenlet/tests/test_greenlet.py @@ -15,7 +15,7 @@ from . import PY314 from . import RUNNING_ON_FREETHREAD_BUILD from .leakcheck import fails_leakcheck -from .leakcheck import ignores_leakcheck_on_py314_or_less +from .leakcheck import fails_leakcheck_on_py314_or_less from .leakcheck import ignores_leakcheck @@ -507,7 +507,7 @@ def __getattribute__(self, name): g = mygreenlet(lambda: None) self.assertRaises(SomeError, g.throw, SomeError()) - @ignores_leakcheck_on_py314_or_less + @fails_leakcheck_on_py314_or_less def _do_test_throw_to_dead_thread_doesnt_crash(self, wait_for_cleanup=False): result = [] def worker(): @@ -565,7 +565,7 @@ def creator(): # See issue 252 self.expect_greenlet_leak = True # direct us not to wait for it to go away - @ignores_leakcheck_on_py314_or_less + @fails_leakcheck_on_py314_or_less def test_throw_to_dead_thread_doesnt_crash(self): self._do_test_throw_to_dead_thread_doesnt_crash() From ab6eff60766e8954d890eef064ee2e1dd7c8b262 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 17 Jun 2026 21:12:38 +0530 Subject: [PATCH 13/13] add ignore for win 3.10 --- src/greenlet/tests/leakcheck.py | 21 +++++++++++++++++++++ src/greenlet/tests/test_greenlet.py | 6 ++++++ 2 files changed, 27 insertions(+) diff --git a/src/greenlet/tests/leakcheck.py b/src/greenlet/tests/leakcheck.py index 5f846bfd..a1037d47 100644 --- a/src/greenlet/tests/leakcheck.py +++ b/src/greenlet/tests/leakcheck.py @@ -86,6 +86,21 @@ class and specify variants of behaviour (such as pool sizes). func.ignore_leakcheck = True return func +def ignores_leakcheck_if(condition, message): + """ + Return a decorator that marks the function to be ignored during + leakchecks (see `ignores_leakcheck`) when *condition* is true. + + *message* describes why the leakcheck is ignored. When *condition* + is false, the function is returned unchanged. + """ + def decorator(func): + if condition: + func = ignores_leakcheck(func) + func.ignore_leakcheck_reason = message + return func + return decorator + def fails_leakcheck(func): """ Mark that the function is known to leak. @@ -329,6 +344,12 @@ def __call__(self, args, kwargs): def wrap_refcount(method): if getattr(method, 'ignore_leakcheck', False) or SKIP_LEAKCHECKS: + reason = getattr(method, 'ignore_leakcheck_reason', None) + if reason and not SKIP_LEAKCHECKS: + print( + "Ignoring leakchecks for %s: %s" % (method.__name__, reason), + file=sys.stderr, + ) return method @wraps(method) diff --git a/src/greenlet/tests/test_greenlet.py b/src/greenlet/tests/test_greenlet.py index 5d5f528d..36d5d1e2 100644 --- a/src/greenlet/tests/test_greenlet.py +++ b/src/greenlet/tests/test_greenlet.py @@ -13,10 +13,12 @@ from . import RUNNING_ON_MANYLINUX from . import PY313 from . import PY314 +from . import WIN from . import RUNNING_ON_FREETHREAD_BUILD from .leakcheck import fails_leakcheck from .leakcheck import fails_leakcheck_on_py314_or_less from .leakcheck import ignores_leakcheck +from .leakcheck import ignores_leakcheck_if # We manually manage locks in many tests @@ -566,6 +568,10 @@ def creator(): self.expect_greenlet_leak = True # direct us not to wait for it to go away @fails_leakcheck_on_py314_or_less + @ignores_leakcheck_if( + WIN and sys.version_info[:2] == (3, 10), + "does not leaks on Windows 3.10, but does on other platforms" + ) def test_throw_to_dead_thread_doesnt_crash(self): self._do_test_throw_to_dead_thread_doesnt_crash()