Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/dill/logger.py: 37%

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

134 statements  

1#!/usr/bin/env python 

2# -*- coding: utf-8 -*- 

3# 

4# Author: Leonardo Gama (@leogama) 

5# Copyright (c) 2022-2026 The Uncertainty Quantification Foundation. 

6# License: 3-clause BSD. The full license text is available at: 

7# - https://github.com/uqfoundation/dill/blob/master/LICENSE 

8""" 

9Logging utilities for dill. 

10 

11The 'logger' object is dill's top-level logger. 

12 

13The 'adapter' object wraps the logger and implements a 'trace()' method that 

14generates a detailed tree-style trace for the pickling call at log level INFO. 

15 

16The 'trace()' function sets and resets dill's logger log level, enabling and 

17disabling the pickling trace. 

18 

19The trace shows a tree structure depicting the depth of each object serialized 

20*with dill save functions*, but not the ones that use save functions from 

21'pickle._Pickler.dispatch'. If the information is available, it also displays 

22the size in bytes that the object contributed to the pickle stream (including 

23its child objects). Sample trace output: 

24 

25 >>> import dill, dill.tests 

26 >>> dill.detect.trace(True) 

27 >>> dill.dump_session(main=dill.tests) 

28 ┬ M1: <module 'dill.tests' from '.../dill/tests/__init__.py'> 

29 ├┬ F2: <function _import_module at 0x7f0d2dce1b80> 

30 │└ # F2 [32 B] 

31 ├┬ D2: <dict object at 0x7f0d2e98a540> 

32 │├┬ T4: <class '_frozen_importlib.ModuleSpec'> 

33 ││└ # T4 [35 B] 

34 │├┬ D2: <dict object at 0x7f0d2ef0e8c0> 

35 ││├┬ T4: <class '_frozen_importlib_external.SourceFileLoader'> 

36 │││└ # T4 [50 B] 

37 ││├┬ D2: <dict object at 0x7f0d2e988a40> 

38 │││└ # D2 [84 B] 

39 ││└ # D2 [413 B] 

40 │└ # D2 [763 B] 

41 └ # M1 [813 B] 

42""" 

43 

44__all__ = ['adapter', 'logger', 'trace'] 

45 

46import codecs 

47import contextlib 

48import locale 

49import logging 

50import math 

51import os 

52from functools import partial 

53from typing import TextIO, Union 

54 

55import dill 

56 

57# Tree drawing characters: Unicode to ASCII map. 

58ASCII_MAP = str.maketrans({"│": "|", "├": "|", "┬": "+", "└": "`"}) 

59 

60## Notes about the design choices ## 

61 

62# Here is some domumentation of the Standard Library's logging internals that 

63# can't be found completely in the official documentation. dill's logger is 

64# obtained by calling logging.getLogger('dill') and therefore is an instance of 

65# logging.getLoggerClass() at the call time. As this is controlled by the user, 

66# in order to add some functionality to it it's necessary to use a LoggerAdapter 

67# to wrap it, overriding some of the adapter's methods and creating new ones. 

68# 

69# Basic calling sequence 

70# ====================== 

71# 

72# Python's logging functionality can be conceptually divided into five steps: 

73# 0. Check logging level -> abort if call level is greater than logger level 

74# 1. Gather information -> construct a LogRecord from passed arguments and context 

75# 2. Filter (optional) -> discard message if the record matches a filter 

76# 3. Format -> format message with args, then format output string with message plus record 

77# 4. Handle -> write the formatted string to output as defined in the handler 

78# 

79# dill.logging.logger.log -> # or logger.info, etc. 

80# Logger.log -> \ 

81# Logger._log -> }- accept 'extra' parameter for custom record entries 

82# Logger.makeRecord -> / 

83# LogRecord.__init__ 

84# Logger.handle -> 

85# Logger.callHandlers -> 

86# Handler.handle -> 

87# Filterer.filter -> 

88# Filter.filter 

89# StreamHandler.emit -> 

90# Handler.format -> 

91# Formatter.format -> 

92# LogRecord.getMessage # does: record.message = msg % args 

93# Formatter.formatMessage -> 

94# PercentStyle.format # does: self._fmt % vars(record) 

95# 

