Skip to content

Commit 1c55f9e

Browse files
authored
Add test.support.interpreters at 3.13.2 (#5684)
1 parent 1e6da5f commit 1c55f9e

File tree

4 files changed

+930
-0
lines changed

4 files changed

+930
-0
lines changed
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
"""Subinterpreters High Level Module."""
2+
3+
import threading
4+
import weakref
5+
import _interpreters
6+
7+
# aliases:
8+
from _interpreters import (
9+
InterpreterError, InterpreterNotFoundError, NotShareableError,
10+
is_shareable,
11+
)
12+
13+
14+
__all__ = [
15+
'get_current', 'get_main', 'create', 'list_all', 'is_shareable',
16+
'Interpreter',
17+
'InterpreterError', 'InterpreterNotFoundError', 'ExecutionFailed',
18+
'NotShareableError',
19+
'create_queue', 'Queue', 'QueueEmpty', 'QueueFull',
20+
]
21+
22+
23+
_queuemod = None
24+
25+
def __getattr__(name):
26+
if name in ('Queue', 'QueueEmpty', 'QueueFull', 'create_queue'):
27+
global create_queue, Queue, QueueEmpty, QueueFull
28+
ns = globals()
29+
from .queues import (
30+
create as create_queue,
31+
Queue, QueueEmpty, QueueFull,
32+
)
33+
return ns[name]
34+
else:
35+
raise AttributeError(name)
36+
37+
38+
_EXEC_FAILURE_STR = """
39+
{superstr}
40+
41+
Uncaught in the interpreter:
42+
43+
{formatted}
44+
""".strip()
45+
46+
class ExecutionFailed(InterpreterError):
47+
"""An unhandled exception happened during execution.
48+
49+
This is raised from Interpreter.exec() and Interpreter.call().
50+
"""
51+
52+
def __init__(self, excinfo):
53+
msg = excinfo.formatted
54+
if not msg:
55+
if excinfo.type and excinfo.msg:
56+
msg = f'{excinfo.type.__name__}: {excinfo.msg}'
57+
else:
58+
msg = excinfo.type.__name__ or excinfo.msg
59+
super().__init__(msg)
60+
self.excinfo = excinfo
61+
62+
def __str__(self):
63+
try:
64+
formatted = self.excinfo.errdisplay
65+
except Exception:
66+
return super().__str__()
67+
else:
68+
return _EXEC_FAILURE_STR.format(
69+
superstr=super().__str__(),
70+
formatted=formatted,
71+
)
72+
73+
74+
def create():
75+
"""Return a new (idle) Python interpreter."""
76+
id = _interpreters.create(reqrefs=True)
77+
return Interpreter(id, _ownsref=True)
78+
79+
80+
def list_all():
81+
"""Return all existing interpreters."""
82+
return [Interpreter(id, _whence=whence)
83+
for id, whence in _interpreters.list_all(require_ready=True)]
84+
85+
86+
def get_current():
87+
"""Return the currently running interpreter."""
88+
id, whence = _interpreters.get_current()
89+
return Interpreter(id, _whence=whence)
90+
91+
92+
def get_main():
93+
"""Return the main interpreter."""
94+
id, whence = _interpreters.get_main()
95+
assert whence == _interpreters.WHENCE_RUNTIME, repr(whence)
96+
return Interpreter(id, _whence=whence)
97+
98+
99+
_known = weakref.WeakValueDictionary()
100+
101+
class Interpreter:
102+
"""A single Python interpreter.
103+
104+
Attributes:
105+
106+
"id" - the unique process-global ID number for the interpreter
107+
"whence" - indicates where the interpreter was created
108+
109+
If the interpreter wasn't created by this module
110+
then any method that modifies the interpreter will fail,
111+
i.e. .close(), .prepare_main(), .exec(), and .call()
112+
"""
113+
114+
_WHENCE_TO_STR = {
115+
_interpreters.WHENCE_UNKNOWN: 'unknown',
116+
_interpreters.WHENCE_RUNTIME: 'runtime init',
117+
_interpreters.WHENCE_LEGACY_CAPI: 'legacy C-API',
118+
_interpreters.WHENCE_CAPI: 'C-API',
119+
_interpreters.WHENCE_XI: 'cross-interpreter C-API',
120+
_interpreters.WHENCE_STDLIB: '_interpreters module',
121+
}
122+
123+
def __new__(cls, id, /, _whence=None, _ownsref=None):
124+
# There is only one instance for any given ID.
125+
if not isinstance(id, int):
126+
raise TypeError(f'id must be an int, got {id!r}')
127+
id = int(id)
128+
if _whence is None:
129+
if _ownsref:
130+
_whence = _interpreters.WHENCE_STDLIB
131+
else:
132+
_whence = _interpreters.whence(id)
133+
assert _whence in cls._WHENCE_TO_STR, repr(_whence)
134+
if _ownsref is None:
135+
_ownsref = (_whence == _interpreters.WHENCE_STDLIB)
136+
try:
137+
self = _known[id]
138+
assert hasattr(self, '_ownsref')
139+
except KeyError:
140+
self = super().__new__(cls)
141+
_known[id] = self
142+
self._id = id
143+
self._whence = _whence
144+
self._ownsref = _ownsref
145+
if _ownsref:
146+
# This may raise InterpreterNotFoundError:
147+
_interpreters.incref(id)
148+
return self
149+
150+
def __repr__(self):
151+
return f'{type(self).__name__}({self.id})'
152+
153+
def __hash__(self):
154+
return hash(self._id)
155+
156+
def __del__(self):
157+
self._decref()
158+
159+
# for pickling:
160+
def __getnewargs__(self):
161+
return (self._id,)
162+
163+
# for pickling:
164+
def __getstate__(self):
165+
return None
166+
167+
def _decref(self):
168+
if not self._ownsref:
169+
return
170+
self._ownsref = False
171+
try:
172+
_interpreters.decref(self._id)
173+
except InterpreterNotFoundError:
174+
pass
175+
176+
@property
177+
def id(self):
178+
return self._id
179+
180+
@property
181+
def whence(self):
182+
return self._WHENCE_TO_STR[self._whence]
183+
184+
def is_running(self):
185+
"""Return whether or not the identified interpreter is running."""
186+
return _interpreters.is_running(self._id)
187+
188+
# Everything past here is available only to interpreters created by
189+
# interpreters.create().
190+
191+
def close(self):
192+
"""Finalize and destroy the interpreter.
193+
194+
Attempting to destroy the current interpreter results
195+
in an InterpreterError.
196+
"""
197+
return _interpreters.destroy(self._id, restrict=True)
198+
199+
def prepare_main(self, ns=None, /, **kwargs):
200+
"""Bind the given values into the interpreter's __main__.
201+
202+
The values must be shareable.
203+
"""
204+
ns = dict(ns, **kwargs) if ns is not None else kwargs
205+
_interpreters.set___main___attrs(self._id, ns, restrict=True)
206+
207+
def exec(self, code, /):
208+
"""Run the given source code in the interpreter.
209+
210+
This is essentially the same as calling the builtin "exec"
211+
with this interpreter, using the __dict__ of its __main__
212+
module as both globals and locals.
213+
214+
There is no return value.
215+
216+
If the code raises an unhandled exception then an ExecutionFailed
217+
exception is raised, which summarizes the unhandled exception.
218+
The actual exception is discarded because objects cannot be
219+
shared between interpreters.
220+
221+
This blocks the current Python thread until done. During
222+
that time, the previous interpreter is allowed to run
223+
in other threads.
224+
"""
225+
excinfo = _interpreters.exec(self._id, code, restrict=True)
226+
if excinfo is not None:
227+
raise ExecutionFailed(excinfo)
228+
229+
def call(self, callable, /):
230+
"""Call the object in the interpreter with given args/kwargs.
231+
232+
Only functions that take no arguments and have no closure
233+
are supported.
234+
235+
The return value is discarded.
236+
237+
If the callable raises an exception then the error display
238+
(including full traceback) is send back between the interpreters
239+
and an ExecutionFailed exception is raised, much like what
240+
happens with Interpreter.exec().
241+
"""
242+
# XXX Support args and kwargs.
243+
# XXX Support arbitrary callables.
244+
# XXX Support returning the return value (e.g. via pickle).
245+
excinfo = _interpreters.call(self._id, callable, restrict=True)
246+
if excinfo is not None:
247+
raise ExecutionFailed(excinfo)
248+
249+
def call_in_thread(self, callable, /):
250+
"""Return a new thread that calls the object in the interpreter.
251+
252+
The return value and any raised exception are discarded.
253+
"""
254+
def task():
255+
self.call(callable)
256+
t = threading.Thread(target=task)
257+
t.start()
258+
return t
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""Common code between queues and channels."""
2+
3+
4+
class ItemInterpreterDestroyed(Exception):
5+
"""Raised when trying to get an item whose interpreter was destroyed."""
6+
7+
8+
class classonly:
9+
"""A non-data descriptor that makes a value only visible on the class.
10+
11+
This is like the "classmethod" builtin, but does not show up on
12+
instances of the class. It may be used as a decorator.
13+
"""
14+
15+
def __init__(self, value):
16+
self.value = value
17+
self.getter = classmethod(value).__get__
18+
self.name = None
19+
20+
def __set_name__(self, cls, name):
21+
if self.name is not None:
22+
raise TypeError('already used')
23+
self.name = name
24+
25+
def __get__(self, obj, cls):
26+
if obj is not None:
27+
raise AttributeError(self.name)
28+
# called on the class
29+
return self.getter(None, cls)
30+
31+
32+
class UnboundItem:
33+
"""Represents a cross-interpreter item no longer bound to an interpreter.
34+
35+
An item is unbound when the interpreter that added it to the
36+
cross-interpreter container is destroyed.
37+
"""
38+
39+
__slots__ = ()
40+
41+
@classonly
42+
def singleton(cls, kind, module, name='UNBOUND'):
43+
doc = cls.__doc__.replace('cross-interpreter container', kind)
44+
doc = doc.replace('cross-interpreter', kind)
45+
subclass = type(
46+
f'Unbound{kind.capitalize()}Item',
47+
(cls,),
48+
dict(
49+
_MODULE=module,
50+
_NAME=name,
51+
__doc__=doc,
52+
),
53+
)
54+
return object.__new__(subclass)
55+
56+
_MODULE = __name__
57+
_NAME = 'UNBOUND'
58+
59+
def __new__(cls):
60+
raise Exception(f'use {cls._MODULE}.{cls._NAME}')
61+
62+
def __repr__(self):
63+
return f'{self._MODULE}.{self._NAME}'
64+
# return f'interpreters.queues.UNBOUND'
65+
66+
67+
UNBOUND = object.__new__(UnboundItem)
68+
UNBOUND_ERROR = object()
69+
UNBOUND_REMOVE = object()
70+
71+
_UNBOUND_CONSTANT_TO_FLAG = {
72+
UNBOUND_REMOVE: 1,
73+
UNBOUND_ERROR: 2,
74+
UNBOUND: 3,
75+
}
76+
_UNBOUND_FLAG_TO_CONSTANT = {v: k
77+
for k, v in _UNBOUND_CONSTANT_TO_FLAG.items()}
78+
79+
80+
def serialize_unbound(unbound):
81+
op = unbound
82+
try:
83+
flag = _UNBOUND_CONSTANT_TO_FLAG[op]
84+
except KeyError:
85+
raise NotImplementedError(f'unsupported unbound replacement op {op!r}')
86+
return flag,
87+
88+
89+
def resolve_unbound(flag, exctype_destroyed):
90+
try:
91+
op = _UNBOUND_FLAG_TO_CONSTANT[flag]
92+
except KeyError:
93+
raise NotImplementedError(f'unsupported unbound replacement op {flag!r}')
94+
if op is UNBOUND_REMOVE:
95+
# "remove" not possible here
96+
raise NotImplementedError
97+
elif op is UNBOUND_ERROR:
98+
raise exctype_destroyed("item's original interpreter destroyed")
99+
elif op is UNBOUND:
100+
return UNBOUND
101+
else:
102+
raise NotImplementedError(repr(op))

0 commit comments

Comments
 (0)