Skip to content

Commit 515fce4

Browse files
bpo-40275: Avoid importing logging in test.support (GH-19601)
Import logging lazily in assertLogs() in unittest. Move TestHandler from test.support to logging_helper.
1 parent 1699491 commit 515fce4

File tree

7 files changed

+105
-109
lines changed

7 files changed

+105
-109
lines changed

Doc/library/test.rst

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,11 +1410,6 @@ The :mod:`test.support` module defines the following classes:
14101410
Run *test* and return the result.
14111411

14121412

1413-
.. class:: TestHandler(logging.handlers.BufferingHandler)
1414-
1415-
Class for logging support.
1416-
1417-
14181413
.. class:: FakePath(path)
14191414

14201415
Simple :term:`path-like object`. It implements the :meth:`__fspath__`

Lib/test/support/__init__.py

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import importlib
1616
import importlib.util
1717
import locale
18-
import logging.handlers
1918
import os
2019
import platform
2120
import re
@@ -99,8 +98,6 @@
9998
"open_urlresource",
10099
# processes
101100
'temp_umask', "reap_children",
102-
# logging
103-
"TestHandler",
104101
# threads
105102
"threading_setup", "threading_cleanup", "reap_threads", "start_threads",
106103
# miscellaneous
@@ -2368,37 +2365,6 @@ def optim_args_from_interpreter_flags():
23682365
optimization settings in sys.flags."""
23692366
return subprocess._optim_args_from_interpreter_flags()
23702367

2371-
#============================================================
2372-
# Support for assertions about logging.
2373-
#============================================================
2374-
2375-
class TestHandler(logging.handlers.BufferingHandler):
2376-
def __init__(self, matcher):
2377-
# BufferingHandler takes a "capacity" argument
2378-
# so as to know when to flush. As we're overriding
2379-
# shouldFlush anyway, we can set a capacity of zero.
2380-
# You can call flush() manually to clear out the
2381-
# buffer.
2382-
logging.handlers.BufferingHandler.__init__(self, 0)
2383-
self.matcher = matcher
2384-
2385-
def shouldFlush(self):
2386-
return False
2387-
2388-
def emit(self, record):
2389-
self.format(record)
2390-
self.buffer.append(record.__dict__)
2391-
2392-
def matches(self, **kwargs):
2393-
"""
2394-
Look for a saved dict whose keys/values match the supplied arguments.
2395-
"""
2396-
result = False
2397-
for d in self.buffer:
2398-
if self.matcher.matches(d, **kwargs):
2399-
result = True
2400-
break
2401-
return result
24022368

24032369
class Matcher(object):
24042370

Lib/test/support/logging_helper.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import logging.handlers
2+
3+
class TestHandler(logging.handlers.BufferingHandler):
4+
def __init__(self, matcher):
5+
# BufferingHandler takes a "capacity" argument
6+
# so as to know when to flush. As we're overriding
7+
# shouldFlush anyway, we can set a capacity of zero.
8+
# You can call flush() manually to clear out the
9+
# buffer.
10+
logging.handlers.BufferingHandler.__init__(self, 0)
11+
self.matcher = matcher
12+
13+
def shouldFlush(self):
14+
return False
15+
16+
def emit(self, record):
17+
self.format(record)
18+
self.buffer.append(record.__dict__)
19+
20+
def matches(self, **kwargs):
21+
"""
22+
Look for a saved dict whose keys/values match the supplied arguments.
23+
"""
24+
result = False
25+
for d in self.buffer:
26+
if self.matcher.matches(d, **kwargs):
27+
result = True
28+
break
29+
return result

Lib/test/test_logging.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from test.support.script_helper import assert_python_ok, assert_python_failure
4545
from test import support
4646
from test.support import socket_helper
47+
from test.support.logging_helper import TestHandler
4748
import textwrap
4849
import threading
4950
import time
@@ -3524,7 +3525,7 @@ def test_formatting(self):
35243525
@unittest.skipUnless(hasattr(logging.handlers, 'QueueListener'),
35253526
'logging.handlers.QueueListener required for this test')
35263527
def test_queue_listener(self):
3527-
handler = support.TestHandler(support.Matcher())
3528+
handler = TestHandler(support.Matcher())
35283529
listener = logging.handlers.QueueListener(self.queue, handler)
35293530
listener.start()
35303531
try:
@@ -3540,7 +3541,7 @@ def test_queue_listener(self):
35403541

35413542
# Now test with respect_handler_level set
35423543

3543-
handler = support.TestHandler(support.Matcher())
3544+
handler = TestHandler(support.Matcher())
35443545
handler.setLevel(logging.CRITICAL)
35453546
listener = logging.handlers.QueueListener(self.queue, handler,
35463547
respect_handler_level=True)

Lib/unittest/_log.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import logging
2+
import collections
3+
4+
from .case import _BaseTestCaseContext
5+
6+
7+
_LoggingWatcher = collections.namedtuple("_LoggingWatcher",
8+
["records", "output"])
9+
10+
class _CapturingHandler(logging.Handler):
11+
"""
12+
A logging handler capturing all (raw and formatted) logging output.
13+
"""
14+
15+
def __init__(self):
16+
logging.Handler.__init__(self)
17+
self.watcher = _LoggingWatcher([], [])
18+
19+
def flush(self):
20+
pass
21+
22+
def emit(self, record):
23+
self.watcher.records.append(record)
24+
msg = self.format(record)
25+
self.watcher.output.append(msg)
26+
27+
28+
class _AssertLogsContext(_BaseTestCaseContext):
29+
"""A context manager used to implement TestCase.assertLogs()."""
30+
31+
LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s"
32+
33+
def __init__(self, test_case, logger_name, level):
34+
_BaseTestCaseContext.__init__(self, test_case)
35+
self.logger_name = logger_name
36+
if level:
37+
self.level = logging._nameToLevel.get(level, level)
38+
else:
39+
self.level = logging.INFO
40+
self.msg = None
41+
42+
def __enter__(self):
43+
if isinstance(self.logger_name, logging.Logger):
44+
logger = self.logger = self.logger_name
45+
else:
46+
logger = self.logger = logging.getLogger(self.logger_name)
47+
formatter = logging.Formatter(self.LOGGING_FORMAT)
48+
handler = _CapturingHandler()
49+
handler.setFormatter(formatter)
50+
self.watcher = handler.watcher
51+
self.old_handlers = logger.handlers[:]
52+
self.old_level = logger.level
53+
self.old_propagate = logger.propagate
54+
logger.handlers = [handler]
55+
logger.setLevel(self.level)
56+
logger.propagate = False
57+
return handler.watcher
58+
59+
def __exit__(self, exc_type, exc_value, tb):
60+
self.logger.handlers = self.old_handlers
61+
self.logger.propagate = self.old_propagate
62+
self.logger.setLevel(self.old_level)
63+
if exc_type is not None:
64+
# let unexpected exceptions pass through
65+
return False
66+
if len(self.watcher.records) == 0:
67+
self._raiseFailure(
68+
"no logs of level {} or higher triggered on {}"
69+
.format(logging.getLevelName(self.level), self.logger.name))

Lib/unittest/case.py

Lines changed: 2 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import sys
44
import functools
55
import difflib
6-
import logging
76
import pprint
87
import re
98
import warnings
@@ -297,73 +296,6 @@ def __exit__(self, exc_type, exc_value, tb):
297296

298297

299298

300-
_LoggingWatcher = collections.namedtuple("_LoggingWatcher",
301-
["records", "output"])
302-
303-
304-
class _CapturingHandler(logging.Handler):
305-
"""
306-
A logging handler capturing all (raw and formatted) logging output.
307-
"""
308-
309-
def __init__(self):
310-
logging.Handler.__init__(self)
311-
self.watcher = _LoggingWatcher([], [])
312-
313-
def flush(self):
314-
pass
315-
316-
def emit(self, record):
317-
self.watcher.records.append(record)
318-
msg = self.format(record)
319-
self.watcher.output.append(msg)
320-
321-
322-
323-
class _AssertLogsContext(_BaseTestCaseContext):
324-
"""A context manager used to implement TestCase.assertLogs()."""
325-
326-
LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s"
327-
328-
def __init__(self, test_case, logger_name, level):
329-
_BaseTestCaseContext.__init__(self, test_case)
330-
self.logger_name = logger_name
331-
if level:
332-
self.level = logging._nameToLevel.get(level, level)
333-
else:
334-
self.level = logging.INFO
335-
self.msg = None
336-
337-
def __enter__(self):
338-
if isinstance(self.logger_name, logging.Logger):
339-
logger = self.logger = self.logger_name
340-
else:
341-
logger = self.logger = logging.getLogger(self.logger_name)
342-
formatter = logging.Formatter(self.LOGGING_FORMAT)
343-
handler = _CapturingHandler()
344-
handler.setFormatter(formatter)
345-
self.watcher = handler.watcher
346-
self.old_handlers = logger.handlers[:]
347-
self.old_level = logger.level
348-
self.old_propagate = logger.propagate
349-
logger.handlers = [handler]
350-
logger.setLevel(self.level)
351-
logger.propagate = False
352-
return handler.watcher
353-
354-
def __exit__(self, exc_type, exc_value, tb):
355-
self.logger.handlers = self.old_handlers
356-
self.logger.propagate = self.old_propagate
357-
self.logger.setLevel(self.old_level)
358-
if exc_type is not None:
359-
# let unexpected exceptions pass through
360-
return False
361-
if len(self.watcher.records) == 0:
362-
self._raiseFailure(
363-
"no logs of level {} or higher triggered on {}"
364-
.format(logging.getLevelName(self.level), self.logger.name))
365-
366-
367299
class _OrderedChainMap(collections.ChainMap):
368300
def __iter__(self):
369301
seen = set()
@@ -854,6 +786,8 @@ def assertLogs(self, logger=None, level=None):
854786
self.assertEqual(cm.output, ['INFO:foo:first message',
855787
'ERROR:foo.bar:second message'])
856788
"""
789+
# Lazy import to avoid importing logging if it is not needed.
790+
from ._log import _AssertLogsContext
857791
return _AssertLogsContext(self, logger, level)
858792

859793
def _getAssertEqualityFunc(self, first, second):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The :mod:`logging` package is now imported lazily in :mod:`unittest` only
2+
when the :meth:`~unittest.TestCase.assertLogs` assertion is used.

0 commit comments

Comments
 (0)