Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/dill/logger.py: 36%
124 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Author: Leonardo Gama (@leogama)
5# Copyright (c) 2022 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.
11The 'logger' object is dill's top-level logger.
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.
16The 'trace()' function sets and resets dill's logger log level, enabling and
17disabling the pickling trace.
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:
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"""
44__all__ = ['adapter', 'logger', 'trace']
46import codecs
47import contextlib
48import locale
49import logging
50import math
51import os
52from functools import partial
53from typing import TextIO, Union
55import dill
57# Tree drawing characters: Unicode to ASCII map.
58ASCII_MAP = str.maketrans({"│": "|", "├": "|", "┬": "+", "└": "`"})
60## Notes about the design choices ##
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
98class TraceAdapter(logging.LoggerAdapter):
99 """
100 Tracks object tree depth and calculates pickled object size.
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()'.
108 Usage of logger with 'trace()' method:
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
170class TraceFormatter(logging.Formatter):
171 """
172 Generates message prefix and suffix from record.
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'):
204 # Show object size in human-redable 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)
211logger = logging.getLogger('dill')
212logger.propagate = False
213adapter = TraceAdapter(logger)
214stderr_handler = logging._StderrHandler()
215adapter.addHandler(stderr_handler)
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
220 With a single boolean argument, enable or disable the tracing.
222 Example usage:
224 >>> import dill
225 >>> dill.detect.trace(True)
226 >>> dill.dump_session()
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.
235 Example usage:
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)
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 not isinstance(arg, bool):
257 return TraceManager(file=arg, mode=mode)
258 logger.setLevel(logging.INFO if arg else logging.WARNING)
260class TraceManager(contextlib.AbstractContextManager):
261 """context manager version of trace(); can redirect the trace to a file"""
262 def __init__(self, file, mode):
263 self.file = file
264 self.mode = mode
265 self.redirect = file is not None
266 self.file_is_stream = hasattr(file, 'write')
267 def __enter__(self):
268 if self.redirect:
269 stderr_handler.flush()
270 if self.file_is_stream:
271 self.handler = logging.StreamHandler(self.file)
272 else:
273 self.handler = logging.FileHandler(self.file, self.mode)
274 adapter.removeHandler(stderr_handler)
275 adapter.addHandler(self.handler)
276 self.old_level = adapter.getEffectiveLevel()
277 adapter.setLevel(logging.INFO)
278 return adapter.info
279 def __exit__(self, *exc_info):
280 adapter.setLevel(self.old_level)
281 if self.redirect:
282 adapter.removeHandler(self.handler)
283 adapter.addHandler(stderr_handler)
284 if not self.file_is_stream:
285 self.handler.close()