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?
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={})
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With