Skip to content

Commit

Permalink
Added support for global and instance variables access to the byteco…
Browse files Browse the repository at this point in the history
…de tracer.
  • Loading branch information
mkwiatkowski committed Dec 6, 2010
1 parent 67b106c commit fc78415
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 9 deletions.
35 changes: 27 additions & 8 deletions bytecode_tracer/bytecode_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ def is_c_func(func):
"""
return not hasattr(func, 'func_code')

def name_from_arg(frame, bcode):
return frame.f_code.co_names[bcode.arg1]


class StandardBytecodeTracer(object):
"""A tracer that goes over each bytecode and reports events that couldn't
Expand Down Expand Up @@ -191,6 +194,13 @@ def trace(self, frame, event):
* ('print', value)
* ('print_to', (value, output))
A print statement is about to be executed.
* ('store_attr', (object, name, value))
* ('delete_attr', (object, name))
An instance variable of object is about to be changed or deleted.
* ('load_global', name)
* ('store_global', (name, value))
* ('delete_global', name)
A global variable is about to be read, written or deleted.
It is a generator and it yields a sequence of events, as a single
bytecode may generate more than one event. Canonical example is
Expand Down Expand Up @@ -226,15 +236,24 @@ def trace(self, frame, event):
yield 'c_call', (function, pargs, kargs)
elif bcode.name == "PRINT_NEWLINE":
yield 'print', os.linesep
elif bcode.name == "PRINT_NEWLINE_TO":
stack = get_value_stack_top(frame)
yield 'print_to', (os.linesep, stack[-1])
elif bcode.name == "PRINT_ITEM":
stack = get_value_stack_top(frame)
yield 'print', stack[-1]
elif bcode.name == "PRINT_ITEM_TO":
else:
stack = get_value_stack_top(frame)
yield 'print_to', (stack[-2], stack[-1])
if bcode.name == "PRINT_NEWLINE_TO":
yield 'print_to', (os.linesep, stack[-1])
elif bcode.name == "PRINT_ITEM":
yield 'print', stack[-1]
elif bcode.name == "PRINT_ITEM_TO":
yield 'print_to', (stack[-2], stack[-1])
elif bcode.name == "STORE_ATTR":
yield 'store_attr', (stack[-1], name_from_arg(frame, bcode), stack[-2])
elif bcode.name == "DELETE_ATTR":
yield 'delete_attr', (stack[-1], name_from_arg(frame, bcode))
elif bcode.name == "LOAD_GLOBAL":
yield 'load_global', name_from_arg(frame, bcode)
elif bcode.name == "STORE_GLOBAL":
yield 'store_global', (name_from_arg(frame, bcode), stack[-1])
elif bcode.name == "DELETE_GLOBAL":
yield 'delete_global', name_from_arg(frame, bcode)
elif event == 'call':
self.call_stack.append(False)
# When an exception happens in Python >= 2.4 code, 'exception' and
Expand Down
79 changes: 78 additions & 1 deletion test/test_bytecode_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@
class TestBytecodeTracer:
def setup(self):
self._traces = []
self._ignored_events = ['load_global', 'store_global']
self.btracer = BytecodeTracer()

def _trace(self, frame, event, arg):
try:
if arg is not sys.settrace:
for ret in self.btracer.trace(frame, event):
if ret[0] is not None:
if ret[0] is not None and ret[0] not in self._ignored_events:
self._traces.append(ret)
except TypeError:
pass
Expand Down Expand Up @@ -465,6 +466,82 @@ def fun():
('c_call', (abs, [-1], {})),
('c_return', 1))

class TestBytecodeTracerRebinding(TestBytecodeTracer):
def test_handles_instance_variable_rebinding(self):
class Class(object):
def method(self, x):
self.x = x
def fun():
global return_value
return_value = c = Class()
c.method(-1)
self.trace_function(fun)
self.assert_trace(('c_call', (Class, [], {})),
('c_return', return_value),
('store_attr', (return_value, 'x', -1)))

def test_handles_instance_variable_rebinding_with_setattr(self):
class Class(object):
def method(self, x):
setattr(self, 'x', x)
def fun():
global return_value
return_value = c = Class()
c.method(-1)
self.trace_function(fun)
self.assert_trace(('c_call', (Class, [], {})),
('c_return', return_value),
('c_call', (setattr, [return_value, 'x', -1], {})),
('c_return', None))

def test_handles_class_variable_rebinding_with_setattr(self):
class Class(object):
pass
def fun():
Class.x = 1
self.trace_function(fun)
self.assert_trace(('store_attr', (Class, 'x', 1)))

def test_handles_instance_variable_unbinding(self):
class Class(object):
def __init__(self):
self.x = 1
def method(self):
del self.x
def fun():
global return_value
return_value = c = Class()
c.method()
self.trace_function(fun)
self.assert_trace(('c_call', (Class, [], {})),
('c_return', return_value),
('delete_attr', (return_value, 'x')))

class TestBytecodeTracerGlobalAccessAndRebinding(TestBytecodeTracer):
def setup(self):
TestBytecodeTracer.setup(self)
self._ignored_events = []

def test_handles_global_variable_reading(self):
def fun():
return return_value
self.trace_function(fun)
self.assert_trace(('load_global', 'return_value'))

def test_handles_global_variable_rebinding(self):
def fun():
global return_value
return_value = 123
self.trace_function(fun)
self.assert_trace(('store_global', ('return_value', 123)))

def test_handles_global_variable_unbinding(self):
def fun():
global return_value
del return_value
self.trace_function(fun)
self.assert_trace(('delete_global', 'return_value'))

class TestRewriteFunction:
def test_handles_functions_with_free_variables(self):
x = 1
Expand Down

0 comments on commit fc78415

Please sign in to comment.