diff --git a/CHANGELOG.md b/CHANGELOG.md
index 723eb24c..7066ff44 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+## [0.25.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.25.0) - 2024-04-27
+
+[Compare with 0.24.3](https://github.com/mkdocstrings/mkdocstrings/compare/0.24.3...0.25.0)
+
+### Features
+
+- Support `once` parameter in logging methods, allowing to log a message only once with a given logger ([1532b59](https://github.com/mkdocstrings/mkdocstrings/commit/1532b59a6efd99fed846cf7edfd0b26525700d3f) by Timothée Mazzucotelli).
+- Support blank line between `::: path` and YAML options ([d799d2f](https://github.com/mkdocstrings/mkdocstrings/commit/d799d2f3903bce44fb751f8cf3fb8078d25549da) by Timothée Mazzucotelli). [Issue-450](https://github.com/mkdocstrings/mkdocstrings/issues/450)
+
+### Code Refactoring
+
+- Allow specifying name of template loggers ([c5b5f69](https://github.com/mkdocstrings/mkdocstrings/commit/c5b5f697c83271d961c7ac795412d6b4964ba2b7) by Timothée Mazzucotelli).
+
## [0.24.3](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.24.3) - 2024-04-05
[Compare with 0.24.2](https://github.com/mkdocstrings/mkdocstrings/compare/0.24.2...0.24.3)
diff --git a/mkdocs.yml b/mkdocs.yml
index 860ce66e..30afc977 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -137,6 +137,8 @@ plugins:
- https://docs.python.org/3/objects.inv
- https://installer.readthedocs.io/en/stable/objects.inv # demonstration purpose in the docs
- https://mkdocstrings.github.io/autorefs/objects.inv
+ - https://www.mkdocs.org/objects.inv
+ - https://python-markdown.github.io/objects.inv
paths: [src]
options:
docstring_options:
@@ -146,6 +148,7 @@ plugins:
heading_level: 1
inherited_members: true
merge_init_into_class: true
+ parameter_headings: true
separate_signature: true
show_root_heading: true
show_root_full_path: false
diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py
index bef8c799..23e90cff 100644
--- a/src/mkdocstrings/extension.py
+++ b/src/mkdocstrings/extension.py
@@ -116,6 +116,10 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None:
block, the_rest = self.detab(block)
+ if not block and blocks and blocks[0].startswith((" handler:", " options:")):
+ # YAML options were separated from the `:::` line by a blank line.
+ block = blocks.pop(0)
+
if match:
identifier = match["name"]
heading_level = match["heading"].count("#")
diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py
index f52e17dc..27c22db1 100644
--- a/src/mkdocstrings/handlers/base.py
+++ b/src/mkdocstrings/handlers/base.py
@@ -74,6 +74,7 @@ class BaseHandler:
To add custom CSS, add an `extra_css` variable or create an 'style.css' file beside the templates.
"""
+ # TODO: Make name mandatory?
name: str = ""
"""The handler's name, for example "python"."""
domain: str = "default"
@@ -132,7 +133,7 @@ def __init__(self, handler: str, theme: str, custom_templates: str | None = None
auto_reload=False, # Editing a template in the middle of a build is not useful.
)
self.env.filters["any"] = do_any
- self.env.globals["log"] = get_template_logger()
+ self.env.globals["log"] = get_template_logger(self.name)
self._headings: list[Element] = []
self._md: Markdown = None # type: ignore[assignment] # To be populated in `update_env`.
diff --git a/src/mkdocstrings/loggers.py b/src/mkdocstrings/loggers.py
index 63502474..240e1808 100644
--- a/src/mkdocstrings/loggers.py
+++ b/src/mkdocstrings/loggers.py
@@ -17,7 +17,7 @@
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:
@@ -25,7 +25,25 @@
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.
@@ -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.
@@ -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.
@@ -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
@@ -136,10 +177,14 @@ def get_logger(name: str) -> LoggerAdapter:
return LoggerAdapter(name.split(".", 1)[0], logger)
-def get_template_logger() -> TemplateLogger:
+def get_template_logger(handler_name: str | None = None) -> TemplateLogger:
"""Return a logger usable in templates.
+ Parameters:
+ handler_name: The name of the handler.
+
Returns:
A template logger.
"""
- return TemplateLogger(get_logger("mkdocstrings.templates"))
+ handler_name = handler_name or "base"
+ return TemplateLogger(get_logger(f"mkdocstrings_handlers.{handler_name}.templates"))
diff --git a/tests/test_extension.py b/tests/test_extension.py
index affd6c6a..976f376c 100644
--- a/tests/test_extension.py
+++ b/tests/test_extension.py
@@ -172,6 +172,11 @@ def test_use_options_yaml_key(ext_markdown: Markdown) -> None:
assert "h1" not in ext_markdown.convert("::: tests.fixtures.headings\n options:\n heading_level: 2")
+def test_use_yaml_options_after_blank_line(ext_markdown: Markdown) -> None:
+ """Check that YAML options are detected even after a blank line."""
+ assert "h1" not in ext_markdown.convert("::: tests.fixtures.headings\n\n options:\n heading_level: 2")
+
+
@pytest.mark.parametrize("ext_markdown", [{"markdown_extensions": [{"admonition": {}}]}], indirect=["ext_markdown"])
def test_removing_duplicated_headings(ext_markdown: Markdown) -> None:
"""Assert duplicated headings are removed from the output."""
diff --git a/tests/test_loggers.py b/tests/test_loggers.py
new file mode 100644
index 00000000..1644c0f0
--- /dev/null
+++ b/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