Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
15 changes: 14 additions & 1 deletion Doc/library/unittest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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``,
Copy link
Member

Choose a reason for hiding this comment

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

If we were to add an additional parameter I think a keyword-only parameter is better.

Copy link
Member

Choose a reason for hiding this comment

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

This is overly verbose.Usually if the default is a boolean we assume that the reader is smart enough to infer that the type is a boolean (though at runtime we just use truthiness)

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.

Expand Down Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

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

For instance we should also allow a keep_handlers keyword here.


A context manager to test that no messages are logged on
Expand Down
22 changes: 22 additions & 0 deletions Lib/test/test_unittest/test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down
11 changes: 8 additions & 3 deletions Lib/unittest/_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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):
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

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

Thie sets propagate before the level but it was not case previously.

logger.setLevel(self.level)
logger.propagate = False

if self.no_logs:
return
return handler.watcher
Expand Down
9 changes: 7 additions & 2 deletions Lib/unittest/case.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add *keep_handlers* parameter to :meth:`unittest.TestCase.assertLogs` to
Copy link
Member

Choose a reason for hiding this comment

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

This would require more than just a NEWS entry. We would also need a What's New.

optionally preserve existing logger handlers during assertion.
Loading