96# NOTE: All methods from the second line on are from logging.__init__.py 

97 

98class TraceAdapter(logging.LoggerAdapter): 

99 """ 

100 Tracks object tree depth and calculates pickled object size. 

101 

102 A single instance of this wraps the module's logger, as the logging API 

103 doesn't allow setting it directly with a custom Logger subclass. The added 

104 'trace()' method receives a pickle instance as the first argument and 

105 creates extra values to be added in the LogRecord from it, then calls 

106 'info()'. 

107 

108 Usage of logger with 'trace()' method: 

109 

110 >>> from dill.logger import adapter as logger #NOTE: not dill.logger.logger 

111 >>> ... 

112 >>> def save_atype(pickler, obj): 

113 >>> logger.trace(pickler, "Message with %s and %r etc. placeholders", 'text', obj) 

114 >>> ... 

115 """ 

116 def __init__(self, logger): 

117 self.logger = logger 

118 def addHandler(self, handler): 

119 formatter = TraceFormatter("%(prefix)s%(message)s%(suffix)s", handler=handler) 

120 handler.setFormatter(formatter) 

121 self.logger.addHandler(handler) 

122 def removeHandler(self, handler): 

123 self.logger.removeHandler(handler) 

124 def process(self, msg, kwargs): 

125 # A no-op override, as we don't have self.extra. 

126 return msg, kwargs 

127 def trace_setup(self, pickler): 

128 # Called by Pickler.dump(). 

129 if not dill._dill.is_dill(pickler, child=False): 

130 return 

131 if self.isEnabledFor(logging.INFO): 

132 pickler._trace_depth = 1 

133 pickler._size_stack = [] 

134 else: 

135 pickler._trace_depth = None 

136 def trace(self, pickler, msg, *args, **kwargs): 

137 if not hasattr(pickler, '_trace_depth'): 

138 logger.info(msg, *args, **kwargs) 

139 return 

140 if pickler._trace_depth is None: 

141 return 

142 extra = kwargs.get('extra', {}) 

143 pushed_obj = msg.startswith('#') 

144 size = None 

145 try: 

146 # Streams are not required to be tellable. 

147 size = pickler._file.tell() 

148 frame = pickler.framer.current_frame 

149 try: 

150 size += frame.tell() 

151 except AttributeError: 

152 # PyPy may use a BytesBuilder as frame 

153 size += len(frame) 

154 except (AttributeError, TypeError): 

155 pass 

156 if size is not None: 

157 if not pushed_obj: 

158 pickler._size_stack.append(size) 

159 else: 

160 size -= pickler._size_stack.pop() 

161 extra['size'] = size 

162 if pushed_obj: 

163 pickler._trace_depth -= 1 

164 extra['depth'] = pickler._trace_depth 

165 kwargs['extra'] = extra 

166 self.info(msg, *args, **kwargs) 

167 if not pushed_obj: 

168 pickler._trace_depth += 1 

169 

170class TraceFormatter(logging.Formatter): 

171 """ 

172 Generates message prefix and suffix from record. 

173 

174 This Formatter adds prefix and suffix strings to the log message in trace 

175 mode (an also provides empty string defaults for normal logs). 

176 """ 

177 def __init__(self, *args, handler=None, **kwargs): 

178 super().__init__(*args, **kwargs) 

179 try: 

180 encoding = handler.stream.encoding 

181 if encoding is None: 

182 raise AttributeError 

183 except AttributeError: 

184 encoding = locale.getpreferredencoding() 

185 try: 

186 encoding = codecs.lookup(encoding).name 

187 except LookupError: 

188 self.is_utf8 = False 

189 else: 

190 self.is_utf8 = (encoding == codecs.lookup('utf-8').name) 

191 def format(self, record): 

192 fields = {'prefix': "", 'suffix': ""} 

193 if getattr(record, 'depth', 0) > 0: 

194 if record.msg.startswith("#"): 

195 prefix = (record.depth - 1)*"│" + "└" 

196 elif record.depth == 1: 

197 prefix = "┬" 

198 else: 

199 prefix = (record.depth - 2)*"│" + "├┬" 

200 if not self.is_utf8: 

201 prefix = prefix.translate(ASCII_MAP) + "-" 

