Wednesday, March 9, 2011

Computing function call binding

A lot of times, in Python, you write decorators that handle any type/number of arguments by using *a, **kw. Inside you usually call the wrapped function/method by passing the arguments as you got them by using *a, **kw again. You can do tracing, caching, retries and similar stuff very easily this way.

However, sometimes you want to write something generic like that, but also be able to be aware of which values were passed in to the wrapped method.

For example, I have some sort of hook framework for testing my code, where a user can register a hook that will throw an exception upon entering or exiting some method, or make it wait until it's released before calling the method (this one is good for testing and reproducing race conditions).

This is all good, but suppose I want to register a hook so it will only apply if the method is called with a certain value for parameter X. Or I want to be able to intercept and change the value that's passed into parameter X. The problem is that I need a way to understand what value will be assigned to parameter X and that value can come from positional arguments (*a), keyword arguments (**kw), or a default value.

The code below helps with this - it gets a callable, and values passed as *a, **kw, and returns a dictionary of values for each of the method's arguments.
For example, if the function was

  def foo(a,b,c=3)

and I called it with


then the binding computation will return


Here's the code:

def compute_call_binding(f,a,kw):
binding = {} # varname -> value
spec = inspect.getargspec(f)

# if it's a bound method, then getargspec returns self, even though client shouldn't supply it
# so we need to identify that case and pretend 'self' was given explicitly in our positional arguments
if getattr(f,'im_self',None) is not None: # XXX - better way to identify bound methods?
a = [f.im_self] + list(a)

# assign positional args
for varname,val in zip(spec.args,a):
binding[varname] = val

# find any extra positional values
extra_a = a[len(spec.args):]

# handle keyword args and find any extra ones
extra_kw = {} # will collect name->value for assignments to variables that weren't in the spec.args
for varname,val in kw.iteritems():
assertions.fail_if(varname in binding, "Got duplicate value for argument", varname=varname)
if varname in spec.args:
binding[varname] = val
extra_kw[varname] = val

# assign varargs and keyword variables if they appear in the argspec
if spec.varargs is None:
assertions.fail_if(extra_a, "Got extra positional arguments", argspec=spec, args=a, kwargs=kw)
binding[spec.varargs] = extra_a
if spec.keywords is None:
assertions.fail_if(extra_kw, "Got extra named arguments", argspec=spec, args=a, kwargs=kw)
binding[spec.keywords] = extra_kw

# assign defaults to vars in spec.args that have them and weren't assigned till now
if spec.defaults is not None:
for varname,val in zip(spec.args[-len(spec.defaults):],spec.defaults):
if varname not in binding:
binding[varname] = val

# check all variables in spec.args have values
for varname in spec.args:
assertions.fail_unless(varname in binding, "No value for variable", varname=varname, argspec=spec, args=a, kwargs=kw, method=f)

return binding