Skip to content

Commit a91f174

Browse files
committed
Use same code for setting up cli/non-cli formatter
A method _create_formatter was introduced that is used for both the log_cli_formatter and the log_formatter. Consequences of this commit are: * Captured logs that are output for each failing test are formatted using the ColoredLevelFromatter. * The formatter used for writing to a file still uses the non-colored logging.Formatter class.
1 parent 10ca84f commit a91f174

File tree

3 files changed

+74
-19
lines changed

3 files changed

+74
-19
lines changed

changelog/5311.feature.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Captured logs that are output for each failing test are formatted using the
2+
ColoredLevelFromatter. As a consequence caplog.text contains the ANSI
3+
escape sequences used for coloring the level names now.

src/_pytest/logging.py

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717

1818
DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s"
1919
DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S"
20+
_ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m")
21+
22+
23+
def _remove_ansi_escape_sequences(text):
24+
return _ANSI_ESCAPE_SEQ.sub("", text)
2025

2126

2227
class ColoredLevelFormatter(logging.Formatter):
@@ -256,8 +261,8 @@ def get_records(self, when):
256261

257262
@property
258263
def text(self):
259-
"""Returns the log text."""
260-
return self.handler.stream.getvalue()
264+
"""Returns the formatted log text."""
265+
return _remove_ansi_escape_sequences(self.handler.stream.getvalue())
261266

262267
@property
263268
def records(self):
@@ -393,7 +398,7 @@ def __init__(self, config):
393398
config.option.verbose = 1
394399

395400
self.print_logs = get_option_ini(config, "log_print")
396-
self.formatter = logging.Formatter(
401+
self.formatter = self._create_formatter(
397402
get_option_ini(config, "log_format"),
398403
get_option_ini(config, "log_date_format"),
399404
)
@@ -427,6 +432,19 @@ def __init__(self, config):
427432
if self._log_cli_enabled():
428433
self._setup_cli_logging()
429434

435+
def _create_formatter(self, log_format, log_date_format):
436+
# color option doesn't exist if terminal plugin is disabled
437+
color = getattr(self._config.option, "color", "no")
438+
if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(
439+
log_format
440+
):
441+
formatter = ColoredLevelFormatter(
442+
create_terminal_writer(self._config), log_format, log_date_format
443+
)
444+
else:
445+
formatter = logging.Formatter(log_format, log_date_format)
446+
return formatter
447+
430448
def _setup_cli_logging(self):
431449
config = self._config
432450
terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
@@ -437,23 +455,12 @@ def _setup_cli_logging(self):
437455
capture_manager = config.pluginmanager.get_plugin("capturemanager")
438456
# if capturemanager plugin is disabled, live logging still works.
439457
log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
440-
log_cli_format = get_option_ini(config, "log_cli_format", "log_format")
441-
log_cli_date_format = get_option_ini(
442-
config, "log_cli_date_format", "log_date_format"
458+
459+
log_cli_formatter = self._create_formatter(
460+
get_option_ini(config, "log_cli_format", "log_format"),
461+
get_option_ini(config, "log_cli_date_format", "log_date_format"),
443462
)
444-
if (
445-
config.option.color != "no"
446-
and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format)
447-
):
448-
log_cli_formatter = ColoredLevelFormatter(
449-
create_terminal_writer(config),
450-
log_cli_format,
451-
datefmt=log_cli_date_format,
452-
)
453-
else:
454-
log_cli_formatter = logging.Formatter(
455-
log_cli_format, datefmt=log_cli_date_format
456-
)
463+
457464
log_cli_level = get_actual_log_level(config, "log_cli_level", "log_level")
458465
self.log_cli_handler = log_cli_handler
459466
self.live_logs_context = lambda: catching_logs(

testing/logging/test_reporting.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,3 +1084,48 @@ def test_second():
10841084
with open(os.path.join(report_dir_base, "test_second"), "r") as rfh:
10851085
content = rfh.read()
10861086
assert "message from test 2" in content
1087+
1088+
1089+
def test_colored_captured_log(testdir):
1090+
"""
1091+
Test that the level names of captured log messages of a failing test are
1092+
colored.
1093+
"""
1094+
testdir.makepyfile(
1095+
"""
1096+
import logging
1097+
1098+
logger = logging.getLogger(__name__)
1099+
1100+
def test_foo():
1101+
logger.info('text going to logger from call')
1102+
assert False
1103+
"""
1104+
)
1105+
result = testdir.runpytest("--log-level=INFO", "--color=yes")
1106+
assert result.ret == 1
1107+
result.stdout.fnmatch_lines(
1108+
[
1109+
"*-- Captured log call --*",
1110+
"\x1b[32mINFO \x1b[0m*text going to logger from call",
1111+
]
1112+
)
1113+
1114+
1115+
def test_colored_ansi_esc_caplogtext(testdir):
1116+
"""
1117+
Make sure that caplog.text does not contain ANSI escape sequences.
1118+
"""
1119+
testdir.makepyfile(
1120+
"""
1121+
import logging
1122+
1123+
logger = logging.getLogger(__name__)
1124+
1125+
def test_foo(caplog):
1126+
logger.info('text going to logger from call')
1127+
assert '\x1b' not in caplog.text
1128+
"""
1129+
)
1130+
result = testdir.runpytest("--log-level=INFO", "--color=yes")
1131+
assert result.ret == 0

0 commit comments

Comments
 (0)