From 8d071b998983bccde9e0363ffe833d1cae96cf09 Mon Sep 17 00:00:00 2001 From: Kuang Yu Heng Date: Sun, 18 Jan 2026 00:18:23 +0800 Subject: [PATCH 1/2] gh-89900: Add option to not disable existing handlers for assertLogs added documentation for new keep_handlers kwarg for assertLogs function in unittest --- Doc/library/unittest.rst | 15 ++++++++++++++- Lib/test/test_unittest/test_case.py | 22 ++++++++++++++++++++++ Lib/unittest/_log.py | 11 ++++++++--- Lib/unittest/case.py | 9 +++++++-- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index 0bc0a953fd921c..85d6071d5c2e10 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -1131,7 +1131,7 @@ Test cases .. versionchanged:: 3.3 Added the *msg* keyword argument when used as a context manager. - .. method:: assertLogs(logger=None, level=None, formatter=None) + .. method:: assertLogs(logger=None, level=None, formatter=None, keep_handlers=False) A context manager to test that at least one message is logged on the *logger* or one of its children, with at least the given @@ -1150,6 +1150,15 @@ Test cases The default is a formatter with format string ``"%(levelname)s:%(name)s:%(message)s"`` + If given, *keep_handlers* should be a boolean value. If ``True``, + existing handlers attached to the logger will be preserved and + continue to function normally alongside the capturing handler. + If ``False`` (the default), existing handlers are temporarily + removed during the assertion to prevent log output during tests. + Note that when *keep_handlers* is ``True``, the logger's level + is still temporarily set to the requested level, which may cause + existing handlers to process more messages than they normally would. + The test passes if at least one message emitted inside the ``with`` block matches the *logger* and *level* conditions, otherwise it fails. @@ -1180,6 +1189,10 @@ Test cases .. versionchanged:: 3.15 Now accepts a *formatter* to control how messages are formatted. + .. versionchanged:: 3.16 + Added the *keep_handlers* parameter to optionally preserve + existing handlers. + .. method:: assertNoLogs(logger=None, level=None) A context manager to test that no messages are logged on diff --git a/Lib/test/test_unittest/test_case.py b/Lib/test/test_unittest/test_case.py index cf10e956bf2bdc..e8e62705d88258 100644 --- a/Lib/test/test_unittest/test_case.py +++ b/Lib/test/test_unittest/test_case.py @@ -2005,6 +2005,28 @@ def testAssertNoLogsYieldsNone(self): pass self.assertIsNone(value) + def testAssertLogsKeepHandlers(self): + # Verify keep_handlers=True preserves existing handlers + handler_records = [] + handler = logging.Handler() + handler.emit = lambda record: handler_records.append(record) + log_foo.addHandler(handler) + test_message = "test message" + + try: + with self.assertNoStderr(): + with self.assertLogs('foo', level='INFO', keep_handlers=True) as cm: + log_foo.info(test_message) + + self.assertEqual(cm.output, [f'INFO:foo:{test_message}']) + + self.assertEqual(len(handler_records), 1) + self.assertEqual(handler_records[0].getMessage(), test_message) + + self.assertEqual(log_foo.handlers, [handler]) + finally: + log_foo.removeHandler(handler) + def testAssertStartsWith(self): self.assertStartsWith('ababahalamaha', 'ababa') self.assertStartsWith('ababahalamaha', ('x', 'ababa', 'y')) diff --git a/Lib/unittest/_log.py b/Lib/unittest/_log.py index 3d69385ea243e7..bfe32841277ca9 100644 --- a/Lib/unittest/_log.py +++ b/Lib/unittest/_log.py @@ -30,7 +30,7 @@ class _AssertLogsContext(_BaseTestCaseContext): LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s" - def __init__(self, test_case, logger_name, level, no_logs, formatter=None): + def __init__(self, test_case, logger_name, level, no_logs, formatter=None, keep_handlers=False): _BaseTestCaseContext.__init__(self, test_case) self.logger_name = logger_name if level: @@ -40,6 +40,7 @@ def __init__(self, test_case, logger_name, level, no_logs, formatter=None): self.msg = None self.no_logs = no_logs self.formatter = formatter + self.keep_handlers = keep_handlers def __enter__(self): if isinstance(self.logger_name, logging.Logger): @@ -54,9 +55,13 @@ def __enter__(self): self.old_handlers = logger.handlers[:] self.old_level = logger.level self.old_propagate = logger.propagate - logger.handlers = [handler] + if self.keep_handlers: + logger.addHandler(handler) + else: + logger.handlers = [handler] + logger.propagate = False logger.setLevel(self.level) - logger.propagate = False + if self.no_logs: return return handler.watcher diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index eba50839cd33ae..3c86a019be5d8e 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -849,7 +849,7 @@ def _assertNotWarns(self, expected_warning, *args, **kwargs): context = _AssertNotWarnsContext(expected_warning, self) return context.handle('_assertNotWarns', args, kwargs) - def assertLogs(self, logger=None, level=None, formatter=None): + def assertLogs(self, logger=None, level=None, formatter=None, keep_handlers=False): """Fail unless a log message of level *level* or higher is emitted on *logger_name* or its children. If omitted, *level* defaults to INFO and *logger* defaults to the root logger. @@ -863,6 +863,11 @@ def assertLogs(self, logger=None, level=None, formatter=None): Optionally supply `formatter` to control how messages are formatted. + Optionally supply `keep_handlers` to control whether to preserve existing handlers. + Note that the logger's level will still be temporarily set to the requested level, + which may cause existing handlers to process more messages than usual + during the context manager. + Example:: with self.assertLogs('foo', level='INFO') as cm: @@ -873,7 +878,7 @@ def assertLogs(self, logger=None, level=None, formatter=None): """ # Lazy import to avoid importing logging if it is not needed. from ._log import _AssertLogsContext - return _AssertLogsContext(self, logger, level, no_logs=False, formatter=formatter) + return _AssertLogsContext(self, logger, level, no_logs=False, formatter=formatter, keep_handlers=keep_handlers) def assertNoLogs(self, logger=None, level=None): """ Fail unless no log messages of level *level* or higher are emitted From bf37637a83de62aee1e061873104bfd4c42aedce Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 16:57:45 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-01-17-16-57-44.gh-issue-89900.vFsyFn.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-01-17-16-57-44.gh-issue-89900.vFsyFn.rst diff --git a/Misc/NEWS.d/next/Library/2026-01-17-16-57-44.gh-issue-89900.vFsyFn.rst b/Misc/NEWS.d/next/Library/2026-01-17-16-57-44.gh-issue-89900.vFsyFn.rst new file mode 100644 index 00000000000000..3cac188daf6921 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-17-16-57-44.gh-issue-89900.vFsyFn.rst @@ -0,0 +1,2 @@ +Add *keep_handlers* parameter to :meth:`unittest.TestCase.assertLogs` to + optionally preserve existing logger handlers during assertion.