eric6/DebugClients/Python/coverage/debug.py

changeset 7427
362cd1b6f81a
parent 6942
2602857055c5
equal deleted inserted replaced
7426:dc171b1d8261 7427:362cd1b6f81a
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 2 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
3 3
4 """Control of and utilities for debugging.""" 4 """Control of and utilities for debugging."""
5 5
6 import contextlib 6 import contextlib
7 import functools
7 import inspect 8 import inspect
9 import itertools
8 import os 10 import os
9 import re 11 import pprint
10 import sys 12 import sys
11 try: 13 try:
12 import _thread 14 import _thread
13 except ImportError: 15 except ImportError:
14 import thread as _thread 16 import thread as _thread
15 17
16 from coverage.backward import StringIO 18 from coverage.backward import reprlib, StringIO
17 from coverage.misc import isolate_module 19 from coverage.misc import isolate_module
18 20
19 os = isolate_module(os) 21 os = isolate_module(os)
20 22
21 23
22 # When debugging, it can be helpful to force some options, especially when 24 # When debugging, it can be helpful to force some options, especially when
23 # debugging the configuration mechanisms you usually use to control debugging! 25 # debugging the configuration mechanisms you usually use to control debugging!
24 # This is a list of forced debugging options. 26 # This is a list of forced debugging options.
25 FORCED_DEBUG = [] 27 FORCED_DEBUG = []
26 28 FORCED_DEBUG_FILE = None
27 # A hack for debugging testing in sub-processes.
28 _TEST_NAME_FILE = "" # "/tmp/covtest.txt"
29 29
30 30
31 class DebugControl(object): 31 class DebugControl(object):
32 """Control and output for debugging.""" 32 """Control and output for debugging."""
33
34 show_repr_attr = False # For SimpleReprMixin
33 35
34 def __init__(self, options, output): 36 def __init__(self, options, output):
35 """Configure the options and output file for debugging.""" 37 """Configure the options and output file for debugging."""
36 self.options = list(options) + FORCED_DEBUG 38 self.options = list(options) + FORCED_DEBUG
37 self.raw_output = output
38 self.suppress_callers = False 39 self.suppress_callers = False
39 40
40 filters = [] 41 filters = []
41 if self.should('pid'): 42 if self.should('pid'):
42 filters.append(add_pid_and_tid) 43 filters.append(add_pid_and_tid)
43 self.output = DebugOutputFile( 44 self.output = DebugOutputFile.get_one(
44 self.raw_output, 45 output,
45 show_process=self.should('process'), 46 show_process=self.should('process'),
46 filters=filters, 47 filters=filters,
47 ) 48 )
49 self.raw_output = self.output.outfile
48 50
49 def __repr__(self): 51 def __repr__(self):
50 return "<DebugControl options=%r raw_output=%r>" % (self.options, self.raw_output) 52 return "<DebugControl options=%r raw_output=%r>" % (self.options, self.raw_output)
51 53
52 def should(self, option): 54 def should(self, option):
70 72
71 `msg` is the line to write. A newline will be appended. 73 `msg` is the line to write. A newline will be appended.
72 74
73 """ 75 """
74 self.output.write(msg+"\n") 76 self.output.write(msg+"\n")
77 if self.should('self'):
78 caller_self = inspect.stack()[1][0].f_locals.get('self')
79 if caller_self is not None:
80 self.output.write("self: {!r}\n".format(caller_self))
75 if self.should('callers'): 81 if self.should('callers'):
76 dump_stack_frames(out=self.output, skip=1) 82 dump_stack_frames(out=self.output, skip=1)
77 self.output.flush() 83 self.output.flush()
78 84
79 85
85 def get_output(self): 91 def get_output(self):
86 """Get the output text from the `DebugControl`.""" 92 """Get the output text from the `DebugControl`."""
87 return self.raw_output.getvalue() 93 return self.raw_output.getvalue()
88 94
89 95
96 class NoDebugging(object):
97 """A replacement for DebugControl that will never try to do anything."""
98 def should(self, option): # pylint: disable=unused-argument
99 """Should we write debug messages? Never."""
100 return False
101
102
90 def info_header(label): 103 def info_header(label):
91 """Make a nice header string.""" 104 """Make a nice header string."""
92 return "--{0:-<60s}".format(" "+label+" ") 105 return "--{:-<60s}".format(" "+label+" ")
93 106
94 107
95 def info_formatter(info): 108 def info_formatter(info):
96 """Produce a sequence of formatted lines from info. 109 """Produce a sequence of formatted lines from info.
97 110
100 113
101 """ 114 """
102 info = list(info) 115 info = list(info)
103 if not info: 116 if not info:
104 return 117 return
105 label_len = max(len(l) for l, _d in info) 118 label_len = 30
119 assert all(len(l) < label_len for l, _ in info)
106 for label, data in info: 120 for label, data in info:
107 if data == []: 121 if data == []:
108 data = "-none-" 122 data = "-none-"
109 if isinstance(data, (list, set, tuple)): 123 if isinstance(data, (list, set, tuple)):
110 prefix = "%*s:" % (label_len, label) 124 prefix = "%*s:" % (label_len, label)
139 `skip` is the number of frames to skip, so that debugging functions can 153 `skip` is the number of frames to skip, so that debugging functions can
140 call this and not be included in the result. 154 call this and not be included in the result.
141 155
142 """ 156 """
143 stack = inspect.stack()[limit:skip:-1] 157 stack = inspect.stack()[limit:skip:-1]
144 return "\n".join("%30s : %s @%d" % (t[3], t[1], t[2]) for t in stack) 158 return "\n".join("%30s : %s:%d" % (t[3], t[1], t[2]) for t in stack)
145 159
146 160
147 def dump_stack_frames(limit=None, out=None, skip=0): 161 def dump_stack_frames(limit=None, out=None, skip=0):
148 """Print a summary of the stack to stdout, or someplace else.""" 162 """Print a summary of the stack to stdout, or someplace else."""
149 out = out or sys.stdout 163 out = out or sys.stdout
150 out.write(short_stack(limit=limit, skip=skip+1)) 164 out.write(short_stack(limit=limit, skip=skip+1))
151 out.write("\n") 165 out.write("\n")
166
167
168 def clipped_repr(text, numchars=50):
169 """`repr(text)`, but limited to `numchars`."""
170 r = reprlib.Repr()
171 r.maxstring = numchars
172 return r.repr(text)
152 173
153 174
154 def short_id(id64): 175 def short_id(id64):
155 """Given a 64-bit id, make a shorter 16-bit one.""" 176 """Given a 64-bit id, make a shorter 16-bit one."""
156 id16 = 0 177 id16 = 0
160 181
161 182
162 def add_pid_and_tid(text): 183 def add_pid_and_tid(text):
163 """A filter to add pid and tid to debug messages.""" 184 """A filter to add pid and tid to debug messages."""
164 # Thread ids are useful, but too long. Make a shorter one. 185 # Thread ids are useful, but too long. Make a shorter one.
165 tid = "{0:04x}".format(short_id(_thread.get_ident())) 186 tid = "{:04x}".format(short_id(_thread.get_ident()))
166 text = "{0:5d}.{1}: {2}".format(os.getpid(), tid, text) 187 text = "{:5d}.{}: {}".format(os.getpid(), tid, text)
167 return text 188 return text
189
190
191 class SimpleReprMixin(object):
192 """A mixin implementing a simple __repr__."""
193 simple_repr_ignore = ['simple_repr_ignore', '$coverage.object_id']
194
195 def __repr__(self):
196 show_attrs = (
197 (k, v) for k, v in self.__dict__.items()
198 if getattr(v, "show_repr_attr", True)
199 and not callable(v)
200 and k not in self.simple_repr_ignore
201 )
202 return "<{klass} @0x{id:x} {attrs}>".format(
203 klass=self.__class__.__name__,
204 id=id(self),
205 attrs=" ".join("{}={!r}".format(k, v) for k, v in show_attrs),
206 )
207
208
209 def simplify(v): # pragma: debugging
210 """Turn things which are nearly dict/list/etc into dict/list/etc."""
211 if isinstance(v, dict):
212 return {k:simplify(vv) for k, vv in v.items()}
213 elif isinstance(v, (list, tuple)):
214 return type(v)(simplify(vv) for vv in v)
215 elif hasattr(v, "__dict__"):
216 return simplify({'.'+k: v for k, v in v.__dict__.items()})
217 else:
218 return v
219
220
221 def pp(v): # pragma: debugging
222 """Debug helper to pretty-print data, including SimpleNamespace objects."""
223 # Might not be needed in 3.9+
224 pprint.pprint(simplify(v))
168 225
169 226
170 def filter_text(text, filters): 227 def filter_text(text, filters):
171 """Run `text` through a series of filters. 228 """Run `text` through a series of filters.
172 229
195 252
196 def filter(self, text): 253 def filter(self, text):
197 """Add a cwd message for each new cwd.""" 254 """Add a cwd message for each new cwd."""
198 cwd = os.getcwd() 255 cwd = os.getcwd()
199 if cwd != self.cwd: 256 if cwd != self.cwd:
200 text = "cwd is now {0!r}\n".format(cwd) + text 257 text = "cwd is now {!r}\n".format(cwd) + text
201 self.cwd = cwd 258 self.cwd = cwd
202 return text 259 return text
203 260
204 261
205 class DebugOutputFile(object): # pragma: debugging 262 class DebugOutputFile(object): # pragma: debugging
208 self.outfile = outfile 265 self.outfile = outfile
209 self.show_process = show_process 266 self.show_process = show_process
210 self.filters = list(filters) 267 self.filters = list(filters)
211 268
212 if self.show_process: 269 if self.show_process:
213 self.filters.append(CwdTracker().filter) 270 self.filters.insert(0, CwdTracker().filter)
214 cmd = " ".join(getattr(sys, 'argv', ['???'])) 271 self.write("New process: executable: %r\n" % (sys.executable,))
215 self.write("New process: executable: %s\n" % (sys.executable,)) 272 self.write("New process: cmd: %r\n" % (getattr(sys, 'argv', None),))
216 self.write("New process: cmd: %s\n" % (cmd,))
217 if hasattr(os, 'getppid'): 273 if hasattr(os, 'getppid'):
218 self.write("New process: parent pid: %s\n" % (os.getppid(),)) 274 self.write("New process: pid: %r, parent pid: %r\n" % (os.getpid(), os.getppid()))
219 275
220 SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one' 276 SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one'
221 277
222 @classmethod 278 @classmethod
223 def the_one(cls, fileobj=None, show_process=True, filters=()): 279 def get_one(cls, fileobj=None, show_process=True, filters=(), interim=False):
224 """Get the process-wide singleton DebugOutputFile. 280 """Get a DebugOutputFile.
225 281
226 If it doesn't exist yet, then create it as a wrapper around the file 282 If `fileobj` is provided, then a new DebugOutputFile is made with it.
227 object `fileobj`. `show_process` controls whether the debug file adds 283
228 process-level information. 284 If `fileobj` isn't provided, then a file is chosen
285 (COVERAGE_DEBUG_FILE, or stderr), and a process-wide singleton
286 DebugOutputFile is made.
287
288 `show_process` controls whether the debug file adds process-level
289 information, and filters is a list of other message filters to apply.
290
291 `filters` are the text filters to apply to the stream to annotate with
292 pids, etc.
293
294 If `interim` is true, then a future `get_one` can replace this one.
229 295
230 """ 296 """
297 if fileobj is not None:
298 # Make DebugOutputFile around the fileobj passed.
299 return cls(fileobj, show_process, filters)
300
231 # Because of the way igor.py deletes and re-imports modules, 301 # Because of the way igor.py deletes and re-imports modules,
232 # this class can be defined more than once. But we really want 302 # this class can be defined more than once. But we really want
233 # a process-wide singleton. So stash it in sys.modules instead of 303 # a process-wide singleton. So stash it in sys.modules instead of
234 # on a class attribute. Yes, this is aggressively gross. 304 # on a class attribute. Yes, this is aggressively gross.
235 the_one = sys.modules.get(cls.SYS_MOD_NAME) 305 the_one, is_interim = sys.modules.get(cls.SYS_MOD_NAME, (None, True))
236 if the_one is None: 306 if the_one is None or is_interim:
237 assert fileobj is not None 307 if fileobj is None:
238 sys.modules[cls.SYS_MOD_NAME] = the_one = cls(fileobj, show_process, filters) 308 debug_file_name = os.environ.get("COVERAGE_DEBUG_FILE", FORCED_DEBUG_FILE)
309 if debug_file_name:
310 fileobj = open(debug_file_name, "a")
311 else:
312 fileobj = sys.stderr
313 the_one = cls(fileobj, show_process, filters)
314 sys.modules[cls.SYS_MOD_NAME] = (the_one, interim)
239 return the_one 315 return the_one
240 316
241 def write(self, text): 317 def write(self, text):
242 """Just like file.write, but filter through all our filters.""" 318 """Just like file.write, but filter through all our filters."""
243 self.outfile.write(filter_text(text, self.filters)) 319 self.outfile.write(filter_text(text, self.filters))
248 self.outfile.flush() 324 self.outfile.flush()
249 325
250 326
251 def log(msg, stack=False): # pragma: debugging 327 def log(msg, stack=False): # pragma: debugging
252 """Write a log message as forcefully as possible.""" 328 """Write a log message as forcefully as possible."""
253 out = DebugOutputFile.the_one() 329 out = DebugOutputFile.get_one(interim=True)
254 out.write(msg+"\n") 330 out.write(msg+"\n")
255 if stack: 331 if stack:
256 dump_stack_frames(out=out, skip=1) 332 dump_stack_frames(out=out, skip=1)
257 333
258 334
259 def filter_aspectlib_frames(text): # pragma: debugging 335 def decorate_methods(decorator, butnot=(), private=False): # pragma: debugging
260 """Aspectlib prints stack traces, but includes its own frames. Scrub those out.""" 336 """A class decorator to apply a decorator to methods."""
261 # <<< aspectlib/__init__.py:257:function_wrapper < igor.py:143:run_tests < ... 337 def _decorator(cls):
262 text = re.sub(r"(?<= )aspectlib/[^.]+\.py:\d+:\w+ < ", "", text) 338 for name, meth in inspect.getmembers(cls, inspect.isroutine):
263 return text 339 if name not in cls.__dict__:
264 340 continue
265 341 if name != "__init__":
266 def enable_aspectlib_maybe(): # pragma: debugging 342 if not private and name.startswith("_"):
267 """For debugging, we can use aspectlib to trace execution. 343 continue
268 344 if name in butnot:
269 Define COVERAGE_ASPECTLIB to enable and configure aspectlib to trace 345 continue
270 execution:: 346 setattr(cls, name, decorator(meth))
271 347 return cls
272 $ export COVERAGE_LOG=covaspect.txt 348 return _decorator
273 $ export COVERAGE_ASPECTLIB=coverage.Coverage:coverage.data.CoverageData 349
274 $ coverage run blah.py ... 350
275 351 def break_in_pudb(func): # pragma: debugging
276 This will trace all the public methods on Coverage and CoverageData, 352 """A function decorator to stop in the debugger for each call."""
277 writing the information to covaspect.txt. 353 @functools.wraps(func)
278 354 def _wrapper(*args, **kwargs):
279 """ 355 import pudb
280 aspects = os.environ.get("COVERAGE_ASPECTLIB", "") 356 sys.stdout = sys.__stdout__
281 if not aspects: 357 pudb.set_trace()
282 return 358 return func(*args, **kwargs)
283 359 return _wrapper
284 import aspectlib # pylint: disable=import-error 360
285 import aspectlib.debug # pylint: disable=import-error 361
286 362 OBJ_IDS = itertools.count()
287 filename = os.environ.get("COVERAGE_LOG", "/tmp/covlog.txt") 363 CALLS = itertools.count()
288 filters = [add_pid_and_tid, filter_aspectlib_frames] 364 OBJ_ID_ATTR = "$coverage.object_id"
289 aspects_file = DebugOutputFile.the_one(open(filename, "a"), show_process=True, filters=filters) 365
290 aspect_log = aspectlib.debug.log( 366 def show_calls(show_args=True, show_stack=False, show_return=False): # pragma: debugging
291 print_to=aspects_file, attributes=['id'], stacktrace=30, use_logging=False 367 """A method decorator to debug-log each call to the function."""
292 ) 368 def _decorator(func):
293 public_methods = re.compile(r'^(__init__|[a-zA-Z].*)$') 369 @functools.wraps(func)
294 for aspect in aspects.split(':'): 370 def _wrapper(self, *args, **kwargs):
295 aspectlib.weave(aspect, aspect_log, methods=public_methods) 371 oid = getattr(self, OBJ_ID_ATTR, None)
372 if oid is None:
373 oid = "{:08d} {:04d}".format(os.getpid(), next(OBJ_IDS))
374 setattr(self, OBJ_ID_ATTR, oid)
375 extra = ""
376 if show_args:
377 eargs = ", ".join(map(repr, args))
378 ekwargs = ", ".join("{}={!r}".format(*item) for item in kwargs.items())
379 extra += "("
380 extra += eargs
381 if eargs and ekwargs:
382 extra += ", "
383 extra += ekwargs
384 extra += ")"
385 if show_stack:
386 extra += " @ "
387 extra += "; ".join(_clean_stack_line(l) for l in short_stack().splitlines())
388 callid = next(CALLS)
389 msg = "{} {:04d} {}{}\n".format(oid, callid, func.__name__, extra)
390 DebugOutputFile.get_one(interim=True).write(msg)
391 ret = func(self, *args, **kwargs)
392 if show_return:
393 msg = "{} {:04d} {} return {!r}\n".format(oid, callid, func.__name__, ret)
394 DebugOutputFile.get_one(interim=True).write(msg)
395 return ret
396 return _wrapper
397 return _decorator
398
399
400 def _clean_stack_line(s): # pragma: debugging
401 """Simplify some paths in a stack trace, for compactness."""
402 s = s.strip()
403 s = s.replace(os.path.dirname(__file__) + '/', '')
404 s = s.replace(os.path.dirname(os.__file__) + '/', '')
405 s = s.replace(sys.prefix + '/', '')
406 return s

eric ide

mercurial