This talk's objective:
Early debugging
manual tracer
def mytracer(frame, event, args): print(frame.f_code.co_filename, ... import sys sys.settrace(mytracer)
aspectlib (usecase: deadlocks) - fancy-pants monkeypatching to replace the previous tedious patch code
aspectlib.weave('absolutely.anything.anywhere', aspectlib.debug.log, subclasses=True, methods=NORMAL_METHODS, lazy=False, aliases=True)
manhole (usecase: memory leaks) - robust debugging shell (inspired by twisted.manole but no big dependencies, no insecure stuff)
The defaults:
It's verbose ("function" over "func", "CallPrinter" over "Print")
Running this:
PYTHONHUNTER='module="posixpath", action=CallPrinter(filename_alignment=12)' python3
Gets you this:
>>> os.path.join('a', 'b') [...]path.py:75 call => join(a='a') [...]path.py:80 line a = os.fspath(a) [...]path.py:81 line sep = _get_sep(a) [...]path.py:41 call => _get_sep(path='a') [...]path.py:42 line if isinstance(path, bytes): [...]path.py:45 line return '/' [...]path.py:45 return <= _get_sep: '/' [...]path.py:82 line path = a [...]path.py:83 line try: [...]path.py:84 line if not p: [...]path.py:86 line for b in map(os.fspath, p): [...]path.py:87 line if b.startswith(sep): [...]path.py:89 line elif not path or path.endswith(sep): [...]path.py:92 line path += sep + b [...]path.py:86 line for b in map(os.fspath, p): [...]path.py:96 line return path [...]path.py:96 return <= join: 'a/b' 'a/b'
Get a tree with just the calls/returns/exceptions:
trace( ~Q(kind="line"), ~Q(module_in=["six","pkg_resources"]), ~Q(module_contains=["importlib"]), ~Q(filename=""), stdlib=False )
Breakpoints:
trace( Q(lambda ev: ev.locals.get("host") == "localhost", action=Debugger), module="redis.client" )
def error(): raise RuntimeError() def silenced1(): try: error() except Exception: pass def silenced2(): try: error() except Exception as exc: pass return 'x' def silenced3(): try: error() finally: return "mwhahaha"
Doesn't help:
grep -A 5 -R "except:" .
What if you could detect this with a tracer, instead of reading all the code?
The plan:
class PlainDumpExceptions(hunter.CodePrinter): should_trace = False depth = 0 count = 0 def __call__(self, event): self.count += 1 if event.kind == 'exception': # something interesting happened ;) self.should_trace = True self.depth = event.depth self.count = 0 self.output("{} tracing on exception: {}\n", ">" * 46, self._try_repr(event.arg[1])) super(DumpExceptions, self).__call__(event) elif self.should_trace: super(DumpExceptions, self).__call__(event) if event.kind == 'return': # stop if function returned self.should_trace = False self.output("{} function exit\n", "-" * 46) elif event.depth > self.depth + 1: # too many details return elif self.count > 10: # bail out on too many lines self.should_trace = False self.output("{:>46} too many lines\n", "-" * 46)
What's missing?
Colors:
self.output("{BRIGHT}{fore(BLUE)} ...", ...
Skip cases where exception is reraised?
- Buffer up all the events up to a return
- If return is bad (not a reraise) dump all the events.
class SmartDumpExceptions(hunter.CodePrinter): events = None depth = 0 count = 0 def __call__(self, event): self.count += 1 if event.kind == 'exception': # something interesting happened ;) self.events = [event] # not great, not terrible self.exc = self.try_repr(event.arg[1]) self.depth = event.depth self.count = 0 elif self.events: if event.kind == 'return': # stop if function returned if opcode.opname[event.code.co_code[event.frame.f_lasti]] == 'RETURN_VALUE': self.output("{BRIGHT}{fore(BLUE)}{} tracing {} on {}{RESET}\n", ">" * 46, event.function, self.exc) for event in self.events: super(SmartDumpExceptions, self).__call__(event) if self.count > 10: self.output("{BRIGHT}{fore(BLACK)}{} too many lines{RESET}\n", "-" * 46) else: self.output("{BRIGHT}{fore(BLACK)}{} function exit{RESET}\n", "-" * 46) self.events = [] elif event.depth > self.depth + 1: # too many details return elif self.count < 10: self.events.append(event)
self.events.append(event)
Has some problems:
Solution: strip event of "live" data like event.arg, event.globals, event.locals - who needs those?
self.events.append(event.detach())
Stripping event of "live" data:
self.events.append(event.detach())
Has some problems:
- call arguments are in event.locals
- return value is in event.arg
Solution: sanitize values by converting to string representations (we don't need anything else anyway):
self.events.append(event.detach(self.try_repr))
The usecase:
Solution:
def probe(qualname, *actions, **filters): def tracing_decorator(func): @functools.wraps(func) def tracing_wrapper(*args, **kwargs): # create the Tracer manually to avoid spending time # in likely useless things like: # - loading PYTHONHUNTERCONFIG # - setting up the clear_env_var or thread_support options # - atexit cleanup registration with hunter.Tracer().trace( hunter.When(hunter.Query(**filters), *actions) ): return func(*args, **kwargs) return tracing_wrapper aspectlib.weave(qualname, tracing_decorator) # module.func = tracing_decorator(module.func) probe('module.func', hunter.VarsPrinter('var1', 'var2'), kind="return", depth=0)
Table of Contents | t |
---|---|
Exposé | ESC |
Presenter View | p |
Source Files | s |
Slide Numbers | n |
Toggle screen blanking | b |
Show/hide next slide | c |
Notes | 2 |
Help | h |