202 fields['prefix'] = prefix + " " 

203 if hasattr(record, 'size') and record.size is not None and record.size >= 1: 

204 # Show object size in human-readable form. 

205 power = int(math.log(record.size, 2)) // 10 

206 size = record.size >> power*10 

207 fields['suffix'] = " [%d %sB]" % (size, "KMGTP"[power] + "i" if power else "") 

208 vars(record).update(fields) 

209 return super().format(record) 

210 

211logger = logging.getLogger('dill') 

212logger.propagate = False 

213adapter = TraceAdapter(logger) 

214stderr_handler = logging._StderrHandler() 

215adapter.addHandler(stderr_handler) 

216 

217def trace(arg: Union[bool, TextIO, str, os.PathLike] = None, *, mode: str = 'a') -> None: 

218 """print a trace through the stack when pickling; useful for debugging 

219 

220 With a single boolean argument, enable or disable the tracing. 

221 

222 Example usage: 

223 

224 >>> import dill 

225 >>> dill.detect.trace(True) 

226 >>> dill.dump_session() 

227 

228 Alternatively, ``trace()`` can be used as a context manager. With no 

229 arguments, it just takes care of restoring the tracing state on exit. 

230 Either a file handle, or a file name and (optionally) a file mode may be 

231 specitfied to redirect the tracing output in the ``with`` block context. A 

232 log function is yielded by the manager so the user can write extra 

233 information to the file. 

234 

235 Example usage: 

236 

237 >>> from dill import detect 

238 >>> D = {'a': 42, 'b': {'x': None}} 

239 >>> with detect.trace(): 

240 >>> dumps(D) 

241 ┬ D2: <dict object at 0x7f2721804800> 

242 ├┬ D2: <dict object at 0x7f27217f5c40> 

243 │└ # D2 [8 B] 

244 └ # D2 [22 B] 

245 >>> squared = lambda x: x**2 

246 >>> with detect.trace('output.txt', mode='w') as log: 

247 >>> log("> D = %r", D) 

248 >>> dumps(D) 

249 >>> log("> squared = %r", squared) 

250 >>> dumps(squared) 

251 

252 Arguments: 

253 arg: a boolean value, or an optional file-like or path-like object for the context manager 

254 mode: mode string for ``open()`` if a file name is passed as the first argument 

255 """ 

256 if repr(arg) not in ('False', 'True'): 

257 return TraceManager(file=arg, mode=mode) 

258 logger.setLevel(logging.INFO if arg else logging.WARNING) 

259 

260def _restrict_new_logfile(file): 

261 # logging.FileHandler creates the log at the process umask (world-readable 

262 # by default), but a redirected trace embeds object reprs and whatever the 

263 # yielded log() function writes. Pre-create a *new* target owner-only so 

264 # the handler inherits 0o600; leave an existing file's permissions alone. 

265 if os.name != 'posix': 

266 return 

267 try: 

268 fd = os.open(os.fspath(file), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600) 

269 except (FileExistsError, OSError): 

270 return 

271 os.close(fd) 

272 

273class TraceManager(contextlib.AbstractContextManager): 

274 """context manager version of trace(); can redirect the trace to a file""" 

275 def __init__(self, file, mode): 

276 self.file = file 

277 self.mode = mode 

278 self.redirect = file is not None 

279 self.file_is_stream = hasattr(file, 'write') 

280 def __enter__(self): 

281 if self.redirect: 

282 stderr_handler.flush() 

283 if self.file_is_stream: 

284 self.handler = logging.StreamHandler(self.file) 

285 else: 

286 _restrict_new_logfile(self.file) 

287 self.handler = logging.FileHandler(self.file, self.mode) 

288 adapter.removeHandler(stderr_handler) 

289 adapter.addHandler(self.handler) 

290 self.old_level = adapter.getEffectiveLevel() 

291 adapter.setLevel(logging.INFO) 

292 return adapter.info 

293 def __exit__(self, *exc_info): 

294 adapter.setLevel(self.old_level) 

295 if self.redirect: 

296 adapter.removeHandler(self.handler) 

297 adapter.addHandler(stderr_handler) 

298 if not self.file_is_stream: 

299 self.handler.close()