Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Parsing args and kwargs in decorators

I've got a function that takes args and kwargs, and I need to do something in my decorator based on the value of the 2nd arg in the function, like in the code below:

def workaround_func():
    def decorator(fn):
        def case_decorator(*args, **kwargs):
            if args[1] == 2:
                print('The second argument is a 2!')
            return fn(*args, **kwargs)
        return case_decorator
    return decorator

@workaround_func()
def my_func(arg1, arg2, kwarg1=None):
    print('arg1: {} arg2: {}, kwargs: {}'.format(arg1, arg2, kwarg1))

The problem is that python allows users to call the function with the second argument as a regular argument OR a keyword-argument, so if the user calls my_func with arg2 as a kwarg, it raises an IndexError, see below:

In [8]: d.my_func(1, 2, kwarg1=3)
The second argument is a 2!
arg1: 1 arg2: 2, kwargs: 3

In [9]: d.my_func(1, arg2=2, kwarg1=3)
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-9-87dc89222a9e> in <module>()
----> 1 d.my_func(1, arg2=2, kwarg1=3)

/home/camsparr/decoratorargs.py in case_decorator(*args, **kwargs)
      2     def decorator(fn):
      3         def case_decorator(*args, **kwargs):
----> 4             if args[1] == 2:
      5                 print('The second argument is a 2!')
      6             return fn(*args, **kwargs)

IndexError: tuple index out of range

Is there a way around this without just doing a try/except and catch the IndexError?

like image 220
Cameron Sparr Avatar asked Sep 19 '13 23:09

Cameron Sparr


2 Answers

I found an answer using the python decorator package. One feature of this package is that it preserves positional/keyword args no matter how the user passes them. It has the added benefit of reducing a lot of code, so my original code:

def workaround_func():
    def decorator(fn):
        def case_decorator(*args, **kwargs):
            if args[1] == 2:
                print('The second argument is a 2!')
            return fn(*args, **kwargs)
        return case_decorator
    return decorator

@workaround_func()
def my_func(arg1, arg2, kwarg1=None):
    print('arg1: {} arg2: {}, kwargs: {}'.format(arg1, arg2, kwarg1))

becomes:

from decorator import decorator

@decorator
def workaround_decorator(f, *args, **kwargs):
    if args[1] == 2:
        print('The second argument is 2!')
    return f(*args, **kwargs)

@workaround_decorator
def my_func(arg1, arg2, kwarg1=None):
    print('arg1: {} arg2: {}, kwargs: {}'.format(arg1, arg2, kwarg1))
like image 180
Cameron Sparr Avatar answered Oct 16 '22 16:10

Cameron Sparr


This is the most robust way that I can think of to handle it... The trick is to inspect the name of the second argument. Then, in the decorator, you check to see if that name is present in kwargs. If yes, then you use that. If no, then you use args.

from inspect import getargspec    

def decorate(fn):
    argspec = getargspec(fn)
    second_argname = argspec[0][1]
    def inner(*args, **kwargs):
        special_value = (kwargs[second_argname] 
                         if second_argname in kwargs else args[1])
        if special_value == 2:
            print "foo"
        else:
            print "no foo for you"
        return fn(*args, **kwargs)
    return inner

@decorate
def foo(a, b, c=3):
    pass

foo(1,2,3)
foo(1,b=2,c=4)
foo(1,3,5)
foo(1,b=6,c=5)

running this results in:

foo
foo
no foo for you
no foo for you

as expected.

like image 26
mgilson Avatar answered Oct 16 '22 16:10

mgilson