pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/python/cpython/commit/fb864c76cd5e450e789a7b4095832e118cc49a39

55097560d244c08.css" /> gh-121723: Relax constraints on queue objects for `logging.handlers.Q… · python/cpython@fb864c7 · GitHub
Skip to content

Commit fb864c7

Browse files
authored
gh-121723: Relax constraints on queue objects for logging.handlers.QueueHandler. (GH-122154)
1 parent addbb73 commit fb864c7

File tree

4 files changed

+124
-50
lines changed

4 files changed

+124
-50
lines changed

Doc/library/logging.config.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -753,9 +753,12 @@ The ``queue`` and ``listener`` keys are optional.
753753

754754
If the ``queue`` key is present, the corresponding value can be one of the following:
755755

756-
* An actual instance of :class:`queue.Queue` or a subclass thereof. This is of course
757-
only possible if you are constructing or modifying the configuration dictionary in
758-
code.
756+
* An object implementing the :class:`queue.Queue` public API. For instance,
757+
this may be an actual instance of :class:`queue.Queue` or a subclass thereof,
758+
or a proxy obtained by :meth:`multiprocessing.managers.SyncManager.Queue`.
759+
760+
This is of course only possible if you are constructing or modifying
761+
the configuration dictionary in code.
759762

760763
* A string that resolves to a callable which, when called with no arguments, returns
761764
the :class:`queue.Queue` instance to use. That callable could be a

Lib/logging/config.py

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,33 @@ def as_tuple(self, value):
497497
value = tuple(value)
498498
return value
499499

