Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/structlog/testing.py: 55%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

64 statements  

1# SPDX-License-Identifier: MIT OR Apache-2.0 

2# This file is dual licensed under the terms of the Apache License, Version 

3# 2.0, and the MIT License. See the LICENSE file in the root of this 

4# repository for complete details. 

5 

6""" 

7Helpers to test your application's logging behavior. 

8 

9.. versionadded:: 20.1.0 

10 

11See :doc:`testing`. 

12""" 

13 

14from __future__ import annotations 

15 

16from contextlib import contextmanager 

17from typing import Any, Generator, Iterable, NamedTuple, NoReturn 

18 

19from ._config import configure, get_config 

20from ._log_levels import map_method_name 

21from .exceptions import DropEvent 

22from .typing import EventDict, Processor, WrappedLogger 

23 

24 

25__all__ = [ 

26 "CapturedCall", 

27 "CapturingLogger", 

28 "CapturingLoggerFactory", 

29 "LogCapture", 

30 "ReturnLogger", 

31 "ReturnLoggerFactory", 

32 "capture_logs", 

33] 

34 

35 

36class LogCapture: 

37 """ 

38 Class for capturing log messages in its entries list. 

39 Generally you should use `structlog.testing.capture_logs`, 

40 but you can use this class if you want to capture logs with other patterns. 

41 

42 :ivar List[structlog.typing.EventDict] entries: The captured log entries. 

43 

44 .. versionadded:: 20.1.0 

45 

46 .. versionchanged:: 24.3.0 

47 Added mapping from "exception" to "error" 

48 Added mapping from "warn" to "warning" 

49 """ 

50 

51 entries: list[EventDict] 

52 

53 def __init__(self) -> None: 

54 self.entries = [] 

55 

56 def __call__( 

57 self, _: WrappedLogger, method_name: str, event_dict: EventDict 

58 ) -> NoReturn: 

59 event_dict["log_level"] = map_method_name(method_name) 

60 self.entries.append(event_dict) 

61 

62 raise DropEvent 

63 

64 

65@contextmanager 

66def capture_logs( 

67 processors: Iterable[Processor] = (), 

68) -> Generator[list[EventDict], None, None]: 

69 """ 

70 Context manager that appends all logging statements to its yielded list 

71 while it is active. Disables all configured processors for the duration 

72 of the context manager. 

73 

74 Attention: this is **not** thread-safe! 

75 

76 Args: 

77 processors: Processors to apply before the logs are captured. 

78 

79 .. versionadded:: 20.1.0 

80 .. versionadded:: 25.5.0 *processors* parameter 

81 """ 

82 cap = LogCapture() 

83 # Modify `_Configuration.default_processors` set via `configure` but always 

84 # keep the list instance intact to not break references held by bound 

85 # loggers. 

86 configured_processors = get_config()["processors"] 

87 old_processors = configured_processors.copy() 

88 try: 

89 # clear processors list and use LogCapture for testing 

90 configured_processors.clear() 

91 configured_processors.extend(processors) 

92 configured_processors.append(cap) 

93 configure(processors=configured_processors) 

94 yield cap.entries 

95 finally: 

96 # remove LogCapture and restore original processors 

97 configured_processors.clear() 

98 configured_processors.extend(old_processors) 

99 configure(processors=configured_processors) 

100 

101 

102class ReturnLogger: 

103 """ 

104 Return the arguments that it's called with. 

105 

106 >>> from structlog import ReturnLogger 

107 >>> ReturnLogger().info("hello") 

108 'hello' 

109 >>> ReturnLogger().info("hello", when="again") 

110 (('hello',), {'when': 'again'}) 

111 

112 .. versionchanged:: 0.3.0 

113 Allow for arbitrary arguments and keyword arguments to be passed in. 

114 """ 

115 

116 def msg(self, *args: Any, **kw: Any) -> Any: 

117 """ 

118 Return tuple of ``args, kw`` or just ``args[0]`` if only one arg passed 

119 """ 

120 # Slightly convoluted for backwards compatibility. 

121 if len(args) == 1 and not kw: 

122 return args[0] 

123 

124 return args, kw 

125 

126 log = debug = info = warn = warning = msg 

127 fatal = failure = err = error = critical = exception = msg 

128 

129 

130class ReturnLoggerFactory: 

131 r""" 

132 Produce and cache `ReturnLogger`\ s. 

133 

134 To be used with `structlog.configure`\ 's *logger_factory*. 

135 

136 Positional arguments are silently ignored. 

137 

138 .. versionadded:: 0.4.0 

139 """ 

140 

141 def __init__(self) -> None: 

142 self._logger = ReturnLogger() 

143 

144 def __call__(self, *args: Any) -> ReturnLogger: 

145 return self._logger 

146 

147 

148class CapturedCall(NamedTuple): 

149 """ 

150 A call as captured by `CapturingLogger`. 

151 

152 Can also be unpacked like a tuple. 

153 

154 Args: 

155 method_name: The method name that got called. 

156 

157 args: A tuple of the positional arguments. 

158 

159 kwargs: A dict of the keyword arguments. 

160 

161 .. versionadded:: 20.2.0 

162 """ 

163 

164 method_name: str 

165 args: tuple[Any, ...] 

166 kwargs: dict[str, Any] 

167 

168 

169class CapturingLogger: 

170 """ 

171 Store the method calls that it's been called with. 

172 

173 This is nicer than `ReturnLogger` for unit tests because the bound logger 

174 doesn't have to cooperate. 

175 

176 **Any** method name is supported. 

177 

178 .. versionadded:: 20.2.0 

179 """ 

180 

181 calls: list[CapturedCall] 

182 

183 def __init__(self) -> None: 

184 self.calls = [] 

185 

186 def __repr__(self) -> str: 

187 return f"<CapturingLogger with {len(self.calls)} call(s)>" 

188 

189 def __getattr__(self, name: str) -> Any: 

190 """ 

191 Capture call to `calls` 

192 """ 

193 

194 def log(*args: Any, **kw: Any) -> None: 

195 self.calls.append(CapturedCall(name, args, kw)) 

196 

197 return log 

198 

199 

200class CapturingLoggerFactory: 

201 r""" 

202 Produce and cache `CapturingLogger`\ s. 

203 

204 Each factory produces and reuses only **one** logger. 

205 

206 You can access it via the ``logger`` attribute. 

207 

208 To be used with `structlog.configure`\ 's *logger_factory*. 

209 

210 Positional arguments are silently ignored. 

211 

212 .. versionadded:: 20.2.0 

213 """ 

214 

215 logger: CapturingLogger 

216 

217 def __init__(self) -> None: 

218 self.logger = CapturingLogger() 

219 

220 def __call__(self, *args: Any) -> CapturingLogger: 

221 return self.logger