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

1"""Logging functions.""" 

2 

3from __future__ import annotations 

4 

5import logging 

6from contextlib import suppress 

7from pathlib import Path 

8from typing import TYPE_CHECKING, Any, Callable 

9 

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] 

14 

15try: 

16 import mkdocstrings_handlers 

17except ImportError: 

18 TEMPLATES_DIRS: Sequence[Path] = () 

19else: 

20 TEMPLATES_DIRS = tuple(mkdocstrings_handlers.__path__) 

21 

22 

23if TYPE_CHECKING: 

24 from collections.abc import MutableMapping, Sequence 

25 

26 from jinja2.runtime import Context 

27 

28 

29class LoggerAdapter(logging.LoggerAdapter): 

30 """A logger adapter to prefix messages. 

31 

32 This adapter also adds an additional parameter to logging methods 

33 called `once`: if `True`, the message will only be logged once. 

34 

35 Examples: 

36 In Python code: 

37 

38 >>> logger = get_logger("myplugin") 

39 >>> logger.debug("This is a debug message.") 

40 >>> logger.info("This is an info message.", once=True) 

41 

42 In Jinja templates (logger available in context as `log`): 

43 

44 ```jinja 

45 {{ log.debug("This is a debug message.") }} 

46 {{ log.info("This is an info message.", once=True) }} 

47 ``` 

48 """ 

49 

50 def __init__(self, prefix: str, logger: logging.Logger): 

51 """Initialize the object. 

52 

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() 

60 

61 def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, Any]: 

62 """Process the message. 

63 

64 Arguments: 

65 msg: The message: 

66 kwargs: Remaining arguments. 

67 

68 Returns: 

69 The processed message. 

70 """ 

71 return f"{self.prefix}: {msg}", kwargs 

72 

73 def log(self, level: int, msg: object, *args: object, **kwargs: object) -> None: 

74 """Log a message. 

75 

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] 

87 

88 

89class TemplateLogger: 

90 """A wrapper class to allow logging in templates. 

91 

92 The logging methods provided by this class all accept 

93 two parameters: 

94 

95 - `msg`: The message to log. 

96 - `once`: If `True`, the message will only be logged once. 

97 

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 """ 

105 

106 def __init__(self, logger: LoggerAdapter): 

107 """Initialize the object. 

108 

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) 

117 

118 

119def get_template_logger_function(logger_func: Callable) -> Callable: 

120 """Create a wrapper function that automatically receives the Jinja template context. 

121 

122 Arguments: 

123 logger_func: The logger function to use within the wrapper. 

124 

125 Returns: 

126 A function. 

127 """ 

128 

129 @pass_context 

130 def wrapper(context: Context, msg: str | None = None, **kwargs: Any) -> str: 

131 """Log a message. 

132 

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. 

137 

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 "" 

144 

145 return wrapper 

146 

147 

148def get_template_path(context: Context) -> str: 

149 """Return the path to the template currently using the given context. 

150 

151 Arguments: 

152 context: The template context. 

153 

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 

167 

168 

169def get_logger(name: str) -> LoggerAdapter: 

170 """Return a pre-configured logger. 

171 

172 Arguments: 

173 name: The name to use with `logging.getLogger`. 

174 

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) 

180 

181 

182def get_template_logger(handler_name: str | None = None) -> TemplateLogger: 

183 """Return a logger usable in templates. 

184 

185 Parameters: 

186 handler_name: The name of the handler. 

187 

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"))