Skip to content
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

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
14 changes: 6 additions & 8 deletions Doc/howto/logging-cookbook.rst
Original file line number Diff line number Diff line change
Expand Up @@ -589,18 +589,16 @@ An example of using these two classes follows (imports omitted)::
que = queue.Queue(-1) # no limit on size
queue_handler = QueueHandler(que)
handler = logging.StreamHandler()
listener = QueueListener(que, handler)
root = logging.getLogger()
root.addHandler(queue_handler)
formatter = logging.Formatter('%(threadName)s: %(message)s')
handler.setFormatter(formatter)
listener.start()
# The log output will display the thread which generated
# the event (the main thread) rather than the internal
# thread which monitors the internal queue. This is what
# you want to happen.
root.warning('Look out!')
listener.stop()
with QueueListener(que, handler) as listener:
# The log output will display the thread which generated
# the event (the main thread) rather than the internal
# thread which monitors the internal queue. This is what
# you want to happen.
root.warning('Look out!')

which, when run, will produce:

Expand Down
11 changes: 11 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 Expand Up @@ -1179,6 +1186,10 @@ possible, while any potentially slow operations (such as sending an email via
This starts up a background thread to monitor the queue for
LogRecords to process.

.. versionchanged:: next
Raises :exc:`RuntimeError` if called and the listener is already
running.

.. method:: stop()

Stops the listener.
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
15 changes: 15 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 All @@ -1548,6 +1561,8 @@ def start(self):
This starts up a background thread to monitor the queue for
LogRecords to process.
"""
if self._thread is not None:
raise RuntimeError("Listener already started")
self._thread = t = threading.Thread(target=self._monitor)
t.daemon = True
t.start()
Expand Down
26 changes: 26 additions & 0 deletions Lib/test/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -4347,6 +4347,32 @@ 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)

@unittest.skipUnless(hasattr(logging.handlers, 'QueueListener'),
'logging.handlers.QueueListener required for this test')
def test_queue_listener_multi_start(self):
handler = TestHandler(support.Matcher())
with logging.handlers.QueueListener(self.queue, handler) as listener:
self.assertRaises(RuntimeError, listener.start)

with listener:
self.assertRaises(RuntimeError, listener.start)

listener.start()
listener.stop()

@unittest.skipUnless(hasattr(logging.handlers, 'QueueListener'),
'logging.handlers.QueueListener required for this test')
def test_queue_listener_with_StreamHandler(self):
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