Skip to content

gh-132106: Allow logging.handlers.QueueListener to be used as a context manager #132107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 12, 2025
13 changes: 13 additions & 0 deletions Doc/howto/logging-cookbook.rst
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,19 @@ which, when run, will produce:
of each message with the handler's level, and only passes a message to a
handler if it's appropriate to do so.

.. versionchanged:: next
The :class:`QueueListener` can be started (and stopped) via the
:keyword:`with` statement. For example:

.. code-block:: python

with QueueListener(que, handler) as listener:
# The queue listener automatically starts
# when the 'with' block is entered.
pass
# The queue listener automatically stops once
# the 'with' block is exited.

.. _network-logging:

Sending and receiving logging events across a network
Expand Down
7 changes: 7 additions & 0 deletions Doc/library/logging.handlers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,13 @@ possible, while any potentially slow operations (such as sending an email via
.. versionchanged:: 3.5
The ``respect_handler_level`` argument was added.

.. versionchanged:: next
:class:`QueueListener` can now be used as a context manager via
:keyword:`with`. When entering the context, the listener is started. When
exiting the context, the listener is stopped.
:meth:`~contextmanager.__enter__` returns the
:class:`QueueListener` object.

.. method:: dequeue(block)

Dequeues a record and return it, optionally blocking.
Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,14 @@ linecache
(Contributed by Tian Gao in :gh:`131638`.)


logging.handlers
----------------

* :class:`logging.handlers.QueueListener` now implements the context
manager protocol, allowing it to be used in a :keyword:`with` statement.
(Contributed by Charles Machalow in :gh:`132106`.)


mimetypes
---------

Expand Down
13 changes: 13 additions & 0 deletions Lib/logging/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1532,6 +1532,19 @@ def __init__(self, queue, *handlers, respect_handler_level=False):
self._thread = None
self.respect_handler_level = respect_handler_level

def __enter__(self):
"""
For use as a context manager. Starts the listener.
"""
self.start()
return self

def __exit__(self, *args):
"""
For use as a context manager. Stops the listener.
"""
self.stop()

def dequeue(self, block):
"""
Dequeue a record and return it, optionally blocking.
Expand Down
17 changes: 11 additions & 6 deletions Lib/test/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -4311,8 +4311,6 @@ def test_formatting(self):
self.assertEqual(formatted_msg, log_record.msg)
self.assertEqual(formatted_msg, log_record.message)

@unittest.skipUnless(hasattr(logging.handlers, 'QueueListener'),
'logging.handlers.QueueListener required for this test')
def test_queue_listener(self):
handler = TestHandler(support.Matcher())
listener = logging.handlers.QueueListener(self.queue, handler)
Expand Down Expand Up @@ -4347,8 +4345,17 @@ def test_queue_listener(self):
self.assertTrue(handler.matches(levelno=logging.CRITICAL, message='6'))
handler.close()

@unittest.skipUnless(hasattr(logging.handlers, 'QueueListener'),
'logging.handlers.QueueListener required for this test')
def test_queue_listener_context_manager(self):
handler = TestHandler(support.Matcher())
with logging.handlers.QueueListener(self.queue, handler) as listener:
self.assertIsInstance(listener, logging.handlers.QueueListener)
self.assertIsNotNone(listener._thread)
self.assertIsNone(listener._thread)

# doesn't hurt to call stop() more than once.
listener.stop()
self.assertIsNone(listener._thread)

def test_queue_listener_with_StreamHandler(self):
# Test that traceback and stack-info only appends once (bpo-34334, bpo-46755).
listener = logging.handlers.QueueListener(self.queue, self.root_hdlr)
Expand All @@ -4363,8 +4370,6 @@ def test_queue_listener_with_StreamHandler(self):
self.assertEqual(self.stream.getvalue().strip().count('Traceback'), 1)
self.assertEqual(self.stream.getvalue().strip().count('Stack'), 1)

@unittest.skipUnless(hasattr(logging.handlers, 'QueueListener'),
'logging.handlers.QueueListener required for this test')
def test_queue_listener_with_multiple_handlers(self):
# Test that queue handler format doesn't affect other handler formats (bpo-35726).
self.que_hdlr.setFormatter(self.root_formatter)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:class:`logging.handlers.QueueListener` now implements the context
manager protocol, allowing it to be used in a :keyword:`with` statement.
Loading