Skip to content

Commit 1532b59

Browse files
committed
feat: Support once parameter in logging methods, allowing to log a 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.
1 parent d799d2f commit 1532b59

File tree

2 files changed

+110
-5
lines changed

2 files changed

+110
-5
lines changed

src/mkdocstrings/loggers.py

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,33 @@
1717
except ImportError:
1818
TEMPLATES_DIRS: Sequence[Path] = ()
1919
else:
20-
TEMPLATES_DIRS = tuple(mkdocstrings_handlers.__path__) # type: ignore[arg-type]
20+
TEMPLATES_DIRS = tuple(mkdocstrings_handlers.__path__)
2121

2222

2323
if TYPE_CHECKING:
2424
from jinja2.runtime import Context
2525

2626

2727
class LoggerAdapter(logging.LoggerAdapter):
28-
"""A logger adapter to prefix messages."""
28+
"""A logger adapter to prefix messages.
29+
30+
This adapter also adds an additional parameter to logging methods
31+
called `once`: if `True`, the message will only be logged once.
32+
33+
Examples:
34+
In Python code:
35+
36+
>>> logger = get_logger("myplugin")
37+
>>> logger.debug("This is a debug message.")
38+
>>> logger.info("This is an info message.", once=True)
39+
40+
In Jinja templates (logger available in context as `log`):
41+
42+
```jinja
43+
{{ log.debug("This is a debug message.") }}
44+
{{ log.info("This is an info message.", once=True) }}
45+
```
46+
"""
2947

3048
def __init__(self, prefix: str, logger: logging.Logger):
3149
"""Initialize the object.
@@ -36,6 +54,7 @@ def __init__(self, prefix: str, logger: logging.Logger):
3654
"""
3755
super().__init__(logger, {})
3856
self.prefix = prefix
57+
self._logged: set[tuple[LoggerAdapter, str]] = set()
3958

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

71+
def log(self, level: int, msg: object, *args: object, **kwargs: object) -> None:
72+
"""Log a message.
73+
74+
Arguments:
75+
level: The logging level.
76+
msg: The message.
77+
*args: Additional arguments passed to parent method.
78+
**kwargs: Additional keyword arguments passed to parent method.
79+
"""
80+
if kwargs.pop("once", False):
81+
if (key := (self, str(msg))) in self._logged:
82+
return
83+
self._logged.add(key)
84+
super().log(level, msg, *args, **kwargs) # type: ignore[arg-type]
85+
5286

5387
class TemplateLogger:
5488
"""A wrapper class to allow logging in templates.
5589
56-
Attributes:
90+
The logging methods provided by this class all accept
91+
two parameters:
92+
93+
- `msg`: The message to log.
94+
- `once`: If `True`, the message will only be logged once.
95+
96+
Methods:
5797
debug: Function to log a DEBUG message.
5898
info: Function to log an INFO message.
5999
warning: Function to log a WARNING message.
@@ -85,18 +125,19 @@ def get_template_logger_function(logger_func: Callable) -> Callable:
85125
"""
86126

87127
@pass_context
88-
def wrapper(context: Context, msg: str | None = None) -> str:
128+
def wrapper(context: Context, msg: str | None = None, **kwargs: Any) -> str:
89129
"""Log a message.
90130
91131
Arguments:
92132
context: The template context, automatically provided by Jinja.
93133
msg: The message to log.
134+
**kwargs: Additional arguments passed to the logger function.
94135
95136
Returns:
96137
An empty string.
97138
"""
98139
template_path = get_template_path(context)
99-
logger_func(f"{template_path}: {msg or 'Rendering'}")
140+
logger_func(f"{template_path}: {msg or 'Rendering'}", **kwargs)
100141
return ""
101142

102143
return wrapper

tests/test_loggers.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Tests for the loggers module."""
2+
3+
from unittest.mock import MagicMock
4+
5+
import pytest
6+
7+
from mkdocstrings.loggers import get_logger, get_template_logger
8+
9+
10+
@pytest.mark.parametrize(
11+
"kwargs",
12+
[
13+
{},
14+
{"once": False},
15+
{"once": True},
16+
],
17+
)
18+
def test_logger(kwargs: dict, caplog: pytest.LogCaptureFixture) -> None:
19+
"""Test logger methods.
20+
21+
Parameters:
22+
kwargs: Keyword arguments passed to the logger methods.
23+
"""
24+
logger = get_logger("mkdocstrings.test")
25+
caplog.set_level(0)
26+
for _ in range(2):
27+
logger.debug("Debug message", **kwargs)
28+
logger.info("Info message", **kwargs)
29+
logger.warning("Warning message", **kwargs)
30+
logger.error("Error message", **kwargs)
31+
logger.critical("Critical message", **kwargs)
32+
if kwargs.get("once", False):
33+
assert len(caplog.records) == 5
34+
else:
35+
assert len(caplog.records) == 10
36+
37+
38+
@pytest.mark.parametrize(
39+
"kwargs",
40+
[
41+
{},
42+
{"once": False},
43+
{"once": True},
44+
],
45+
)
46+
def test_template_logger(kwargs: dict, caplog: pytest.LogCaptureFixture) -> None:
47+
"""Test template logger methods.
48+
49+
Parameters:
50+
kwargs: Keyword arguments passed to the template logger methods.
51+
"""
52+
logger = get_template_logger()
53+
mock = MagicMock()
54+
caplog.set_level(0)
55+
for _ in range(2):
56+
logger.debug(mock, "Debug message", **kwargs)
57+
logger.info(mock, "Info message", **kwargs)
58+
logger.warning(mock, "Warning message", **kwargs)
59+
logger.error(mock, "Error message", **kwargs)
60+
logger.critical(mock, "Critical message", **kwargs)
61+
if kwargs.get("once", False):
62+
assert len(caplog.records) == 5
63+
else:
64+
assert len(caplog.records) == 10

0 commit comments

Comments
 (0)