Coverage for src/mkdocstrings/loggers.py: 90.91%
58 statements
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-12 18:59 +0200
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-12 18:59 +0200
1"""Logging functions."""
3from __future__ import annotations
5import logging
6from contextlib import suppress
7from pathlib import Path
8from typing import TYPE_CHECKING, Any, Callable
10try:
11 from jinja2 import pass_context
12except ImportError: # TODO: remove once Jinja2 < 3.1 is dropped
13 from jinja2 import contextfunction as pass_context # type: ignore[attr-defined,no-redef]
15try:
16 import mkdocstrings_handlers
17except ImportError:
18 TEMPLATES_DIRS: Sequence[Path] = ()
19else:
20 TEMPLATES_DIRS = tuple(mkdocstrings_handlers.__path__)
23if TYPE_CHECKING:
24 from collections.abc import MutableMapping, Sequence
26 from jinja2.runtime import Context
29class LoggerAdapter(logging.LoggerAdapter):
30 """A logger adapter to prefix messages.
32 This adapter also adds an additional parameter to logging methods
33 called `once`: if `True`, the message will only be logged once.
35 Examples:
36 In Python code:
38 >>> logger = get_logger("myplugin")
39 >>> logger.debug("This is a debug message.")
40 >>> logger.info("This is an info message.", once=True)
42 In Jinja templates (logger available in context as `log`):
44 ```jinja
45 {{ log.debug("This is a debug message.") }}
46 {{ log.info("This is an info message.", once=True) }}
47 ```
48 """
50 def __init__(self, prefix: str, logger: logging.Logger):
51 """Initialize the object.
53 Arguments:
54 prefix: The string to insert in front of every message.
55 logger: The logger instance.
56 """
57 super().__init__(logger, {})
58 self.prefix = prefix
59 self._logged: set[tuple[LoggerAdapter, str]] = set()
61 def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, Any]:
62 """Process the message.
64 Arguments:
65 msg: The message:
66 kwargs: Remaining arguments.
68 Returns:
69 The processed message.
70 """
71 return f"{self.prefix}: {msg}", kwargs
73 def log(self, level: int, msg: object, *args: object, **kwargs: object) -> None:
74 """Log a message.
76 Arguments:
77 level: The logging level.
78 msg: The message.
79 *args: Additional arguments passed to parent method.
80 **kwargs: Additional keyword arguments passed to parent method.
81 """
82 if kwargs.pop("once", False):
83 if (key := (self, str(msg))) in self._logged:
84 return
85 self._logged.add(key)
86 super().log(level, msg, *args, **kwargs) # type: ignore[arg-type]
89class TemplateLogger:
90 """A wrapper class to allow logging in templates.
92 The logging methods provided by this class all accept
93 two parameters:
95 - `msg`: The message to log.
96 - `once`: If `True`, the message will only be logged once.
98 Methods:
99 debug: Function to log a DEBUG message.
100 info: Function to log an INFO message.
101 warning: Function to log a WARNING message.
102 error: Function to log an ERROR message.
103 critical: Function to log a CRITICAL message.
104 """
106 def __init__(self, logger: LoggerAdapter):
107 """Initialize the object.
109 Arguments:
110 logger: A logger adapter.
111 """
112 self.debug = get_template_logger_function(logger.debug)
113 self.info = get_template_logger_function(logger.info)
114 self.warning = get_template_logger_function(logger.warning)
115 self.error = get_template_logger_function(logger.error)
116 self.critical = get_template_logger_function(logger.critical)
119def get_template_logger_function(logger_func: Callable) -> Callable:
120 """Create a wrapper function that automatically receives the Jinja template context.
122 Arguments:
123 logger_func: The logger function to use within the wrapper.
125 Returns:
126 A function.
127 """
129 @pass_context
130 def wrapper(context: Context, msg: str | None = None, **kwargs: Any) -> str:
131 """Log a message.
133 Arguments:
134 context: The template context, automatically provided by Jinja.
135 msg: The message to log.
136 **kwargs: Additional arguments passed to the logger function.
138 Returns:
139 An empty string.
140 """
141 template_path = get_template_path(context)
142 logger_func(f"{template_path}: {msg or 'Rendering'}", **kwargs)
143 return ""
145 return wrapper
148def get_template_path(context: Context) -> str:
149 """Return the path to the template currently using the given context.
151 Arguments:
152 context: The template context.
154 Returns:
155 The relative path to the template.
156 """
157 context_name: str = str(context.name)
158 filename = context.environment.get_template(context_name).filename
159 if filename: 159 ↛ 166line 159 didn't jump to line 166 because the condition on line 159 was always true
160 for template_dir in TEMPLATES_DIRS:
161 with suppress(ValueError):
162 return str(Path(filename).relative_to(template_dir))
163 with suppress(ValueError):
164 return str(Path(filename).relative_to(Path.cwd()))
165 return filename
166 return context_name
169def get_logger(name: str) -> LoggerAdapter:
170 """Return a pre-configured logger.
172 Arguments:
173 name: The name to use with `logging.getLogger`.
175 Returns:
176 A logger configured to work well in MkDocs.
177 """
178 logger = logging.getLogger(f"mkdocs.plugins.{name}")
179 return LoggerAdapter(name.split(".", 1)[0], logger)
182def get_template_logger(handler_name: str | None = None) -> TemplateLogger:
183 """Return a logger usable in templates.
185 Parameters:
186 handler_name: The name of the handler.
188 Returns:
189 A template logger.
190 """
191 handler_name = handler_name or "base"
192 return TemplateLogger(get_logger(f"mkdocstrings_handlers.{handler_name}.templates"))