gh-76785: More Fixes for test.support.interpreters (gh-113012)

This brings the module (along with the associated extension modules) mostly in sync with PEP 734.  There are only a few small things to wrap up.
This commit is contained in:
Eric Snow 2023-12-12 10:43:30 -07:00 committed by GitHub
parent cde1417175
commit a49b427b02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1899 additions and 88 deletions

View file

@ -3,13 +3,11 @@
import queue import queue
import time import time
import weakref import weakref
import _xxinterpchannels as _channels import _xxinterpqueues as _queues
import _xxinterpchannels as _queues
# aliases: # aliases:
from _xxinterpchannels import ( from _xxinterpqueues import (
ChannelError as QueueError, QueueError, QueueNotFoundError,
ChannelNotFoundError as QueueNotFoundError,
) )
__all__ = [ __all__ = [
@ -19,14 +17,27 @@
] ]
class QueueEmpty(_queues.QueueEmpty, queue.Empty):
"""Raised from get_nowait() when the queue is empty.
It is also raised from get() if it times out.
"""
class QueueFull(_queues.QueueFull, queue.Full):
"""Raised from put_nowait() when the queue is full.
It is also raised from put() if it times out.
"""
def create(maxsize=0): def create(maxsize=0):
"""Return a new cross-interpreter queue. """Return a new cross-interpreter queue.
The queue may be used to pass data safely between interpreters. The queue may be used to pass data safely between interpreters.
""" """
# XXX honor maxsize qid = _queues.create(maxsize)
qid = _queues.create() return Queue(qid)
return Queue._with_maxsize(qid, maxsize)
def list_all(): def list_all():
@ -35,53 +46,37 @@ def list_all():
for qid in _queues.list_all()] for qid in _queues.list_all()]
class QueueEmpty(queue.Empty):
"""Raised from get_nowait() when the queue is empty.
It is also raised from get() if it times out.
"""
class QueueFull(queue.Full):
"""Raised from put_nowait() when the queue is full.
It is also raised from put() if it times out.
"""
_known_queues = weakref.WeakValueDictionary() _known_queues = weakref.WeakValueDictionary()
class Queue: class Queue:
"""A cross-interpreter queue.""" """A cross-interpreter queue."""
@classmethod
def _with_maxsize(cls, id, maxsize):
if not isinstance(maxsize, int):
raise TypeError(f'maxsize must be an int, got {maxsize!r}')
elif maxsize < 0:
maxsize = 0
else:
maxsize = int(maxsize)
self = cls(id)
self._maxsize = maxsize
return self
def __new__(cls, id, /): def __new__(cls, id, /):
# There is only one instance for any given ID. # There is only one instance for any given ID.
if isinstance(id, int): if isinstance(id, int):
id = _channels._channel_id(id, force=False) id = int(id)
elif not isinstance(id, _channels.ChannelID): else:
raise TypeError(f'id must be an int, got {id!r}') raise TypeError(f'id must be an int, got {id!r}')
key = int(id)
try: try:
self = _known_queues[key] self = _known_queues[id]
except KeyError: except KeyError:
self = super().__new__(cls) self = super().__new__(cls)
self._id = id self._id = id
self._maxsize = 0 _known_queues[id] = self
_known_queues[key] = self _queues.bind(id)
return self return self
def __del__(self):
try:
_queues.release(self._id)
except QueueNotFoundError:
pass
try:
del _known_queues[self._id]
except KeyError:
pass
def __repr__(self): def __repr__(self):
return f'{type(self).__name__}({self.id})' return f'{type(self).__name__}({self.id})'
@ -90,39 +85,58 @@ def __hash__(self):
@property @property
def id(self): def id(self):
return int(self._id) return self._id
@property @property
def maxsize(self): def maxsize(self):
return self._maxsize try:
return self._maxsize
@property except AttributeError:
def _info(self): self._maxsize = _queues.get_maxsize(self._id)
return _channels.get_info(self._id) return self._maxsize
def empty(self): def empty(self):
return self._info.count == 0 return self.qsize() == 0
def full(self): def full(self):
if self._maxsize <= 0: return _queues.is_full(self._id)
return False
return self._info.count >= self._maxsize
def qsize(self): def qsize(self):
return self._info.count return _queues.get_count(self._id)
def put(self, obj, timeout=None): def put(self, obj, timeout=None, *,
# XXX block if full _delay=10 / 1000, # 10 milliseconds
_channels.send(self._id, obj, blocking=False) ):
"""Add the object to the queue.
This blocks while the queue is full.
"""
if timeout is not None:
timeout = int(timeout)
if timeout < 0:
raise ValueError(f'timeout value must be non-negative')
end = time.time() + timeout
while True:
try:
_queues.put(self._id, obj)
except _queues.QueueFull as exc:
if timeout is not None and time.time() >= end:
exc.__class__ = QueueFull
raise # re-raise
time.sleep(_delay)
else:
break
def put_nowait(self, obj): def put_nowait(self, obj):
# XXX raise QueueFull if full try:
return _channels.send(self._id, obj, blocking=False) return _queues.put(self._id, obj)
except _queues.QueueFull as exc:
exc.__class__ = QueueFull
raise # re-raise
def get(self, timeout=None, *, def get(self, timeout=None, *,
_sentinel=object(), _delay=10 / 1000, # 10 milliseconds
_delay=10 / 1000, # 10 milliseconds ):
):
"""Return the next object from the queue. """Return the next object from the queue.
This blocks while the queue is empty. This blocks while the queue is empty.
@ -132,25 +146,27 @@ def get(self, timeout=None, *,
if timeout < 0: if timeout < 0:
raise ValueError(f'timeout value must be non-negative') raise ValueError(f'timeout value must be non-negative')
end = time.time() + timeout end = time.time() + timeout
obj = _channels.recv(self._id, _sentinel) while True:
while obj is _sentinel: try:
time.sleep(_delay) return _queues.get(self._id)
if timeout is not None and time.time() >= end: except _queues.QueueEmpty as exc:
raise QueueEmpty if timeout is not None and time.time() >= end:
obj = _channels.recv(self._id, _sentinel) exc.__class__ = QueueEmpty
raise # re-raise
time.sleep(_delay)
return obj return obj
def get_nowait(self, *, _sentinel=object()): def get_nowait(self):
"""Return the next object from the channel. """Return the next object from the channel.
If the queue is empty then raise QueueEmpty. Otherwise this If the queue is empty then raise QueueEmpty. Otherwise this
is the same as get(). is the same as get().
""" """
obj = _channels.recv(self._id, _sentinel) try:
if obj is _sentinel: return _queues.get(self._id)
raise QueueEmpty except _queues.QueueEmpty as exc:
return obj exc.__class__ = QueueEmpty
raise # re-raise
# XXX add this: _queues._register_queue_type(Queue)
#_channels._register_queue_type(Queue)

View file

@ -5,13 +5,21 @@
from test.support import import_helper from test.support import import_helper
# Raise SkipTest if subinterpreters not supported. # Raise SkipTest if subinterpreters not supported.
import_helper.import_module('_xxinterpchannels') _queues = import_helper.import_module('_xxinterpqueues')
#import_helper.import_module('_xxinterpqueues')
from test.support import interpreters from test.support import interpreters
from test.support.interpreters import queues from test.support.interpreters import queues
from .utils import _run_output, TestBase from .utils import _run_output, TestBase
class TestBase(TestBase):
def tearDown(self):
for qid in _queues.list_all():
try:
_queues.destroy(qid)
except Exception:
pass
class QueueTests(TestBase): class QueueTests(TestBase):
def test_create(self): def test_create(self):
@ -32,20 +40,47 @@ def test_create(self):
self.assertEqual(queue.maxsize, 0) self.assertEqual(queue.maxsize, 0)
with self.subTest('negative maxsize'): with self.subTest('negative maxsize'):
queue = queues.create(-1) queue = queues.create(-10)
self.assertEqual(queue.maxsize, 0) self.assertEqual(queue.maxsize, -10)
with self.subTest('bad maxsize'): with self.subTest('bad maxsize'):
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
queues.create('1') queues.create('1')
@unittest.expectedFailure
def test_shareable(self): def test_shareable(self):
queue1 = queues.create() queue1 = queues.create()
queue2 = queues.create()
queue1.put(queue2) interp = interpreters.create()
queue3 = queue1.get() interp.exec_sync(dedent(f"""
self.assertIs(queue3, queue1) from test.support.interpreters import queues
queue1 = queues.Queue({queue1.id})
"""));
with self.subTest('same interpreter'):
queue2 = queues.create()
queue1.put(queue2)
queue3 = queue1.get()
self.assertIs(queue3, queue2)
with self.subTest('from current interpreter'):
queue4 = queues.create()
queue1.put(queue4)
out = _run_output(interp, dedent("""
queue4 = queue1.get()
print(queue4.id)
"""))
qid = int(out)
self.assertEqual(qid, queue4.id)
with self.subTest('from subinterpreter'):
out = _run_output(interp, dedent("""
queue5 = queues.create()
queue1.put(queue5)
print(queue5.id)
"""))
qid = int(out)
queue5 = queue1.get()
self.assertEqual(queue5.id, qid)
def test_id_type(self): def test_id_type(self):
queue = queues.create() queue = queues.create()
@ -137,7 +172,6 @@ def test_put_get_main(self):
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
@unittest.expectedFailure
def test_put_timeout(self): def test_put_timeout(self):
queue = queues.create(2) queue = queues.create(2)
queue.put(None) queue.put(None)
@ -147,7 +181,6 @@ def test_put_timeout(self):
queue.get() queue.get()
queue.put(None) queue.put(None)
@unittest.expectedFailure
def test_put_nowait(self): def test_put_nowait(self):
queue = queues.create(2) queue = queues.create(2)
queue.put_nowait(None) queue.put_nowait(None)
@ -179,31 +212,64 @@ def test_put_get_same_interpreter(self):
assert obj is not orig, 'expected: obj is not orig' assert obj is not orig, 'expected: obj is not orig'
""")) """))
@unittest.expectedFailure
def test_put_get_different_interpreters(self): def test_put_get_different_interpreters(self):
interp = interpreters.create()
queue1 = queues.create() queue1 = queues.create()
queue2 = queues.create() queue2 = queues.create()
self.assertEqual(len(queues.list_all()), 2)
obj1 = b'spam' obj1 = b'spam'
queue1.put(obj1) queue1.put(obj1)
out = _run_output( out = _run_output(
interpreters.create(), interp,
dedent(f""" dedent(f"""
import test.support.interpreters.queue as queues from test.support.interpreters import queues
queue1 = queues.Queue({queue1.id}) queue1 = queues.Queue({queue1.id})
queue2 = queues.Queue({queue2.id}) queue2 = queues.Queue({queue2.id})
assert queue1.qsize() == 1, 'expected: queue1.qsize() == 1'
obj = queue1.get() obj = queue1.get()
assert queue1.qsize() == 0, 'expected: queue1.qsize() == 0'
assert obj == b'spam', 'expected: obj == obj1' assert obj == b'spam', 'expected: obj == obj1'
# When going to another interpreter we get a copy. # When going to another interpreter we get a copy.
assert id(obj) != {id(obj1)}, 'expected: obj is not obj1' assert id(obj) != {id(obj1)}, 'expected: obj is not obj1'
obj2 = b'eggs' obj2 = b'eggs'
print(id(obj2)) print(id(obj2))
assert queue2.qsize() == 0, 'expected: queue2.qsize() == 0'
queue2.put(obj2) queue2.put(obj2)
assert queue2.qsize() == 1, 'expected: queue2.qsize() == 1'
""")) """))
obj2 = queue2.get() self.assertEqual(len(queues.list_all()), 2)
self.assertEqual(queue1.qsize(), 0)
self.assertEqual(queue2.qsize(), 1)
obj2 = queue2.get()
self.assertEqual(obj2, b'eggs') self.assertEqual(obj2, b'eggs')
self.assertNotEqual(id(obj2), int(out)) self.assertNotEqual(id(obj2), int(out))
def test_put_cleared_with_subinterpreter(self):
interp = interpreters.create()
queue = queues.create()
out = _run_output(
interp,
dedent(f"""
from test.support.interpreters import queues
queue = queues.Queue({queue.id})
obj1 = b'spam'
obj2 = b'eggs'
queue.put(obj1)
queue.put(obj2)
"""))
self.assertEqual(queue.qsize(), 2)
obj1 = queue.get()
self.assertEqual(obj1, b'spam')
self.assertEqual(queue.qsize(), 1)
del interp
self.assertEqual(queue.qsize(), 0)
def test_put_get_different_threads(self): def test_put_get_different_threads(self):
queue1 = queues.create() queue1 = queues.create()
queue2 = queues.create() queue2 = queues.create()

View file

@ -273,6 +273,7 @@ PYTHONPATH=$(COREPYTHONPATH)
#_xxsubinterpreters _xxsubinterpretersmodule.c #_xxsubinterpreters _xxsubinterpretersmodule.c
#_xxinterpchannels _xxinterpchannelsmodule.c #_xxinterpchannels _xxinterpchannelsmodule.c
#_xxinterpqueues _xxinterpqueuesmodule.c
#_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c #_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c
#_testbuffer _testbuffer.c #_testbuffer _testbuffer.c
#_testinternalcapi _testinternalcapi.c #_testinternalcapi _testinternalcapi.c

View file

@ -41,8 +41,11 @@
@MODULE__QUEUE_TRUE@_queue _queuemodule.c @MODULE__QUEUE_TRUE@_queue _queuemodule.c
@MODULE__RANDOM_TRUE@_random _randommodule.c @MODULE__RANDOM_TRUE@_random _randommodule.c
@MODULE__STRUCT_TRUE@_struct _struct.c @MODULE__STRUCT_TRUE@_struct _struct.c
# build supports subinterpreters
@MODULE__XXSUBINTERPRETERS_TRUE@_xxsubinterpreters _xxsubinterpretersmodule.c @MODULE__XXSUBINTERPRETERS_TRUE@_xxsubinterpreters _xxsubinterpretersmodule.c
@MODULE__XXINTERPCHANNELS_TRUE@_xxinterpchannels _xxinterpchannelsmodule.c @MODULE__XXINTERPCHANNELS_TRUE@_xxinterpchannels _xxinterpchannelsmodule.c
@MODULE__XXINTERPQUEUES_TRUE@_xxinterpqueues _xxinterpqueuesmodule.c
@MODULE__ZONEINFO_TRUE@_zoneinfo _zoneinfo.c @MODULE__ZONEINFO_TRUE@_zoneinfo _zoneinfo.c
# needs libm # needs libm

View file

@ -2629,10 +2629,11 @@ _get_current_channelend_type(int end)
cls = state->recv_channel_type; cls = state->recv_channel_type;
} }
if (cls == NULL) { if (cls == NULL) {
PyObject *highlevel = PyImport_ImportModule("interpreters"); // Force the module to be loaded, to register the type.
PyObject *highlevel = PyImport_ImportModule("interpreters.channel");
if (highlevel == NULL) { if (highlevel == NULL) {
PyErr_Clear(); PyErr_Clear();
highlevel = PyImport_ImportModule("test.support.interpreters"); highlevel = PyImport_ImportModule("test.support.interpreters.channel");
if (highlevel == NULL) { if (highlevel == NULL) {
return NULL; return NULL;
} }

File diff suppressed because it is too large Load diff

View file

@ -37,6 +37,7 @@ extern PyObject* PyInit__weakref(void);
extern PyObject* PyInit_xxsubtype(void); extern PyObject* PyInit_xxsubtype(void);
extern PyObject* PyInit__xxsubinterpreters(void); extern PyObject* PyInit__xxsubinterpreters(void);
extern PyObject* PyInit__xxinterpchannels(void); extern PyObject* PyInit__xxinterpchannels(void);
extern PyObject* PyInit__xxinterpqueues(void);
extern PyObject* PyInit__random(void); extern PyObject* PyInit__random(void);
extern PyObject* PyInit_itertools(void); extern PyObject* PyInit_itertools(void);
extern PyObject* PyInit__collections(void); extern PyObject* PyInit__collections(void);
@ -142,6 +143,7 @@ struct _inittab _PyImport_Inittab[] = {
{"xxsubtype", PyInit_xxsubtype}, {"xxsubtype", PyInit_xxsubtype},
{"_xxsubinterpreters", PyInit__xxsubinterpreters}, {"_xxsubinterpreters", PyInit__xxsubinterpreters},
{"_xxinterpchannels", PyInit__xxinterpchannels}, {"_xxinterpchannels", PyInit__xxinterpchannels},
{"_xxinterpqueues", PyInit__xxinterpqueues},
#ifdef _Py_HAVE_ZLIB #ifdef _Py_HAVE_ZLIB
{"zlib", PyInit_zlib}, {"zlib", PyInit_zlib},
#endif #endif

View file

@ -458,6 +458,7 @@
<ClCompile Include="..\Modules\xxsubtype.c" /> <ClCompile Include="..\Modules\xxsubtype.c" />
<ClCompile Include="..\Modules\_xxsubinterpretersmodule.c" /> <ClCompile Include="..\Modules\_xxsubinterpretersmodule.c" />
<ClCompile Include="..\Modules\_xxinterpchannelsmodule.c" /> <ClCompile Include="..\Modules\_xxinterpchannelsmodule.c" />
<ClCompile Include="..\Modules\_xxinterpqueuesmodule.c" />
<ClCompile Include="..\Modules\_io\fileio.c" /> <ClCompile Include="..\Modules\_io\fileio.c" />
<ClCompile Include="..\Modules\_io\bytesio.c" /> <ClCompile Include="..\Modules\_io\bytesio.c" />
<ClCompile Include="..\Modules\_io\stringio.c" /> <ClCompile Include="..\Modules\_io\stringio.c" />

View file

@ -1505,6 +1505,9 @@
<ClCompile Include="..\Modules\_xxinterpchannelsmodule.c"> <ClCompile Include="..\Modules\_xxinterpchannelsmodule.c">
<Filter>Modules</Filter> <Filter>Modules</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="..\Modules\_xxinterpqueuesmodule.c">
<Filter>Modules</Filter>
</ClCompile>
<ClCompile Include="..\Parser\string_parser.c"> <ClCompile Include="..\Parser\string_parser.c">
<Filter>Parser</Filter> <Filter>Parser</Filter>
</ClCompile> </ClCompile>

View file

@ -36,6 +36,7 @@
'_testsinglephase', '_testsinglephase',
'_xxsubinterpreters', '_xxsubinterpreters',
'_xxinterpchannels', '_xxinterpchannels',
'_xxinterpqueues',
'_xxtestfuzz', '_xxtestfuzz',
'idlelib.idle_test', 'idlelib.idle_test',
'test', 'test',

View file

@ -165,6 +165,7 @@ Python/pylifecycle.c fatal_error reentrant -
# explicitly protected, internal-only # explicitly protected, internal-only
Modules/_xxinterpchannelsmodule.c - _globals - Modules/_xxinterpchannelsmodule.c - _globals -
Modules/_xxinterpqueuesmodule.c - _globals -
# set once during module init # set once during module init
Modules/_decimal/_decimal.c - minalloc_is_set - Modules/_decimal/_decimal.c - minalloc_is_set -

Can't render this file because it has a wrong number of fields in line 4.

29
configure generated vendored
View file

@ -769,6 +769,8 @@ MODULE__MULTIPROCESSING_FALSE
MODULE__MULTIPROCESSING_TRUE MODULE__MULTIPROCESSING_TRUE
MODULE__ZONEINFO_FALSE MODULE__ZONEINFO_FALSE
MODULE__ZONEINFO_TRUE MODULE__ZONEINFO_TRUE
MODULE__XXINTERPQUEUES_FALSE
MODULE__XXINTERPQUEUES_TRUE
MODULE__XXINTERPCHANNELS_FALSE MODULE__XXINTERPCHANNELS_FALSE
MODULE__XXINTERPCHANNELS_TRUE MODULE__XXINTERPCHANNELS_TRUE
MODULE__XXSUBINTERPRETERS_FALSE MODULE__XXSUBINTERPRETERS_FALSE
@ -28025,6 +28027,7 @@ case $ac_sys_system in #(
py_cv_module__tkinter=n/a py_cv_module__tkinter=n/a
py_cv_module__xxsubinterpreters=n/a py_cv_module__xxsubinterpreters=n/a
py_cv_module__xxinterpchannels=n/a py_cv_module__xxinterpchannels=n/a
py_cv_module__xxinterpqueues=n/a
py_cv_module_grp=n/a py_cv_module_grp=n/a
py_cv_module_pwd=n/a py_cv_module_pwd=n/a
py_cv_module_resource=n/a py_cv_module_resource=n/a
@ -28524,6 +28527,28 @@ then :
fi
if test "$py_cv_module__xxinterpqueues" != "n/a"
then :
py_cv_module__xxinterpqueues=yes
fi
if test "$py_cv_module__xxinterpqueues" = yes; then
MODULE__XXINTERPQUEUES_TRUE=
MODULE__XXINTERPQUEUES_FALSE='#'
else
MODULE__XXINTERPQUEUES_TRUE='#'
MODULE__XXINTERPQUEUES_FALSE=
fi
as_fn_append MODULE_BLOCK "MODULE__XXINTERPQUEUES_STATE=$py_cv_module__xxinterpqueues$as_nl"
if test "x$py_cv_module__xxinterpqueues" = xyes
then :
fi fi
@ -30760,6 +30785,10 @@ if test -z "${MODULE__XXINTERPCHANNELS_TRUE}" && test -z "${MODULE__XXINTERPCHAN
as_fn_error $? "conditional \"MODULE__XXINTERPCHANNELS\" was never defined. as_fn_error $? "conditional \"MODULE__XXINTERPCHANNELS\" was never defined.
Usually this means the macro was only invoked conditionally." "$LINENO" 5 Usually this means the macro was only invoked conditionally." "$LINENO" 5
fi fi
if test -z "${MODULE__XXINTERPQUEUES_TRUE}" && test -z "${MODULE__XXINTERPQUEUES_FALSE}"; then
as_fn_error $? "conditional \"MODULE__XXINTERPQUEUES\" was never defined.
Usually this means the macro was only invoked conditionally." "$LINENO" 5
fi
if test -z "${MODULE__ZONEINFO_TRUE}" && test -z "${MODULE__ZONEINFO_FALSE}"; then if test -z "${MODULE__ZONEINFO_TRUE}" && test -z "${MODULE__ZONEINFO_FALSE}"; then
as_fn_error $? "conditional \"MODULE__ZONEINFO\" was never defined. as_fn_error $? "conditional \"MODULE__ZONEINFO\" was never defined.
Usually this means the macro was only invoked conditionally." "$LINENO" 5 Usually this means the macro was only invoked conditionally." "$LINENO" 5

View file

@ -7120,6 +7120,7 @@ AS_CASE([$ac_sys_system],
[_tkinter], [_tkinter],
[_xxsubinterpreters], [_xxsubinterpreters],
[_xxinterpchannels], [_xxinterpchannels],
[_xxinterpqueues],
[grp], [grp],
[pwd], [pwd],
[resource], [resource],
@ -7236,6 +7237,7 @@ PY_STDLIB_MOD_SIMPLE([_struct])
PY_STDLIB_MOD_SIMPLE([_typing]) PY_STDLIB_MOD_SIMPLE([_typing])
PY_STDLIB_MOD_SIMPLE([_xxsubinterpreters]) PY_STDLIB_MOD_SIMPLE([_xxsubinterpreters])
PY_STDLIB_MOD_SIMPLE([_xxinterpchannels]) PY_STDLIB_MOD_SIMPLE([_xxinterpchannels])
PY_STDLIB_MOD_SIMPLE([_xxinterpqueues])
PY_STDLIB_MOD_SIMPLE([_zoneinfo]) PY_STDLIB_MOD_SIMPLE([_zoneinfo])
dnl multiprocessing modules dnl multiprocessing modules