From 730a73a7c6480269dbd76a593a9936adb41ca431 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 23 Oct 2025 19:39:21 +0300 Subject: [PATCH 1/2] gh-138162: Fix logging.LoggerAdapter with merge_extra=True and without the extra argument --- Doc/library/logging.rst | 15 ++++++++++----- Lib/logging/__init__.py | 9 +++++---- Lib/test/test_logging.py | 14 +++++++++++++- ...2025-10-23-19-39-16.gh-issue-138162.Znw5DN.rst | 2 ++ 4 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-10-23-19-39-16.gh-issue-138162.Znw5DN.rst diff --git a/Doc/library/logging.rst b/Doc/library/logging.rst index 425025931d9835..0cf5b1c0d9bc3e 100644 --- a/Doc/library/logging.rst +++ b/Doc/library/logging.rst @@ -1082,12 +1082,13 @@ LoggerAdapter Objects information into logging calls. For a usage example, see the section on :ref:`adding contextual information to your logging output `. -.. class:: LoggerAdapter(logger, extra, merge_extra=False) +.. class:: LoggerAdapter(logger, extra=None, merge_extra=False) Returns an instance of :class:`LoggerAdapter` initialized with an - underlying :class:`Logger` instance, a dict-like object (*extra*), and a - boolean (*merge_extra*) indicating whether or not the *extra* argument of - individual log calls should be merged with the :class:`LoggerAdapter` extra. + underlying :class:`Logger` instance, an optional dict-like object (*extra*), + and an optional boolean (*merge_extra*) indicating whether or not + the *extra* argument of individual log calls should be merged with + the :class:`LoggerAdapter` extra. The default behavior is to ignore the *extra* argument of individual log calls and only use the one of the :class:`LoggerAdapter` instance @@ -1127,9 +1128,13 @@ information into logging calls. For a usage example, see the section on Attribute :attr:`!manager` and method :meth:`!_log` were added, which delegate to the underlying logger and allow adapters to be nested. + .. versionchanged:: 3.10 + + The *extra* argument is now optional. + .. versionchanged:: 3.13 - The *merge_extra* argument was added. + The *merge_extra* parameter was added. Thread Safety diff --git a/Lib/logging/__init__.py b/Lib/logging/__init__.py index 431ff41b352048..927fa57f6843cf 100644 --- a/Lib/logging/__init__.py +++ b/Lib/logging/__init__.py @@ -1849,9 +1849,9 @@ class LoggerAdapter(object): def __init__(self, logger, extra=None, merge_extra=False): """ - Initialize the adapter with a logger and a dict-like object which - provides contextual information. This constructor signature allows - easy stacking of LoggerAdapters, if so desired. + Initialize the adapter with a logger and an optional dict-like object + which provides contextual information. This constructor signature + allows easy stacking of LoggerAdapters, if so desired. You can effectively pass keyword arguments as shown in the following example: @@ -1883,7 +1883,8 @@ def process(self, msg, kwargs): LoggerAdapter subclass for your specific needs. """ if self.merge_extra and "extra" in kwargs: - kwargs["extra"] = {**self.extra, **kwargs["extra"]} + if self.extra is not None: + kwargs["extra"] = {**self.extra, **kwargs["extra"]} else: kwargs["extra"] = self.extra return msg, kwargs diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 1f7a4d9e197f9c..248602b5a6cda2 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -5826,7 +5826,7 @@ def cleanup(): self.addCleanup(cleanup) self.addCleanup(logging.shutdown) - self.adapter = logging.LoggerAdapter(logger=self.logger, extra=None) + self.adapter = logging.LoggerAdapter(logger=self.logger) def test_exception(self): msg = 'testing exception: %r' @@ -6008,6 +6008,18 @@ def test_extra_merged_log_call_has_precedence(self): self.assertHasAttr(record, 'foo') self.assertEqual(record.foo, '2') + def test_extra_merged_without_extra(self): + self.adapter = logging.LoggerAdapter(logger=self.logger, + merge_extra=True) + + self.adapter.critical('no extra') + self.assertEqual(len(self.recording.records), 1) + self.adapter.critical('foo should be here', extra={'foo': '1'}) + self.assertEqual(len(self.recording.records), 2) + record = self.recording.records[-1] + self.assertHasAttr(record, 'foo') + self.assertEqual(record.foo, '1') + class PrefixAdapter(logging.LoggerAdapter): prefix = 'Adapter' diff --git a/Misc/NEWS.d/next/Library/2025-10-23-19-39-16.gh-issue-138162.Znw5DN.rst b/Misc/NEWS.d/next/Library/2025-10-23-19-39-16.gh-issue-138162.Znw5DN.rst new file mode 100644 index 00000000000000..ef7a90bc37e650 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-23-19-39-16.gh-issue-138162.Znw5DN.rst @@ -0,0 +1,2 @@ +Fix :class:`logging.LoggerAdapter` with ``merge_extra=True`` and without the +*extra* argument. From fcc136e5efdcda4ea398ae15465bb213f50f183e Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 29 Oct 2025 16:36:31 +0200 Subject: [PATCH 2/2] Support extra=None in LogAdapter.log(). --- Lib/logging/__init__.py | 2 +- Lib/test/test_logging.py | 27 +++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Lib/logging/__init__.py b/Lib/logging/__init__.py index 927fa57f6843cf..39689a57e6ecd6 100644 --- a/Lib/logging/__init__.py +++ b/Lib/logging/__init__.py @@ -1882,7 +1882,7 @@ def process(self, msg, kwargs): Normally, you'll only need to override this one method in a LoggerAdapter subclass for your specific needs. """ - if self.merge_extra and "extra" in kwargs: + if self.merge_extra and kwargs.get("extra") is not None: if self.extra is not None: kwargs["extra"] = {**self.extra, **kwargs["extra"]} else: diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 248602b5a6cda2..8815426fc99c39 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -5997,6 +5997,18 @@ def test_extra_merged(self): self.assertEqual(record.foo, '1') self.assertEqual(record.bar, '2') + self.adapter.critical('no extra') # should not fail + self.assertEqual(len(self.recording.records), 2) + record = self.recording.records[-1] + self.assertEqual(record.foo, '1') + self.assertNotHasAttr(record, 'bar') + + self.adapter.critical('none extra', extra=None) # should not fail + self.assertEqual(len(self.recording.records), 3) + record = self.recording.records[-1] + self.assertEqual(record.foo, '1') + self.assertNotHasAttr(record, 'bar') + def test_extra_merged_log_call_has_precedence(self): self.adapter = logging.LoggerAdapter(logger=self.logger, extra={'foo': '1'}, @@ -6012,14 +6024,21 @@ def test_extra_merged_without_extra(self): self.adapter = logging.LoggerAdapter(logger=self.logger, merge_extra=True) - self.adapter.critical('no extra') - self.assertEqual(len(self.recording.records), 1) self.adapter.critical('foo should be here', extra={'foo': '1'}) - self.assertEqual(len(self.recording.records), 2) + self.assertEqual(len(self.recording.records), 1) record = self.recording.records[-1] - self.assertHasAttr(record, 'foo') self.assertEqual(record.foo, '1') + self.adapter.critical('no extra') # should not fail + self.assertEqual(len(self.recording.records), 2) + record = self.recording.records[-1] + self.assertNotHasAttr(record, 'foo') + + self.adapter.critical('none extra', extra=None) # should not fail + self.assertEqual(len(self.recording.records), 3) + record = self.recording.records[-1] + self.assertNotHasAttr(record, 'foo') + class PrefixAdapter(logging.LoggerAdapter): prefix = 'Adapter'