Skip to content

Commit

Permalink
feat: Support once parameter in logging methods, allowing to log a …
Browse files Browse the repository at this point in the history
…message only once with a given logger

This will be useful when issuing warning messages in templates, for example when deprecating things, as we don't want to show the message dozens of time (each time the template is used), but rather just once.
  • Loading branch information
pawamoy committed Apr 27, 2024
1 parent d799d2f commit 1532b59
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 5 deletions.
51 changes: 46 additions & 5 deletions src/mkdocstrings/loggers.py
Expand Up @@ -17,15 +17,33 @@
except ImportError:
TEMPLATES_DIRS: Sequence[Path] = ()
else:
TEMPLATES_DIRS = tuple(mkdocstrings_handlers.__path__) # type: ignore[arg-type]
TEMPLATES_DIRS = tuple(mkdocstrings_handlers.__path__)


if TYPE_CHECKING:
from jinja2.runtime import Context


class LoggerAdapter(logging.LoggerAdapter):
"""A logger adapter to prefix messages."""
"""A logger adapter to prefix messages.
This adapter also adds an additional parameter to logging methods
called `once`: if `True`, the message will only be logged once.
Examples:
In Python code:
>>> logger = get_logger("myplugin")
>>> logger.debug("This is a debug message.")
>>> logger.info("This is an info message.", once=True)
In Jinja templates (logger available in context as `log`):
```jinja
{{ log.debug("This is a debug message.") }}
{{ log.info("This is an info message.", once=True) }}
```
"""

def __init__(self, prefix: str, logger: logging.Logger):
"""Initialize the object.
Expand All @@ -36,6 +54,7 @@ def __init__(self, prefix: str, logger: logging.Logger):
"""
super().__init__(logger, {})
self.prefix = prefix
self._logged: set[tuple[LoggerAdapter, str]] = set()

def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, Any]:
"""Process the message.
Expand All @@ -49,11 +68,32 @@ def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, Any]
"""
return f"{self.prefix}: {msg}", kwargs

def log(self, level: int, msg: object, *args: object, **kwargs: object) -> None:
"""Log a message.
Arguments:
level: The logging level.
msg: The message.
*args: Additional arguments passed to parent method.
**kwargs: Additional keyword arguments passed to parent method.
"""
if kwargs.pop("once", False):
if (key := (self, str(msg))) in self._logged:
return
self._logged.add(key)
super().log(level, msg, *args, **kwargs) # type: ignore[arg-type]


class TemplateLogger:
"""A wrapper class to allow logging in templates.
Attributes:
The logging methods provided by this class all accept
two parameters:
- `msg`: The message to log.
- `once`: If `True`, the message will only be logged once.
Methods:
debug: Function to log a DEBUG message.
info: Function to log an INFO message.
warning: Function to log a WARNING message.
Expand Down Expand Up @@ -85,18 +125,19 @@ def get_template_logger_function(logger_func: Callable) -> Callable:
"""

@pass_context
def wrapper(context: Context, msg: str | None = None) -> str:
def wrapper(context: Context, msg: str | None = None, **kwargs: Any) -> str:
"""Log a message.
Arguments:
context: The template context, automatically provided by Jinja.
msg: The message to log.
**kwargs: Additional arguments passed to the logger function.
Returns:
An empty string.
"""
template_path = get_template_path(context)
logger_func(f"{template_path}: {msg or 'Rendering'}")
logger_func(f"{template_path}: {msg or 'Rendering'}", **kwargs)
return ""

return wrapper
Expand Down
64 changes: 64 additions & 0 deletions tests/test_loggers.py
@@ -0,0 +1,64 @@
"""Tests for the loggers module."""

from unittest.mock import MagicMock

import pytest

from mkdocstrings.loggers import get_logger, get_template_logger


@pytest.mark.parametrize(
"kwargs",
[
{},
{"once": False},
{"once": True},
],
)
def test_logger(kwargs: dict, caplog: pytest.LogCaptureFixture) -> None:
"""Test logger methods.
Parameters:
kwargs: Keyword arguments passed to the logger methods.
"""
logger = get_logger("mkdocstrings.test")
caplog.set_level(0)
for _ in range(2):
logger.debug("Debug message", **kwargs)
logger.info("Info message", **kwargs)
logger.warning("Warning message", **kwargs)
logger.error("Error message", **kwargs)
logger.critical("Critical message", **kwargs)
if kwargs.get("once", False):
assert len(caplog.records) == 5
else:
assert len(caplog.records) == 10


@pytest.mark.parametrize(
"kwargs",
[
{},
{"once": False},
{"once": True},
],
)
def test_template_logger(kwargs: dict, caplog: pytest.LogCaptureFixture) -> None:
"""Test template logger methods.
Parameters:
kwargs: Keyword arguments passed to the template logger methods.
"""
logger = get_template_logger()
mock = MagicMock()
caplog.set_level(0)
for _ in range(2):
logger.debug(mock, "Debug message", **kwargs)
logger.info(mock, "Info message", **kwargs)
logger.warning(mock, "Warning message", **kwargs)
logger.error(mock, "Error message", **kwargs)
logger.critical(mock, "Critical message", **kwargs)
if kwargs.get("once", False):
assert len(caplog.records) == 5
else:
assert len(caplog.records) == 10

0 comments on commit 1532b59

Please sign in to comment.