From 2eb49d278e081b0cde057c1ffc2e8cd24ae39225 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Wed, 14 May 2025 08:45:00 +0200 Subject: [PATCH] gh-133577: Add parameter `formatter` to `logging.basicConfig` (GH-133578) --- Doc/library/logging.rst | 21 ++++++++++-- Lib/logging/__init__.py | 32 +++++++++++++++---- Lib/test/test_logging.py | 23 ++++++++++++- ...-05-07-14-36-30.gh-issue-133577.BggPk9.rst | 1 + 4 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-05-07-14-36-30.gh-issue-133577.BggPk9.rst diff --git a/Doc/library/logging.rst b/Doc/library/logging.rst index 72190e97240..4509da58916 100644 --- a/Doc/library/logging.rst +++ b/Doc/library/logging.rst @@ -1342,8 +1342,9 @@ functions. .. function:: basicConfig(**kwargs) - Does basic configuration for the logging system by creating a - :class:`StreamHandler` with a default :class:`Formatter` and adding it to the + Does basic configuration for the logging system by either creating a + :class:`StreamHandler` with a default :class:`Formatter` + or using the given *formatter* instance, and adding it to the root logger. The functions :func:`debug`, :func:`info`, :func:`warning`, :func:`error` and :func:`critical` will call :func:`basicConfig` automatically if no handlers are defined for the root logger. @@ -1428,6 +1429,19 @@ functions. | | which means that it will be treated the | | | same as passing 'errors'. | +--------------+---------------------------------------------+ + | *formatter* | If specified, set this formatter instance | + | | (see :ref:`formatter-objects`) | + | | for all involved handlers. | + | | If not specified, the default is to create | + | | and use an instance of | + | | :class:`logging.Formatter` based on | + | | arguments *format*, *datefmt* and *style*. | + | | When *formatter* is specified together with | + | | any of the three arguments *format*, | + | | *datefmt* and *style*, a ``ValueError`` is | + | | raised to signal that these arguments would | + | | lose meaning otherwise. | + +--------------+---------------------------------------------+ .. versionchanged:: 3.2 The *style* argument was added. @@ -1444,6 +1458,9 @@ functions. .. versionchanged:: 3.9 The *encoding* and *errors* arguments were added. + .. versionchanged:: 3.15 + The *formatter* argument was added. + .. function:: shutdown() Informs the logging system to perform an orderly shutdown by flushing and diff --git a/Lib/logging/__init__.py b/Lib/logging/__init__.py index 283a1055182..f2d1a02629d 100644 --- a/Lib/logging/__init__.py +++ b/Lib/logging/__init__.py @@ -2057,6 +2057,15 @@ def basicConfig(**kwargs): created FileHandler, causing it to be used when the file is opened in text mode. If not specified, the default value is `backslashreplace`. + formatter If specified, set this formatter instance for all involved + handlers. + If not specified, the default is to create and use an instance of + `logging.Formatter` based on arguments 'format', 'datefmt' and + 'style'. + When 'formatter' is specified together with any of the three + arguments 'format', 'datefmt' and 'style', a `ValueError` + is raised to signal that these arguments would lose meaning + otherwise. Note that you could specify a stream created using open(filename, mode) rather than passing the filename and mode in. However, it should be @@ -2079,6 +2088,9 @@ def basicConfig(**kwargs): .. versionchanged:: 3.9 Added the ``encoding`` and ``errors`` parameters. + + .. versionchanged:: 3.15 + Added the ``formatter`` parameter. """ # Add thread safety in case someone mistakenly calls # basicConfig() from multiple threads @@ -2114,13 +2126,19 @@ def basicConfig(**kwargs): stream = kwargs.pop("stream", None) h = StreamHandler(stream) handlers = [h] - dfs = kwargs.pop("datefmt", None) - style = kwargs.pop("style", '%') - if style not in _STYLES: - raise ValueError('Style must be one of: %s' % ','.join( - _STYLES.keys())) - fs = kwargs.pop("format", _STYLES[style][1]) - fmt = Formatter(fs, dfs, style) + fmt = kwargs.pop("formatter", None) + if fmt is None: + dfs = kwargs.pop("datefmt", None) + style = kwargs.pop("style", '%') + if style not in _STYLES: + raise ValueError('Style must be one of: %s' % ','.join( + _STYLES.keys())) + fs = kwargs.pop("format", _STYLES[style][1]) + fmt = Formatter(fs, dfs, style) + else: + for forbidden_key in ("datefmt", "format", "style"): + if forbidden_key in kwargs: + raise ValueError(f"{forbidden_key!r} should not be specified together with 'formatter'") for h in handlers: if h.formatter is None: h.setFormatter(fmt) diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 1e5adcc8db1..fa5b1e43816 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -61,7 +61,7 @@ import warnings import weakref from http.server import HTTPServer, BaseHTTPRequestHandler -from unittest.mock import patch +from unittest.mock import call, Mock, patch from urllib.parse import urlparse, parse_qs from socketserver import (ThreadingUDPServer, DatagramRequestHandler, ThreadingTCPServer, StreamRequestHandler) @@ -5655,12 +5655,19 @@ class BasicConfigTest(unittest.TestCase): assertRaises = self.assertRaises handlers = [logging.StreamHandler()] stream = sys.stderr + formatter = logging.Formatter() assertRaises(ValueError, logging.basicConfig, filename='test.log', stream=stream) assertRaises(ValueError, logging.basicConfig, filename='test.log', handlers=handlers) assertRaises(ValueError, logging.basicConfig, stream=stream, handlers=handlers) + assertRaises(ValueError, logging.basicConfig, formatter=formatter, + format='%(message)s') + assertRaises(ValueError, logging.basicConfig, formatter=formatter, + datefmt='%H:%M:%S') + assertRaises(ValueError, logging.basicConfig, formatter=formatter, + style='%') # Issue 23207: test for invalid kwargs assertRaises(ValueError, logging.basicConfig, loglevel=logging.INFO) # Should pop both filename and filemode even if filename is None @@ -5795,6 +5802,20 @@ class BasicConfigTest(unittest.TestCase): # didn't write anything due to the encoding error self.assertEqual(data, r'') + def test_formatter_given(self): + mock_formatter = Mock() + mock_handler = Mock(formatter=None) + with patch("logging.Formatter") as mock_formatter_init: + logging.basicConfig(formatter=mock_formatter, handlers=[mock_handler]) + self.assertEqual(mock_handler.setFormatter.call_args_list, [call(mock_formatter)]) + self.assertEqual(mock_formatter_init.call_count, 0) + + def test_formatter_not_given(self): + mock_handler = Mock(formatter=None) + with patch("logging.Formatter") as mock_formatter_init: + logging.basicConfig(handlers=[mock_handler]) + self.assertEqual(mock_formatter_init.call_count, 1) + @support.requires_working_socket() def test_log_taskName(self): async def log_record(): diff --git a/Misc/NEWS.d/next/Library/2025-05-07-14-36-30.gh-issue-133577.BggPk9.rst b/Misc/NEWS.d/next/Library/2025-05-07-14-36-30.gh-issue-133577.BggPk9.rst new file mode 100644 index 00000000000..9d056983439 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-07-14-36-30.gh-issue-133577.BggPk9.rst @@ -0,0 +1 @@ +Add parameter ``formatter`` to :func:`logging.basicConfig`.