From f8ff1279005ae8000deb5e5b9dc3adf229dc7baf Mon Sep 17 00:00:00 2001 From: Garry Cairns <2401853+garry-cairns@users.noreply.github.com> Date: Fri, 23 May 2025 07:59:20 +0100 Subject: [PATCH 1/7] Exposes log format to users in unittest.TestCase.assertLogs --- Lib/test/test_unittest/test_case.py | 16 ++++++++++++++++ Lib/unittest/_log.py | 5 +++-- Lib/unittest/case.py | 6 ++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_unittest/test_case.py b/Lib/test/test_unittest/test_case.py index d66cab146af246..cf10e956bf2bdc 100644 --- a/Lib/test/test_unittest/test_case.py +++ b/Lib/test/test_unittest/test_case.py @@ -1920,6 +1920,22 @@ def testAssertLogsUnexpectedException(self): with self.assertLogs(): raise ZeroDivisionError("Unexpected") + def testAssertLogsWithFormatter(self): + # Check alternative formats will be respected + format = "[No.1: the larch] %(levelname)s:%(name)s:%(message)s" + formatter = logging.Formatter(format) + with self.assertNoStderr(): + with self.assertLogs() as cm: + log_foo.info("1") + log_foobar.debug("2") + self.assertEqual(cm.output, ["INFO:foo:1"]) + self.assertLogRecords(cm.records, [{'name': 'foo'}]) + with self.assertLogs(formatter=formatter) as cm: + log_foo.info("1") + log_foobar.debug("2") + self.assertEqual(cm.output, ["[No.1: the larch] INFO:foo:1"]) + self.assertLogRecords(cm.records, [{'name': 'foo'}]) + def testAssertNoLogsDefault(self): with self.assertRaises(self.failureException) as cm: with self.assertNoLogs(): diff --git a/Lib/unittest/_log.py b/Lib/unittest/_log.py index 94868e5bb95eb3..3d69385ea243e7 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): + def __init__(self, test_case, logger_name, level, no_logs, formatter=None): _BaseTestCaseContext.__init__(self, test_case) self.logger_name = logger_name if level: @@ -39,13 +39,14 @@ def __init__(self, test_case, logger_name, level, no_logs): self.level = logging.INFO self.msg = None self.no_logs = no_logs + self.formatter = formatter def __enter__(self): if isinstance(self.logger_name, logging.Logger): logger = self.logger = self.logger_name else: logger = self.logger = logging.getLogger(self.logger_name) - formatter = logging.Formatter(self.LOGGING_FORMAT) + formatter = self.formatter or logging.Formatter(self.LOGGING_FORMAT) handler = _CapturingHandler() handler.setLevel(self.level) handler.setFormatter(formatter) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 884fc1b21f64d8..ab6bfb45f1961e 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -851,7 +851,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): + def assertLogs(self, logger=None, level=None, formatter=None): """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,8 @@ def assertLogs(self, logger=None, level=None): `records` attribute will be a list of the corresponding LogRecord objects. + Optionally supply `format` to control how messages are formatted. + Example:: with self.assertLogs('foo', level='INFO') as cm: @@ -873,7 +875,7 @@ def assertLogs(self, logger=None, level=None): """ # Lazy import to avoid importing logging if it is not needed. from ._log import _AssertLogsContext - return _AssertLogsContext(self, logger, level, no_logs=False) + return _AssertLogsContext(self, logger, level, no_logs=False, formatter=formatter) def assertNoLogs(self, logger=None, level=None): """ Fail unless no log messages of level *level* or higher are emitted From 59e8f3fe8f54d6ebb1da52ad0b09433cef8b9358 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 09:19:53 +0000 Subject: [PATCH 2/7] =?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/Tests/2025-05-23-09-19-52.gh-issue-134567.hwEIMb.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Tests/2025-05-23-09-19-52.gh-issue-134567.hwEIMb.rst diff --git a/Misc/NEWS.d/next/Tests/2025-05-23-09-19-52.gh-issue-134567.hwEIMb.rst b/Misc/NEWS.d/next/Tests/2025-05-23-09-19-52.gh-issue-134567.hwEIMb.rst new file mode 100644 index 00000000000000..90242dfb821471 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2025-05-23-09-19-52.gh-issue-134567.hwEIMb.rst @@ -0,0 +1,2 @@ +Expose log formatter to users in TestCase.assertLogs. +:func:`unittest.TestCase.assertLogs` will now accept a formatter argument so your assertions can match a custom format where you are using one. From 60098585591c3db4e520e6674f74495abddce6bf Mon Sep 17 00:00:00 2001 From: Garry Cairns <2401853+garry-cairns@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:35:26 +0100 Subject: [PATCH 3/7] Adds docs and corrects docstring --- Doc/whatsnew/3.15.rst | 8 ++++++++ Lib/unittest/case.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 9f327cf904da1b..b7bfa2c53796d9 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -274,6 +274,14 @@ typing (Contributed by Bénédikt Tran in :gh:`133823`.) +unittest +-------- +* Expose log formatter to users in TestCase.assertLogs. + :func:`unittest.TestCase.assertLogs` will now accept a formatter + argument so your assertions can match a custom format where you are using one. + (Contributed by Garry Cairns in :gh:`134567`.) + + wave ---- diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 025aba605e2b67..eba50839cd33ae 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -861,7 +861,7 @@ def assertLogs(self, logger=None, level=None, formatter=None): `records` attribute will be a list of the corresponding LogRecord objects. - Optionally supply `format` to control how messages are formatted. + Optionally supply `formatter` to control how messages are formatted. Example:: From 7881c641e1f3bad0d4f0764854eeebc3e6214882 Mon Sep 17 00:00:00 2001 From: Garry Cairns <2401853+garry-cairns@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:48:55 +0100 Subject: [PATCH 4/7] Updates unittest docs --- Doc/library/unittest.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index dcdda1719bf593..8ba33171862803 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) + .. method:: assertLogs(logger=None, level=None, formatter=None) 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 @@ -1146,6 +1146,10 @@ Test cases its string equivalent (for example either ``"ERROR"`` or :const:`logging.ERROR`). The default is :const:`logging.INFO`. + If given, *formatter* should be a :class:`logging.Formatter` object. + The default is a formatter with + ``LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s"`` + The test passes if at least one message emitted inside the ``with`` block matches the *logger* and *level* conditions, otherwise it fails. @@ -1173,6 +1177,10 @@ Test cases .. versionadded:: 3.4 + .. versionchanged:: 3.15 + Now accepts a formatter argument so your assertions can match + a custom format where you are using one. + .. method:: assertNoLogs(logger=None, level=None) A context manager to test that no messages are logged on From ec353d244df93951f71078ce54331c5461f5efed Mon Sep 17 00:00:00 2001 From: Garry Cairns <2401853+garry-cairns@users.noreply.github.com> Date: Wed, 25 Jun 2025 14:38:18 +0100 Subject: [PATCH 5/7] Resolves review comments from pr 134570 --- Doc/library/unittest.rst | 4 ++-- Doc/whatsnew/3.15.rst | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index 8ba33171862803..80a4c3075934f1 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -1148,7 +1148,7 @@ Test cases If given, *formatter* should be a :class:`logging.Formatter` object. The default is a formatter with - ``LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s"`` + ``"%(levelname)s:%(name)s:%(message)s"`` The test passes if at least one message emitted inside the ``with`` block matches the *logger* and *level* conditions, otherwise it fails. @@ -1177,7 +1177,7 @@ Test cases .. versionadded:: 3.4 - .. versionchanged:: 3.15 + .. versionchanged:: next Now accepts a formatter argument so your assertions can match a custom format where you are using one. diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index b7bfa2c53796d9..c9debcef6eb642 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -276,7 +276,8 @@ typing unittest -------- -* Expose log formatter to users in TestCase.assertLogs. + +* Lets users specify formatter in TestCase.assertLogs. :func:`unittest.TestCase.assertLogs` will now accept a formatter argument so your assertions can match a custom format where you are using one. (Contributed by Garry Cairns in :gh:`134567`.) From 7ee8aba17eaeb095e71fd6b10da5723033f31ac3 Mon Sep 17 00:00:00 2001 From: Garry Cairns <2401853+garry-cairns@users.noreply.github.com> Date: Sun, 29 Jun 2025 14:48:44 +0100 Subject: [PATCH 6/7] Addresses review comments on gh-134567 --- Doc/library/unittest.rst | 4 ++-- .../next/Tests/2025-05-23-09-19-52.gh-issue-134567.hwEIMb.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index 80a4c3075934f1..bf49333fd2ce63 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -1147,7 +1147,7 @@ Test cases :const:`logging.ERROR`). The default is :const:`logging.INFO`. If given, *formatter* should be a :class:`logging.Formatter` object. - The default is a formatter with + The default is a formatter with format string ``"%(levelname)s:%(name)s:%(message)s"`` The test passes if at least one message emitted inside the ``with`` @@ -1178,7 +1178,7 @@ Test cases .. versionadded:: 3.4 .. versionchanged:: next - Now accepts a formatter argument so your assertions can match + Now accepts a *formatter* so your assertions can match a custom format where you are using one. .. method:: assertNoLogs(logger=None, level=None) diff --git a/Misc/NEWS.d/next/Tests/2025-05-23-09-19-52.gh-issue-134567.hwEIMb.rst b/Misc/NEWS.d/next/Tests/2025-05-23-09-19-52.gh-issue-134567.hwEIMb.rst index 90242dfb821471..42e4a01c0ccb4b 100644 --- a/Misc/NEWS.d/next/Tests/2025-05-23-09-19-52.gh-issue-134567.hwEIMb.rst +++ b/Misc/NEWS.d/next/Tests/2025-05-23-09-19-52.gh-issue-134567.hwEIMb.rst @@ -1,2 +1,2 @@ -Expose log formatter to users in TestCase.assertLogs. -:func:`unittest.TestCase.assertLogs` will now accept a formatter argument so your assertions can match a custom format where you are using one. +Expose log formatter to users in TestCase.assertLogs. +:func:`unittest.TestCase.assertLogs` will now optionally accept a formatter that will be used to format the strings in output if provided. From ca72d91429a4d5a361df36c650e999bebed4130d Mon Sep 17 00:00:00 2001 From: Garry Cairns <2401853+garry-cairns@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:24:44 +0100 Subject: [PATCH 7/7] Addresses review comments on gh-134570 --- Doc/library/unittest.rst | 3 +-- Doc/whatsnew/3.15.rst | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index bf49333fd2ce63..d526e835caa18c 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -1178,8 +1178,7 @@ Test cases .. versionadded:: 3.4 .. versionchanged:: next - Now accepts a *formatter* so your assertions can match - a custom format where you are using one. + Now accepts a *formatter* to control how messages are formatted. .. method:: assertNoLogs(logger=None, level=None) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 820c15f3807879..706a816f888b30 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -296,7 +296,7 @@ unittest * Lets users specify formatter in TestCase.assertLogs. :func:`unittest.TestCase.assertLogs` will now accept a formatter - argument so your assertions can match a custom format where you are using one. + to control how messages are formatted. (Contributed by Garry Cairns in :gh:`134567`.)