views:

80

answers:

2

I want to write a python function decorator that tests that certain arguments to a function pass some criterion. For eg, Suppose I want to test that some arguments are always even, then I want to be able to do something like this (not valid python code)

def ensure_even( n )  :
  def decorator( function ) :
    @functools.wraps( function )
    def wrapper(*args, **kwargs):
      assert(n % 2 == 0)
      return function(*args, **kwargs)
    return wrapper
   return decorator


@ensure_even(arg2)
def foo(arg1, arg2, arg3) : pass

@ensure_even(arg3)
def bar(arg1, arg2, arg3) : pass

But I cannot figure how to achieve the above. Is there a way to pass specific arguments to the decorator? (like arg2 for foo and arg3 for bar in the above)

Thanks!

+7  A: 

You can do this:

def ensure_even(argnum):
  def fdec(func):
    def f(*args, **kwargs):
      assert(args[argnum] % 2 == 0)  #or assert(not args[argnum] % 2)
      return func(*args, **kwargs)
    return f
  return fdec

So then:

@ensure_even(1)  #2nd argument must be even
def test(arg1, arg2):
  print(arg2)

test(1,2) #succeeds
test(1,3) #fails
jcao219
yes, that is what I did do, but I was thinking that this is just my C++ background coming up with a hack instead of a more elegant pythonic way :) Thanks anyway.
MK
@MK: If you use keyword arguments instead of positional arguments, then you could pass `ensure_even` the name of the keyword argument. That might be better in sense that there would be one less hard-coded number and your code might survive the addition of new args without needing to change the argnum.
unutbu
@~unutbu Yep, that's a great way.
jcao219
+2  A: 

My previous answer missed the point of your question.

Here's a longer solution:

def ensure_even(*argvars):
  def fdec(func):
    def f(*args,**kwargs):
      for argvar in argvars:
        try:
          assert(not args[func.func_code.co_varnames.index(argvar)] % 2)
        except IndexError:
          assert(not kwargs[argvar] % 2)
      return func(*args,**kwargs)
    return f
  return fdec

and so this is the usage:

@ensure_even('a','b')
def both_even(a,b):
    print a,b

@ensure_even('even')
def first_even(even, arg2):
    print even, arg2

both_even(2,2)
first_even(2,3)
both_even(2,1) #fails

although I'm not sure if it will work in all situations.

jcao219
ah introspection. i had to first go find out what the above is doing but i get the idea and it is cool. I am going to stick with the above solution though
MK