Python – In Python, how do you define a function wrapper to validate parameters with specific names?

In Python, how do you define a function wrapper to validate parameters with specific names?… here is a solution to the problem.

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

Related Problems and Solutions