[3.13] gh-124653: Relax (again) detection of queue API for logging handlers (GH-124897) (GH-125059)

(cherry picked from commit 7ffe94fb24)
This commit is contained in:
Miss Islington (bot) 2024-10-08 08:23:40 +02:00 committed by GitHub
parent 761c3b280b
commit 1e820e63e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 78 additions and 55 deletions

View file

@ -753,16 +753,17 @@ The ``queue`` and ``listener`` keys are optional.
If the ``queue`` key is present, the corresponding value can be one of the following: If the ``queue`` key is present, the corresponding value can be one of the following:
* An object implementing the :class:`queue.Queue` public API. For instance, * An object implementing the :meth:`Queue.put_nowait <queue.Queue.put_nowait>`
this may be an actual instance of :class:`queue.Queue` or a subclass thereof, and :meth:`Queue.get <queue.Queue.get>` public API. For instance, this may be
or a proxy obtained by :meth:`multiprocessing.managers.SyncManager.Queue`. an actual instance of :class:`queue.Queue` or a subclass thereof, or a proxy
obtained by :meth:`multiprocessing.managers.SyncManager.Queue`.
This is of course only possible if you are constructing or modifying This is of course only possible if you are constructing or modifying
the configuration dictionary in code. the configuration dictionary in code.
* A string that resolves to a callable which, when called with no arguments, returns * A string that resolves to a callable which, when called with no arguments, returns
the :class:`queue.Queue` instance to use. That callable could be a the queue instance to use. That callable could be a :class:`queue.Queue` subclass
:class:`queue.Queue` subclass or a function which returns a suitable queue instance, or a function which returns a suitable queue instance,
such as ``my.module.queue_factory()``. such as ``my.module.queue_factory()``.
* A dict with a ``'()'`` key which is constructed in the usual way as discussed in * A dict with a ``'()'`` key which is constructed in the usual way as discussed in

View file

@ -499,7 +499,7 @@ class BaseConfigurator(object):
def _is_queue_like_object(obj): def _is_queue_like_object(obj):
"""Check that *obj* implements the Queue API.""" """Check that *obj* implements the Queue API."""
if isinstance(obj, queue.Queue): if isinstance(obj, (queue.Queue, queue.SimpleQueue)):
return True return True
# defer importing multiprocessing as much as possible # defer importing multiprocessing as much as possible
from multiprocessing.queues import Queue as MPQueue from multiprocessing.queues import Queue as MPQueue
@ -516,13 +516,13 @@ def _is_queue_like_object(obj):
# Ideally, we would have wanted to simply use strict type checking # Ideally, we would have wanted to simply use strict type checking
# instead of a protocol-based type checking since the latter does # instead of a protocol-based type checking since the latter does
# not check the method signatures. # not check the method signatures.
queue_interface = [ #
'empty', 'full', 'get', 'get_nowait', # Note that only 'put_nowait' and 'get' are required by the logging
'put', 'put_nowait', 'join', 'qsize', # queue handler and queue listener (see gh-124653) and that other
'task_done', # methods are either optional or unused.
] minimal_queue_interface = ['put_nowait', 'get']
return all(callable(getattr(obj, method, None)) return all(callable(getattr(obj, method, None))
for method in queue_interface) for method in minimal_queue_interface)
class DictConfigurator(BaseConfigurator): class DictConfigurator(BaseConfigurator):
""" """

View file

