Skip to content

Commit c1da614

Browse files
committed
live-logging: Colorize levelname
1 parent a580990 commit c1da614

File tree

3 files changed

+92
-1
lines changed

3 files changed

+92
-1
lines changed

_pytest/logging.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import logging
44
from contextlib import closing, contextmanager
5+
import re
56
import six
67

78
import pytest
@@ -12,6 +13,60 @@
1213
DEFAULT_LOG_DATE_FORMAT = '%H:%M:%S'
1314

1415

16+
class ColoredLevelFormatter(logging.Formatter):
17+
"""
18+
Colorize the %(levelname)..s part of the log format passed to __init__.
19+
"""
20+
21+
LOGLEVEL_COLOROPTS = {
22+
logging.CRITICAL: {'red'},
23+
logging.ERROR: {'red', 'bold'},
24+
logging.WARNING: {'yellow'},
25+
logging.WARN: {'yellow'},
26+
logging.INFO: {'green'},
27+
logging.DEBUG: {'purple'},
28+
logging.NOTSET: set(),
29+
}
30+
LEVELNAME_FMT_REGEX = re.compile(r'%\(levelname\)([+-]?\d*s)')
31+
32+
def __init__(self, *args, **kwargs):
33+
super(ColoredLevelFormatter, self).__init__(
34+
*args, **kwargs)
35+
if six.PY2:
36+
self._original_fmt = self._fmt
37+
else:
38+
self._original_fmt = self._style._fmt
39+
self._level_to_fmt_mapping = {}
40+
41+
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
42+
if not levelname_fmt_match:
43+
return
44+
levelname_fmt = levelname_fmt_match.group()
45+
46+
tw = py.io.TerminalWriter()
47+
48+
for level, color_opts in self.LOGLEVEL_COLOROPTS.items():
49+
formatted_levelname = levelname_fmt % {
50+
'levelname': logging.getLevelName(level)}
51+
52+
# add ANSI escape sequences around the formatted levelname
53+
color_kwargs = {name: True for name in color_opts}
54+
colorized_formatted_levelname = tw.markup(
55+
formatted_levelname, **color_kwargs)
56+
self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub(
57+
colorized_formatted_levelname,
58+
self._fmt)
59+
60+
def format(self, record):
61+
fmt = self._level_to_fmt_mapping.get(
62+
record.levelno, self._original_fmt)
63+
if six.PY2:
64+
self._fmt = fmt
65+
else:
66+
self._style._fmt = fmt
67+
return super(ColoredLevelFormatter, self).format(record)
68+
69+
1570
def get_option_ini(config, *names):
1671
for name in names:
1772
ret = config.getoption(name) # 'default' arg won't work as expected
@@ -376,7 +431,10 @@ def _setup_cli_logging(self):
376431
log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
377432
log_cli_format = get_option_ini(self._config, 'log_cli_format', 'log_format')
378433
log_cli_date_format = get_option_ini(self._config, 'log_cli_date_format', 'log_date_format')
379-
log_cli_formatter = logging.Formatter(log_cli_format, datefmt=log_cli_date_format)
434+
if ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format):
435+
log_cli_formatter = ColoredLevelFormatter(log_cli_format, datefmt=log_cli_date_format)
436+
else:
437+
log_cli_formatter = logging.Formatter(log_cli_format, datefmt=log_cli_date_format)
380438
log_cli_level = get_actual_log_level(self._config, 'log_cli_level', 'log_level')
381439
self.log_cli_handler = log_cli_handler
382440
self.live_logs_context = catching_logs(log_cli_handler, formatter=log_cli_formatter, level=log_cli_level)

changelog/3142.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Colorize the levelname column in the live-log output.

testing/logging/test_formatter.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import logging
2+
import os
3+
4+
import six
5+
6+
from py.io import TerminalWriter
7+
import pytest
8+
from _pytest.logging import ColoredLevelFormatter
9+
10+
11+
def test_coloredlogformatter():
12+
# TODO remove usage of mock module in this test
13+
pytest.importorskip("mock")
14+
import mock
15+
16+
logfmt = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s'
17+
18+
tw = TerminalWriter()
19+
tw.hasmarkup = True # otherwise tw.markup does not add any escape
20+
# sequences
21+
22+
record = logging.LogRecord(
23+
name='dummy', level=logging.INFO, pathname='dummypath', lineno=10,
24+
msg='Test Message', args=(), exc_info=False)
25+
26+
with mock.patch('py.io.TerminalWriter') as mock_tw:
27+
mock_tw.return_value = tw
28+
formatter = ColoredLevelFormatter(logfmt)
29+
30+
output = formatter.format(record)
31+
assert output == ('dummypath 10 '
32+
'\x1b[32mINFO \x1b[0m Test Message')

0 commit comments

Comments
 (0)