Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions colorlog/formatter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""The ColoredFormatter class."""

import copy
import logging
import os
import sys
Expand Down Expand Up @@ -179,6 +180,16 @@ def _append_reset(self, message: str, escapes: EscapeCodes) -> str:

if sys.version_info >= (3, 13):

def format(self, record: logging.LogRecord) -> str:
"""Keep exception colors local to this formatter."""
if record.exc_info:
if not record.exc_text:
# Keep plain exception text available to record forwarders.
record.exc_text = super().formatException(record.exc_info)
record = copy.copy(record)
record.exc_text = None
return super().format(record)

def formatException(self, ei) -> str:
"""
Format and return the specified exception information as a string.
Expand Down
57 changes: 57 additions & 0 deletions colorlog/tests/test_colorlog.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Test the colorlog.colorlog module."""

import io
import logging
import pickle
import sys
import unittest.mock

Expand Down Expand Up @@ -158,3 +160,58 @@ def test_formatException(tty: bool, monkeypatch):
text = formatter.formatException(ei)

assert ("\x1b[" in text) == tty

@pytest.fixture
def exception_record() -> logging.LogRecord:
try:
raise ValueError("example failure")
except ValueError:
return logging.LogRecord(
"example", logging.ERROR, "example.py", 1, "failed", (), sys.exc_info()
)

@pytest.mark.parametrize(
"plain_formatter",
[
logging.Formatter("%(message)s"),
colorlog.ColoredFormatter("%(message)s", no_color=True),
colorlog.ColoredFormatter("%(message)s", stream=io.StringIO()),
],
ids=["stdlib", "no_color", "non_tty"],
)
@pytest.mark.parametrize("color_first", [True, False])
def test_exception_colors_are_per_formatter(
exception_record, plain_formatter, color_first
):
colored_formatter = colorlog.ColoredFormatter("%(message)s", force_color=True)
if color_first:
colored = colored_formatter.format(exception_record)
plain = plain_formatter.format(exception_record)
else:
plain = plain_formatter.format(exception_record)
colored = colored_formatter.format(exception_record)

assert "\x1b[" not in plain
assert "\x1b[" in colored.split("\n", 1)[1]

def test_exception_text_survives_record_forwarding(exception_record):
colorlog.ColoredFormatter("%(message)s", force_color=True).format(
exception_record
)
exception_record.exc_info = None
forwarded = pickle.loads(pickle.dumps(exception_record))
output = logging.Formatter("%(message)s").format(forwarded)

assert "ValueError: example failure" in output
assert "\x1b[" not in output

@pytest.mark.parametrize("force_color", [True, False])
def test_cached_exception_text_without_exc_info(exception_record, force_color):
exception_record.exc_info = None
exception_record.exc_text = "ValueError: remotely cached failure"
formatter = colorlog.ColoredFormatter(
"%(message)s", force_color=force_color, no_color=not force_color
)

assert formatter.format(exception_record).endswith(exception_record.exc_text)
assert exception_record.exc_text == "ValueError: remotely cached failure"