From 9718ce5a23ea3360232b78a806a837d6c3d6183d Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Wed, 20 May 2026 06:11:48 -0500 Subject: [PATCH] Add Py 3.15; make both API versions of getcurrent() consistent in raising RuntimeError during shutdown. --- .github/workflows/tests.yml | 5 +++- CHANGES.rst | 11 ++++++- make-manylinux | 2 +- pyproject.toml | 1 + src/greenlet/PyModule.cpp | 3 +- .../tests/test_interpreter_shutdown.py | 29 +++++++++---------- tox.ini | 2 +- 7 files changed, 32 insertions(+), 21 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 29a1f0d2..d9c7551a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,6 +29,8 @@ jobs: - "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 +101,8 @@ jobs: 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/* diff --git a/CHANGES.rst b/CHANGES.rst index d2ee90e1..2053e5a1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,16 @@ 3.5.1 (unreleased) ================== -- Nothing changed yet. +- 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 breaks greenlet's type + annotations, so ``greenlet.getcurrent`` now raises a + ``RuntimeError`` as well. 3.5.0 (2026-04-27) diff --git a/make-manylinux b/make-manylinux index 904e9c9c..5b689833 100755 --- a/make-manylinux +++ b/make-manylinux @@ -41,7 +41,7 @@ if [ -d /greenlet -a -d /opt/python ]; then 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 diff --git a/pyproject.toml b/pyproject.toml index 8f781ab2..2f77ada8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ classifiers = [ "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 --git a/src/greenlet/PyModule.cpp b/src/greenlet/PyModule.cpp index cde2c85b..f6190206 100644 --- a/src/greenlet/PyModule.cpp +++ b/src/greenlet/PyModule.cpp @@ -28,7 +28,8 @@ static PyObject* 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 --git a/src/greenlet/tests/test_interpreter_shutdown.py b/src/greenlet/tests/test_interpreter_shutdown.py index 5ac5ab5e..6cc0d77a 100644 --- a/src/greenlet/tests/test_interpreter_shutdown.py +++ b/src/greenlet/tests/test_interpreter_shutdown.py @@ -532,7 +532,7 @@ def create_at_exit(): # ----------------------------------------------------------------- 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 @@ def test_getcurrent_returns_none_during_gc_finalization(self): 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 @@ def __del__(self): """) 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 @@ def test_getcurrent_returns_none_during_gc_finalization_with_active_greenlets(se 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 @@ def worker(): """) 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 @@ def test_getcurrent_returns_none_during_gc_finalization_cross_thread(self): 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 @@ def body(): """) 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 --git a/tox.ini b/tox.ini index 3b510703..04fc2e71 100644 --- a/tox.ini +++ b/tox.ini @@ -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 =