Skip to content

Commit e80abd5

Browse files
gh-76785: Update test.support.interpreters to Align With PEP 734 (gh-115566)
This brings the code under test.support.interpreters, and the corresponding extension modules, in line with recent updates to PEP 734. (Note: PEP 734 has not been accepted at this time. However, we are using an internal copy of the implementation in the test suite to exercise the existing subinterpreters feature.)
1 parent 67c19e5 commit e80abd5

File tree

11 files changed

+624
-157
lines changed

11 files changed

+624
-157
lines changed

Lib/test/support/interpreters/__init__.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@
66

77
# aliases:
88
from _xxsubinterpreters import (
9-
InterpreterError, InterpreterNotFoundError,
9+
InterpreterError, InterpreterNotFoundError, NotShareableError,
1010
is_shareable,
1111
)
1212

1313

1414
__all__ = [
1515
'get_current', 'get_main', 'create', 'list_all', 'is_shareable',
1616
'Interpreter',
17-
'InterpreterError', 'InterpreterNotFoundError', 'ExecFailure',
17+
'InterpreterError', 'InterpreterNotFoundError', 'ExecutionFailed',
18+
'NotShareableError',
1819
'create_queue', 'Queue', 'QueueEmpty', 'QueueFull',
1920
]
2021

@@ -42,7 +43,11 @@ def __getattr__(name):
4243
{formatted}
4344
""".strip()
4445

45-
class ExecFailure(RuntimeError):
46+
class ExecutionFailed(RuntimeError):
47+
"""An unhandled exception happened during execution.
48+
49+
This is raised from Interpreter.exec() and Interpreter.call().
50+
"""
4651

4752
def __init__(self, excinfo):
4853
msg = excinfo.formatted
@@ -157,7 +162,7 @@ def prepare_main(self, ns=None, /, **kwargs):
157162
ns = dict(ns, **kwargs) if ns is not None else kwargs
158163
_interpreters.set___main___attrs(self._id, ns)
159164

160-
def exec_sync(self, code, /):
165+
def exec(self, code, /):
161166
"""Run the given source code in the interpreter.
162167
163168
This is essentially the same as calling the builtin "exec"
@@ -166,22 +171,46 @@ def exec_sync(self, code, /):
166171
167172
There is no return value.
168173
169-
If the code raises an unhandled exception then an ExecFailure
170-
is raised, which summarizes the unhandled exception. The actual
171-
exception is discarded because objects cannot be shared between
172-
interpreters.
174+
If the code raises an unhandled exception then an ExecutionFailed
175+
exception is raised, which summarizes the unhandled exception.
176+
The actual exception is discarded because objects cannot be
177+
shared between interpreters.
173178
174179
This blocks the current Python thread until done. During
175180
that time, the previous interpreter is allowed to run
176181
in other threads.
177182
"""
178183
excinfo = _interpreters.exec(self._id, code)
179184
if excinfo is not None:
180-
raise ExecFailure(excinfo)
185+
raise ExecutionFailed(excinfo)
186+
187+
def call(self, callable, /):
188+
"""Call the object in the interpreter with given args/kwargs.
189+
190+
Only functions that take no arguments and have no closure
191+
are supported.
181192
182-
def run(self, code, /):
193+
The return value is discarded.
194+
195+
If the callable raises an exception then the error display
196+
(including full traceback) is send back between the interpreters
197+
and an ExecutionFailed exception is raised, much like what
198+
happens with Interpreter.exec().
199+
"""
200+
# XXX Support args and kwargs.
201+
# XXX Support arbitrary callables.
202+
# XXX Support returning the return value (e.g. via pickle).
203+
excinfo = _interpreters.call(self._id, callable)
204+
if excinfo is not None:
205+
raise ExecutionFailed(excinfo)
206+
207+
def call_in_thread(self, callable, /):
208+
"""Return a new thread that calls the object in the interpreter.
209+
210+
The return value and any raised exception are discarded.
211+
"""
183212
def task():
184-
self.exec_sync(code)
213+
self.call(callable)
185214
t = threading.Thread(target=task)
186215
t.start()
187216
return t

Lib/test/support/interpreters/queues.py

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Cross-interpreter Queues High Level Module."""
22

