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-06-29 17:29:58
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-greenlet (Old)
and /work/SRC/openSUSE:Factory/.python-greenlet.new.11887 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-greenlet"
Mon Jun 29 17:29:58 2026 rev:61 rq:1362225 version:3.5.3
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-greenlet/python-greenlet.changes
2026-05-04 21:17:17.732029066 +0200
+++
/work/SRC/openSUSE:Factory/.python-greenlet.new.11887/python-greenlet.changes
2026-06-29 17:30:33.874451496 +0200
@@ -1,0 +2,40 @@
+Sun Jun 28 17:08:17 UTC 2026 - Matej Cepl <[email protected]>
+
+- Add build-Cpp-linking.patch fixing linking C++ problems with
+ Python 3.14.6 (see
+
https://discuss.python.org/t/c-linking-in-3-14-6-is-there-a-regression/107932)
+
+-------------------------------------------------------------------
+Sat Jun 27 16:08:02 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 3.5.3:
+ * Fix a crash on free-threaded builds when multiple greenlets
+ were holding a critical section on an object and the GIL for
+ the thread was dropped. See issue 513. Thanks to ddorian.
+
+-------------------------------------------------------------------
+Fri Jun 26 13:00:13 UTC 2026 - John Paul Adrian Glaubitz
<[email protected]>
+
+- Update to 3.5.2
+ * The minimum supported version of Python 3.15 is now 3.15b2.
+ * Fix some garbage-collection related crashes on free-threaded
+ Python 3.15. Thanks to Kumar Aditya in PR #511.
+ * Improve garbage collection of greenlets. This mostly applies
+ to Python 3.15. Thanks to Kumar Aditya in PR #512.
+
+-------------------------------------------------------------------
+Mon Jun 1 09:45:26 UTC 2026 - John Paul Adrian Glaubitz
<[email protected]>
+
+- Update to 3.5.1
+ * Add preliminary support for Python 3.15b1. This has not been
+ reviewed by CPython core developers, but all tests pass. Binary
+ wheels of this version won't work on earlier Python 3.15 builds and
+ may not work on later 3.15 builds.
+ * Fix the discrepancy in the way the two ``getcurrent`` APIs behave
+ during greenlet teardown. One API (the C API used by, e.g., gevent
+ raised a ``RuntimeError``; the other (the Python ``greenlet.getcurrent``
+ API) returned ``None``. This second way is incompatible with greenlet's
+ type annotations, so ``greenlet.getcurrent`` now raises a ``RuntimeError``
+ as well.
+
+-------------------------------------------------------------------
Old:
----
greenlet-3.5.0.tar.gz
New:
----
build-Cpp-linking.patch
greenlet-3.5.3.tar.gz
----------(New B)----------
New:
- Add build-Cpp-linking.patch fixing linking C++ problems with
Python 3.14.6 (see
----------(New E)----------
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-greenlet.spec ++++++
--- /var/tmp/diff_new_pack.8cbwGs/_old 2026-06-29 17:30:34.546474664 +0200
+++ /var/tmp/diff_new_pack.8cbwGs/_new 2026-06-29 17:30:34.550474803 +0200
@@ -22,7 +22,7 @@
%{?sle15_python_module_pythons}
Name: python-greenlet
-Version: 3.5.0
+Version: 3.5.3
Release: 0
Summary: Lightweight in-process concurrent programming
License: MIT
@@ -30,6 +30,10 @@
URL: https://github.com/python-greenlet/greenlet
Source0:
https://files.pythonhosted.org/packages/source/g/greenlet/greenlet-%{version}.tar.gz
Source9: python-greenlet-rpmlintrc
+# PATCH-FIX-OPENSUSE build-Cpp-linking.patch [email protected]
+# Fix linking of C++ module
+# https://discuss.python.org/t/c-linking-in-3-14-6-is-there-a-regression/107932
+Patch0: build-Cpp-linking.patch
BuildRequires: %{python_module devel >= 3.10}
BuildRequires: %{python_module objgraph}
BuildRequires: %{python_module pip}
++++++ build-Cpp-linking.patch ++++++
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
Index: greenlet-3.5.3/setup.py
===================================================================
--- greenlet-3.5.3.orig/setup.py 2026-06-26 20:18:23.000000000 +0200
+++ greenlet-3.5.3/setup.py 2026-06-28 18:51:06.330582819 +0200
@@ -18,7 +18,7 @@
cpp_compile_args = []
# Extra linker arguments passed to C++ extensions
-cpp_link_args = []
+cpp_link_args = ['-lstdc++']
# Extra compiler arguments passed to the main extension
main_compile_args = []
++++++ greenlet-3.5.0.tar.gz -> greenlet-3.5.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/.github/workflows/tests.yml
new/greenlet-3.5.3/.github/workflows/tests.yml
--- old/greenlet-3.5.0/.github/workflows/tests.yml 2026-04-27
14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/.github/workflows/tests.yml 2026-06-26
20:18:23.000000000 +0200
@@ -21,6 +21,7 @@
test:
runs-on: ${{ matrix.os }}
strategy:
+ fail-fast: false
matrix:
python-version:
- "3.10"
@@ -29,6 +30,8 @@
- "3.13"
- "3.14"
- "3.14t"
+ - "3.15-dev"
+ - "3.15t-dev"
# Recall the macOS and windows builds upload built wheels so all
supported versions
# need to run on mac.
@@ -99,7 +102,8 @@
ARCHFLAGS: "-arch x86_64 -arch arm64"
- name: Check greenlet build
- if: ${{ ! startsWith(runner.os, 'Windows') }}
+ # 3.15b1 has a problem with readme renderer, ModuleNotFoundError: No
module named 'nh3.nh3'
+ if: ${{ !startsWith(runner.os, 'Windows') &&
!endsWith(matrix.python-version, '-dev')}}
run: |
ls -l dist
twine check dist/*
@@ -127,7 +131,8 @@
# standalone in a unit test doesn't produce the error.
#
# So this is a temporary workaround.
- PYTHON_TLBC: "0"
+ # This has been fixed for 3.15+
+ PYTHON_TLBC: ${{ matrix.python-version == '3.14t' && '0' || '1' }}
run: |
sphinx-build -b doctest -d docs/_build/doctrees2 docs
docs/_build/doctest2
- name: Lint
@@ -138,7 +143,7 @@
pip install -U pylint
python -m pylint --rcfile=.pylintrc greenlet
- - name: Publish package to PyPI (mac)
+ - name: Publish package to PyPI (Mac/Win)
# We cannot 'uses: pypa/[email protected]' because
# that's apparently a container action, and those don't run on
# the Mac.
@@ -181,8 +186,16 @@
manylinux:
runs-on: ubuntu-latest
+ # If we have 'needs: test', then these wait to start running until
+ # all the test matrix passes. That's good, because these take a
+ # long time, and they take a long time to kill if something goes
+ # wrong. OTOH, if one of the tests fail, and this is a release tag,
+ # we have to notice that and try restarting things so that the
+ # wheels get built and uploaded.
+ needs: test
# We use a regular Python matrix entry to share as much code as possible.
strategy:
+ fail-fast: false
matrix:
python-version:
- "3.14"
@@ -217,7 +230,7 @@
path: wheelhouse/*whl
name: ${{ matrix.image }}_wheels.zip
- name: Publish package to PyPI
- uses: pypa/[email protected]
+ uses: pypa/[email protected]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
with:
user: __token__
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/CHANGES.rst
new/greenlet-3.5.3/CHANGES.rst
--- old/greenlet-3.5.0/CHANGES.rst 2026-04-27 14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/CHANGES.rst 2026-06-26 20:18:23.000000000 +0200
@@ -2,6 +2,43 @@
Changes
=========
+3.5.3 (2026-06-26)
+==================
+
+- Fix a crash on free-threaded builds when multiple greenlets were
+ holding a critical section on an object and the GIL for the thread
+ was dropped. See `issue 513
+ <https://github.com/python-greenlet/greenlet/issues/513>`_. Thanks
+ to ddorian.
+
+
+3.5.2 (2026-06-17)
+==================
+
+- The minimum supported version of Python 3.15 is now 3.15b2.
+- Fix some garbage-collection related crashes on free-threaded Python
+ 3.15. Thanks to Kumar Aditya in `PR #511
+ <https://github.com/python-greenlet/greenlet/pull/511>`_.
+- Improve garbage collection of greenlets. This mostly applies to
+ Python 3.15. Thanks to Kumar Aditya in `PR #512
+ <https://github.com/python-greenlet/greenlet/pull/512>`_.
+
+
+3.5.1 (2026-05-20)
+==================
+
+- Add preliminary support for Python 3.15b1. This has not been
+ reviewed by CPython core developers, but all tests pass. Binary
+ wheels of this version won't work on earlier Python 3.15 builds and
+ may not work on later 3.15 builds.
+- Fix the discrepancy in the way the two ``getcurrent`` APIs behave
+ during greenlet teardown. One API (the C API used by, e.g., gevent) raised a
+ ``RuntimeError``; the other (the Python ``greenlet.getcurrent`` API)
+ returned ``None``. This second way is incompatible with greenlet's type
+ annotations, so ``greenlet.getcurrent`` now raises a
+ ``RuntimeError`` as well.
+
+
3.5.0 (2026-04-27)
==================
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/PKG-INFO new/greenlet-3.5.3/PKG-INFO
--- old/greenlet-3.5.0/PKG-INFO 2026-04-27 14:18:23.167599400 +0200
+++ new/greenlet-3.5.3/PKG-INFO 2026-06-26 20:18:27.252837400 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: greenlet
-Version: 3.5.0
+Version: 3.5.3
Summary: Lightweight in-process concurrent programming
Author-email: Alexey Borzenkov <[email protected]>
Maintainer-email: Jason Madden <[email protected]>
@@ -22,6 +22,7 @@
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
+Classifier: Programming Language :: Python :: 3.15
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/docs/greenlet_gc.rst
new/greenlet-3.5.3/docs/greenlet_gc.rst
--- old/greenlet-3.5.0/docs/greenlet_gc.rst 2026-04-27 14:18:19.000000000
+0200
+++ new/greenlet-3.5.3/docs/greenlet_gc.rst 2026-06-26 20:18:23.000000000
+0200
@@ -164,53 +164,3 @@
Collecting garbage
(Running finalizer)
(Running finalizer)
-
-A Cycle Of Greenlets Is A Leak
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-What if we introduce a cycle among the greenlets themselves while also
-leaving a greenlet suspended? Here, the frames of the ``inner``
-greenlet refer to the ``outer`` (as the ``inner`` greenlet itself
-does), and both the frames of the ``outer``, as well as the ``outer``
-greenlet itself, refer to the ``inner``:
-
-.. doctest::
- :pyversion: >= 3.5
-
- >>> def inner():
- ... cycle1 = Cycle()
- ... cycle2 = Cycle()
- ... cycle1.cycle = cycle2
- ... cycle2.cycle = cycle1
- ... parent = getcurrent().parent
- ... parent.switch()
- >>> def outer():
- ... glet = greenlet(inner)
- ... getcurrent().child_greenlet = glet
- ... glet.switch()
- ... collect_it()
-
-This time, even letting the outer and inner greenlets die doesn't find
-the cycle hidden in the inner greenlet's frame:
-
-.. doctest::
- :pyversion: >= 3.5
-
- >>> outer_glet = greenlet(outer)
- >>> outer_glet.switch()
- Collecting garbage
- >>> outer_glet.dead
- True
- >>> collect_it()
- Collecting garbage
-
-Even explicitly deleting the outer greenlet doesn't find and clear the
-cycle; we have created a legitimate memory leak, not just of the
-greenlet objects, but also the objects in any suspended frames:
-
-.. doctest::
- :pyversion: >= 3.5
-
- >>> del outer_glet
- >>> collect_it()
- Collecting garbage
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/make-manylinux
new/greenlet-3.5.3/make-manylinux
--- old/greenlet-3.5.0/make-manylinux 2026-04-27 14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/make-manylinux 2026-06-26 20:18:23.000000000 +0200
@@ -18,6 +18,10 @@
if [ -d /greenlet -a -d /opt/python ]; then
# Running inside docker
export GREENLET_MANYLINUX=1
+ # This is implied by the former, but "explicit better than implicit."
+ # One one machine, skipping the leakchecks speed up test_greenlet from
+ # 42s to 17s
+ export GREENLET_SKIP_LEAKCHECKS=1
# Build for speed (we're going to test this anyway) and without assertions.
# Note: -Ofast includes -ffast-math which affects process-wide
floating-point flags (e.g. can affect numpy).
# It may also violate standards compliance in a few ways. Rather than
opt-out with -fno-fast-math,
@@ -41,7 +45,7 @@
which auditwheel
echo "Installed Python versions"
ls -l /opt/python
- for variant in /opt/python/cp{314,313,312,311,310}*; do
+ for variant in /opt/python/cp{315,314,313,312,311,310}*; do
if [[ "$variant" == *3t ]]; then
echo "Skipping no-gil build for 3.13; only 3.14 is fully
supported."
continue
@@ -53,7 +57,10 @@
python -mpip install -U setuptools wheel
python -mpip wheel -v --wheel-dir ./dist .
python -mpip install -U ".[test]"
- python -m unittest discover -v greenlet.tests
+ # Only do basic functionality tests because the full test
+ # suite takes a very long time to run on the emulated
+ # environments, even with leakchecks disabled.
+ python -m unittest -v greenlet.tests.test_greenlet
PATH="$OPATH" auditwheel repair dist/greenlet*.whl
cp wheelhouse/greenlet*.whl /greenlet/wheelhouse
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/pyproject.toml
new/greenlet-3.5.3/pyproject.toml
--- old/greenlet-3.5.0/pyproject.toml 2026-04-27 14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/pyproject.toml 2026-06-26 20:18:23.000000000 +0200
@@ -31,6 +31,7 @@
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
+ "Programming Language :: Python :: 3.15",
"Operating System :: OS Independent",
"Topic :: Software Development :: Libraries :: Python Modules",
]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/src/greenlet/PyGreenlet.cpp
new/greenlet-3.5.3/src/greenlet/PyGreenlet.cpp
--- old/greenlet-3.5.0/src/greenlet/PyGreenlet.cpp 2026-04-27
14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/src/greenlet/PyGreenlet.cpp 2026-06-26
20:18:23.000000000 +0200
@@ -808,7 +808,7 @@
.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 !GREENLET_PY315 && !(GREENLET_PY314 && defined(Py_GIL_DISABLED))
/*
We may have been handling this wrong all along.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/src/greenlet/PyModule.cpp
new/greenlet-3.5.3/src/greenlet/PyModule.cpp
--- old/greenlet-3.5.0/src/greenlet/PyModule.cpp 2026-04-27
14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/src/greenlet/PyModule.cpp 2026-06-26
20:18:23.000000000 +0200
@@ -28,7 +28,8 @@
mod_getcurrent(PyObject* UNUSED(module))
{
if (greenlet::IsShuttingDown()) {
- Py_RETURN_NONE;
+ PyErr_SetString(PyExc_RuntimeError, "greenlet is being finalized");
+ return nullptr;
}
return GET_THREAD_STATE().state().get_current().relinquish_ownership_o();
}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/src/greenlet/TGreenlet.cpp
new/greenlet-3.5.3/src/greenlet/TGreenlet.cpp
--- old/greenlet-3.5.0/src/greenlet/TGreenlet.cpp 2026-04-27
14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/src/greenlet/TGreenlet.cpp 2026-06-26
20:18:23.000000000 +0200
@@ -607,6 +607,10 @@
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 saved stack state since the owned frame is cleared.
+ this->stack_state.set_inactive();
+ }
return 0;
}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/src/greenlet/TGreenlet.hpp
new/greenlet-3.5.3/src/greenlet/TGreenlet.hpp
--- old/greenlet-3.5.0/src/greenlet/TGreenlet.hpp 2026-04-27
14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/src/greenlet/TGreenlet.hpp 2026-06-26
20:18:23.000000000 +0200
@@ -42,6 +42,14 @@
#endif
#endif
+
+#if GREENLET_PY315
+#include "internal/pycore_gc.h"
+#if !defined(_MSC_VER) && !defined(__MINGW64__)
+#include "internal/pycore_stackref.h"
+#endif
+#endif
+
// XXX: TODO: Work to remove all virtual functions
// for speed of calling and size of objects (no vtable).
// One pattern is the Curiously Recurring Template
@@ -139,7 +147,8 @@
int recursion_depth;
#endif
#if GREENLET_PY313
- PyObject *delete_later;
+ PyObject* delete_later;
+ uintptr_t critical_section;
#else
int trash_delete_nesting;
#endif
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/src/greenlet/TPythonState.cpp
new/greenlet-3.5.3/src/greenlet/TPythonState.cpp
--- old/greenlet-3.5.0/src/greenlet/TPythonState.cpp 2026-04-27
14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/src/greenlet/TPythonState.cpp 2026-06-26
20:18:23.000000000 +0200
@@ -27,6 +27,7 @@
#endif
#if GREENLET_PY313
,delete_later(nullptr)
+ ,critical_section(0)
#else
,trash_delete_nesting(0)
#endif
@@ -191,6 +192,7 @@
// ``greenlet.tests.test_greenlet_trash`` tries, but under 3.14,
// at least, fails to do so.
this->delete_later = Py_XNewRef(tstate->delete_later);
+ this->critical_section = tstate->critical_section;
#elif GREENLET_PY312
this->trash_delete_nesting = tstate->trash.delete_nesting;
#else // not 312 or 3.13+
@@ -298,7 +300,7 @@
tstate->delete_later = this->delete_later;
Py_CLEAR(this->delete_later);
}
-
+ tstate->critical_section = this->critical_section;
#elif GREENLET_PY312
tstate->trash.delete_nesting = this->trash_delete_nesting;
@@ -348,21 +350,38 @@
#endif
}
// TODO: Better state management about when we own the top frame.
-int PythonState::tp_traverse(visitproc visit, void* arg, bool own_top_frame)
noexcept
+int PythonState::tp_traverse(visitproc visit, void* arg, bool visit_top_frame)
noexcept
{
Py_VISIT(this->_context.borrow());
- if (own_top_frame) {
+ if (visit_top_frame) {
Py_VISIT(this->_top_frame.borrow());
}
-#if GREENLET_PY314
- // TODO: Should we be visiting the c_stack_refs objects?
- // CPython uses a specific macro to do that which takes into
- // account boxing and null values and then calls
- // ``_PyGC_VisitStackRef``, but we don't have access to that, and
- // we can't duplicate it ourself (because it compares
- // ``visitproc`` to another function we can't access).
- // The naive way of looping over c_stack_refs->ref and visiting
- // those crashes the process (at least with GIL disabled).
+#if GREENLET_PY315
+ // Visit the references held by our suspended frames.
+ // This is important specially on free-threading where the
+ // the suspended frames may contain deferred references to
+ // objects, and if they are not traversed then the interpreter
+ // can free objects early causing a use-after-free crash
+ // at runtime exit.
+ if (this->_top_frame) {
+ for (_PyInterpreterFrame* iframe = this->_top_frame->f_frame;
+ iframe != nullptr; iframe = iframe->previous) {
+ // Skip generator/coroutine frames; their object's traverse
+ // already visits them (gen_traverse), so we'd double-count.
+ // expose_frames leaves them in the ->previous chain.
+ if (iframe->owner != FRAME_OWNED_BY_THREAD) {
+ continue;
+ }
+ Py_VISIT(iframe->frame_obj);
+ Py_VISIT(iframe->f_locals);
+ _Py_VISIT_STACKREF(iframe->f_funcobj);
+ _Py_VISIT_STACKREF(iframe->f_executable);
+ int frame_result = _PyGC_VisitFrameStack(iframe, visit, arg);
+ if (frame_result) {
+ return frame_result;
+ }
+ }
+ }
#endif
// Note that we DO NOT visit ``delete_later``. Even if it's
// non-null and we technically own a reference to it, its
@@ -380,7 +399,36 @@
// 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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/src/greenlet/TUserGreenlet.cpp
new/greenlet-3.5.3/src/greenlet/TUserGreenlet.cpp
--- old/greenlet-3.5.0/src/greenlet/TUserGreenlet.cpp 2026-04-27
14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/src/greenlet/TUserGreenlet.cpp 2026-06-26
20:18:23.000000000 +0200
@@ -422,11 +422,13 @@
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 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;
// The first switch we need to manually call the trace
@@ -467,7 +469,7 @@
// 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 +519,8 @@
}
// 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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/src/greenlet/__init__.py
new/greenlet-3.5.3/src/greenlet/__init__.py
--- old/greenlet-3.5.0/src/greenlet/__init__.py 2026-04-27 14:18:19.000000000
+0200
+++ new/greenlet-3.5.3/src/greenlet/__init__.py 2026-06-26 20:18:23.000000000
+0200
@@ -22,7 +22,7 @@
###
# Metadata
###
-__version__ = '3.5.0'
+__version__ = '3.5.3'
from ._greenlet import _C_API # pylint:disable=no-name-in-module
###
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/src/greenlet/greenlet_msvc_compat.hpp
new/greenlet-3.5.3/src/greenlet/greenlet_msvc_compat.hpp
--- old/greenlet-3.5.0/src/greenlet/greenlet_msvc_compat.hpp 2026-04-27
14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/src/greenlet/greenlet_msvc_compat.hpp 2026-06-26
20:18:23.000000000 +0200
@@ -50,12 +50,34 @@
#else
#define Py_TAG_BITS ((uintptr_t)1)
#define Py_TAG_DEFERRED (1)
+#define Py_INT_TAG 3
#endif
static const _PyStackRef PyStackRef_NULL = { .bits = Py_TAG_DEFERRED};
#define PyStackRef_IsNull(stackref) ((stackref).bits == PyStackRef_NULL.bits)
+static inline bool
+PyStackRef_IsTaggedInt(_PyStackRef i)
+{
+ return (i.bits & Py_INT_TAG) == Py_INT_TAG;
+}
+
+static inline bool
+PyStackRef_IsNullOrInt(_PyStackRef stackref)
+{
+ return PyStackRef_IsNull(stackref) || PyStackRef_IsTaggedInt(stackref);
+}
+
+#define _Py_VISIT_STACKREF(ref) \
+ do { \
+ if (!PyStackRef_IsNullOrInt(ref)) { \
+ int vret = _PyGC_VisitStackRef(&(ref), visit, arg); \
+ if (vret) \
+ return vret; \
+ } \
+ } while (0)
+
static inline PyObject *
PyStackRef_AsPyObjectBorrow(_PyStackRef stackref)
{
@@ -63,8 +85,29 @@
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_IsNull(f->f_executable));
+ assert(!PyStackRef_IsNullOrInt(f->f_executable));
PyObject *executable = PyStackRef_AsPyObjectBorrow(f->f_executable);
assert(PyCode_Check(executable));
return (PyCodeObject *)executable;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/src/greenlet/tests/leakcheck.py
new/greenlet-3.5.3/src/greenlet/tests/leakcheck.py
--- old/greenlet-3.5.0/src/greenlet/tests/leakcheck.py 2026-04-27
14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/src/greenlet/tests/leakcheck.py 2026-06-26
20:18:23.000000000 +0200
@@ -86,6 +86,21 @@
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.
@@ -95,6 +110,14 @@
func = unittest.skip("Skipping known failures")(func)
return func
+def fails_leakcheck_on_py314_or_less(func):
+ """
+ Mark the function as known to leak (fails refcount leakchecks) on Python
3.14 or less.
+ """
+ if sys.version_info[:2] <= (3, 14):
+ return fails_leakcheck(func)
+ return func
+
class LeakCheckError(AssertionError):
pass
@@ -321,6 +344,12 @@
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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/src/greenlet/tests/test_gc.py
new/greenlet-3.5.3/src/greenlet/tests/test_gc.py
--- old/greenlet-3.5.0/src/greenlet/tests/test_gc.py 2026-04-27
14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/src/greenlet/tests/test_gc.py 2026-06-26
20:18:23.000000000 +0200
@@ -1,12 +1,12 @@
import gc
import weakref
-
+import sys
import greenlet
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
@@ -16,7 +16,6 @@
o = weakref.ref(greenlet.greenlet(greenlet.getcurrent).switch())
gc.collect()
if o() is not None:
- import sys
print("O IS NOT NONE.", sys.getrefcount(o()))
self.assertIsNone(o())
self.assertFalse(gc.garbage, gc.garbage)
@@ -44,7 +43,7 @@
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
@@ -84,3 +83,55 @@
del g
greenlet.getcurrent()
gc.collect()
+
+ def test_crashing_deferred_object(self):
+ if sys.version_info < (3, 15):
+ self.skipTest("Test is 3.15+ only")
+ import doctest
+ def with_doctest():
+ """
+ >>> import gc
+ >>> from greenlet import getcurrent, greenlet, GreenletExit
+ >>> def outer():
+ ... gc.collect()
+ >>> outer_glet = greenlet(outer)
+ >>> outer_glet.switch()
+ """
+ doctest.run_docstring_examples(with_doctest, dict())
+
+ def test_cycle_in_suspended_frame(self):
+ if sys.version_info < (3, 15):
+ self.skipTest("Test is 3.15+ only")
+ import doctest
+ def with_doctest():
+ """
+ >>> import gc
+ >>> from greenlet import getcurrent, greenlet
+ >>> class Cycle:
+ ... def __del__(self):
+ ... print("(Running finalizer)")
+ >>> def collect_it():
+ ... print("Collecting garbage")
+ ... gc.collect()
+ >>> def inner():
+ ... cycle1 = Cycle()
+ ... cycle2 = Cycle()
+ ... cycle1.cycle = cycle2
+ ... cycle2.cycle = cycle1
+ ... getcurrent().parent.switch()
+ >>> def outer():
+ ... glet = greenlet(inner)
+ ... glet.switch()
+ ... collect_it()
+
+ >>> outer_glet = greenlet(outer)
+ >>> outer_glet.switch()
+ Collecting garbage
+ >>> outer_glet.dead
+ True
+ >>> collect_it()
+ Collecting garbage
+ (Running finalizer)
+ (Running finalizer)
+ """
+ doctest.run_docstring_examples(with_doctest, dict())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/src/greenlet/tests/test_greenlet.py
new/greenlet-3.5.3/src/greenlet/tests/test_greenlet.py
--- old/greenlet-3.5.0/src/greenlet/tests/test_greenlet.py 2026-04-27
14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/src/greenlet/tests/test_greenlet.py 2026-06-26
20:18:23.000000000 +0200
@@ -13,9 +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
@@ -506,7 +509,7 @@
g = mygreenlet(lambda: None)
self.assertRaises(SomeError, g.throw, SomeError())
- @fails_leakcheck
+ @fails_leakcheck_on_py314_or_less
def _do_test_throw_to_dead_thread_doesnt_crash(self,
wait_for_cleanup=False):
result = []
def worker():
@@ -564,7 +567,11 @@
# See issue 252
self.expect_greenlet_leak = True # direct us not to wait for it to go
away
- @fails_leakcheck
+ @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()
@@ -1020,6 +1027,46 @@
# The next line crashes on 3.12 if we haven't exposed the frames.
self.assertIsNone(frame.f_back)
+ def test_switching_holding_critical_section_no_crash(self):
+ # https://github.com/python-greenlet/greenlet/issues/513
+ # In no-GIL builds, we were failing to restore the
+ # critical_section pointer, leading to
+ # ``Fatal Python error: PyMutex_Unlock: unlocking mutex that is not
locked``
+ # when two greenlets both held a lock on an object, and then
+ # the GIL was released, which, according to
+ # ``Include/cpython/critical_section.h`` causes the critical
+ # section to get released.
+ # The field is present on Python 3.13+, and only used
+ # in no-GIL builds.
+ # Initial version of this test case provided in the
+ # github issue by ddorian
+ def k1(x):
+ g2.switch() # into G2 while holding G1's sort critical section
+ return x
+
+
+ def k2(x):
+ g1.switch() # back into G1 while holding G2's sort critical
section
+ return x
+
+
+ def g1_body():
+ sorted([0], key=k1)
+ g2.switch()
+
+
+ def g2_body():
+ sorted([0], key=k2)
+ # Do a blocking I/O operation which would cause the
+ # critical sections to get suspended
+ with open(__file__, "rt", encoding='utf-8') as f:
+ f.read()
+
+
+ g1 = greenlet.greenlet(g1_body)
+ g2 = greenlet.greenlet(g2_body)
+ g1.switch()
+
class TestGreenletSetParentErrors(TestCase):
def test_threaded_reparent(self):
@@ -1301,6 +1348,15 @@
self._check_current_is_main()
self.assertIsInstance(greenlet.getcurrent(), RawGreenlet)
+ def test_greenlet_run(self):
+ def do_it():
+ with self.assertRaises(AttributeError):
+ getattr(g, 'run')
+
+ g = greenlet.greenlet(do_it)
+ g.switch()
+ with self.assertRaises(AttributeError):
+ getattr(g, 'run')
class TestBrokenGreenlets(TestCase):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/greenlet-3.5.0/src/greenlet/tests/test_interpreter_shutdown.py
new/greenlet-3.5.3/src/greenlet/tests/test_interpreter_shutdown.py
--- old/greenlet-3.5.0/src/greenlet/tests/test_interpreter_shutdown.py
2026-04-27 14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/src/greenlet/tests/test_interpreter_shutdown.py
2026-06-26 20:18:23.000000000 +0200
@@ -532,7 +532,7 @@
# -----------------------------------------------------------------
def test_getcurrent_returns_none_during_gc_finalization(self):
- # greenlet.getcurrent() must return None when called from a
+ # greenlet.getcurrent() must raise an exception when called from a
# __del__ method during Py_FinalizeEx's GC collection pass.
# On Python >= 3.11, _Py_IsFinalizing() is True during this
@@ -549,8 +549,9 @@
class CleanupChecker:
def __del__(self):
try:
- cur = greenlet.getcurrent()
- if cur is None:
+ try:
+ greenlet.getcurrent()
+ except RuntimeError:
os.write(1, b"GUARDED: getcurrent=None\\n")
else:
os.write(1, b"UNGUARDED: getcurrent="
@@ -568,9 +569,7 @@
""")
self.assertEqual(rc, 0, f"Process crashed
(rc={rc}):\n{stdout}{stderr}")
self.assertIn("OK: deferred cycle created", stdout)
- self.assertIn("GUARDED: getcurrent=None", stdout,
- "getcurrent() must return None during GC finalization; "
- "returned a live object instead (missing Py_IsFinalizing
guard)")
+ self.assertIn("GUARDED: getcurrent=None", stdout)
def
test_getcurrent_returns_none_during_gc_finalization_with_active_greenlets(self):
# Same as above but with active greenlets at shutdown, which
@@ -586,8 +585,9 @@
class CleanupChecker:
def __del__(self):
try:
- cur = greenlet.getcurrent()
- if cur is None:
+ try:
+ greenlet.getcurrent()
+ except RuntimeError:
os.write(1, b"GUARDED: getcurrent=None\\n")
else:
os.write(1, b"UNGUARDED: getcurrent="
@@ -614,9 +614,7 @@
""")
self.assertEqual(rc, 0, f"Process crashed
(rc={rc}):\n{stdout}{stderr}")
self.assertIn("OK: 10 active greenlets, cycle deferred", stdout)
- self.assertIn("GUARDED: getcurrent=None", stdout,
- "getcurrent() must return None during GC finalization; "
- "returned a live object instead (missing Py_IsFinalizing
guard)")
+ self.assertIn("GUARDED: getcurrent=None", stdout)
def test_getcurrent_returns_none_during_gc_finalization_cross_thread(self):
# Combines cross-thread greenlet deallocation (deleteme list)
@@ -636,8 +634,9 @@
class CleanupChecker:
def __del__(self):
try:
- cur = greenlet.getcurrent()
- if cur is None:
+ try:
+ greenlet.getcurrent()
+ except RuntimeError:
os.write(1, b"GUARDED: getcurrent=None\\n")
else:
os.write(1, b"UNGUARDED: getcurrent="
@@ -669,9 +668,7 @@
""")
self.assertEqual(rc, 0, f"Process crashed
(rc={rc}):\n{stdout}{stderr}")
self.assertIn("OK: cross-thread cleanup + cycle deferred", stdout)
- self.assertIn("GUARDED: getcurrent=None", stdout,
- "getcurrent() must return None during GC finalization; "
- "returned a live object instead (missing Py_IsFinalizing
guard)")
+ self.assertIn("GUARDED: getcurrent=None", stdout)
# -----------------------------------------------------------------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/src/greenlet/tests/test_leaks.py
new/greenlet-3.5.3/src/greenlet/tests/test_leaks.py
--- old/greenlet-3.5.0/src/greenlet/tests/test_leaks.py 2026-04-27
14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/src/greenlet/tests/test_leaks.py 2026-06-26
20:18:23.000000000 +0200
@@ -445,9 +445,28 @@
# due to working set trimming, page table updates, etc. Allow a
# small tolerance so OS-level noise doesn't cause false failures.
# Real leaks produce MBs of growth (each iteration creates 20k
- # greenlets), so 512 KB is well below the detection threshold for
- # genuine issues.
- tolerance = 512 * 1024 if WIN else 0
+ # greenlets; the greenlet Python object alone, not counting the C++
+ # state it points to, is 56 bytes so 20k greenlets alone is
+ # slightly over a megabyte; the C++ UserGreenlet clocks in at 208
+ # bytes, which adds another 4MB), so 512 KB is well below the detection
+ # threshold for genuine issues.
+ #
+ # 2025-06-16: We have now observed the same problem on both
+ # ubuntu-latest 3.14t (2948K "leak") and macos-latest 3.15t (192K
+ # "leak"). This was after the merge of PR #511, but that change
+ # should be unrelated. In particular, the ubuntu failure was after
+ # 30 loops and the macos failure was after only 3 --- both much
+ # smaller than UNTRACK_ATTEMPTS=100! We break out of the loop if we
+ # see a USS at the end of the loop that's smaller than at the start
+ # of the loop, and then we wait for pending cleanups and get the
+ # USS again. So this means that we think we have success, break out
+ # of the loop, and the pending cleanups cause our USS to grow -
+ # perhaps by touching pages that had been paged out? An immediately
+ # prior attempt of both platforms had been successful.
+ #
+ # At any rate, we need a larger tolerance than 512K, and we
+ # need it on all platforms, not just Windows.
+ tolerance = 3 * 1024 * 1024
self.assertLessEqual(uss_after, uss_before + tolerance,
"after attempts %d" % (count,))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/src/greenlet.egg-info/PKG-INFO
new/greenlet-3.5.3/src/greenlet.egg-info/PKG-INFO
--- old/greenlet-3.5.0/src/greenlet.egg-info/PKG-INFO 2026-04-27
14:18:23.000000000 +0200
+++ new/greenlet-3.5.3/src/greenlet.egg-info/PKG-INFO 2026-06-26
20:18:27.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: greenlet
-Version: 3.5.0
+Version: 3.5.3
Summary: Lightweight in-process concurrent programming
Author-email: Alexey Borzenkov <[email protected]>
Maintainer-email: Jason Madden <[email protected]>
@@ -22,6 +22,7 @@
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
+Classifier: Programming Language :: Python :: 3.15
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/greenlet-3.5.0/tox.ini new/greenlet-3.5.3/tox.ini
--- old/greenlet-3.5.0/tox.ini 2026-04-27 14:18:19.000000000 +0200
+++ new/greenlet-3.5.3/tox.ini 2026-06-26 20:18:23.000000000 +0200
@@ -1,6 +1,6 @@
[tox]
envlist =
- py{310,311,312,313,314},docs,py314t,tsan-314,tsan-314t
+ py{310,311,312,313,314,315},docs,py314t,py315t,tsan-314,tsan-314t
[testenv]
commands =