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 (ie 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?
Rewrite args to kwargs??? maybe it not looks good, but it's working, and if you realy must...
def check_decorator(f):
def wrapper(self, **kwargs):
if "a" in kwargs.keys():
a = kwargs["a"]
if "b" in kwargs.keys():
b = kwargs["b"]
if a < b:
raise ValueError("a must be strictly greater than b")
f(self, a, b)
return wrapper
def change_args_decorator(fun):
def wrapper_f(self, *args, **kwargs):
if len(args) > 0:
kwargs["a"] = args[0]
if len(args) > 1:
kwargs["b"] = args[1]
fun(self, **kwargs)
return wrapper_f
class MyClass(object):
@change_args_decorator
@check_decorator
def f(self, a, b):
print(a + b)
return a + b
MyClass().f(2, 1)
MyClass().f(a=2, b=1)
MyClass().f(b=2, a=5)
MyClass().f(2, b=1)
3
3
7
3
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={})
Since a
and b
are required keyword parameters, why not explicitly mark them in the decorator?
from functools import wraps
def check_args(f):
@wraps(f)
def decorated_function(self, *args, a, b, **kwargs):
if a < b:
raise ValueError("a must be strictly greater than b")
return f(self, *args, a=a, b=b, **kwargs)
return decorated_function
Test:
In [16]: MyClass().f(2, 1)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Input In [16], in <cell line: 1>()
----> 1 MyClass().f(2, 1)
TypeError: MyClass.f() missing 2 required keyword-only arguments: 'a' and 'b'
In [17]: MyClass().f(a=2, b=1)
Out[17]: 3
I think this decorator could perfectly works. And allows:
decorator
from functools import partial, wraps
def check_args(func=None, *, paramters_to_eval: list[str], condition=">"):
if not func:
return partial(check_args, paramters_to_eval=paramters_to_eval,
condition=condition)
@wraps(func)
def decorated_function(*args, **kwargs):
argnames = func.__code__.co_varnames # get the argument names of the function
if len(argnames) > 0:
if argnames[0] == 'self':
argnames = argnames[1:]
values = dict(zip(argnames, args), **kwargs) # get arguments plus kwargs
if set(paramters_to_eval).issubset(list(values.keys())): # only if both argument are given check conditon
# create a condition as string to use eval
string = f"{values[paramters_to_eval[0]]} {condition} {values[paramters_to_eval[1]]}"
out = eval(string) # evaluate the condition
if not out:
raise ValueError(f'param "{paramters_to_eval[0]}" must be {condition} than param "{paramters_to_eval[1]}"')
else:
# check which params are missing and raise an error
missing = [name for name in paramters_to_eval if name not in values.keys()]
func_name = func.__name__
raise Exception(f'function "{func_name}" missing parameter: "{", ".join(missing)}"')
return func(*args, **kwargs)
return decorated_function
now to use it we need to declare what are the variables of the that we want to eval and pass the name of parameters and the conditon to eval.
paramters_to_eval=['b', 'c'], condition='<' # b < c
paramters_to_eval=['c', 'b'], condition='<' # c < b
example1
# this force b to be smaller than c always
@check_args(paramters_to_eval=['b', 'c'], condition='<')
def f1(a, b, c):
return a + b + c
outputs
f1(1, 2, 1)
>>>
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In [162], line 1
----> 1 f1(1, 2, 1)
Cell In [147], line 20, in check_args.<locals>.decorated_function(*args, **kwargs)
18 out = eval(string)
19 if not out:
---> 20 raise ValueError(f'param "{paramters_to_eval[0]}" must be {condition} than param "{paramters_to_eval[1]}"')
22 return func(*args, **kwargs)
ValueError: param "b" must be < than param "c"
f1(1, 2)
>>>
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
Cell In [179], line 1
----> 1 f1(1, 2)
Cell In [177], line 26, in check_args.<locals>.decorated_function(*args, **kwargs)
24 missing = [name for name in paramters_to_eval if name not in values.keys()]
25 func_name = func.__name__
---> 26 raise Exception(f'function "{func_name}" missing parameter: "{", ".join(missing)}"')
27 return func(*args, **kwargs)
Exception: function "f1" missing parameter: "c"
f1(1, 2, 4)
>>>7
Example2
#c need to be smaller or equal to a
@check_args(paramters_to_eval=['c', 'a'], condition='<=')
def f2(a, b, c):
return a + b + c
outputs
f2(1, 2, 2)
>>>
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In [169], line 1
----> 1 f2(1, 2, 2)
Cell In [147], line 20, in check_args.<locals>.decorated_function(*args, **kwargs)
18 out = eval(string)
19 if not out:
---> 20 raise ValueError(f'param "{paramters_to_eval[0]}" must be {condition} than param "{paramters_to_eval[1]}"')
22 return func(*args, **kwargs)
ValueError: param "c" must be <= than param "a"
f2(1, 2, 1)
>>> 4
Example3 This decorator works perfectly with classes
class MyClass(object):
@check_args(paramters_to_eval=['a', 'b'], condition='>')
def f(self, a, b):
return a + b
MyClass.f(1,2)
>>>
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In [172], line 6
3 @check_args(paramters_to_eval=['a', 'b'], condition='>')
4 def f(self, a, b):
5 return a + b
----> 6 MyClass.f(1,2)
Cell In [147], line 20, in check_args.<locals>.decorated_function(*args, **kwargs)
18 out = eval(string)
19 if not out:
---> 20 raise ValueError(f'param "{paramters_to_eval[0]}" must be {condition} than param "{paramters_to_eval[1]}"')
22 return func(*args, **kwargs)
ValueError: param "a" must be > than param "b"
Note that if any of two parameters arent pass to fucntion decorated the decorator raise an error indicating which parameter is missing
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.