Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to require positional arguments when using decorators?

I need to a create a generic decorator that validates parameters passed to multiple python functions, that have similar arguments, but not necessarily in the same order.

The python functions are part of an SDK, so the arguments need to be readable (i.e. can't just be *args and **kwargs as that would require the user to dig through the code.)

Let's consider the following decorator, which enforces the constraint that a > b:

from functools import wraps

def check_args(f):
    @wraps(f)
    def decorated_function(self, *args, **kwargs):
        a = kwargs["a"]
        b = kwargs["b"]
        
        if a < b:
            raise ValueError("a must be strictly greater than b")

        return f(self, *args, **kwargs)

    return decorated_function

Now consider the following example:

class MyClass(object):
    
    @check_args
    def f(self, *, a, b):
        return a + b

Let's call the method f and pass in a and b as keyword-arguments:

MyClass().f(a=2, b=1)

This works as expected, no errors.

Now's let again call the method f, but this time using arguments:

MyClass().f(1, 2)

This raises a KeyError:

  ---------------------------------------------------------------------------
  KeyError                                  Traceback (most recent call last)
  Input In [15], in <cell line: 7>()
        3     @check_args
        4     def f(self, *, a, b):
        5         return a + b
  ----> 7 MyClass().f(1, 2)

  Input In [14], in check_args.<locals>.decorated_function(self, *args, **kwargs)
        4 @wraps(f)
        5 def decorated_function(self, *args, **kwargs):
  ----> 6     a = kwargs["a"]
        7     b = kwargs["b"]
        9     if a < b:

  KeyError: 'a'

The parameters are now coming into the decorator as args, which means I would need to reference them as args[0], args[1]. But then how would I make the decorator generic? What if I want to use the decorator on a different function, which has a different starting parameter?

Moreover, I added the * to the list of arguments for f, to force the user to use keyword arguments, but instead, the decorator raised a KeyError.

If I remove the decorator:

class MyClass(object):
    
    def f(self, *, a, b):
        return a + b
    
MyClass().f(2, 1)

I get a different error:

  ---------------------------------------------------------------------------
  TypeError                                 Traceback (most recent call last)
  Input In [19], in <cell line: 6>()
        3     def f(self, *, a, b):
        4         return a + b
  ----> 6 MyClass().f(2, 1)

  TypeError: f() takes 1 positional argument but 3 were given

which is the error I want, as that forces the user to use keyword arguments!

What's the proper solution for this problem? How do I force the user to use keyword arguments when using decorators?

Edit: A hack would be to examine the list of args and raise an error if this list is non-empty. But that sounds like a cheat, is there a proper solution?

like image 382
vgoklani Avatar asked Oct 27 '25 21:10

vgoklani


1 Answers

try inspect.getfullargspec()

from functools import wraps
import inspect

def check_args(f):
    @wraps(f)
    def decorated_function(self, *args, **kwargs):
        print(inspect.getfullargspec(f))
        a = kwargs["a"]
        b = kwargs["b"]
        
        if a < b:
            raise ValueError("a must be strictly greater than b")

        return f(self, *args, **kwargs)

    return decorated_function

class MyClass(object):
    
    @check_args
    def f(self, *, a, b):
        return a + b

MyClass().f(1, 2)

it see the names:

FullArgSpec(args=['self'], varargs=None, varkw=None, defaults=None, kwonlyargs=['a', 'b'], kwonlydefaults=None, annotations={})
like image 165
Aksakal almost surely binary Avatar answered Oct 29 '25 11:10

Aksakal almost surely binary



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!