diff --git a/colorlog/formatter.py b/colorlog/formatter.py index e4fb14d..f67d44e 100644 --- a/colorlog/formatter.py +++ b/colorlog/formatter.py @@ -212,6 +212,12 @@ def __init__(self, fmt: typing.Mapping[str, str], **kwargs: typing.Any) -> None: - fmt (dict): A mapping of log levels (represented as strings, e.g. 'WARNING') to format strings. (*New in version 2.7.0) + + Levels that are not present in the mapping fall back to a default + formatter, so records logged at custom or unlisted levels are + formatted rather than raising a ``KeyError``. To customise the + fallback, provide a format string under the special ``"DEFAULT"`` + key; otherwise the default format for the chosen ``style`` is used. (All other parameters are the same as in colorlog.ColoredFormatter) Example: @@ -229,9 +235,15 @@ def __init__(self, fmt: typing.Mapping[str, str], **kwargs: typing.Any) -> None: self.formatters = { level: ColoredFormatter(fmt=f, **kwargs) for level, f in fmt.items() } + # Used for any level not present in ``fmt``. An explicit "DEFAULT" entry + # takes precedence; otherwise fall back to the default format string. + self.default_formatter = self.formatters.get( + "DEFAULT", ColoredFormatter(**kwargs) + ) def format(self, record: logging.LogRecord) -> str: - return self.formatters[record.levelname].format(record) + formatter = self.formatters.get(record.levelname, self.default_formatter) + return formatter.format(record) # Provided for backwards compatibility. The features provided by this subclass are now diff --git a/colorlog/tests/test_colorlog.py b/colorlog/tests/test_colorlog.py index 23270ce..8ac11e1 100644 --- a/colorlog/tests/test_colorlog.py +++ b/colorlog/tests/test_colorlog.py @@ -1,5 +1,6 @@ """Test the colorlog.colorlog module.""" +import logging import sys import unittest.mock @@ -100,6 +101,29 @@ def test_level_formatter(self, create_and_test_logger): }, ) + def test_unlisted_level_uses_default(self): + """A level missing from fmt must not raise; it uses the fallback.""" + logging.addLevelName(25, "NOTICE") + formatter = colorlog.LevelFormatter(fmt={"INFO": "%(message)s"}) + record = logging.LogRecord( + "test", 25, "path", 1, "a notice message", None, None + ) + assert "a notice message" in formatter.format(record) + + def test_explicit_default_key(self): + """A 'DEFAULT' entry overrides the fallback format for unlisted levels.""" + logging.addLevelName(25, "NOTICE") + formatter = colorlog.LevelFormatter( + fmt={ + "INFO": "%(message)s", + "DEFAULT": "DEFAULT: %(message)s", + } + ) + record = logging.LogRecord( + "test", 25, "path", 1, "a notice message", None, None + ) + assert "DEFAULT: a notice message" in formatter.format(record) + def test_ttycolorlog(create_and_test_logger, monkeypatch): monkeypatch.setattr(sys.stderr, "isatty", lambda: True)