Skip to content

Commit ebab1b6

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

File tree

3 files changed

+89
-1
lines changed

3 files changed

+89
-1
lines changed

_pytest/logging.py

Lines changed: 59 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,58 @@
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, terminalwriter, *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+
for level, color_opts in self.LOGLEVEL_COLOROPTS.items():
48+
formatted_levelname = levelname_fmt % {
49+
'levelname': logging.getLevelName(level)}
50+
51+
# add ANSI escape sequences around the formatted levelname
52+
color_kwargs = {name: True for name in color_opts}
53+
colorized_formatted_levelname = terminalwriter.markup(
54+
formatted_levelname, **color_kwargs)
55+
self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub(
56+
colorized_formatted_levelname,
57+
self._fmt)
58+
59+
def format(self, record):
60+
fmt = self._level_to_fmt_mapping.get(
61+
record.levelno, self._original_fmt)
62+
if six.PY2:
63+
self._fmt = fmt
64+
else:
65+
self._style._fmt = fmt
66+
return super(ColoredLevelFormatter, self).format(record)
67+
68+
1569
def get_option_ini(config, *names):
1670
for name in names:
1771
ret = config.getoption(name) # 'default' arg won't work as expected
@@ -376,7 +430,11 @@ def _setup_cli_logging(self):
376430
log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
377431
log_cli_format = get_option_ini(self._config, 'log_cli_format', 'log_format')
378432
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)
433+
if self._config.option.color != 'no' and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format):
434+
log_cli_formatter = ColoredLevelFormatter(create_terminal_writer(self._config),
435+
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: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import logging
2+
3+
import py.io
4+
from _pytest.logging import ColoredLevelFormatter
5+
6+
7+
def test_coloredlogformatter():
8+
logfmt = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s'
9+
10+
record = logging.LogRecord(
11+
name='dummy', level=logging.INFO, pathname='dummypath', lineno=10,
12+
msg='Test Message', args=(), exc_info=False)
13+
14+
class ColorConfig(object):
15+
class option(object):
16+
pass
17+
18+
tw = py.io.TerminalWriter()
19+
tw.hasmarkup = True
20+
formatter = ColoredLevelFormatter(tw, logfmt)
21+
output = formatter.format(record)
22+
assert output == ('dummypath 10 '
23+
'\x1b[32mINFO \x1b[0m Test Message')
24+
25+
tw.hasmarkup = False
26+
formatter = ColoredLevelFormatter(tw, logfmt)
27+
output = formatter.format(record)
28+
assert output == ('dummypath 10 '
29+
'INFO Test Message')

0 commit comments

Comments
 (0)