In Python, how do you define a function wrapper to validate parameters with specific names?
I’m writing several functions that accept arguments called policy
that are only allowed to have certain values (i.e. ‘allow
‘ or 'deny'
).
For brevity, I’d like to define a decorator for this. So far I’ve come up with the following:
def validate_policy(function):
'''Wrapper which ensures that if the function accepts a 'policy' argument, that argument is either 'allow' or 'deny'.'''
def wrapped_function(policy, *args, **kwargs):
if policy not in ['allow', 'deny']:
raise ValueError("The policy must be either 'allow' or 'deny'.")
return function(policy, *args, **kwargs)
return wrapped_function
The problem is that this only works if policy
is the first positional argument of the function. However, I want to allow policy
to appear anywhere.
Specifically, here are some (virtual) functions called make_decision
and make_informed_decision
that accept the parameter policy in different places, and some test cases that go with them:
import pytest
@validate_policy
def make_decision(policy): # The 'policy' might be the first positional argument
if policy == 'allow':
print "Allowed."
elif policy == 'deny':
print "Denied."
@validate_policy
def make_informed_decision(data, policy): # It also might be the second one
if policy == 'allow':
print "Based on the data {data} it is allowed.". format(data=data)
elif policy == 'deny':
print "Based on the data {data} it is denied.". format(data=data)
'''Tests'''
def test_make_decision_with_invalid_policy_as_positional_argument():
with pytest.raises(ValueError):
make_decision('foobar')
def test_make_decision_with_invalid_policy_as_keyword_argument():
with pytest.raises(ValueError):
make_decision(policy='foobar')
def test_make_informed_decision_with_invalid_policy_as_positional_argument():
with pytest.raises(ValueError):
make_informed_decision("allow", "foobar")
def test_make_informed_decision_with_invalid_policy_as_keyword_argument():
with pytest.raises(ValueError):
make_informed_decision(data="allow", policy="foobar")
if __name__ == "__main__":
pytest.main([__file__])
Currently all tests pass, except for the third, because the first positional parameter ‘allow'
is interpreted as policy
instead of data
should be like this.
How do I adjust the validate_policy
decorator so that all tests pass?
Solution
You can use the of the inspect
module Signature.bin
feature:
import inspect
def validate_policy(function):
'''Wrapper which ensures that if the function accepts a 'policy' argument, that argument is either 'allow' or 'deny'.'''
signature= inspect.signature(function)
def wrapped_function(*args, **kwargs):
bound_args= signature.bind(*args, **kwargs)
bound_args.apply_defaults()
if bound_args.arguments.get('policy') not in ['allow', 'deny']:
raise ValueError("The policy must be either 'allow' or 'deny'.")
return function(*args, **kwargs)
return wrapped_function