From 4b5642c84a2ed35a3dce128158fe2ecc7d04b58c Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Fri, 4 Apr 2025 19:19:20 -0700 Subject: [PATCH 01/10] Add __enter__/__exit__ to QueueListener --- Doc/howto/logging-cookbook.rst | 14 ++++++-------- Doc/library/logging.handlers.rst | 5 +++++ Doc/whatsnew/3.14.rst | 8 ++++++++ Lib/logging/handlers.py | 13 +++++++++++++ Lib/test/test_logging.py | 12 ++++++++++++ 5 files changed, 44 insertions(+), 8 deletions(-) diff --git a/Doc/howto/logging-cookbook.rst b/Doc/howto/logging-cookbook.rst index f08f45179980f3..ba93c75a4e705d 100644 --- a/Doc/howto/logging-cookbook.rst +++ b/Doc/howto/logging-cookbook.rst @@ -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: diff --git a/Doc/library/logging.handlers.rst b/Doc/library/logging.handlers.rst index ffb54591b3563b..a37321ab68ffbe 100644 --- a/Doc/library/logging.handlers.rst +++ b/Doc/library/logging.handlers.rst @@ -1148,6 +1148,11 @@ 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. + .. method:: dequeue(block) Dequeues a record and return it, optionally blocking. diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index d97874fe7a88d4..feb7f558f11412 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -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:`TBD`.) + + mimetypes --------- diff --git a/Lib/logging/handlers.py b/Lib/logging/handlers.py index 9abadbf5cdd1df..0571ed2356345a 100644 --- a/Lib/logging/handlers.py +++ b/Lib/logging/handlers.py @@ -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. diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index e34fe45fd68e52..9366d2e852fceb 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -4347,6 +4347,18 @@ 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.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_with_StreamHandler(self): From 6727664c3387531e909a9c655dfe75a6933d01c9 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 5 Apr 2025 02:22:52 +0000 Subject: [PATCH 02/10] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-04-05-02-22-49.gh-issue-132106.XMjhQJ.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-04-05-02-22-49.gh-issue-132106.XMjhQJ.rst diff --git a/Misc/NEWS.d/next/Library/2025-04-05-02-22-49.gh-issue-132106.XMjhQJ.rst b/Misc/NEWS.d/next/Library/2025-04-05-02-22-49.gh-issue-132106.XMjhQJ.rst new file mode 100644 index 00000000000000..376f986adcab83 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-05-02-22-49.gh-issue-132106.XMjhQJ.rst @@ -0,0 +1,2 @@ +:class:`logging.handlers.QueueListener` now implements the context +manager protocol, allowing it to be used in a :keyword:`with` statement. From 7d63d8d98f900c3a1298fc2637b7338b6f0a02b2 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Fri, 4 Apr 2025 19:24:26 -0700 Subject: [PATCH 03/10] Add gh number --- Doc/whatsnew/3.14.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index feb7f558f11412..8e63a5653f081a 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -791,7 +791,7 @@ 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:`TBD`.) + (Contributed by Charles Machalow in :gh:`132107`.) mimetypes From 72b6940486c42066299ecab88d671865a2c82868 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sat, 5 Apr 2025 08:32:54 -0700 Subject: [PATCH 04/10] Update Doc/whatsnew/3.14.rst Co-authored-by: Brian Schubert --- Doc/whatsnew/3.14.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 8e63a5653f081a..82c4962d9d4833 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -791,7 +791,7 @@ 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:`132107`.) + (Contributed by Charles Machalow in :gh:`132106`.) mimetypes From 71997f98dca24eb4111bc9295cdcaf5d48c045a8 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sat, 5 Apr 2025 08:33:15 -0700 Subject: [PATCH 05/10] Update Lib/test/test_logging.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_logging.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 9366d2e852fceb..af67964a5dcdd3 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -4352,6 +4352,7 @@ def test_queue_listener(self): def test_queue_listener_context_manager(self): handler = TestHandler(support.Matcher()) with logging.handlers.QueueListener(self.queue, handler) as listener: + self.assertIsIsintance(listener, logging.handlers.QueueListener) self.assertIsNotNone(listener._thread) self.assertIsNone(listener._thread) From 1fe89a8d517441bac08c526c163f7ca7ef54a63b Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sat, 5 Apr 2025 08:41:45 -0700 Subject: [PATCH 06/10] PR feedback. While here, make it so start can't get called again while running (prevents a thread leak) Update docs --- Doc/library/logging.handlers.rst | 6 ++++++ Lib/logging/handlers.py | 2 ++ Lib/test/test_logging.py | 15 ++++++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Doc/library/logging.handlers.rst b/Doc/library/logging.handlers.rst index a37321ab68ffbe..72312b512a5884 100644 --- a/Doc/library/logging.handlers.rst +++ b/Doc/library/logging.handlers.rst @@ -1152,6 +1152,8 @@ possible, while any potentially slow operations (such as sending an email via :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) @@ -1184,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. diff --git a/Lib/logging/handlers.py b/Lib/logging/handlers.py index 0571ed2356345a..5b3a12faf8b192 100644 --- a/Lib/logging/handlers.py +++ b/Lib/logging/handlers.py @@ -1561,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() diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index af67964a5dcdd3..75315af64afef1 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -4352,7 +4352,7 @@ def test_queue_listener(self): def test_queue_listener_context_manager(self): handler = TestHandler(support.Matcher()) with logging.handlers.QueueListener(self.queue, handler) as listener: - self.assertIsIsintance(listener, logging.handlers.QueueListener) + self.assertIsInstance(listener, logging.handlers.QueueListener) self.assertIsNotNone(listener._thread) self.assertIsNone(listener._thread) @@ -4360,6 +4360,19 @@ def test_queue_listener_context_manager(self): 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): From d802f2744387f6f3882f0fec2be25fc3b22c5114 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sat, 5 Apr 2025 19:26:16 -0700 Subject: [PATCH 07/10] Split out double-starting the QueueListener --- Doc/library/logging.handlers.rst | 4 ---- Lib/logging/handlers.py | 2 -- Lib/test/test_logging.py | 13 ------------- 3 files changed, 19 deletions(-) diff --git a/Doc/library/logging.handlers.rst b/Doc/library/logging.handlers.rst index 72312b512a5884..b737fe311dfb6e 100644 --- a/Doc/library/logging.handlers.rst +++ b/Doc/library/logging.handlers.rst @@ -1186,10 +1186,6 @@ 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. diff --git a/Lib/logging/handlers.py b/Lib/logging/handlers.py index 5b3a12faf8b192..0571ed2356345a 100644 --- a/Lib/logging/handlers.py +++ b/Lib/logging/handlers.py @@ -1561,8 +1561,6 @@ 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() diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 75315af64afef1..e07816d313ad02 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -4360,19 +4360,6 @@ def test_queue_listener_context_manager(self): 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): From d894fa540d7acc538502ed2dd6d2eca30749620b Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 6 Apr 2025 09:52:24 -0700 Subject: [PATCH 08/10] PR feedback: move context manager usage of QueueListener into a versionchanged block --- Doc/howto/logging-cookbook.rst | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/Doc/howto/logging-cookbook.rst b/Doc/howto/logging-cookbook.rst index ba93c75a4e705d..54644b001416a6 100644 --- a/Doc/howto/logging-cookbook.rst +++ b/Doc/howto/logging-cookbook.rst @@ -589,16 +589,18 @@ 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) - 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!') + 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() which, when run, will produce: @@ -624,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 From bfafcf04df3383ba91fc8fba14aff640154d3f5c Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Thu, 10 Apr 2025 22:17:20 -0700 Subject: [PATCH 09/10] PR feedback. Remove skipUnless for QueueListener since threading is always available now --- Lib/test/test_logging.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index e07816d313ad02..11f6b64abe28fb 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -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) @@ -4347,8 +4345,6 @@ 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: @@ -4360,8 +4356,6 @@ def test_queue_listener_context_manager(self): listener.stop() self.assertIsNone(listener._thread) - @unittest.skipUnless(hasattr(logging.handlers, 'QueueListener'), - 'logging.handlers.QueueListener required for this test') 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) @@ -4376,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) From c8001b64854e51aaf4eecd83e1608a71649feb88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 12 Apr 2025 13:35:59 +0200 Subject: [PATCH 10/10] align code-block indentation --- Doc/howto/logging-cookbook.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Doc/howto/logging-cookbook.rst b/Doc/howto/logging-cookbook.rst index 54644b001416a6..661d6c290f6186 100644 --- a/Doc/howto/logging-cookbook.rst +++ b/Doc/howto/logging-cookbook.rst @@ -630,14 +630,14 @@ which, when run, will produce: 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 + .. 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: