gh-138162: Fix logging.LoggerAdapter with merge_extra=True and without the extra argument (GH-140511)

This commit is contained in:
Serhiy Storchaka 2025-10-30 12:52:02 +02:00 committed by GitHub
parent 622d97b8bb
commit 327dbbedff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 50 additions and 11 deletions

View file

@ -1082,12 +1082,13 @@ LoggerAdapter Objects
information into logging calls. For a usage example, see the section on information into logging calls. For a usage example, see the section on
:ref:`adding contextual information to your logging output <context-info>`. :ref:`adding contextual information to your logging output <context-info>`.
.. class:: LoggerAdapter(logger, extra, merge_extra=False) .. class:: LoggerAdapter(logger, extra=None, merge_extra=False)
Returns an instance of :class:`LoggerAdapter` initialized with an Returns an instance of :class:`LoggerAdapter` initialized with an
underlying :class:`Logger` instance, a dict-like object (*extra*), and a underlying :class:`Logger` instance, an optional dict-like object (*extra*),
boolean (*merge_extra*) indicating whether or not the *extra* argument of and an optional boolean (*merge_extra*) indicating whether or not
individual log calls should be merged with the :class:`LoggerAdapter` extra. 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 The default behavior is to ignore the *extra* argument of individual log
calls and only use the one of the :class:`LoggerAdapter` instance 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 Attribute :attr:`!manager` and method :meth:`!_log` were added, which
delegate to the underlying logger and allow adapters to be nested. delegate to the underlying logger and allow adapters to be nested.
.. versionchanged:: 3.10
The *extra* argument is now optional.
.. versionchanged:: 3.13 .. versionchanged:: 3.13
The *merge_extra* argument was added. The *merge_extra* parameter was added.
Thread Safety Thread Safety

View file

@ -1849,9 +1849,9 @@ class LoggerAdapter(object):
def __init__(self, logger, extra=None, merge_extra=False): def __init__(self, logger, extra=None, merge_extra=False):
""" """
Initialize the adapter with a logger and a dict-like object which Initialize the adapter with a logger and an optional dict-like object
provides contextual information. This constructor signature allows which provides contextual information. This constructor signature
easy stacking of LoggerAdapters, if so desired. allows easy stacking of LoggerAdapters, if so desired.
You can effectively pass keyword arguments as shown in the You can effectively pass keyword arguments as shown in the
following example: following example:
@ -1882,8 +1882,9 @@ def process(self, msg, kwargs):
Normally, you'll only need to override this one method in a Normally, you'll only need to override this one method in a
LoggerAdapter subclass for your specific needs. 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:
kwargs["extra"] = {**self.extra, **kwargs["extra"]} if self.extra is not None:
kwargs["extra"] = {**self.extra, **kwargs["extra"]}
else: else:
kwargs["extra"] = self.extra kwargs["extra"] = self.extra
return msg, kwargs return msg, kwargs

View file

@ -5826,7 +5826,7 @@ def cleanup():
self.addCleanup(cleanup) self.addCleanup(cleanup)
self.addCleanup(logging.shutdown) self.addCleanup(logging.shutdown)
self.adapter = logging.LoggerAdapter(logger=self.logger, extra=None) self.adapter = logging.LoggerAdapter(logger=self.logger)
def test_exception(self): def test_exception(self):
msg = 'testing exception: %r' msg = 'testing exception: %r'
@ -5997,6 +5997,18 @@ def test_extra_merged(self):
self.assertEqual(record.foo, '1') self.assertEqual(record.foo, '1')
self.assertEqual(record.bar, '2') 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): def test_extra_merged_log_call_has_precedence(self):
self.adapter = logging.LoggerAdapter(logger=self.logger, self.adapter = logging.LoggerAdapter(logger=self.logger,
extra={'foo': '1'}, extra={'foo': '1'},
@ -6008,6 +6020,25 @@ def test_extra_merged_log_call_has_precedence(self):
self.assertHasAttr(record, 'foo') self.assertHasAttr(record, 'foo')
self.assertEqual(record.foo, '2') 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('foo should be here', extra={'foo': '1'})
self.assertEqual(len(self.recording.records), 1)
record = self.recording.records[-1]
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): class PrefixAdapter(logging.LoggerAdapter):
prefix = 'Adapter' prefix = 'Adapter'

View file

@ -0,0 +1,2 @@
Fix :class:`logging.LoggerAdapter` with ``merge_extra=True`` and without the
*extra* argument.