Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Make improvements based on feedback.
  • Loading branch information
vsajip committed May 30, 2022
commit 16778fe051c5e030d0fc11614c59bd3ef2f8e408
20 changes: 16 additions & 4 deletions Doc/library/logging.config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -671,15 +671,15 @@ is normally used in conjunction with a :class:`~logging.handlers.QueueListener`,
can configure both together. After the configuration, the ``QueueListener`` instance
will be available as the :attr:`listener` attribute of the created handler, and that
in turn will be available to you using :func:`~logging.getHandlerByName` and passing
whatever name you have used for the ``QueueHandler`` in your configuration. The
the name you have used for the ``QueueHandler`` in your configuration. The
dictionary schema for configuring the pair is shown in the example YAML snippet below.

.. code-block:: yaml

handlers:
qhand:
class: logging.handlers.QueueHandler
queue: my.module.queuefactory
queue: my.module.queue_factory
listener: my.package.CustomListener
handlers:
- hand_name_1
Expand All @@ -696,7 +696,12 @@ If the ``queue`` key is present, the corresponding value can be one of the follo

* 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
:class:`queue.Queue` subclass or a function which returns a suitable queue instance.
:class:`queue.Queue` subclass or a function which returns a suitable queue instance,
such as :func:`my.module.queue_factory`.

* A dict with a ``()`` key which is constructed in the usual way as discussed in
:ref:`logging-config-dict-userdef`. The result of this construction should be a
:class:`queue.Queue` instance.

If the ``queue`` key is absent, a standard unbounded :class:`queue.Queue` instance is
created and used.
Expand All @@ -707,13 +712,20 @@ If the ``listener`` key is present, the corresponding value can be one of the fo
possible if you are constructing or modifying the configuration dictionary in
code.

* A string which resolves to a class which is a subclass of ``QueueListener``, such as
``'my,package.CustomListener'``.

* A dict with a ``()`` key which is constructed in the usual way as discussed in
:ref:`logging-config-dict-userdef`. The result of this construction should be a
callable with the same signature as the ``QueueListener`` initializer.
Comment on lines +720 to +721

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The result of this construction should be a callable with the same signature...

Does that mean () will be resolved, then called with the dict args and the result of that call should return a callable again?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The callable will be called just as QueueListener would be.

@spacemanspiff2007 spacemanspiff2007 Jun 3, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that's different than the normal behavior of '()':

In order to provide complete flexibility for user-defined object instantiation, the user needs to provide a ‘factory’ - 
a callable which is called with a configuration dictionary and which returns the instantiated object.

from docs.
Note that the return normally is not a callable but an instantiated object.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but in this case the value to be returned is documented explicitly. How can it be returned as an instantiated object in this case, if it has no access to the queue?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also note that "instantiated object" is a generic term. It does not imply any specific type of object, and a callable is also an instantiated object.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be possible to provide an example in the docs?
That'll make things more clear.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to submit a documentation PR with what you think should be in there, and I'll take a look.


If the ``listener`` key is absent, :class:`logging.handlers.QueueListener` is used.

The values under the ``handlers`` key are the names of other handlers in the
configuration (not shown in the above snippet) which will be passed to the queue
listener.

Any custom queue handler and listener classes will need to deal with the same
Any custom queue handler and listener classes will need to be defined with the same
initialization signatures as :class:`~logging.handlers.QueueHandler` and
:class:`~logging.handlers.QueueListener`.

Expand Down
48 changes: 35 additions & 13 deletions Lib/logging/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2001-2019 by Vinay Sajip. All Rights Reserved.
# Copyright 2001-2022 by Vinay Sajip. All Rights Reserved.
#
# Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose and without fee is hereby granted,
Expand All @@ -19,7 +19,7 @@
is based on PEP 282 and comments thereto in comp.lang.python, and influenced
by Apache's log4j system.

