Skip to content

Commit 1ad52b9

Browse files
committed
live-logging: Colorize levelname
1 parent 49773b5 commit 1ad52b9

File tree

3 files changed

+89
-1
lines changed

3 files changed

+89
-1
lines changed

_pytest/logging.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

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

8+
from _pytest.config import create_terminal_writer
79
import pytest
810
import py
911

@@ -12,6 +14,60 @@
1214
DEFAULT_LOG_DATE_FORMAT = '%H:%M:%S'
1315

1416

17+
class ColoredLevelFormatter(logging.Formatter):
18+
"""
19+
Colorize the %(levelname)..s part of the log format passed to __init__.
20+
"""
21+
22+
LOGLEVEL_COLOROPTS = {
23+
logging.CRITICAL: {'red'},
24+
logging.ERROR: {'red', 'bold'},
25+
logging.WARNING: {'yellow'},
26+
logging.WARN: {'yellow'},
27+
logging.INFO: {'green'},
28+
logging.DEBUG: {'purple'},
29+
logging.NOTSET: set(),
30+
}
31+
LEVELNAME_FMT_REGEX = re.compile(r'%\(levelname\)([+-]?\d*s)')
32+
33+
def __init__(self, config, *args, **kwargs):
34+
super(ColoredLevelFormatter, self).__init__(
35+
*args, **kwargs)
36+
if six.PY2:
37+
self._original_fmt = self._fmt
38+
else:
39+
self._original_fmt = self._style._fmt
40+
self._level_to_fmt_mapping = {}
41+
42+
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
43+
if not levelname_fmt_match:
44+
return
45+
levelname_fmt = levelname_fmt_match.group()
46+
47+
tw = create_terminal_writer(config)
48+
49+
for level, color_opts in self.LOGLEVEL_COLOROPTS.items():
50+
formatted_levelname = levelname_fmt % {
51+
'levelname': logging.getLevelName(level)}
52+
53+
# add ANSI escape sequences around the formatted levelname
54+
color_kwargs = {name: True for name in color_opts}
55+
colorized_formatted_levelname = tw.markup(
56+
formatted_levelname, **color_kwargs)
57+
self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub(
58+
colorized_formatted_levelname,
59+
self._fmt)
60+
61+
def format(self, record):
62+
fmt = self._level_to_fmt_mapping.get(
63+
record.levelno, self._original_fmt)
64+
if six.PY2:
65+
self._fmt = fmt
66+
else:
67+
self._style._fmt = fmt
68+
return super(ColoredLevelFormatter, self).format(record)
69+
70+
1571
def get_option_ini(config, *names):
1672
for name in names:
1773
ret = config.getoption(name) # 'default' arg won't work as expected
@@ -376,7 +432,10 @@ def _setup_cli_logging(self):
376432
log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
377433
log_cli_format = get_option_ini(self._config, 'log_cli_format', 'log_format')
378434
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)
435+
if ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format):
436+
log_cli_formatter = ColoredLevelFormatter(self._config, log_cli_format, datefmt=log_cli_date_format)
437+
else:
438+
log_cli_formatter = logging.Formatter(log_cli_format, datefmt=log_cli_date_format)
380439
log_cli_level = get_actual_log_level(self._config, 'log_cli_level', 'log_level')
381440
self.log_cli_handler = log_cli_handler
382441
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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import logging
2+
3+
from _pytest.logging import ColoredLevelFormatter
4+
5+
6+
def test_coloredlogformatter():
7+
logfmt = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s'
8+
9+
record = logging.LogRecord(
10+
name='dummy', level=logging.INFO, pathname='dummypath', lineno=10,
11+
msg='Test Message', args=(), exc_info=False)
12+
13+
class ColorConfig(object):
14+
class option(object):
15+
pass
16+
17+
config = ColorConfig()
18+
config.option.color = 'yes'
19+
formatter = ColoredLevelFormatter(config, logfmt)
20+
output = formatter.format(record)
21+
assert output == ('dummypath 10 '
22+
'\x1b[32mINFO \x1b[0m Test Message')
23+
24+
config.option.color = 'no'
25+
formatter = ColoredLevelFormatter(config, logfmt)
26+
output = formatter.format(record)
27+
assert output == ('dummypath 10 '
28+
'INFO Test Message')

0 commit comments

Comments
 (0)