diff --git a/Doc/library/pydoc.rst b/Doc/library/pydoc.rst index f236eba84576575..a0cfb440a36ffa9 100644 --- a/Doc/library/pydoc.rst +++ b/Doc/library/pydoc.rst @@ -68,6 +68,11 @@ will start a HTTP server on port 1234, allowing you to browse the documentation at ``http://localhost:1234/`` in your preferred web browser. Specifying ``0`` as the port number will select an arbitrary unused port. +.. warning:: + + The :mod:`!pydoc` HTTP server is intended for local use during + development and is not suitable for production use. + :program:`python -m pydoc -n ` will start the server listening at the given hostname. By default the hostname is 'localhost' but if you want the server to be reached from other machines, you may want to change the host name that the diff --git a/Lib/logging/config.py b/Lib/logging/config.py index 9a8b7016886eeeb..e39dac432f6ab5c 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 @@ -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 @@ 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 = {} #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 @@ 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.