Copyright (C) 2001-2019 Vinay Sajip. All Rights Reserved.
Copyright (C) 2001-2022 Vinay Sajip. All Rights Reserved.

To use, simply 'import logging' and log away!
"""
Expand Down Expand Up @@ -758,26 +758,48 @@ def configure_handler(self, config):
# Another special case for handler which refers to other handlers
if 'handlers' not in config:
raise ValueError('No handlers specified for a QueueHandler')
if 'queue' in config and\
not isinstance(qspec := config['queue'], queue.Queue):
q = self.resolve(qspec)
if not callable(q):
raise TypeError('Invalid queue specifier %r' % qspec)
config['queue'] = q()
if 'queue' in config:
qspec = config['queue']
if not isinstance(qspec, queue.Queue):
if isinstance(qspec, str):
q = self.resolve(qspec)
if not callable(q):
raise TypeError('Invalid queue specifier %r' % qspec)
q = q()
elif isinstance(qspec, dict):
if '()' not in qspec:
raise TypeError('Invalid queue specifier %r' % qspec)
q = self.configure_custom(dict(qspec))
else:
raise TypeError('Invalid queue specifier %r' % qspec)
config['queue'] = q
if 'listener' in config:
lspec = config['listener']
if isinstance(lspec, str):
listener = self.resolve(lspec)
if isinstance(lspec, type):
if not issubclass(lspec, logging.handlers.QueueListener):
raise TypeError('Invalid listener specifier %r' % lspec)
else:
if isinstance(lspec, str):
listener = self.resolve(lspec)
if isinstance(listener, type) and\
not issubclass(listener, logging.handlers.QueueListener):
raise TypeError('Invalid listener specifier %r' % lspec)
elif isinstance(lspec, dict):
if '()' not in lspec:
raise TypeError('Invalid listener specifier %r' % lspec)
listener = self.configure_custom(dict(lspec))
else:
raise TypeError('Invalid listener specifier %r' % lspec)
if not callable(listener):
raise TypeError('Invalid listener specifier %r' % lspec)
config['listener'] = listener
elif not issubclass(lspec, logging.handlers.QueueListener):
raise TypeError('Invalid listener spec %r' % lspec)
hlist = []
try:
for hn in config['handlers']:
h = self.config['handlers'][hn]
if not isinstance(h, logging.Handler):
config.update(config_copy) # restore for deferred cfg
raise TypeError('needed handler %r '
raise TypeError('Required handler %r '
'is not configured yet' % hn)
hlist.append(h)
except Exception as e:
Expand Down
20 changes: 18 additions & 2 deletions Lib/test/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -2242,6 +2242,12 @@ class CustomQueue(queue.Queue):
def queueMaker():
return queue.Queue()

def listenerMaker(arg1, arg2, respect_handler_level=False):
def func(queue, *handlers, **kwargs):
kwargs.setdefault('respect_handler_level', respect_handler_level)
return CustomListener(queue, *handlers, **kwargs)
return func

class ConfigDictTest(BaseTest):

"""Reading logging config from a dictionary."""
Expand Down Expand Up @@ -3542,8 +3548,18 @@ class NotAFilter: pass

def test_config_queue_handler(self):
q = CustomQueue()
qvalues = (None, __name__ + '.queueMaker', __name__ + '.CustomQueue', q)
lvalues = (None, __name__ + '.CustomListener', CustomListener)
dq = {
'()': __name__ + '.CustomQueue',
'maxsize': 10
}
dl = {
'()': __name__ + '.listenerMaker',
'arg1': None,
'arg2': None,
'respect_handler_level': True
}
qvalues = (None, __name__ + '.queueMaker', __name__ + '.CustomQueue', dq, q)
lvalues = (None, __name__ + '.CustomListener', dl, CustomListener)
for qspec, lspec in itertools.product(qvalues, lvalues):
cd = copy.deepcopy(self.config_queue_handler)
fn = make_temp_file('.log', 'test_logging-cqh-')
Expand Down