3

I'm writing several functions which accept an argument called policy, which is allowed only to have certain values (namely, 'allow' or 'deny'). If it doesn't, I would like a ValueError to be raised.

For brevity, I would like to define a decorator for this. So far, I have 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 would like to allow for policy to appear at any position.

To be specific, here are some (dummy) functions called make_decision and make_informed_decision which accept an argument policy at different positions, and some test cases to 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 the tests pass except the third one, because the first positional argument 'allow' is interpreted as the policy rather than as the data as it should be.

How can I adapt the validate_policy decorator such that all the tests pass?

3
  • 1
    Make policy a required keyword argument. Explicit is better than implicit. Commented Mar 14, 2017 at 14:19
  • Rick Teachey, that seems like a good idea, except my code base is in Python 2, so PEP 3102 has not been implemented. I assume that is what you meant by making policy a required keyword argument? Commented Mar 14, 2017 at 14:40
  • Bummer......... Commented Mar 15, 2017 at 1:13

2 Answers 2

2

You can use the inspect module's Signature.bind function:

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
Sign up to request clarification or add additional context in comments.

Comments

1

Here is another solution using inspect.getcallargs:

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(*args, **kwargs):
        call_args = inspect.getcallargs(function, *args, **kwargs)
        if 'policy' in call_args:
            if call_args['policy'] not in ['allow', 'deny']:
                raise ValueError("The policy must be either 'allow' or 'deny'.")
        return function(*args, **kwargs)
    return wrapped_function

It makes all the tests pass.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.