500+
def _is_queue_like_object(obj):
501+
"""Check that *obj* implements the Queue API."""
502+
if isinstance(obj, queue.Queue):
503+
return True
504+
# defer importing multiprocessing as much as possible
505+
from multiprocessing.queues import Queue as MPQueue
506+
if isinstance(obj, MPQueue):
507+
return True
508+
# Depending on the multiprocessing start context, we cannot create
509+
# a multiprocessing.managers.BaseManager instance 'mm' to get the
510+
# runtime type of mm.Queue() or mm.JoinableQueue() (see gh-119819).
511+
#
512+
# Since we only need an object implementing the Queue API, we only
513+
# do a protocol check, but we do not use typing.runtime_checkable()
514+
# and typing.Protocol to reduce import time (see gh-121723).
515+
#
516+
# Ideally, we would have wanted to simply use strict type checking
517+
# instead of a protocol-based type checking since the latter does
518+
# not check the method signatures.
519+
queue_interface = [
520+
'empty', 'full', 'get', 'get_nowait',
521+
'put', 'put_nowait', 'join', 'qsize',
522+
'task_done',
523+
]
524+
return all(callable(getattr(obj, method, None))
525+
for method in queue_interface)
526+
500527
class DictConfigurator(BaseConfigurator):
501528
"""
502529
Configure logging using a dictionary-like object to describe the
@@ -791,32 +818,8 @@ def configure_handler(self, config):
791818
if '()' not in qspec:
792819
raise TypeError('Invalid queue specifier %r' % qspec)
793820
config['queue'] = self.configure_custom(dict(qspec))
794-
else:
795-
from multiprocessing.queues import Queue as MPQueue
796-
797-
if not isinstance(qspec, (queue.Queue, MPQueue)):
798-
# Safely check if 'qspec' is an instance of Manager.Queue
799-
# / Manager.JoinableQueue
800-
801-
from multiprocessing import Manager as MM
802-
from multiprocessing.managers import BaseProxy
803-
804-
# if it's not an instance of BaseProxy, it also can't be
805-
# an instance of Manager.Queue / Manager.JoinableQueue
806-
if isinstance(qspec, BaseProxy):
807-
# Sometimes manager or queue creation might fail
808-
# (e.g. see issue gh-120868). In that case, any
809-
# exception during the creation of these queues will
810-
# propagate up to the caller and be wrapped in a
811-
# `ValueError`, whose cause will indicate the details of
812-
# the failure.
813-
mm = MM()
814-
proxy_queue = mm.Queue()
815-
proxy_joinable_queue = mm.JoinableQueue()
816-
if not isinstance(qspec, (type(proxy_queue), type(proxy_joinable_queue))):
817-
raise TypeError('Invalid queue specifier %r' % qspec)
818-
else:
819-
raise TypeError('Invalid queue specifier %r' % qspec)
821+
elif not _is_queue_like_object(qspec):
822+
raise TypeError('Invalid queue specifier %r' % qspec)
820823

821824
if 'listener' in config:
822825
lspec = config['listener']

Lib/test/test_logging.py

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2368,6 +2368,26 @@ class CustomListener(logging.handlers.QueueListener):
23682368
class CustomQueue(queue.Queue):
23692369
pass
23702370

2371+
class CustomQueueProtocol:
2372+
def __init__(self, maxsize=0):
2373+
self.queue = queue.Queue(maxsize)
2374+
2375+
def __getattr__(self, attribute):
2376+
queue = object.__getattribute__(self, 'queue')
2377+
return getattr(queue, attribute)
2378+
2379+
class CustomQueueFakeProtocol(CustomQueueProtocol):
2380+
# An object implementing the Queue API (incorrect signatures).
2381+
# The object will be considered a valid queue class since we
2382+
# do not check the signatures (only callability of methods)
2383+
# but will NOT be usable in production since a TypeError will
2384+
# be raised due to a missing argument.
2385+
def empty(self, x):
2386+
pass
2387+
2388+
class CustomQueueWrongProtocol(CustomQueueProtocol):
2389+
empty = None
2390+
23712391
def queueMaker():
23722392
return queue.Queue()
23732393

@@ -3901,18 +3921,16 @@ def do_queuehandler_configuration(self, qspec, lspec):
39013921
@threading_helper.requires_working_threading()
39023922
@support.requires_subprocess()
39033923
def test_config_queue_handler(self):
3904-
q = CustomQueue()
3905-
dq = {
3906-
'()': __name__ + '.CustomQueue',
3907-
'maxsize': 10
3908-
}
3924+
qs = [CustomQueue(), CustomQueueProtocol()]
3925+
dqs = [{'()': f'{__name__}.{cls}', 'maxsize': 10}
3926+
for cls in ['CustomQueue', 'CustomQueueProtocol']]
39093927
dl = {
39103928
'()': __name__ + '.listenerMaker',
39113929
'arg1': None,
39123930
'arg2': None,
39133931
'respect_handler_level': True
39143932
}
3915-
qvalues = (None, __name__ + '.queueMaker', __name__ + '.CustomQueue', dq, q)
3933+
qvalues = (None, __name__ + '.queueMaker', __name__ + '.CustomQueue', *dqs, *qs)
39163934
lvalues = (None, __name__ + '.CustomListener', dl, CustomListener)
39173935
for qspec, lspec in itertools.product(qvalues, lvalues):
39183936
self.do_queuehandler_configuration(qspec, lspec)
@@ -3932,15 +3950,21 @@ def test_config_queue_handler(self):
39323950
@support.requires_subprocess()
39333951
@patch("multiprocessing.Manager")
39343952
def test_config_queue_handler_does_not_create_multiprocessing_manager(self, manager):
3935-
# gh-120868
3953+
# gh-120868, gh-121723
39363954

39373955
from multiprocessing import Queue as MQ
39383956

39393957
q1 = {"()": "queue.Queue", "maxsize": -1}
39403958
q2 = MQ()
39413959
q3 = queue.Queue()
3942-
3943-
for qspec in (q1, q2, q3):
3960+
# CustomQueueFakeProtocol passes the checks but will not be usable
3961+
# since the signatures are incompatible. Checking the Queue API
3962+
# without testing the type of the actual queue is a trade-off
3963+
# between usability and the work we need to do in order to safely
3964+
# check that the queue object correctly implements the API.
3965+
q4 = CustomQueueFakeProtocol()
3966+
3967+
for qspec in (q1, q2, q3, q4):
39443968
self.apply_config(
39453969
{
39463970
"version": 1,
@@ -3956,21 +3980,62 @@ def test_config_queue_handler_does_not_create_multiprocessing_manager(self, mana
39563980

39573981
@patch("multiprocessing.Manager")
39583982
def test_config_queue_handler_invalid_config_does_not_create_multiprocessing_manager(self, manager):
3959-
# gh-120868
3983+
# gh-120868, gh-121723
39603984

3961-
with self.assertRaises(ValueError):
3962-
self.apply_config(
3963-
{
3964-
"version": 1,
3965-
"handlers": {
3966-
"queue_listener": {
3967-
"class": "logging.handlers.QueueHandler",
3968-
"queue": object(),
3985+
for qspec in [object(), CustomQueueWrongProtocol()]:
3986+
with self.assertRaises(ValueError):
3987+
self.apply_config(
3988+
{
3989+
"version": 1,
3990+
"handlers": {
3991+
"queue_listener": {
3992+
"class": "logging.handlers.QueueHandler",
3993+
"queue": qspec,
3994+
},
39693995
},
3970-
},
3996+
}
3997+
)
3998+
manager.assert_not_called()
3999+
4000+
@skip_if_tsan_fork
4001+
@support.requires_subprocess()
4002+
@unittest.skipUnless(support.Py_DEBUG, "requires a debug build for testing"
4003+
"assertions in multiprocessing")
4004+
def test_config_queue_handler_multiprocessing_context(self):
4005+
# regression test for gh-121723
4006+
if support.MS_WINDOWS:
4007+
start_methods = ['spawn']
4008+
else:
4009+
start_methods = ['spawn', 'fork', 'forkserver']
4010+
for start_method in start_methods:
4011+
with self.subTest(start_method=start_method):
4012+
ctx = multiprocessing.get_context(start_method)
4013+
with ctx.Manager() as manager:
4014+
q = manager.Queue()
4015+
records = []
4016+
# use 1 process and 1 task per child to put 1 record
4017+
with ctx.Pool(1, initializer=self._mpinit_issue121723,
4018+
initargs=(q, "text"), maxtasksperchild=1):
4019+
records.append(q.get(timeout=60))
4020+
self.assertTrue(q.empty())
4021+
self.assertEqual(len(records), 1)
4022+
4023+
@staticmethod
4024+
def _mpinit_issue121723(qspec, message_to_log):
4025+
# static method for pickling support
4026+
logging.config.dictConfig({
4027+
'version': 1,
4028+
'disable_existing_loggers': True,
4029+
'handlers': {
4030+
'log_to_parent': {
4031+
'class': 'logging.handlers.QueueHandler',
4032+
'queue': qspec
39714033
}
3972-
)
3973-
manager.assert_not_called()
4034+
},
4035+
'root': {'handlers': ['log_to_parent'], 'level': 'DEBUG'}
4036+
})
4037+
# log a message (this creates a record put in the queue)
4038+
logging.getLogger().info(message_to_log)
39744039

39754040
@skip_if_tsan_fork
39764041
@support.requires_subprocess()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Make :func:`logging.config.dictConfig` accept any object implementing the
2+
Queue public API. See the :ref:`queue configuration <configure-queue>`
3+
section for details. Patch by Bénédikt Tran.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy