mirror of
https://github.com/python/cpython.git
synced 2025-08-01 07:33:08 +00:00
[3.13] gh-121723: Relax constraints on queue objects for logging.handlers.QueueHandler
. (GH-122154) (GH-122603)
(cherry picked from commit fb864c76cd
)
This commit is contained in:
parent
9f488f9358
commit
56435a88c4
4 changed files with 123 additions and 49 deletions
|
@ -2367,6 +2367,26 @@ class CustomListener(logging.handlers.QueueListener):
|
|||
class CustomQueue(queue.Queue):
|
||||
pass
|
||||
|
||||
class CustomQueueProtocol:
|
||||
def __init__(self, maxsize=0):
|
||||
self.queue = queue.Queue(maxsize)
|
||||
|
||||
def __getattr__(self, attribute):
|
||||
queue = object.__getattribute__(self, 'queue')
|
||||
return getattr(queue, attribute)
|
||||
|
||||
class CustomQueueFakeProtocol(CustomQueueProtocol):
|
||||
# An object implementing the Queue API (incorrect signatures).
|
||||
# The object will be considered a valid queue class since we
|
||||
# do not check the signatures (only callability of methods)
|
||||
# but will NOT be usable in production since a TypeError will
|
||||
# be raised due to a missing argument.
|
||||
def empty(self, x):
|
||||
pass
|
||||
|
||||
class CustomQueueWrongProtocol(CustomQueueProtocol):
|
||||
empty = None
|
||||
|
||||
def queueMaker():
|
||||
return queue.Queue()
|
||||
|
||||
|
@ -3900,18 +3920,16 @@ class ConfigDictTest(BaseTest):
|
|||
@threading_helper.requires_working_threading()
|
||||
@support.requires_subprocess()
|
||||
def test_config_queue_handler(self):
|
||||
q = CustomQueue()
|
||||
dq = {
|
||||
'()': __name__ + '.CustomQueue',
|
||||
'maxsize': 10
|
||||
}
|
||||
qs = [CustomQueue(), CustomQueueProtocol()]
|
||||
dqs = [{'()': f'{__name__}.{cls}', 'maxsize': 10}
|
||||
for cls in ['CustomQueue', 'CustomQueueProtocol']]
|
||||
dl = {
|
||||
'()': __name__ + '.listenerMaker',
|
||||
'arg1': None,
|
||||
'arg2': None,
|
||||
'respect_handler_level': True
|
||||
}
|
||||
qvalues = (None, __name__ + '.queueMaker', __name__ + '.CustomQueue', dq, q)
|
||||
qvalues = (None, __name__ + '.queueMaker', __name__ + '.CustomQueue', *dqs, *qs)
|
||||
lvalues = (None, __name__ + '.CustomListener', dl, CustomListener)
|
||||
for qspec, lspec in itertools.product(qvalues, lvalues):
|
||||
self.do_queuehandler_configuration(qspec, lspec)
|
||||
|
@ -3931,15 +3949,21 @@ class ConfigDictTest(BaseTest):
|
|||
@support.requires_subprocess()
|
||||
@patch("multiprocessing.Manager")
|
||||
def test_config_queue_handler_does_not_create_multiprocessing_manager(self, manager):
|
||||
# gh-120868
|
||||
# gh-120868, gh-121723
|
||||
|
||||
from multiprocessing import Queue as MQ
|
||||
|
||||
q1 = {"()": "queue.Queue", "maxsize": -1}
|
||||
q2 = MQ()
|
||||
q3 = queue.Queue()
|
||||
# CustomQueueFakeProtocol passes the checks but will not be usable
|
||||
# since the signatures are incompatible. Checking the Queue API
|
||||
# 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
|
||||
# check that the queue object correctly implements the API.
|
||||
q4 = CustomQueueFakeProtocol()
|
||||
|
||||
for qspec in (q1, q2, q3):
|
||||
for qspec in (q1, q2, q3, q4):
|
||||
self.apply_config(
|
||||
{
|
||||
"version": 1,
|
||||
|
@ -3955,21 +3979,62 @@ class ConfigDictTest(BaseTest):
|
|||
|
||||
@patch("multiprocessing.Manager")
|
||||
def test_config_queue_handler_invalid_config_does_not_create_multiprocessing_manager(self, manager):
|
||||
# gh-120868
|
||||
# gh-120868, gh-121723
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
self.apply_config(
|
||||
{
|
||||
"version": 1,
|
||||
"handlers": {
|
||||
"queue_listener": {
|
||||
"class": "logging.handlers.QueueHandler",
|
||||
"queue": object(),
|
||||
for qspec in [object(), CustomQueueWrongProtocol()]:
|
||||
with self.assertRaises(ValueError):
|
||||
self.apply_config(
|
||||
{
|
||||
"version": 1,
|
||||
"handlers": {
|
||||
"queue_listener": {
|
||||
"class": "logging.handlers.QueueHandler",
|
||||
"queue": qspec,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
manager.assert_not_called()
|
||||
|
||||
@skip_if_tsan_fork
|
||||
@support.requires_subprocess()
|
||||
@unittest.skipUnless(support.Py_DEBUG, "requires a debug build for testing"
|
||||
"assertions in multiprocessing")
|
||||
def test_config_queue_handler_multiprocessing_context(self):
|
||||
# regression test for gh-121723
|
||||
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)
|
||||
with ctx.Manager() as manager:
|
||||
q = manager.Queue()
|
||||
records = []
|
||||
# use 1 process and 1 task per child to put 1 record
|
||||
with ctx.Pool(1, initializer=self._mpinit_issue121723,
|
||||
initargs=(q, "text"), maxtasksperchild=1):
|
||||
records.append(q.get(timeout=60))
|
||||
self.assertTrue(q.empty())
|
||||
self.assertEqual(len(records), 1)
|
||||
|
||||
@staticmethod
|
||||
def _mpinit_issue121723(qspec, message_to_log):
|
||||
# static method for pickling support
|
||||
logging.config.dictConfig({
|
||||
'version': 1,
|
||||
'disable_existing_loggers': True,
|
||||
'handlers': {
|
||||
'log_to_parent': {
|
||||
'class': 'logging.handlers.QueueHandler',
|
||||
'queue': qspec
|
||||
}
|
||||
)
|
||||
manager.assert_not_called()
|
||||
},
|
||||
'root': {'handlers': ['log_to_parent'], 'level': 'DEBUG'}
|
||||
})
|
||||
# log a message (this creates a record put in the queue)
|
||||
logging.getLogger().info(message_to_log)
|
||||
|
||||
@skip_if_tsan_fork
|
||||
@support.requires_subprocess()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue