diff --git a/Lib/logging/__init__.py b/Lib/logging/__init__.py index 549a6b3c457b2c..ee408c577e9fa9 100644 --- a/Lib/logging/__init__.py +++ b/Lib/logging/__init__.py @@ -29,6 +29,8 @@ from string import Template from string import Formatter as StrFormatter +lazy import pickle + __all__ = ['BASIC_FORMAT', 'BufferingFormatter', 'CRITICAL', 'DEBUG', 'ERROR', 'FATAL', 'FileHandler', 'Filter', 'Formatter', 'Handler', 'INFO', @@ -1846,7 +1848,6 @@ def __repr__(self): def __reduce__(self): if getLogger(self.name) is not self: - import pickle raise pickle.PicklingError('logger cannot be pickled') return getLogger, (self.name,) @@ -2370,9 +2371,7 @@ def captureWarnings(capture): def __getattr__(name): if name in ("__version__", "__date__"): - from warnings import _deprecated - - _deprecated(name, remove=(3, 20)) + warnings._deprecated(name, remove=(3, 20)) return { # Do not change "__version__": "0.5.1.2", "__date__": "07 February 2010", diff --git a/Lib/logging/config.py b/Lib/logging/config.py index f566de5750dbf5..6478d475277418 100644 --- a/Lib/logging/config.py +++ b/Lib/logging/config.py @@ -28,17 +28,19 @@ import functools import io import logging -import logging.handlers import os -import queue import re -import socket -import struct import threading import traceback - -from bisect import bisect_left -from socketserver import ThreadingTCPServer, StreamRequestHandler +lazy import configparser +lazy import json +lazy import logging.handlers +lazy import queue +lazy import select +lazy import socket +lazy import struct +lazy from bisect import bisect_left +lazy from socketserver import StreamRequestHandler, ThreadingTCPServer DEFAULT_LOGGING_CONFIG_PORT = 9030 @@ -61,8 +63,6 @@ def fileConfig(fname, defaults=None, disable_existing_loggers=True, encoding=Non developer provides a mechanism to present the choices and load the chosen configuration). """ - import configparser - if isinstance(fname, str): if not os.path.exists(fname): raise FileNotFoundError(f"{fname} doesn't exist") @@ -978,7 +978,6 @@ def handle(self): if chunk is not None: # verified, can process chunk = chunk.decode("utf-8") try: - import json d =json.loads(chunk) assert isinstance(d, dict) dictConfig(d) @@ -1023,7 +1022,6 @@ def __init__(self, host='localhost', port=DEFAULT_LOGGING_CONFIG_PORT, self.verify = verify def serve_until_stopped(self): - import select abort = 0 while not abort: rd, wr, ex = select.select([self.socket.fileno()], diff --git a/Lib/logging/handlers.py b/Lib/logging/handlers.py index e225a21da408fa..b3d0613ec07c8a 100644 --- a/Lib/logging/handlers.py +++ b/Lib/logging/handlers.py @@ -23,17 +23,23 @@ To use, simply 'import logging.handlers' and log away! """ -import copy import io import logging import os -import pickle -import queue import re -import socket -import struct import threading import time +lazy import base64 +lazy import copy +lazy import email.utils +lazy import http.client +lazy import pickle +lazy import queue +lazy import smtplib +lazy import socket +lazy import struct +lazy import urllib.parse +lazy from email.message import EmailMessage # # Some constants... @@ -609,6 +615,8 @@ def __init__(self, host, port): self.retryStart = 1.0 self.retryMax = 30.0 self.retryFactor = 2.0 + # resolve what emit() needs now: it may run during finalization + _ = pickle, socket, struct def makeSocket(self, timeout=1): """ @@ -1118,10 +1126,6 @@ def emit(self, record): Format the record and send it to the specified addressees. """ try: - import smtplib - from email.message import EmailMessage - import email.utils - port = self.mailport if not port: port = smtplib.SMTP_PORT @@ -1318,7 +1322,6 @@ def getConnection(self, host, secure): Override when a custom connection is required, for example if there is a proxy. """ - import http.client if secure: connection = http.client.HTTPSConnection(host, context=self.context) else: @@ -1332,7 +1335,6 @@ def emit(self, record): Send the record to the web server as a percent-encoded dictionary """ try: - import urllib.parse host = self.host h = self.getConnection(host, self.secure) url = self.url @@ -1357,7 +1359,6 @@ def emit(self, record): "application/x-www-form-urlencoded") h.putheader("Content-length", str(len(data))) if self.credentials: - import base64 s = ('%s:%s' % self.credentials).encode('utf-8') s = 'Basic ' + base64.b64encode(s).strip().decode('ascii') h.putheader('Authorization', s) diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index b1852ee43b594f..a9455d09641756 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -1865,6 +1865,34 @@ def test_defaults_do_no_interpolation(self): finally: os.unlink(fn) + def test_class_from_handlers_module(self): + # gh-156777: "class=handlers.X" is evaluated against vars(logging). + # Use a subprocess: this module already imports logging.handlers. + ini = textwrap.dedent("""\ + [loggers] + keys=root + [handlers] + keys=hand1 + [formatters] + keys= + [logger_root] + handlers=hand1 + [handler_hand1] + class=handlers.MemoryHandler + args=(10,) + """) + fd, fn = tempfile.mkstemp(prefix='test_logging_', suffix='.ini') + self.addCleanup(os.unlink, fn) + os.write(fd, ini.encode('ascii')) + os.close(fd) + code = textwrap.dedent(f""" + import logging, logging.config + logging.config.fileConfig({fn!r}, encoding="utf-8") + assert isinstance(logging.getLogger().handlers[0], + logging.handlers.MemoryHandler) + """) + assert_python_ok("-c", code) + @support.requires_working_socket() @threading_helper.requires_working_threading() @@ -5437,6 +5465,30 @@ def __del__(self): with open(filename, encoding="utf-8") as fp: self.assertEqual(fp.read().rstrip(), "ERROR:root:log in __del__") + def test_socket_handler_at_shutdown(self): + # gh-156777: SocketHandler pickles the record before sending it, and + # that must keep working when importing no longer can. + code = textwrap.dedent(""" + import logging, logging.handlers, os + + class Handler(logging.handlers.SocketHandler): + def send(self, s): + os.write(1, b"sent") + def handleError(self, record): + os.write(1, b"dropped") + + h = Handler('localhost', 9020) + r = logging.LogRecord('n', logging.INFO, 'p', 1, 'msg', None, None) + + class A: + def __del__(self, h=h, r=r): # globals are cleared by now + h.emit(r) + + a = A() + """) + rc, out, err = assert_python_ok("-c", code) + self.assertEqual(out, b"sent") + def test_recursion_error(self): # Issue 36272 code = textwrap.dedent(""" @@ -7576,6 +7628,45 @@ def test_without_pywin32(self): h.emit(r) +class LazyImportTest(unittest.TestCase): + + """Tests for the module level lazy imports of the logging package.""" + + def test_lazy_imports_config(self): + import_helper.ensure_lazy_imports( + "logging.config", + {"configparser", "json", "multiprocessing", "select", "socket", + "socketserver", "struct"}, + additional_code="logging.config.dictConfig({'version': 1})\n", + ) + + def test_lazy_imports_handlers(self): + import_helper.ensure_lazy_imports( + "logging.handlers", + {"base64", "copy", "email", "http", "pickle", "queue", "smtplib", + "socket", "ssl", "struct", "urllib"}, + ) + + def test_socket_handler_resolves_imports_when_created(self): + # gh-156777: emit() may run when importing no longer works + code = textwrap.dedent(""" + import sys, logging.handlers + logging.handlers.SocketHandler('localhost', 9020) + assert {'pickle', 'socket', 'struct'} <= sys.modules.keys() + """) + assert_python_ok("-S", "-c", code) + + def test_getmembers_without_ssl(self): + # gh-156777: getmembers() resolves lazy imports, so ssl must not be one + code = textwrap.dedent(""" + import sys + sys.modules['_ssl'] = None + import inspect, logging.handlers + inspect.getmembers(logging.handlers) + """) + assert_python_ok("-c", code) + + class MiscTestCase(unittest.TestCase): def test__all__(self): not_exported = { diff --git a/Misc/NEWS.d/next/Library/2026-08-31-08-20-00.gh-issue-156777.La7yLg.rst b/Misc/NEWS.d/next/Library/2026-08-31-08-20-00.gh-issue-156777.La7yLg.rst new file mode 100644 index 00000000000000..8e24f2091b1d93 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-31-08-20-00.gh-issue-156777.La7yLg.rst @@ -0,0 +1,2 @@ +Optimize import time for :mod:`logging.handlers` and :mod:`logging.config` with +lazy imports.