diff --git a/Lib/concurrent/interpreters/_crossinterp.py b/Lib/concurrent/interpreters/_crossinterp.py index a5f46b20fbb4c5..69b25a48fd4154 100644 --- a/Lib/concurrent/interpreters/_crossinterp.py +++ b/Lib/concurrent/interpreters/_crossinterp.py @@ -5,71 +5,7 @@ class ItemInterpreterDestroyed(Exception): """Raised when trying to get an item whose interpreter was destroyed.""" -class classonly: - """A non-data descriptor that makes a value only visible on the class. - - This is like the "classmethod" builtin, but does not show up on - instances of the class. It may be used as a decorator. - """ - - def __init__(self, value): - self.value = value - self.getter = classmethod(value).__get__ - self.name = None - - def __set_name__(self, cls, name): - if self.name is not None: - raise TypeError('already used') - self.name = name - - def __get__(self, obj, cls): - if obj is not None: - raise AttributeError(self.name) - # called on the class - return self.getter(None, cls) - - -class UnboundItem: - """Represents a cross-interpreter item no longer bound to an interpreter. - - An item is unbound when the interpreter that added it to the - cross-interpreter container is destroyed. - """ - - __slots__ = () - - @classonly - def singleton(cls, kind, module, name='UNBOUND'): - doc = cls.__doc__ - if doc: - doc = doc.replace( - 'cross-interpreter container', kind, - ).replace( - 'cross-interpreter', kind, - ) - subclass = type( - f'Unbound{kind.capitalize()}Item', - (cls,), - { - "_MODULE": module, - "_NAME": name, - "__doc__": doc, - }, - ) - return object.__new__(subclass) - - _MODULE = __name__ - _NAME = 'UNBOUND' - - def __new__(cls): - raise Exception(f'use {cls._MODULE}.{cls._NAME}') - - def __repr__(self): - return f'{self._MODULE}.{self._NAME}' -# return f'interpreters._queues.UNBOUND' - - -UNBOUND = object.__new__(UnboundItem) +UNBOUND = object() UNBOUND_ERROR = object() UNBOUND_REMOVE = object() diff --git a/Lib/concurrent/interpreters/_queues.py b/Lib/concurrent/interpreters/_queues.py index ee159d7de63827..cb60be18b39e86 100644 --- a/Lib/concurrent/interpreters/_queues.py +++ b/Lib/concurrent/interpreters/_queues.py @@ -11,7 +11,7 @@ QueueError, QueueNotFoundError, ) from ._crossinterp import ( - UNBOUND_ERROR, UNBOUND_REMOVE, + UNBOUND, UNBOUND_ERROR, UNBOUND_REMOVE, ) __all__ = [ @@ -46,9 +46,6 @@ class ItemInterpreterDestroyed(QueueError, _PICKLED = 1 -UNBOUND = _crossinterp.UnboundItem.singleton('queue', __name__) - - def _serialize_unbound(unbound): if unbound is UNBOUND: unbound = _crossinterp.UNBOUND diff --git a/Lib/test/support/channels.py b/Lib/test/support/channels.py index fab1797659b312..1da6a0a9ea8bee 100644 --- a/Lib/test/support/channels.py +++ b/Lib/test/support/channels.py @@ -10,7 +10,7 @@ ChannelEmptyError, ChannelNotEmptyError, # noqa: F401 ) from concurrent.interpreters._crossinterp import ( - UNBOUND_ERROR, UNBOUND_REMOVE, + UNBOUND, UNBOUND_ERROR, UNBOUND_REMOVE, ) @@ -28,9 +28,6 @@ class ItemInterpreterDestroyed(ChannelError, """Raised from get() and get_nowait().""" -UNBOUND = _crossinterp.UnboundItem.singleton('queue', __name__) - - def _serialize_unbound(unbound): if unbound is UNBOUND: unbound = _crossinterp.UNBOUND diff --git a/Lib/test/test_concurrent_futures/test_interpreter_pool.py b/Lib/test/test_concurrent_futures/test_interpreter_pool.py index 7241fcc4b1e74d..2b4f8e98fa11b4 100644 --- a/Lib/test/test_concurrent_futures/test_interpreter_pool.py +++ b/Lib/test/test_concurrent_futures/test_interpreter_pool.py @@ -427,6 +427,10 @@ def run(taskid, ready, blocker): ready.get(timeout=1) # blocking except interpreters.QueueEmpty: pass + except queues.QueueEmpty: + # GH-142414: reloading the _queues module makes get to raise + # queues.QueueEmpty instead of interpreters.QueueEmpty. + pass else: done += 1 pending -= done diff --git a/Lib/test/test_interpreters/test_queues.py b/Lib/test/test_interpreters/test_queues.py index 77334aea3836b9..c5e78235c855df 100644 --- a/Lib/test/test_interpreters/test_queues.py +++ b/Lib/test/test_interpreters/test_queues.py @@ -8,6 +8,7 @@ # Raise SkipTest if subinterpreters not supported. _queues = import_helper.import_module('_interpqueues') from concurrent import interpreters +from concurrent.futures import InterpreterPoolExecutor from concurrent.interpreters import _queues as queues, _crossinterp from .utils import _run_output, TestBase as _TestBase @@ -93,6 +94,14 @@ def test_bind_release(self): with self.assertRaises(queues.QueueError): _queues.release(qid) + def test_interpreter_pool_executor_after_reload(self): + # Regression test for gh-142414 (KeyError in serialize_unbound). + importlib.reload(queues) + code = "import struct" + with InterpreterPoolExecutor(max_workers=1) as executor: + results = executor.map(exec, [code] * 1) + self.assertEqual(list(results), [None] * 1) + class QueueTests(TestBase): diff --git a/Misc/NEWS.d/next/Library/2025-12-10-15-27-58.gh-issue-142414.zTHgP-.rst b/Misc/NEWS.d/next/Library/2025-12-10-15-27-58.gh-issue-142414.zTHgP-.rst new file mode 100644 index 00000000000000..94ec6d9f8fd368 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-10-15-27-58.gh-issue-142414.zTHgP-.rst @@ -0,0 +1 @@ +Fix spurious :exc:`KeyError` when :mod:`!concurrent.interpreters._queues` is reloaded after import.