https://github.com/python/cpython/commit/32c3dcc7a4909afe3f6a6bf720c829b0d00a2968
commit: 32c3dcc7a4909afe3f6a6bf720c829b0d00a2968
branch: 3.13
author: Miss Islington (bot) <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2025-10-30T11:31:54Z
summary:

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

(cherry picked from commit 327dbbedffa3f2c95e70129a11974b83e27864f9)

Co-authored-by: Serhiy Storchaka <[email protected]>

files:
A Misc/NEWS.d/next/Library/2025-10-23-19-39-16.gh-issue-138162.Znw5DN.rst
M Doc/library/logging.rst
M Lib/logging/__init__.py
M Lib/test/test_logging.py

diff --git a/Doc/library/logging.rst b/Doc/library/logging.rst
index 1a90fbd8bda109..c8552befbbfe96 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 <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
-   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 b95c3aacf7d17a..9005f1ef865c90 100644
--- a/Lib/logging/__init__.py
+++ b/Lib/logging/__init__.py
@@ -1852,9 +1852,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:
@@ -1885,8 +1885,9 @@ 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:
-            kwargs["extra"] = {**self.extra, **kwargs["extra"]}
+        if self.merge_extra and kwargs.get("extra") is not None:
+            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 58e0381c4aa934..715fedee7616b3 100644
--- a/Lib/test/test_logging.py
+++ b/Lib/test/test_logging.py
@@ -51,6 +51,7 @@
 from test.support import asyncore
 from test.support import smtpd
 from test.support.logging_helper import TestHandler
+from test.support.testcase import ExtraAssertions
 import textwrap
 import threading
 import asyncio
@@ -5723,7 +5724,7 @@ def test_critical(self):
         self._test_log('critical')
 
 
-class LoggerAdapterTest(unittest.TestCase):
+class LoggerAdapterTest(unittest.TestCase, ExtraAssertions):
     def setUp(self):
         super(LoggerAdapterTest, self).setUp()
         old_handler_list = logging._handlerList[:]
@@ -5739,7 +5740,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'
@@ -5910,6 +5911,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'},
@@ -5921,6 +5934,25 @@ def test_extra_merged_log_call_has_precedence(self):
         self.assertTrue(hasattr(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('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):
     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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to