diff --git a/AUTHORS b/AUTHORS index f68a75095..8f184f353 100644 --- a/AUTHORS +++ b/AUTHORS @@ -309,6 +309,7 @@ Tanvi Mehta Tarcisio Fischer Tareq Alayan Ted Xiao +Terje Runde Thomas Grainger Thomas Hisch Tim Hoffmann diff --git a/changelog/8803.improvement.rst b/changelog/8803.improvement.rst new file mode 100644 index 000000000..608007002 --- /dev/null +++ b/changelog/8803.improvement.rst @@ -0,0 +1,9 @@ +It is now possible to add colors to custom log levels on cli log. + +By using :func:`add_color_level <_pytest.logging.add_color_level` from a ``pytest_configure`` hook, colors can be added:: + + logging_plugin = config.pluginmanager.get_plugin('logging-plugin') + logging_plugin.log_cli_handler.formatter.add_color_level(logging.INFO, 'cyan') + logging_plugin.log_cli_handler.formatter.add_color_level(logging.SPAM, 'blue') + + See :ref:`log_colors` for more information. diff --git a/doc/en/how-to/logging.rst b/doc/en/how-to/logging.rst index ef477dc4f..7d8388116 100644 --- a/doc/en/how-to/logging.rst +++ b/doc/en/how-to/logging.rst @@ -219,6 +219,30 @@ option names are: You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality is considered **experimental**. +.. _log_colors: + +Customizing Colors +^^^^^^^^^^^^^^^^^^ + +Log levels are colored if colored terminal output is enabled. Changing +from default colors or putting color on custom log levels is supported +through ``add_color_level()``. Example: + +.. code-block:: python + + @pytest.hookimpl + def pytest_configure(config): + logging_plugin = config.pluginmanager.get_plugin("logging-plugin") + + # Change color on existing log level + logging_plugin.log_cli_handler.formatter.add_color_level(logging.INFO, "cyan") + + # Add color to a custom log level (a custom log level `SPAM` is already set up) + logging_plugin.log_cli_handler.formatter.add_color_level(logging.SPAM, "blue") +.. warning:: + + This feature and its API are considered **experimental** and might change + between releases without a deprecation notice. .. _log_release_notes: Release notes diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 7ed1820bb..9580d39f9 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -63,28 +63,43 @@ class ColoredLevelFormatter(logging.Formatter): def __init__(self, terminalwriter: TerminalWriter, *args, **kwargs) -> None: super().__init__(*args, **kwargs) + self._terminalwriter = terminalwriter self._original_fmt = self._style._fmt self._level_to_fmt_mapping: Dict[int, str] = {} + for level, color_opts in self.LOGLEVEL_COLOROPTS.items(): + self.add_color_level(level, *color_opts) + + def add_color_level(self, level: int, *color_opts: str) -> None: + """Add or update color opts for a log level. + + :param level: + Log level to apply a style to, e.g. ``logging.INFO``. + :param color_opts: + ANSI escape sequence color options. Capitalized colors indicates + background color, i.e. ``'green', 'Yellow', 'bold'`` will give bold + green text on yellow background. + + .. warning:: + This is an experimental API. + """ + assert self._fmt is not None levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt) if not levelname_fmt_match: return levelname_fmt = levelname_fmt_match.group() - for level, color_opts in self.LOGLEVEL_COLOROPTS.items(): - formatted_levelname = levelname_fmt % { - "levelname": logging.getLevelName(level) - } + formatted_levelname = levelname_fmt % {"levelname": logging.getLevelName(level)} - # add ANSI escape sequences around the formatted levelname - color_kwargs = {name: True for name in color_opts} - colorized_formatted_levelname = terminalwriter.markup( - formatted_levelname, **color_kwargs - ) - self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub( - colorized_formatted_levelname, self._fmt - ) + # add ANSI escape sequences around the formatted levelname + color_kwargs = {name: True for name in color_opts} + colorized_formatted_levelname = self._terminalwriter.markup( + formatted_levelname, **color_kwargs + ) + self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub( + colorized_formatted_levelname, self._fmt + ) def format(self, record: logging.LogRecord) -> str: fmt = self._level_to_fmt_mapping.get(record.levelno, self._original_fmt)