Skip to content

Instantly share code, notes, and snippets.

@leonardbinet
Last active August 9, 2020 22:24
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 leonardbinet/4329f0802af2a31809c916c8915d2006 to your computer and use it in GitHub Desktop.
Save leonardbinet/4329f0802af2a31809c916c8915d2006 to your computer and use it in GitHub Desktop.
Python spier decorator

Suppose you want to want to track all calls (args, kwargs and result) made to a given method while running a command, you can do it using the simple decorator defined in the spy.py module of this gist.

Usage

You can use it to spy either on a single instance or to all instances of given class.

The calls will be stored in a list that must be passed by instantiated beforehand and passed to the decorator.

Spy on single instance

from mock import patch
from spy import spy
from example_spied_class import Incrementer

instance_calls = []
incrementer_instance = Incrementer(0)
with patch.object(incrementer_instance, 'increment', new=spy(incrementer_instance.increment, instance_calls)):
    incrementer_instance.increment(1)
    incrementer_instance.increment(2, with_extra=True)
print(instance_calls)
print('Last result: %d' % instance_calls[1].result)

it will result in:

[<Call> 1 args, no keyword arguments, <type 'int'> result, <Call> 1 args, "with_extra" keyword arguments, <type 'int'> result]
Last result: 4

Spy on whole class

from mock import patch
from spy import spy
from example_spied_class import Incrementer

class_calls = []

with patch.object(Incrementer, 'increment', new=spy(Incrementer.increment, class_calls)):
    incrementer_instance = Incrementer(10)
    incrementer_instance.increment(1)
    incrementer_instance.increment(2, with_extra=True)
print(class_calls)
print('Last result: %d' % class_calls[1].result)

it will result in:

[<Call> 2 args, no keyword arguments, <type 'int'> result, <Call> 2 args, "with_extra" keyword arguments, <type 'int'> result]
Last result: 14

Note

Note that you can as well use patch syntax instead of patch.object syntax, in this case see mock documentation for more details.

class Incrementer(object):
def __init__(self, initial=0):
self.count = initial
def increment(self, increment, with_extra=False):
self.count += increment
if with_extra:
self.count += 1
return self.count
import copy
class Call(object):
def __init__(self, args, kwargs, result):
self.args = args
self.kwargs = kwargs
self.result = result
def __repr__(self):
if self.kwargs:
kwargs_repr = ','.join(map(lambda x: "\"%s\"" % x, self.kwargs.keys()))
else:
kwargs_repr = 'no'
return '<Call> %d args, %s keyword arguments, %s result' % (
len(self.args), kwargs_repr, type(self.result)
)
def spy(wrapped_func, acc, deep=False):
"""Decorate a function to spy on it without changing its initial behavior.
Spy on function, and add at each call in the "acc" list.
:param wrapped_func: function you want to spy on
:param acc: (list) store args, kwargs and results
:param deep: boolean, if True store deepcopy of args, kwargs and result, useful if object passed by reference might
be modified in-between calls
:rtype: list of Call
"""
if not isinstance(acc, list):
raise ValueError('acc must be a list')
def wrapper_func(*args, **kwargs):
args_ = copy.deepcopy(args) if deep else args
kwargs_ = copy.deepcopy(kwargs) if deep else kwargs
r = wrapped_func(*args, **kwargs)
acc.append(Call(
args=args_,
kwargs=kwargs_,
result=copy.deepcopy(r) if deep else r
))
return r
return wrapper_func
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment