While I really enjoy using Michael Foord's Mock library, there is one thing missing and I thought would be a fun project to add - which is the subject of this post.
Fun with Parameters
Python has the most sophisticated function parameter calling mechanics I've ever seen. Given a function f
def f(a, b):
return a + b
You can either call it like normal(positional parameters)
f(1, 2)
or by keyword parameter
f(a=1, b=2)
With keyword parameters, the position doesn't matter anymore, so the above call is equivalent to
f(b=2, a=1)
You can even mix positional and keyword parameters, as long as you put all the positionals first
f(1, b=2)
So, all the ways I've listed of calling the function f with the parameter a as 1 and b as 2 will have the same functional behavior, given what we know of the signature of the function. So, it would be logical that the mock library would consider
f.assert_called_with(a=1, b=2)
a passing assertion even though in reality it was called as
f(1, 2)
but alas, it does not.
With that, it was my mission to make this happen!
Proof-of-Concept
First I created a prototype/proof-of-concept. See code here. Using
inspect.getargspec(self.func)
I was able to get all the info about the signature of a function I needed. When a function call actually comes in, my callable receives them as
def __call__(self, *args, **kwds):
at which point, I resolve all the parameters with the parameter list in the function signature(I called this "combining" the args)
- First, by walking the actual positionals (the args array coming in) and cross referencing them with the ones in the function signature. After this is done, we basically converted the positionals into named form - as a dict.
- Then merge everything in kwds into the same dict. So we have one unified dict with all the actually parameters and keyed by parameter name.
- If there are any left over from the variable position args, they are stored in an extras list.
But the better part is what comes next - we can now implement the assert_called_with with these new semantics. When a call to assert_called_with is called, I "combine" the args coming into that call, and check if they match the previously made function call. It's that simple!
Working in Python-Mock
Well great. But we still can't use for the Mock library. So I did some more hacking on a branch of Foord's code. Not sure it's the cleanest code, but it now works if you want to try it out
hg clone https://airportyh-python-mock.googlecode.com/hg/ airportyh-python-mock
The way you use it with the Mock library is through mocksignature.
from mock import mocksignature
mocksignature wraps a function that you pass it and uses the signature of that function as a guide. Given the function f that we had previously, you can do
f = mocksignature(f)
now call it
f(1, 2)
and then checking the associated mock with a different call style
f.mock.assert_called_with(a=1, b=2)
should now pass.
Cheers!