@ -2376,16 +2376,22 @@ class CustomQueueProtocol:
return getattr(queue, attribute) return getattr(queue, attribute)
class CustomQueueFakeProtocol(CustomQueueProtocol): class CustomQueueFakeProtocol(CustomQueueProtocol):
# An object implementing the Queue API (incorrect signatures). # An object implementing the minimial Queue API for
# the logging module but with incorrect signatures.
#
# The object will be considered a valid queue class since we # The object will be considered a valid queue class since we
# do not check the signatures (only callability of methods) # do not check the signatures (only callability of methods)
# but will NOT be usable in production since a TypeError will # but will NOT be usable in production since a TypeError will
# be raised due to a missing argument. # be raised due to the extra argument in 'put_nowait'.
def empty(self, x): def put_nowait(self):
pass pass
class CustomQueueWrongProtocol(CustomQueueProtocol): class CustomQueueWrongProtocol(CustomQueueProtocol):
empty = None put_nowait = None
class MinimalQueueProtocol:
def put_nowait(self, x): pass
def get(self): pass
def queueMaker(): def queueMaker():
return queue.Queue() return queue.Queue()
@ -3945,56 +3951,70 @@ class ConfigDictTest(BaseTest):
msg = str(ctx.exception) msg = str(ctx.exception)
self.assertEqual(msg, "Unable to configure handler 'ah'") self.assertEqual(msg, "Unable to configure handler 'ah'")
def _apply_simple_queue_listener_configuration(self, qspec):
self.apply_config({
"version": 1,
"handlers": {
"queue_listener": {
"class": "logging.handlers.QueueHandler",
"queue": qspec,
},
},
})
@threading_helper.requires_working_threading() @threading_helper.requires_working_threading()
@support.requires_subprocess() @support.requires_subprocess()
@patch("multiprocessing.Manager") @patch("multiprocessing.Manager")
def test_config_queue_handler_does_not_create_multiprocessing_manager(self, manager): def test_config_queue_handler_does_not_create_multiprocessing_manager(self, manager):
# gh-120868, gh-121723 # gh-120868, gh-121723, gh-124653
from multiprocessing import Queue as MQ for qspec in [
{"()": "queue.Queue", "maxsize": -1},
q1 = {"()": "queue.Queue", "maxsize": -1} queue.Queue(),
q2 = MQ() # queue.SimpleQueue does not inherit from queue.Queue
q3 = queue.Queue() queue.SimpleQueue(),
# CustomQueueFakeProtocol passes the checks but will not be usable # CustomQueueFakeProtocol passes the checks but will not be usable
# since the signatures are incompatible. Checking the Queue API # since the signatures are incompatible. Checking the Queue API
# without testing the type of the actual queue is a trade-off # without testing the type of the actual queue is a trade-off
# between usability and the work we need to do in order to safely # between usability and the work we need to do in order to safely
# check that the queue object correctly implements the API. # check that the queue object correctly implements the API.
q4 = CustomQueueFakeProtocol() CustomQueueFakeProtocol(),
MinimalQueueProtocol(),
for qspec in (q1, q2, q3, q4): ]:
self.apply_config( with self.subTest(qspec=qspec):
{ self._apply_simple_queue_listener_configuration(qspec)
"version": 1, manager.assert_not_called()
"handlers": {
"queue_listener": {
"class": "logging.handlers.QueueHandler",
"queue": qspec,
},
},
}
)
manager.assert_not_called()
@patch("multiprocessing.Manager") @patch("multiprocessing.Manager")
def test_config_queue_handler_invalid_config_does_not_create_multiprocessing_manager(self, manager): def test_config_queue_handler_invalid_config_does_not_create_multiprocessing_manager(self, manager):
# gh-120868, gh-121723 # gh-120868, gh-121723
for qspec in [object(), CustomQueueWrongProtocol()]: for qspec in [object(), CustomQueueWrongProtocol()]:
with self.assertRaises(ValueError): with self.subTest(qspec=qspec), self.assertRaises(ValueError):
self.apply_config( self._apply_simple_queue_listener_configuration(qspec)
{ manager.assert_not_called()
"version": 1,
"handlers": { @skip_if_tsan_fork
"queue_listener": { @support.requires_subprocess()
"class": "logging.handlers.QueueHandler", @unittest.skipUnless(support.Py_DEBUG, "requires a debug build for testing"
"queue": qspec, " assertions in multiprocessing")
}, def test_config_reject_simple_queue_handler_multiprocessing_context(self):
}, # multiprocessing.SimpleQueue does not implement 'put_nowait'
} # and thus cannot be used as a queue-like object (gh-124653)
)
manager.assert_not_called() import multiprocessing
if support.MS_WINDOWS:
start_methods = ['spawn']
else:
start_methods = ['spawn', 'fork', 'forkserver']
for start_method in start_methods:
with self.subTest(start_method=start_method):
ctx = multiprocessing.get_context(start_method)
qspec = ctx.SimpleQueue()
with self.assertRaises(ValueError):
self._apply_simple_queue_listener_configuration(qspec)
@skip_if_tsan_fork @skip_if_tsan_fork
@support.requires_subprocess() @support.requires_subprocess()

View file

@ -0,0 +1,2 @@
Fix detection of the minimal Queue API needed by the :mod:`logging` module.
Patch by Bénédikt Tran.