Skip to content

Commit 9718ce5

Browse files
committed
Add Py 3.15; make both API versions of getcurrent() consistent in raising RuntimeError during shutdown.
1 parent 276e08a commit 9718ce5

7 files changed

Lines changed: 32 additions & 21 deletions

File tree

.github/workflows/tests.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ jobs:
2929
- "3.13"
3030
- "3.14"
3131
- "3.14t"
32+
- "3.15-dev"
33+
- "3.15t-dev"
3234

3335
# Recall the macOS and windows builds upload built wheels so all supported versions
3436
# need to run on mac.
@@ -99,7 +101,8 @@ jobs:
99101
ARCHFLAGS: "-arch x86_64 -arch arm64"
100102

101103
- name: Check greenlet build
102-
if: ${{ ! startsWith(runner.os, 'Windows') }}
104+
# 3.15b1 has a problem with readme renderer, ModuleNotFoundError: No module named 'nh3.nh3'
105+
if: ${{ !startsWith(runner.os, 'Windows') && !endsWith(matrix.python-version, '-dev')}}
103106
run: |
104107
ls -l dist
105108
twine check dist/*

CHANGES.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@
55
3.5.1 (unreleased)
66
==================
77

8-
- Nothing changed yet.
8+
- Add preliminary support for Python 3.15b1. This has not been
9+
reviewed by CPython core developers, but all tests pass. Binary
10+
wheels of this version won't work on earlier Python 3.15 builds and
11+
may not work on later 3.15 builds.
12+
- Fix the discrepancy in the way the two ``getcurrent`` APIs behave
13+
during greenlet teardown. One API (the C API used by, e.g., gevent) raised a
14+
``RuntimeError``; the other (the Python ``greenlet.getcurrent`` API)
15+
returned ``None``. This second way breaks greenlet's type
16+
annotations, so ``greenlet.getcurrent`` now raises a
17+
``RuntimeError`` as well.
918

1019

1120
3.5.0 (2026-04-27)

make-manylinux

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ if [ -d /greenlet -a -d /opt/python ]; then
4141
which auditwheel
4242
echo "Installed Python versions"
4343
ls -l /opt/python
44-
for variant in /opt/python/cp{314,313,312,311,310}*; do
44+
for variant in /opt/python/cp{315,314,313,312,311,310}*; do
4545
if [[ "$variant" == *3t ]]; then
4646
echo "Skipping no-gil build for 3.13; only 3.14 is fully supported."
4747
continue

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ classifiers = [
3131
"Programming Language :: Python :: 3.12",
3232
"Programming Language :: Python :: 3.13",
3333
"Programming Language :: Python :: 3.14",
34+
"Programming Language :: Python :: 3.15",
3435
"Operating System :: OS Independent",
3536
"Topic :: Software Development :: Libraries :: Python Modules",
3637
]

src/greenlet/PyModule.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ static PyObject*
2828
mod_getcurrent(PyObject* UNUSED(module))
2929
{
3030
if (greenlet::IsShuttingDown()) {
31-
Py_RETURN_NONE;
31+
PyErr_SetString(PyExc_RuntimeError, "greenlet is being finalized");
32+
return nullptr;
3233
}
3334
return GET_THREAD_STATE().state().get_current().relinquish_ownership_o();
3435
}

src/greenlet/tests/test_interpreter_shutdown.py

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,7 @@ def create_at_exit():
532532
# -----------------------------------------------------------------
533533

534534
def test_getcurrent_returns_none_during_gc_finalization(self):
535-
# greenlet.getcurrent() must return None when called from a
535+
# greenlet.getcurrent() must raise an exception when called from a
536536
# __del__ method during Py_FinalizeEx's GC collection pass.
537537

538538
# On Python >= 3.11, _Py_IsFinalizing() is True during this
@@ -549,8 +549,9 @@ def test_getcurrent_returns_none_during_gc_finalization(self):
549549
class CleanupChecker:
550550
def __del__(self):
551551
try:
552-
cur = greenlet.getcurrent()
553-
if cur is None:
552+
try:
553+
greenlet.getcurrent()
554+
except RuntimeError:
554555
os.write(1, b"GUARDED: getcurrent=None\\n")
555556
else:
556557
os.write(1, b"UNGUARDED: getcurrent="
@@ -568,9 +569,7 @@ def __del__(self):
568569
""")
569570
self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
570571
self.assertIn("OK: deferred cycle created", stdout)
571-
self.assertIn("GUARDED: getcurrent=None", stdout,
572-
"getcurrent() must return None during GC finalization; "
573-
"returned a live object instead (missing Py_IsFinalizing guard)")
572+
self.assertIn("GUARDED: getcurrent=None", stdout)
574573

575574
def test_getcurrent_returns_none_during_gc_finalization_with_active_greenlets(self):
576575
# 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
586585
class CleanupChecker:
587586
def __del__(self):
588587
try:
589-
cur = greenlet.getcurrent()
590-
if cur is None:
588+
try:
589+
greenlet.getcurrent()
590+
except RuntimeError:
591591
os.write(1, b"GUARDED: getcurrent=None\\n")
592592
else:
593593
os.write(1, b"UNGUARDED: getcurrent="
@@ -614,9 +614,7 @@ def worker():
614614
""")
615615
self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
616616
self.assertIn("OK: 10 active greenlets, cycle deferred", stdout)
617-
self.assertIn("GUARDED: getcurrent=None", stdout,
618-
"getcurrent() must return None during GC finalization; "
619-
"returned a live object instead (missing Py_IsFinalizing guard)")
617+
self.assertIn("GUARDED: getcurrent=None", stdout)
620618

621619
def test_getcurrent_returns_none_during_gc_finalization_cross_thread(self):
622620
# Combines cross-thread greenlet deallocation (deleteme list)
@@ -636,8 +634,9 @@ def test_getcurrent_returns_none_during_gc_finalization_cross_thread(self):
636634
class CleanupChecker:
637635
def __del__(self):
638636
try:
639-
cur = greenlet.getcurrent()
640-
if cur is None:
637+
try:
638+
greenlet.getcurrent()
639+
except RuntimeError:
641640
os.write(1, b"GUARDED: getcurrent=None\\n")
642641
else:
643642
os.write(1, b"UNGUARDED: getcurrent="
@@ -669,9 +668,7 @@ def body():
669668
""")
670669
self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
671670
self.assertIn("OK: cross-thread cleanup + cycle deferred", stdout)
672-
self.assertIn("GUARDED: getcurrent=None", stdout,
673-
"getcurrent() must return None during GC finalization; "
674-
"returned a live object instead (missing Py_IsFinalizing guard)")
671+
self.assertIn("GUARDED: getcurrent=None", stdout)
675672

676673

677674
# -----------------------------------------------------------------

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tox]
22
envlist =
3-
py{310,311,312,313,314},docs,py314t,tsan-314,tsan-314t
3+
py{310,311,312,313,314,315},docs,py314t,py315t,tsan-314,tsan-314t
44

55
[testenv]
66
commands =

0 commit comments

Comments
 (0)