Smart Parameter Checks in Python

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)

  1. 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.
  2. 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.
  3. If there are any left over from the variable position args, they are stored in an extras list.
Now, with this information handy, we can actually do our own safe parameter checking, and we should! So when a call is made, I also check if all the required params have been provided, whether there are any unexpected params and so forth, and actually raise some TypeErrors like a real function if need be.

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!

Ever feel like you are wasting your life away debugging?
It doesn't have to be this way. Check out my debugging course.
blog comments powered by Disqus