From cf6ac24a9f6aa2b4b2dd79ae8acb7fa1a0471a97 Mon Sep 17 00:00:00 2001 From: Kir Chou Date: Thu, 11 Dec 2025 11:19:23 +0900 Subject: [PATCH] Remove singleton design pattern in _crossinterp.py. --- Lib/concurrent/interpreters/_crossinterp.py | 62 +++---------------- Lib/concurrent/interpreters/_queues.py | 4 +- Lib/test/support/channels.py | 4 +- Lib/test/test_interpreters/test_queues.py | 9 +++ ...-12-10-15-27-58.gh-issue-142414.zTHgP-.rst | 1 + 5 files changed, 20 insertions(+), 60 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-12-10-15-27-58.gh-issue-142414.zTHgP-.rst diff --git a/Lib/concurrent/interpreters/_crossinterp.py b/Lib/concurrent/interpreters/_crossinterp.py index a5f46b20fbb4c5..e8c0ad586ccd52 100644 --- a/Lib/concurrent/interpreters/_crossinterp.py +++ b/Lib/concurrent/interpreters/_crossinterp.py @@ -5,30 +5,6 @@ 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. @@ -36,40 +12,16 @@ class UnboundItem: 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 __init__(self, kind, module, name='UNBOUND'): + self._kind = kind + self._module = module + self._name = name def __repr__(self): - return f'{self._MODULE}.{self._NAME}' -# return f'interpreters._queues.UNBOUND' + return f'{self._module}.{self._name}' -UNBOUND = object.__new__(UnboundItem) +UNBOUND = UnboundItem('cross-interpreter', __name__) UNBOUND_ERROR = object() UNBOUND_REMOVE = object() @@ -84,6 +36,8 @@ def __repr__(self): def serialize_unbound(unbound): op = unbound + if isinstance(op, UnboundItem): + return _UNBOUND_CONSTANT_TO_FLAG[UNBOUND], try: flag = _UNBOUND_CONSTANT_TO_FLAG[op] except KeyError: diff --git a/Lib/concurrent/interpreters/_queues.py b/Lib/concurrent/interpreters/_queues.py index ee159d7de63827..15677bf4a3999f 100644 --- a/Lib/concurrent/interpreters/_queues.py +++ b/Lib/concurrent/interpreters/_queues.py @@ -46,12 +46,10 @@ class ItemInterpreterDestroyed(QueueError, _PICKLED = 1 -UNBOUND = _crossinterp.UnboundItem.singleton('queue', __name__) +UNBOUND = _crossinterp.UnboundItem('queue', __name__) def _serialize_unbound(unbound): - if unbound is UNBOUND: - unbound = _crossinterp.UNBOUND return _crossinterp.serialize_unbound(unbound) diff --git a/Lib/test/support/channels.py b/Lib/test/support/channels.py index fab1797659b312..ee8dfad107c700 100644 --- a/Lib/test/support/channels.py +++ b/Lib/test/support/channels.py @@ -28,12 +28,10 @@ class ItemInterpreterDestroyed(ChannelError, """Raised from get() and get_nowait().""" -UNBOUND = _crossinterp.UnboundItem.singleton('queue', __name__) +UNBOUND = _crossinterp.UnboundItem('queue', __name__) def _serialize_unbound(unbound): - if unbound is UNBOUND: - unbound = _crossinterp.UNBOUND return _crossinterp.serialize_unbound(unbound) 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..9a9e03b47d0035 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-10-15-27-58.gh-issue-142414.zTHgP-.rst @@ -0,0 +1 @@ +Fix spurious KeyError when concurrent.interpreters is reloaded after import.