Skip to content

Instantly share code, notes, and snippets.

@gregnavis
Created February 26, 2014 12:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gregnavis/9228589 to your computer and use it in GitHub Desktop.
Save gregnavis/9228589 to your computer and use it in GitHub Desktop.
This is an example of an automatic call tracer that can report the whole call tree. I use it instead of adding lots of logging statements.
import bdb
# This is a custom debugger that calls the specified handler on each function
# call.
class _CallTracerBdb(bdb.Bdb):
def __init__(self, call_handler, return_handler):
bdb.Bdb.__init__(self)
self._call_handler = call_handler
self._return_handler = return_handler
self._level = -1
def trace_dispatch(self, frame, event, arg):
if event == 'call':
self._handle_call(frame)
elif event == 'return':
self._handle_return(frame, arg)
return bdb.Bdb.trace_dispatch(self, frame, event, arg)
def _handle_call(self, frame):
self._level += 1
code, function_name, arguments_tuples, klass = self._read_frame(frame)
self._call_handler(self._level, function_name, klass, arguments_tuples)
def _handle_return(self, frame, return_value):
code, function_name, arguments_tuples, klass = self._read_frame(frame)
self._return_handler(self._level, function_name, klass,
arguments_tuples, return_value)
self._level -= 1
def _read_frame(self, frame):
code = frame.f_code
function_name = code.co_name
argument_names = code.co_varnames[:code.co_argcount]
if argument_names[0] == 'self':
klass = type(frame.f_locals['self'])
argument_names = argument_names[1:]
else:
klass = None
arguments_tuples = [(argument_name, frame.f_locals[argument_name])
for argument_name in argument_names]
return code, function_name, arguments_tuples, klass
# A call tracer has all the logic to trace function calls. It uses the debugger
# above under the hood.
class CallTracer(object):
def __init__(self, call_handler, return_handler):
self._call_handler = call_handler
self._return_handler = return_handler
def run(self, function, *args, **kwargs):
debugger = _CallTracerBdb(self._call_handler, self._return_handler)
return debugger.runcall(function, *args, **kwargs)
# This is an example call handler that prints the call to stdout.
def print_call(level, function_name, klass, arguments_tuples):
print '%s%s' % (
' ' * level,
format_call(function_name, klass, arguments_tuples)
)
def print_return(level, function_name, klass, arguments_tuples, return_value):
print '%s%s = %r' % (
' ' * level,
format_call(function_name, klass, arguments_tuples),
return_value
)
def format_call(function_name, klass, arguments_tuples):
if klass is not None:
function_name = '%s.%s' % (klass.__name__, function_name)
return '%s(%s)' % (
function_name,
', '.join(
'%s=%r' % (argument_name, argument_value)
for argument_name, argument_value in arguments_tuples
)
)
# Let's do an example! The functions below are what we want to trace.
def foo(l):
return A().method(l)
def bar(y):
return 2 * y
class A(object):
def method(self, l):
return map(bar, l)
# Trace'em!
call_tracer = CallTracer(print_call, print_return)
return_value = call_tracer.run(foo, [1, 2, 3])
print
print 'return_value = %r' % return_value
# Output:
#
# foo(l=[1, 2, 3])
# A.method(l=[1, 2, 3])
# bar(y=1)
# bar(y=1) = 2
# bar(y=2)
# bar(y=2) = 4
# bar(y=3)
# bar(y=3) = 6
# A.method(l=[1, 2, 3]) = [2, 4, 6]
# foo(l=[1, 2, 3]) = [2, 4, 6]
#
# return_value = [2, 4, 6]
@floer32
Copy link

floer32 commented May 28, 2019

😻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment