Skip to content

Commit 4d48d10

Browse files
stefanor4383itamarst
authored
Python 3.13 Support (#988)
* reorder python jobs by python versions * Supporting Python 3.13 * Fix YAML syntax * Explicit Python 3.13 version until it's out * fix typo * Use latest 3.13 prerelease * Python 3.13 support Emulate Python 3.13's start_joinable_thread API using greenthreads. We cut some corners, of course: * We aren't maintaining a table of green thread idents to threads, so we can't wait for all threads on shutdown. * Our _make_thread_handle() can only make a handle for the current thread (as we don't have a way to look up green threads by ident). * .join() on a non-GreenThread (e.g. the main thread) just returns immediately. Fixes: #964 * _tstate_lock was removed in Python 3.13 In python/cpython#114271, _tstate_lock was replaced with an event on PyThreadState. * Add Python 3.13 to tox --------- Co-authored-by: Hervé Beraud <hberaud@redhat.com> Co-authored-by: Itamar Turner-Trauring <itamar@itamarst.org>
1 parent 23c1f36 commit 4d48d10

6 files changed

Lines changed: 79 additions & 18 deletions

File tree

‎.github/workflows/test.yaml‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,28 @@ jobs:
3030
matrix:
3131
include:
3232
- { py: 3.7, toxenv: py37-epolls, ignore-error: false, os: ubuntu-latest }
33+
- { py: 3.7, toxenv: py37-asyncio, ignore-error: false, os: ubuntu-latest }
3334
- { py: 3.8, toxenv: py38-epolls, ignore-error: false, os: ubuntu-latest }
3435
- { py: 3.8, toxenv: py38-openssl, ignore-error: false, os: ubuntu-latest }
3536
- { py: 3.8, toxenv: py38-poll, ignore-error: false, os: ubuntu-latest }
3637
- { py: 3.8, toxenv: py38-selects, ignore-error: false, os: ubuntu-latest }
38+
- { py: 3.8, toxenv: py38-asyncio, ignore-error: false, os: ubuntu-latest }
3739
- { py: 3.9, toxenv: py39-epolls, ignore-error: false, os: ubuntu-latest }
3840
- { py: 3.9, toxenv: py39-poll, ignore-error: false, os: ubuntu-latest }
3941
- { py: 3.9, toxenv: py39-selects, ignore-error: false, os: ubuntu-latest }
4042
- { py: 3.9, toxenv: py39-dnspython1, ignore-error: false, os: ubuntu-latest }
43+
- { py: 3.9, toxenv: py39-asyncio, ignore-error: false, os: ubuntu-latest }
4144
- { py: "3.10", toxenv: py310-epolls, ignore-error: false, os: ubuntu-latest }
4245
- { py: "3.10", toxenv: py310-poll, ignore-error: false, os: ubuntu-latest }
4346
- { py: "3.10", toxenv: py310-selects, ignore-error: false, os: ubuntu-latest }
4447
- { py: "3.10", toxenv: ipv6, ignore-error: false, os: ubuntu-latest }
45-
- { py: "3.11", toxenv: py311-epolls, ignore-error: false, os: ubuntu-latest }
46-
- { py: "3.12", toxenv: py312-epolls, ignore-error: false, os: ubuntu-latest }
47-
- { py: "3.7", toxenv: py37-asyncio, ignore-error: false, os: ubuntu-latest }
48-
- { py: "3.8", toxenv: py38-asyncio, ignore-error: false, os: ubuntu-latest }
49-
- { py: "3.9", toxenv: py39-asyncio, ignore-error: false, os: ubuntu-latest }
5048
- { py: "3.10", toxenv: py310-asyncio, ignore-error: false, os: ubuntu-latest }
49+
- { py: "3.11", toxenv: py311-epolls, ignore-error: false, os: ubuntu-latest }
5150
- { py: "3.11", toxenv: py311-asyncio, ignore-error: false, os: ubuntu-latest }
51+
- { py: "3.12", toxenv: py312-epolls, ignore-error: false, os: ubuntu-latest }
5252
- { py: "3.12", toxenv: py312-asyncio, ignore-error: false, os: ubuntu-latest }
53+
- { py: "3.13-dev", toxenv: py313-epolls, ignore-error: false, os: ubuntu-24.04 }
54+
- { py: "3.13-dev", toxenv: py313-asyncio, ignore-error: false, os: ubuntu-24.04 }
5355
- { py: pypy3.9, toxenv: pypy3-epolls, ignore-error: true, os: ubuntu-20.04 }
5456

5557
steps:
@@ -98,6 +100,7 @@ jobs:
98100
matrix:
99101
include:
100102
- { py: "3.12", toxenv: py312-asyncio, ignore-error: false, os: macos-latest }
103+
- { py: "3.13-dev", toxenv: py313-asyncio, ignore-error: false, os: macos-latest }
101104
# This isn't working very well at the moment, but that might just be
102105
# tox config? In any case main focus is on asyncio so someone can
103106
# revisit separately.

‎README.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,4 @@ The built html files can be found in doc/build/html afterward.
8585
Supported Python versions
8686
=========================
8787

88-
Python 3.7-3.12 are currently supported.
88+
Python 3.7-3.13 are currently supported.

‎eventlet/green/thread.py‎

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
import _thread as __thread
33
from eventlet.support import greenlets as greenlet
44
from eventlet import greenthread
5+
from eventlet.timeout import with_timeout
56
from eventlet.lock import Lock
67
import sys
78

89

9-
__patched__ = ['get_ident', 'start_new_thread', 'start_new', 'allocate_lock',
10-
'allocate', 'exit', 'interrupt_main', 'stack_size', '_local',
11-
'LockType', 'Lock', '_count']
10+
__patched__ = ['Lock', 'LockType', '_ThreadHandle', '_count',
11+
'_get_main_thread_ident', '_local', '_make_thread_handle',
12+
'allocate', 'allocate_lock', 'exit', 'get_ident',
13+
'interrupt_main', 'stack_size', 'start_joinable_thread',
14+
'start_new', 'start_new_thread']
1215

1316
error = __thread.error
1417
LockType = Lock
@@ -47,11 +50,40 @@ def __thread_body(func, args, kwargs):
4750
__threadcount -= 1
4851

4952

50-
def start_new_thread(function, args=(), kwargs=None):
51-
if (sys.version_info >= (3, 4)
53+
class _ThreadHandle:
54+
def __init__(self, greenthread=None):
55+
self._greenthread = greenthread
56+
self._done = False
57+
58+
def _set_done(self):
59+
self._done = True
60+
61+
def is_done(self):
62+
return self._done
63+
64+
@property
65+
def ident(self):
66+
return get_ident(self._greenthread)
67+
68+
def join(self, timeout=None):
69+
if not hasattr(self._greenthread, "wait"):
70+
return
71+
if timeout is not None:
72+
return with_timeout(timeout, self._greenthread.wait)
73+
return self._greenthread.wait()
74+
75+
76+
def _make_thread_handle(ident):
77+
greenthread = greenlet.getcurrent()
78+
assert ident == get_ident(greenthread)
79+
return _ThreadHandle(greenthread=greenthread)
80+
81+
82+
def __spawn_green(function, args=(), kwargs=None, joinable=False):
83+
if ((3, 4) <= sys.version_info < (3, 13)
5284
and getattr(function, '__module__', '') == 'threading'
5385
and hasattr(function, '__self__')):
54-
# Since Python 3.4, threading.Thread uses an internal lock
86+
# In Python 3.4-3.12, threading.Thread uses an internal lock
5587
# automatically released when the python thread state is deleted.
5688
# With monkey patching, eventlet uses green threads without python
5789
# thread state, so the lock is not automatically released.
@@ -66,19 +98,40 @@ def wrap_bootstrap_inner():
6698
bootstrap_inner()
6799
finally:
68100
# The lock can be cleared (ex: by a fork())
69-
if thread._tstate_lock is not None:
101+
if getattr(thread, "_tstate_lock", None) is not None:
70102
thread._tstate_lock.release()
71103

72104
thread._bootstrap_inner = wrap_bootstrap_inner
73105

74106
kwargs = kwargs or {}
75-
g = greenthread.spawn_n(__thread_body, function, args, kwargs)
107+
spawn_func = greenthread.spawn if joinable else greenthread.spawn_n
108+
return spawn_func(__thread_body, function, args, kwargs)
109+
110+
111+
def start_joinable_thread(function, handle=None, daemon=True):
112+
g = __spawn_green(function, joinable=True)
113+
if handle is None:
114+
handle = _ThreadHandle(greenthread=g)
115+
else:
116+
handle._greenthread = g
117+
return handle
118+
119+
120+
def start_new_thread(function, args=(), kwargs=None):
121+
g = __spawn_green(function, args=args, kwargs=kwargs)
76122
return get_ident(g)
77123

78124

79125
start_new = start_new_thread
80126

81127

128+
def _get_main_thread_ident():
129+
greenthread = greenlet.getcurrent()
130+
while greenthread.parent is not None:
131+
greenthread = greenthread.parent
132+
return get_ident(greenthread)
133+
134+
82135
def allocate_lock(*a):
83136
return LockType(1)
84137

@@ -118,3 +171,6 @@ def stack_size(size=None):
118171

119172
if hasattr(__thread, 'daemon_threads_allowed'):
120173
daemon_threads_allowed = __thread.daemon_threads_allowed
174+
175+
if hasattr(__thread, '_shutdown'):
176+
_shutdown = __thread._shutdown

‎eventlet/green/threading.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
from eventlet.green import time
55
from eventlet.support import greenlets as greenlet
66

7-
__patched__ = ['_start_new_thread', '_allocate_lock',
8-
'_sleep', 'local', 'stack_size', 'Lock', 'currentThread',
9-
'current_thread', '_after_fork', '_shutdown']
7+
__patched__ = ['Lock', '_after_fork', '_allocate_lock', '_get_main_thread_ident',
8+
'_make_thread_handle', '_shutdown', '_sleep',
9+
'_start_joinable_thread', '_start_new_thread', '_ThreadHandle',
10+
'currentThread', 'current_thread', 'local', 'stack_size']
1011

1112
__patched__ += ['get_ident', '_set_sentinel']
1213

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ classifiers = [
3333
"Programming Language :: Python :: 3.10",
3434
"Programming Language :: Python :: 3.11",
3535
"Programming Language :: Python :: 3.12",
36+
"Programming Language :: Python :: 3.13",
3637
"Programming Language :: Python",
3738
"Topic :: Internet",
3839
"Topic :: Software Development :: Libraries :: Python Modules",

‎tox.ini‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ envlist =
1616
py38-openssl
1717
py39-dnspython1
1818
pypy3-epolls
19-
py{38,39,310,311,312}-{selects,poll,epolls,asyncio}
19+
py{38,39,310,311,312,313}-{selects,poll,epolls,asyncio}
2020
skipsdist = True
2121

2222
[testenv:ipv6]

0 commit comments

Comments
 (0)