Created
February 26, 2014 12:23
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 '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] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
😻