diff --git a/Lib/logging/config.py b/Lib/logging/config.py index 9a8b7016886eeeb..03c1ce510ddf6a6 100644 --- a/Lib/logging/config.py +++ b/Lib/logging/config.py @@ -36,6 +36,7 @@ import threading import traceback +from bisect import bisect_left from socketserver import ThreadingTCPServer, StreamRequestHandler @@ -187,15 +188,35 @@ def _handle_existing_loggers(existing, child_loggers, disable_existing): disabled if disable_existing is false. """ root = logging.root + cache_clear_needed = False for log in existing: logger = root.manager.loggerDict[log] if log in child_loggers: if not isinstance(logger, logging.PlaceHolder): - logger.setLevel(logging.NOTSET) + # Equivalent to setLevel(NOTSET), but clear the cache once. + logger.level = logging.NOTSET logger.handlers = [] logger.propagate = True + cache_clear_needed = True else: logger.disabled = disable_existing + if cache_clear_needed: + root.manager._clear_cache() + +def _discard_existing_logger(name, existing, existing_set, child_loggers): + """Discard a configured logger and record its existing children.""" + if name in existing_set: + 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.add(child) + i += 1 + existing_set.remove(name) def _install_loggers(cp, handlers, disable_existing): """Create and install loggers""" @@ -235,25 +256,17 @@ 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 = set() #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) + _discard_existing_logger(qn, existing, existing_set, child_loggers) if "level" in section: level = section["level"] logger.setLevel(level) @@ -281,6 +294,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 +652,15 @@ def configure(self): #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 = set() #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) + _discard_existing_logger(name, existing, existing_set, + child_loggers) try: self.configure_logger(name, loggers[name]) except Exception as e: @@ -673,6 +680,7 @@ def configure(self): # 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) diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 2ab9e0b336c9fb5..08678119200d427 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -4173,6 +4173,30 @@ def test_90195(self): # 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 index 000000000000000..bcd96e88eac1bfd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-22-15-30-00.gh-issue-132372.YP4a6x.rst @@ -0,0 +1,2 @@ +Speed up :func:`logging.config.fileConfig` and +:func:`logging.config.dictConfig` when handling many existing loggers.