Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-greenlet for openSUSE:Factory
checked in at 2026-05-04 21:17:09
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-greenlet (Old)
and /work/SRC/openSUSE:Factory/.python-greenlet.new.30200 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-greenlet"
Mon May 4 21:17:09 2026 rev:60 rq:1350569 version:3.5.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-greenlet/python-greenlet.changes
2026-04-14 17:48:56.561085148 +0200
+++
/work/SRC/openSUSE:Factory/.python-greenlet.new.30200/python-greenlet.changes
2026-05-04 21:17:17.732029066 +0200
@@ -1,0 +2,20 @@
+Sun May 3 19:11:42 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 3.5.0:
+ * Remove the atexit callback. This callback caused greenlet
+ APIs to become unavailable far too soon during interpreter
+ shutdown. Now they remain available while all atexit
+ callbacks run. Sometime after Py_IsFinalizing becomes true,
+ they may begin misbehaving. Because the order in which C
+ extensions are finalized is undefined, C extensions that are
+ sensitive to this need to check the results of that function
+ before invoking greenlet APIs. As a convenience,
+ PyGreenlet_GetCurrent sets an exception and returns NULL when
+ this happens (and greenlet.getcurrent begins returning None);
+ other greenlet C API functions have undefined behaviour.
+ Methods invoked directly on pre-existing greenlet.greenlet
+ objects will continue to function at least until the greenlet
+ C extension has been garbage collected and finalized. See PR
+ 508.
+
+-------------------------------------------------------------------
Old:
----
greenlet-3.4.0.tar.gz
New:
----
greenlet-3.5.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-greenlet.spec ++++++
--- /var/tmp/diff_new_pack.urT8Ad/_old 2026-05-04 21:17:18.196048068 +0200
+++ /var/tmp/diff_new_pack.urT8Ad/_new 2026-05-04 21:17:18.196048068 +0200
@@ -22,7 +22,7 @@
%{?sle15_python_module_pythons}
Name: python-greenlet
-Version: 3.4.0
+Version: 3.5.0
Release: 0
Summary: Lightweight in-process concurrent programming
License: MIT
++++++ greenlet-3.4.0.tar.gz -> greenlet-3.5.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.4.0/CHANGES.rst
new/greenlet-3.5.0/CHANGES.rst
--- old/greenlet-3.4.0/CHANGES.rst 2026-04-08 17:49:31.000000000 +0200
+++ new/greenlet-3.5.0/CHANGES.rst 2026-04-27 14:18:19.000000000 +0200
@@ -2,6 +2,26 @@
Changes
=========
+3.5.0 (2026-04-27)
+==================
+
+- Remove the ``atexit`` callback. This callback caused greenlet APIs
+ to become unavailable far too soon during interpreter shutdown. Now
+ they remain available while all ``atexit`` callbacks run. Sometime
+ after ``Py_IsFinalizing`` becomes true, they may begin misbehaving.
+ Because the order in which C extensions are finalized is undefined,
+ C extensions that are sensitive to this need to check the results of
+ that function before invoking greenlet APIs. As a convenience,
+ ``PyGreenlet_GetCurrent`` sets an exception and returns ``NULL``
+ when this happens (and ``greenlet.getcurrent`` begins returning
+ ``None``); other greenlet C API functions have undefined behaviour.
+ Methods invoked directly on pre-existing ``greenlet.greenlet``
+ objects will continue to function at least until the greenlet C
+ extension has been garbage collected and finalized.
+
+ See `PR 508 <https://github.com/python-greenlet/greenlet/pull/508>`_.
+
+
3.4.0 (2026-04-08)
==================
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.4.0/PKG-INFO new/greenlet-3.5.0/PKG-INFO
--- old/greenlet-3.4.0/PKG-INFO 2026-04-08 17:49:38.911490200 +0200
+++ new/greenlet-3.5.0/PKG-INFO 2026-04-27 14:18:23.167599400 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: greenlet
-Version: 3.4.0
+Version: 3.5.0
Summary: Lightweight in-process concurrent programming
Author-email: Alexey Borzenkov <[email protected]>
Maintainer-email: Jason Madden <[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.4.0/docs/c_api.rst
new/greenlet-3.5.0/docs/c_api.rst
--- old/greenlet-3.4.0/docs/c_api.rst 2026-04-08 17:49:31.000000000 +0200
+++ new/greenlet-3.5.0/docs/c_api.rst 2026-04-27 14:18:19.000000000 +0200
@@ -30,6 +30,14 @@
Functions
=========
+.. important::
+
+ Because the order in which extension modules are destroyed when the
+ Python interpreter is finalized is undefined, it is undefined
+ behaviour to call these APIs when ``Py_IsFinalizing`` returns true,
+ unless otherwise documented. This is because the internal state of
+ the greenlet module may have been torn down already.
+
.. c:function:: void PyGreenlet_Import(void)
A macro that imports the greenlet module and initializes the C API. This
@@ -67,6 +75,20 @@
Returns the currently active greenlet object.
+ If called during interpreter finalization, returns ``NULL``
+ and raises a :exc:`RuntimeError`.
+
+ .. versionchanged:: 3.4.0
+ Began returning ``NULL`` during interpreter shutdown.
+ This implementation returned ``NULL`` too early, while the
+ interpreter state was still guaranteed to be valid (during
+ ``atexit`` handlers). This has been corrected in 3.5.
+ .. versionchanged:: 3.5.0
+ Now sets an exception before returning ``NULL``. This prevents
+ a :exc:`SystemError` from being generated if this API was
+ exposed directly to Python, and prevents a crash if this API
+ was being called by Cython-generated code.
+
.. c:function:: PyGreenlet* PyGreenlet_New(PyObject* run, PyObject* parent)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.4.0/src/greenlet/CObjects.cpp
new/greenlet-3.5.0/src/greenlet/CObjects.cpp
--- old/greenlet-3.4.0/src/greenlet/CObjects.cpp 2026-04-08
17:49:31.000000000 +0200
+++ new/greenlet-3.5.0/src/greenlet/CObjects.cpp 2026-04-27
14:18:19.000000000 +0200
@@ -30,6 +30,7 @@
PyGreenlet_GetCurrent(void)
{
if (greenlet::IsShuttingDown()) {
+ PyErr_SetString(PyExc_RuntimeError, "greenlet is being finalized");
return nullptr;
}
return GET_THREAD_STATE().state().get_current().relinquish_ownership();
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.4.0/src/greenlet/PyModule.cpp
new/greenlet-3.5.0/src/greenlet/PyModule.cpp
--- old/greenlet-3.4.0/src/greenlet/PyModule.cpp 2026-04-08
17:49:31.000000000 +0200
+++ new/greenlet-3.5.0/src/greenlet/PyModule.cpp 2026-04-27
14:18:19.000000000 +0200
@@ -18,19 +18,6 @@
#endif
-static PyObject*
-_greenlet_atexit_callback(PyObject* UNUSED(self), PyObject* UNUSED(args))
-{
- greenlet::g_greenlet_shutting_down = 1;
- Py_RETURN_NONE;
-}
-
-static PyMethodDef _greenlet_atexit_method = {
- "_greenlet_cleanup", _greenlet_atexit_callback,
- METH_NOARGS, NULL
-};
-
-
PyDoc_STRVAR(mod_getcurrent_doc,
"getcurrent() -> greenlet\n"
"\n"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.4.0/src/greenlet/__init__.py
new/greenlet-3.5.0/src/greenlet/__init__.py
--- old/greenlet-3.4.0/src/greenlet/__init__.py 2026-04-08 17:49:31.000000000
+0200
+++ new/greenlet-3.5.0/src/greenlet/__init__.py 2026-04-27 14:18:19.000000000
+0200
@@ -22,7 +22,7 @@
###
# Metadata
###
-__version__ = '3.4.0'
+__version__ = '3.5.0'
from ._greenlet import _C_API # pylint:disable=no-name-in-module
###
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.4.0/src/greenlet/greenlet.cpp
new/greenlet-3.5.0/src/greenlet/greenlet.cpp
--- old/greenlet-3.4.0/src/greenlet/greenlet.cpp 2026-04-08
17:49:31.000000000 +0200
+++ new/greenlet-3.5.0/src/greenlet/greenlet.cpp 2026-04-27
14:18:19.000000000 +0200
@@ -295,25 +295,6 @@
PyUnstable_Module_SetGIL(m.borrow(), Py_MOD_GIL_NOT_USED);
#endif
- // Register an atexit handler that sets
- // g_greenlet_shutting_down. Python's atexit is LIFO:
- // registered last = called first. By registering here (at
- // import time, after most other libraries), our handler runs
- // before their cleanup code, which may try to call
- // greenlet.getcurrent() on objects whose type has been
- // invalidated. _Py_IsFinalizing() alone is insufficient on
- // ALL Python versions because it is only set AFTER atexit
- // handlers complete inside Py_FinalizeEx.
- {
- NewReference atexit_mod(Require(PyImport_ImportModule("atexit")));
- OwnedObject register_fn = atexit_mod.PyRequireAttr("register");
- NewReference callback(Require(
- PyCFunction_New(&_greenlet_atexit_method, NULL)));
- NewReference args(Require(PyTuple_Pack(1, callback.borrow())));
- NewReference result(Require(
- PyObject_Call(register_fn.borrow(), args.borrow(), NULL)));
- }
-
return m.borrow(); // But really it's the main reference.
}
catch (const LockInitError& e) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.4.0/src/greenlet/greenlet_refs.hpp
new/greenlet-3.5.0/src/greenlet/greenlet_refs.hpp
--- old/greenlet-3.4.0/src/greenlet/greenlet_refs.hpp 2026-04-08
17:49:31.000000000 +0200
+++ new/greenlet-3.5.0/src/greenlet/greenlet_refs.hpp 2026-04-27
14:18:19.000000000 +0200
@@ -27,25 +27,52 @@
namespace greenlet
{
class Greenlet;
- // _Py_IsFinalizing() is only set AFTER atexit handlers complete
- // inside Py_FinalizeEx on ALL Python versions (including 3.11+).
- // Code running in atexit handlers (e.g. uWSGI plugin cleanup
- // calling Py_FinalizeEx, New Relic agent shutdown) can still call
- // greenlet.getcurrent(), but by that time type objects or
- // internal state may have been invalidated. This flag is set by
- // an atexit handler registered at module init (LIFO = runs
- // first).
- //
- // Because this is only set from an atexit handler, by which point
- // we're single threaded, there should be no need to make it
- // std::atomic<int>.
- // TODO: Move this to the GreenletGlobals object?
- static int g_greenlet_shutting_down;
static inline bool
IsShuttingDown()
{
- return greenlet::g_greenlet_shutting_down || Py_IsFinalizing();
+ // This used to check a flag set by an ``atexit`` callback.
+ // This was wrong: the interpreter is still fully functional
+ // while *all* atexit callbacks are run, and it is perfectly
+ // valid for an atexit callback that runs after our atexit
+ // callback (i.e., registered first/before ours) to want to
+ // make use of greenlet services --- this comes up easily with
+ // gevent monkey-patching. Almost immediately after atexit callbacks,
+ // and before any destructive action is taken, Python arranges
+ // for Py_IsFinalizing to become true.
+
+ // It may see me could potentially tighten this check even more (and
+ // eliminate a function call) by setting a flag in a
+ // destructor function for our PyCapsule object (_C_API) to
+ // determine when we're shutting down. ``Py_IsFinalizing``
+ // becomes true relatively early in the shutdown process,
+ // while Capsule destructor functions only run when the module
+ // has actually been torn down --- well, when all of its dicts are
+ // cleared and collected; recall that because we use
+ // single-phase init, there is a "hidden" copy of the module
+ // dict kept by CPython internals used to re-populate a module
+ // if greenlet is imported twice, so Python code can't trigger
+ // C_API to get GC'd early without seriously poking at CPython
+ // internals, e.g., by using `gc.get_referrers` to find the
+ // hidden dict. However, C extensions could have INCREF the
+ // capsule object and prevent it from *ever* getting torn
+ // down, so this isn't reliable.
+
+ // We could probably be even "smarter" and replace values in
+ // _PyGreenlet_API with different values at destruction time.
+ // For the PyObject* returning APIs, we could replace them
+ // with versions that set an exception and return null --- the
+ // benefit being that we don't have to include a
+ // Py_IsFinalizing() call in the normal path; int returning
+ // APIs would be handled on a case-by-case basis; unclear what
+ // to do with the types. This is of questionable benefit
+ // though because by the time our destructor is called, our
+ // module is about to be destroyed which may take our
+ // allocated storage with it (if CPython ever dynamically
+ // unloads loaded shared libraries, which as of 3.14 it never
+ // does).
+
+ return Py_IsFinalizing();
}
namespace refs
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.4.0/src/greenlet/tests/_test_extension.c
new/greenlet-3.5.0/src/greenlet/tests/_test_extension.c
--- old/greenlet-3.4.0/src/greenlet/tests/_test_extension.c 2026-04-08
17:49:31.000000000 +0200
+++ new/greenlet-3.5.0/src/greenlet/tests/_test_extension.c 2026-04-27
14:18:19.000000000 +0200
@@ -189,6 +189,13 @@
Py_RETURN_NONE;
}
+static PyObject*
+getcurrent_api(PyObject* UNUSED(self))
+{
+ return (PyObject*)PyGreenlet_GetCurrent();
+
+}
+
static PyMethodDef test_methods[] = {
{"test_switch",
(PyCFunction)test_switch,
@@ -227,6 +234,11 @@
(PyCFunction)test_throw_exact,
METH_VARARGS,
"Throw exactly the arguments given at the provided greenlet"},
+ {
+ "getcurrent_api",
+ (PyCFunction)getcurrent_api,
+ METH_NOARGS,
+ "Direct call to the PyGreenlet_GetCurrent API."},
{NULL, NULL, 0, NULL}
};
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/greenlet-3.4.0/src/greenlet/tests/test_interpreter_shutdown.py
new/greenlet-3.5.0/src/greenlet/tests/test_interpreter_shutdown.py
--- old/greenlet-3.4.0/src/greenlet/tests/test_interpreter_shutdown.py
2026-04-08 17:49:31.000000000 +0200
+++ new/greenlet-3.5.0/src/greenlet/tests/test_interpreter_shutdown.py
2026-04-27 14:18:19.000000000 +0200
@@ -2,18 +2,6 @@
"""
Tests for greenlet behavior during interpreter shutdown (Py_FinalizeEx).
-During interpreter shutdown, several greenlet code paths can access
-partially-destroyed Python state, leading to SIGSEGV. Two independent
-guards protect against this on ALL Python versions:
-
- 1. g_greenlet_shutting_down — set by an atexit handler registered at
- greenlet import time (LIFO = runs before other cleanup). Covers
- the atexit phase of Py_FinalizeEx, where _Py_IsFinalizing() is
- still False on all Python versions.
-
- 2. Py_IsFinalizing() — covers the GC collection and later phases of
- Py_FinalizeEx, where __del__ methods and destructor code run.
-
These tests are organized into four groups:
A. Core safety (smoke): no crashes with active greenlets at shutdown.
@@ -58,14 +46,14 @@
# -----------------------------------------------------------------
def test_active_greenlet_at_shutdown_no_crash(self):
- """
- An active (suspended) greenlet that is deallocated during
- interpreter shutdown should not crash the process.
- Before the fix, this would SIGSEGV on Python < 3.11 because
- _green_dealloc_kill_started_non_main_greenlet tried to call
- g_switch() during Py_FinalizeEx.
- """
+ # An active (suspended) greenlet that is deallocated during
+ # interpreter shutdown should not crash the process.
+
+ # Before the fix, this would SIGSEGV on Python < 3.11 because
+ # _green_dealloc_kill_started_non_main_greenlet tried to call
+ # g_switch() during Py_FinalizeEx.
+
rc, stdout, stderr = self._run_shutdown_script("""\
import greenlet
@@ -82,10 +70,9 @@
self.assertIn("OK: exiting with active greenlet", stdout)
def test_multiple_active_greenlets_at_shutdown(self):
- """
- Multiple suspended greenlets at shutdown should all be cleaned
- up without crashing.
- """
+ # Multiple suspended greenlets at shutdown should all be cleaned
+ # up without crashing.
+
rc, stdout, stderr = self._run_shutdown_script("""\
import greenlet
@@ -105,9 +92,8 @@
self.assertIn("OK: 10 active greenlets at shutdown", stdout)
def test_nested_greenlets_at_shutdown(self):
- """
- Nested (chained parent) greenlets at shutdown should not crash.
- """
+ # Nested (chained parent) greenlets at shutdown should not crash.
+
rc, stdout, stderr = self._run_shutdown_script("""\
import greenlet
@@ -128,10 +114,9 @@
self.assertIn("OK: nested greenlets at shutdown", stdout)
def test_threaded_greenlets_at_shutdown(self):
- """
- Greenlets in worker threads that are still referenced at
- shutdown should not crash.
- """
+ # Greenlets in worker threads that are still referenced at
+ # shutdown should not crash.
+
rc, stdout, stderr = self._run_shutdown_script("""\
import greenlet
import threading
@@ -171,12 +156,11 @@
# NOT interpreter shutdown; the guards do not fire here.
def test_greenlet_cleanup_during_thread_exit(self):
- """
- When a thread exits normally while holding active greenlets,
- GreenletExit IS thrown and cleanup code runs. This is the
- standard cleanup path used in production (e.g. uWSGI worker
- threads finishing a request).
- """
+ # When a thread exits normally while holding active greenlets,
+ # GreenletExit IS thrown and cleanup code runs. This is the
+ # standard cleanup path used in production (e.g. uWSGI worker
+ # threads finishing a request).
+
rc, stdout, stderr = self._run_shutdown_script("""\
import os
import threading
@@ -208,10 +192,8 @@
self.assertIn("CLEANUP: GreenletExit caught", stdout)
def test_finally_block_during_thread_exit(self):
- """
- try/finally blocks in active greenlets run correctly when the
- owning thread exits.
- """
+ # try/finally blocks in active greenlets run correctly when the
+ # owning thread exits.
rc, stdout, stderr = self._run_shutdown_script("""\
import os
import threading
@@ -239,10 +221,9 @@
self.assertIn("FINALLY: cleanup executed", stdout)
def test_many_greenlets_with_cleanup_at_shutdown(self):
- """
- Stress test: many active greenlets with cleanup code at shutdown.
- Ensures no crashes regardless of deallocation order.
- """
+ # Stress test: many active greenlets with cleanup code at shutdown.
+ # Ensures no crashes regardless of deallocation order.
+
rc, stdout, stderr = self._run_shutdown_script("""\
import sys
import greenlet
@@ -271,10 +252,9 @@
self.assertIn("OK: 50 greenlets about to shut down", stdout)
def test_deeply_nested_greenlets_at_shutdown(self):
- """
- Deeply nested greenlet parent chains at shutdown.
- Tests that the deallocation order doesn't cause issues.
- """
+ # Deeply nested greenlet parent chains at shutdown.
+ # Tests that the deallocation order doesn't cause issues.
+
rc, stdout, stderr = self._run_shutdown_script("""\
import greenlet
@@ -292,10 +272,9 @@
self.assertIn("OK: nested to depth 10", stdout)
def test_greenlet_with_traceback_at_shutdown(self):
- """
- A greenlet that has an active exception context when it's
- suspended should not crash during shutdown cleanup.
- """
+ # A greenlet that has an active exception context when it's
+ # suspended should not crash during shutdown cleanup.
+
rc, stdout, stderr = self._run_shutdown_script("""\
import greenlet
@@ -318,21 +297,13 @@
# -----------------------------------------------------------------
# Group C: getcurrent() / construction / gettrace() / settrace()
# during atexit — registered AFTER greenlet import
- #
- # These atexit handlers are registered AFTER ``import greenlet``,
- # so they run BEFORE greenlet's own cleanup handler (LIFO). At
- # this point g_greenlet_shutting_down is still 0 and
- # _Py_IsFinalizing() is False, so getcurrent() must still return
- # a valid greenlet object. These tests guard against the fix
- # being too aggressive (over-blocking getcurrent early).
# -----------------------------------------------------------------
def test_getcurrent_during_atexit_no_crash(self):
- """
- getcurrent() in an atexit handler registered AFTER greenlet
- import must return a valid greenlet (not None), because LIFO
- ordering means this handler runs BEFORE greenlet's cleanup.
- """
+ # getcurrent() in an atexit handler registered AFTER greenlet
+ # import must return a valid greenlet (not None), because LIFO
+ # ordering means this handler runs BEFORE greenlet's cleanup.
+
rc, stdout, stderr = self._run_shutdown_script("""\
import atexit
import greenlet
@@ -359,9 +330,8 @@
"before greenlet's cleanup handler (LIFO ordering)")
def test_gettrace_during_atexit_no_crash(self):
- """
- Calling greenlet.gettrace() during atexit must not crash.
- """
+ # Calling greenlet.gettrace() during atexit must not crash.
+
rc, stdout, stderr = self._run_shutdown_script("""\
import atexit
import greenlet
@@ -380,9 +350,8 @@
self.assertIn("OK: registered", stdout)
def test_settrace_during_atexit_no_crash(self):
- """
- Calling greenlet.settrace() during atexit must not crash.
- """
+ # Calling greenlet.settrace() during atexit must not crash.
+
rc, stdout, stderr = self._run_shutdown_script("""\
import atexit
import greenlet
@@ -401,11 +370,10 @@
self.assertIn("OK: registered", stdout)
def test_getcurrent_with_active_greenlets_during_atexit(self):
- """
- getcurrent() during atexit (registered after import) with active
- greenlets must still return a valid greenlet, since LIFO means
- this runs before greenlet's cleanup.
- """
+ # getcurrent() during atexit (registered after import) with active
+ # greenlets must still return a valid greenlet, since LIFO means
+ # this runs before greenlet's cleanup.
+
rc, stdout, stderr = self._run_shutdown_script("""\
import atexit
import greenlet
@@ -441,10 +409,9 @@
"before greenlet's cleanup handler (LIFO ordering)")
def test_greenlet_construction_during_atexit_no_crash(self):
- """
- Constructing a new greenlet during atexit (registered after
- import) must succeed, since this runs before greenlet's cleanup.
- """
+ # Constructing a new greenlet during atexit (registered after
+ # import) must succeed, since this runs before greenlet's cleanup.
+
rc, stdout, stderr = self._run_shutdown_script("""\
import atexit
import greenlet
@@ -469,11 +436,10 @@
self.assertIn("OK: created greenlet successfully", stdout)
def test_greenlet_construction_with_active_greenlets_during_atexit(self):
- """
- Constructing new greenlets during atexit when other active
- greenlets already exist (maximizes the chance of a non-empty
- deleteme list).
- """
+ # Constructing new greenlets during atexit when other active
+ # greenlets already exist (maximizes the chance of a non-empty
+ # deleteme list).
+
rc, stdout, stderr = self._run_shutdown_script("""\
import atexit
import greenlet
@@ -504,14 +470,12 @@
self.assertIn("OK: 10 active greenlets, atexit registered", stdout)
def
test_greenlet_construction_with_cross_thread_deleteme_during_atexit(self):
- """
- Create greenlets in a worker thread, transfer them to the main
- thread, then drop them — populating the deleteme list. Then
- construct a new greenlet during atexit. On Python < 3.11
- clear_deleteme_list() could previously crash if the
- PythonAllocator vector copy failed during early Py_FinalizeEx;
- using std::swap eliminates that allocation.
- """
+ # Create greenlets in a worker thread, transfer them to the main
+ # thread, then drop them — populating the deleteme list. Then
+ # construct a new greenlet during atexit. On Python < 3.11
+ # clear_deleteme_list() could previously crash if the
+ # PythonAllocator vector copy failed during early Py_FinalizeEx;
+ # using std::swap eliminates that allocation.
rc, stdout, stderr = self._run_shutdown_script("""\
import atexit
import greenlet
@@ -568,15 +532,13 @@
# -----------------------------------------------------------------
def test_getcurrent_returns_none_during_gc_finalization(self):
- """
- greenlet.getcurrent() must return None when called from a
- __del__ method during Py_FinalizeEx's GC collection pass.
+ # greenlet.getcurrent() must return None when called from a
+ # __del__ method during Py_FinalizeEx's GC collection pass.
- On Python >= 3.11, _Py_IsFinalizing() is True during this
- phase. Without the Py_IsFinalizing() guard in mod_getcurrent,
- this would return a greenlet — the same unguarded code path
- that leads to SIGSEGV in production (uWSGI worker recycling).
- """
+ # On Python >= 3.11, _Py_IsFinalizing() is True during this
+ # phase. Without the Py_IsFinalizing() guard in mod_getcurrent,
+ # this would return a greenlet — the same unguarded code path
+ # that leads to SIGSEGV in production (uWSGI worker recycling).
rc, stdout, stderr = self._run_shutdown_script("""\
import gc
import os
@@ -611,10 +573,9 @@
"returned a live object instead (missing Py_IsFinalizing
guard)")
def
test_getcurrent_returns_none_during_gc_finalization_with_active_greenlets(self):
- """
- Same as above but with active greenlets at shutdown, which
- increases the amount of C++ destructor work during finalization.
- """
+ # Same as above but with active greenlets at shutdown, which
+ # increases the amount of C++ destructor work during finalization.
+
rc, stdout, stderr = self._run_shutdown_script("""\
import gc
import os
@@ -658,13 +619,12 @@
"returned a live object instead (missing Py_IsFinalizing
guard)")
def test_getcurrent_returns_none_during_gc_finalization_cross_thread(self):
- """
- Combines cross-thread greenlet deallocation (deleteme list)
- with the GC finalization check. This simulates the production
- scenario where uWSGI worker threads create greenlets that are
- transferred to the main thread, then cleaned up during
- Py_FinalizeEx.
- """
+ # Combines cross-thread greenlet deallocation (deleteme list)
+ # with the GC finalization check. This simulates the production
+ # scenario where uWSGI worker threads create greenlets that are
+ # transferred to the main thread, then cleaned up during
+ # Py_FinalizeEx.
+
rc, stdout, stderr = self._run_shutdown_script("""\
import gc
import os
@@ -735,15 +695,9 @@
# -----------------------------------------------------------------
def test_getcurrent_returns_none_during_atexit_phase(self):
- """
- greenlet.getcurrent() must return None when called from an
- atexit handler that runs AFTER greenlet's own atexit handler.
+ # greenlet.getcurrent() must NOT return None when called from an
+ # atexit handler that runs AFTER greenlet's own atexit handler.
- This tests the g_greenlet_shutting_down flag, which is needed
- because _Py_IsFinalizing() is still False during the atexit
- phase on ALL Python versions. Without g_greenlet_shutting_down,
- getcurrent() proceeds unguarded into partially-torn-down state.
- """
rc, stdout, stderr = self._run_shutdown_script("""\
import atexit
import os
@@ -770,16 +724,11 @@
""")
self.assertEqual(rc, 0, f"Process crashed
(rc={rc}):\n{stdout}{stderr}")
self.assertIn("OK: atexit registered before greenlet import", stdout)
- self.assertIn("GUARDED: getcurrent=None", stdout,
- "getcurrent() must return None during atexit phase; "
- "returned a live object instead (missing "
- "g_greenlet_shutting_down atexit handler)")
+ self.assertIn("UNGUARDED", stdout)
+
def
test_getcurrent_returns_none_during_atexit_phase_with_active_greenlets(self):
- """
- Same as above but with active greenlets, ensuring the atexit
- guard works even when there is greenlet state to clean up.
- """
+ # Same as above but with active greenlets
rc, stdout, stderr = self._run_shutdown_script("""\
import atexit
import os
@@ -812,10 +761,63 @@
""")
self.assertEqual(rc, 0, f"Process crashed
(rc={rc}):\n{stdout}{stderr}")
self.assertIn("OK: 10 active greenlets, atexit registered", stdout)
- self.assertIn("GUARDED: getcurrent=None", stdout,
- "getcurrent() must return None during atexit phase; "
- "returned a live object instead (missing "
- "g_greenlet_shutting_down atexit handler)")
+ self.assertIn("UNGUARDED", stdout)
+
+ def test_api_getcurrent_no_system_error_at_module_gc_time(self):
+ # If we use the C API directly to return a greenlet AFTER
+ # atexit threads have been run, we don't crash, we get a
+ # specific error. We arrange for this by putting a __del__ on
+ # an object that lives in greenlet's own (extension module)
+ # dict; this is cleaned out sometime during the module cleanup
+ # steps.
+ rc, stdout, stderr = self._run_shutdown_script("""\
+ import greenlet
+ from greenlet.tests import _test_extension
+
+ class WithDel:
+ # must cache the method we want, because by the time we
+ # run, module globals may have been cleaned up.
+ def __del__(self, gc=_test_extension.getcurrent_api):
+ print('Destructor running')
+ gc() # Should print an unraisable RuntimeException
+
+ greenlet._greenlet.with_del = WithDel()
+ """)
+ self.assertEqual(rc, 0, f"Process crashed
(rc={rc}):\n{stdout}{stderr}")
+ self.assertIn('Destructor running', stdout)
+ self.assertIn('RuntimeError: greenlet is being finalized', stderr)
+
+
+ def test_switch_no_error_at_module_gc_time(self):
+ # Switching to a greenlet we've captured during
+ # module tear down doesn't cause a crash
+ rc, stdout, stderr = self._run_shutdown_script("""\
+ import greenlet
+ from greenlet.tests import _test_extension
+
+ gs = []
+ # must cache the objects we want, because by the time we
+ # run, module globals may have been cleaned up.
+ def do_it(gs=gs):
+ print('current', gs)
+ gs[0].parent.switch(1)
+
+
+ gs.append(greenlet.greenlet(do_it))
+ gs.append(greenlet.greenlet(do_it))
+ gs[1].switch()
+
+ class WithDel:
+ def __del__(self, gs=gs):
+ print('Destructor running')
+ r = gs[0].switch()
+ print('Result', r)
+
+ greenlet._greenlet.with_del = WithDel()
+ """)
+ self.assertEqual(rc, 0, f"Process crashed
(rc={rc}):\n{stdout}{stderr}")
+ self.assertIn('Destructor running', stdout)
+ self.assertIn('Result 1', stdout)
if __name__ == '__main__':
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.4.0/src/greenlet.egg-info/PKG-INFO
new/greenlet-3.5.0/src/greenlet.egg-info/PKG-INFO
--- old/greenlet-3.4.0/src/greenlet.egg-info/PKG-INFO 2026-04-08
17:49:38.000000000 +0200
+++ new/greenlet-3.5.0/src/greenlet.egg-info/PKG-INFO 2026-04-27
14:18:23.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: greenlet
-Version: 3.4.0
+Version: 3.5.0
Summary: Lightweight in-process concurrent programming
Author-email: Alexey Borzenkov <[email protected]>
Maintainer-email: Jason Madden <[email protected]>