3+
import pickle
34
import queue
45
import time
56
import weakref
@@ -31,38 +32,47 @@ class QueueFull(_queues.QueueFull, queue.Full):
3132
"""
3233

3334

34-
def create(maxsize=0):
35+
_SHARED_ONLY = 0
36+
_PICKLED = 1
37+
38+
def create(maxsize=0, *, syncobj=False):
3539
"""Return a new cross-interpreter queue.
3640
3741
The queue may be used to pass data safely between interpreters.
42+
43+
"syncobj" sets the default for Queue.put()
44+
and Queue.put_nowait().
3845
"""
39-
qid = _queues.create(maxsize)
40-
return Queue(qid)
46+
fmt = _SHARED_ONLY if syncobj else _PICKLED
47+
qid = _queues.create(maxsize, fmt)
48+
return Queue(qid, _fmt=fmt)
4149

4250

4351
def list_all():
4452
"""Return a list of all open queues."""
45-
return [Queue(qid)
46-
for qid in _queues.list_all()]
47-
53+
return [Queue(qid, _fmt=fmt)
54+
for qid, fmt in _queues.list_all()]
4855

4956

5057
_known_queues = weakref.WeakValueDictionary()
5158

5259
class Queue:
5360
"""A cross-interpreter queue."""
5461

55-
def __new__(cls, id, /):
62+
def __new__(cls, id, /, *, _fmt=None):
5663
# There is only one instance for any given ID.
5764
if isinstance(id, int):
5865
id = int(id)
5966
else:
6067
raise TypeError(f'id must be an int, got {id!r}')
68+
if _fmt is None:
69+
_fmt = _queues.get_default_fmt(id)
6170
try:
6271
self = _known_queues[id]
6372
except KeyError:
6473
self = super().__new__(cls)
6574
self._id = id
75+
self._fmt = _fmt
6676
_known_queues[id] = self
6777
_queues.bind(id)
6878
return self
@@ -105,20 +115,50 @@ def qsize(self):
105115
return _queues.get_count(self._id)
106116

107117
def put(self, obj, timeout=None, *,
118+
syncobj=None,
108119
_delay=10 / 1000, # 10 milliseconds
109120
):
110121
"""Add the object to the queue.
111122
112123
This blocks while the queue is full.
124+
125+
If "syncobj" is None (the default) then it uses the
126+
queue's default, set with create_queue()..
127+
128+
If "syncobj" is false then all objects are supported,
129+
at the expense of worse performance.
130+
131+
If "syncobj" is true then the object must be "shareable".
132+
Examples of "shareable" objects include the builtin singletons,
133+
str, and memoryview. One benefit is that such objects are
134+
passed through the queue efficiently.
135+
136+
The key difference, though, is conceptual: the corresponding
137+
object returned from Queue.get() will be strictly equivalent
138+
to the given obj. In other words, the two objects will be
139+
effectively indistinguishable from each other, even if the
140+
object is mutable. The received object may actually be the
141+
same object, or a copy (immutable values only), or a proxy.
142+
Regardless, the received object should be treated as though
143+
the original has been shared directly, whether or not it
144+
actually is. That's a slightly different and stronger promise
145+
than just (initial) equality, which is all "syncobj=False"
146+
can promise.
113147
"""
148+
if syncobj is None:
149+
fmt = self._fmt
150+
else:
151+
fmt = _SHARED_ONLY if syncobj else _PICKLED
114152
if timeout is not None:
115153
timeout = int(timeout)
116154
if timeout < 0:
117155
raise ValueError(f'timeout value must be non-negative')
118156
end = time.time() + timeout
157+
if fmt is _PICKLED:
158+
obj = pickle.dumps(obj)
119159
while True:
120160
try:
121-
_queues.put(self._id, obj)
161+
_queues.put(self._id, obj, fmt)
122162
except _queues.QueueFull as exc:
123163
if timeout is not None and time.time() >= end:
124164
exc.__class__ = QueueFull
@@ -127,9 +167,15 @@ def put(self, obj, timeout=None, *,
127167
else:
128168
break
129169

130-
def put_nowait(self, obj):
170+
def put_nowait(self, obj, *, syncobj=None):
171+
if syncobj is None:
172+
fmt = self._fmt
173+
else:
174+
fmt = _SHARED_ONLY if syncobj else _PICKLED
175+
if fmt is _PICKLED:
176+
obj = pickle.dumps(obj)
131177
try:
132-
return _queues.put(self._id, obj)
178+
_queues.put(self._id, obj, fmt)
133179
except _queues.QueueFull as exc:
134180
exc.__class__ = QueueFull
135181
raise # re-raise
@@ -148,12 +194,18 @@ def get(self, timeout=None, *,
148194
end = time.time() + timeout
149195
while True:
150196
try:
151-
return _queues.get(self._id)
197+
obj, fmt = _queues.get(self._id)
152198
except _queues.QueueEmpty as exc:
153199
if timeout is not None and time.time() >= end:
154200
exc.__class__ = QueueEmpty
155201
raise # re-raise
156202
time.sleep(_delay)
203+
else:
204+
break
205+
if fmt == _PICKLED:
206+
obj = pickle.loads(obj)
207+
else:
208+
assert fmt == _SHARED_ONLY
157209
return obj
158210

159211
def get_nowait(self):

0 commit comments

Comments
 (0)