]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-132372: Speed up logging.config existing logger handling (GH-150242)
authoresadomer <54475303+esadomer@users.noreply.github.com>
Fri, 29 May 2026 15:50:05 +0000 (18:50 +0300)
committerGitHub <noreply@github.com>
Fri, 29 May 2026 15:50:05 +0000 (16:50 +0100)
Co-authored-by: Pieter Eendebak <pieter.eendebak@gmail.com>
Lib/logging/config.py
Lib/test/test_logging.py
Misc/NEWS.d/next/Library/2026-05-22-15-30-00.gh-issue-132372.YP4a6x.rst [new file with mode: 0644]

index 9a8b7016886eeeb1f99c39a70dfe0140e022a8b9..e39dac432f6ab5c27f9153ddae4c449dc5cc7666 100644 (file)
@@ -36,6 +36,7 @@ import struct
 import threading
 import traceback
 
+from bisect import bisect_left
 from socketserver import ThreadingTCPServer, StreamRequestHandler
 
 
@@ -186,9 +187,8 @@ def _handle_existing_loggers(existing, child_loggers, disable_existing):
     what was intended by the user. Also, allow existing loggers to NOT be
     disabled if disable_existing is false.
     """
-    root = logging.root
     for log in existing:
-        logger = root.manager.loggerDict[log]
+        logger = logging.root.manager.loggerDict[log]
         if log in child_loggers:
             if not isinstance(logger, logging.PlaceHolder):
                 logger.setLevel(logging.NOTSET)
@@ -197,6 +197,20 @@ def _handle_existing_loggers(existing, child_loggers, disable_existing):
         else:
             logger.disabled = disable_existing
 
+def _forget_existing_logger(name, existing, existing_set, child_loggers):
+    """Forget a configured logger and record its existing children."""
+    prefixed = name + "."
+    i = bisect_left(existing, prefixed)
+    num_existing = len(existing)
+    while i < num_existing:
+        child = existing[i]
+        if not child.startswith(prefixed):
+            break
+        if child in existing_set:
+            child_loggers[child] = None
+        i += 1
+    existing_set.remove(name)
+
 def _install_loggers(cp, handlers, disable_existing):
     """Create and install loggers"""
 
@@ -235,25 +249,18 @@ def _install_loggers(cp, handlers, disable_existing):
     #named loggers. With a sorted list it is easier
     #to find the child loggers.
     existing.sort()
+    existing_set = set(existing)
     #We'll keep the list of existing loggers
     #which are children of named loggers here...
-    child_loggers = []
+    child_loggers = {}
     #now set up the new ones...
     for log in llist:
         section = cp["logger_%s" % log]
         qn = section["qualname"]
         propagate = section.getint("propagate", fallback=1)
         logger = logging.getLogger(qn)
-        if qn in existing:
-            i = existing.index(qn) + 1 # start with the entry after qn
-            prefixed = qn + "."
-            pflen = len(prefixed)
-            num_existing = len(existing)
-            while i < num_existing:
-                if existing[i][:pflen] == prefixed:
-                    child_loggers.append(existing[i])
-                i += 1
-            existing.remove(qn)
+        if qn in existing_set:
+            _forget_existing_logger(qn, existing, existing_set, child_loggers)
         if "level" in section:
             level = section["level"]
             logger.setLevel(level)
@@ -281,6 +288,7 @@ def _install_loggers(cp, handlers, disable_existing):
     #        logger.propagate = 1
     #    elif disable_existing_loggers:
     #        logger.disabled = 1
+    existing = [name for name in existing if name in existing_set]
     _handle_existing_loggers(existing, child_loggers, disable_existing)
 
 
@@ -638,22 +646,16 @@ class DictConfigurator(BaseConfigurator):
                 #named loggers. With a sorted list it is easier
                 #to find the child loggers.
                 existing.sort()
+                existing_set = set(existing)
                 #We'll keep the list of existing loggers
                 #which are children of named loggers here...
-                child_loggers = []
+                child_loggers = {}
                 #now set up the new ones...
                 loggers = config.get('loggers', EMPTY_DICT)
                 for name in loggers:
-                    if name in existing:
-                        i = existing.index(name) + 1 # look after name
-                        prefixed = name + "."
-                        pflen = len(prefixed)
-                        num_existing = len(existing)
-                        while i < num_existing:
-                            if existing[i][:pflen] == prefixed:
-                                child_loggers.append(existing[i])
-                            i += 1
-                        existing.remove(name)
+                    if name in existing_set:
+                        _forget_existing_logger(name, existing, existing_set,
+                                                child_loggers)
                     try:
                         self.configure_logger(name, loggers[name])
                     except Exception as e:
@@ -673,6 +675,7 @@ class DictConfigurator(BaseConfigurator):
                 #        logger.propagate = True
                 #    elif disable_existing:
                 #        logger.disabled = True
+                existing = [name for name in existing if name in existing_set]
                 _handle_existing_loggers(existing, child_loggers,
                                          disable_existing)
 
index 2ab9e0b336c9fb555bf8aa64a6f7f08d1c17188d..08678119200d4271898250e5f63555be56113de2 100644 (file)
@@ -4173,6 +4173,30 @@ class ConfigDictTest(BaseTest):
         # Logger should be enabled, since explicitly mentioned
         self.assertFalse(logger.disabled)
 
+    def test_disable_existing_loggers_preserves_children(self):
+        parent = logging.getLogger('many')
+        child = logging.getLogger('many.child')
+        child.setLevel(logging.CRITICAL)
+        self.assertFalse(child.isEnabledFor(logging.INFO))
+        cousin = logging.getLogger('many-child')
+        for i in range(20):
+            logging.getLogger(f'many-sibling-{i}')
+
+        self.apply_config({
+            'version': 1,
+            'loggers': {
+                'many': {
+                    'level': 'INFO',
+                },
+            },
+        })
+
+        self.assertFalse(parent.disabled)
+        self.assertFalse(child.disabled)
+        self.assertEqual(child.level, logging.NOTSET)
+        self.assertTrue(child.isEnabledFor(logging.INFO))
+        self.assertTrue(cousin.disabled)
+
     def test_111615(self):
         # See gh-111615
         import_helper.import_module('_multiprocessing')  # see gh-113692
diff --git a/Misc/NEWS.d/next/Library/2026-05-22-15-30-00.gh-issue-132372.YP4a6x.rst b/Misc/NEWS.d/next/Library/2026-05-22-15-30-00.gh-issue-132372.YP4a6x.rst
new file mode 100644 (file)
index 0000000..bcd96e8
--- /dev/null
@@ -0,0 +1,2 @@
+Speed up :func:`logging.config.fileConfig` and
+:func:`logging.config.dictConfig` when handling many existing loggers.