Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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/*
Expand Down
11 changes: 10 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion make-manylinux
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
3 changes: 2 additions & 1 deletion src/greenlet/PyModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
29 changes: 13 additions & 16 deletions src/greenlet/tests/test_interpreter_shutdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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="
Expand All @@ -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
Expand All @@ -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="
Expand All @@ -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)
Expand All @@ -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="
Expand Down Expand Up @@ -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)


# -----------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -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 =
Expand Down
Loading