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 =

Reply via email to