Skip to content
7 changes: 3 additions & 4 deletions Lib/logging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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,)

Expand Down Expand Up @@ -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",
Expand Down
20 changes: 9 additions & 11 deletions Lib/logging/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,19 @@
import functools
import io
import logging
import logging.handlers
Comment thread
brittanyrey marked this conversation as resolved.
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
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()],
Expand Down
25 changes: 13 additions & 12 deletions Lib/logging/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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...
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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)
Expand Down
91 changes: 91 additions & 0 deletions Lib/test/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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("""
Expand Down Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Optimize import time for :mod:`logging.handlers` and :mod:`logging.config` with
lazy imports.
Loading