2

My goal is to create a trivial unit test decorator, which executes a function and, if it succeeds, do nothing, if it doesn't, print "FAILURE" and all its parameters. I do know about the builtin unittest package. I'm doing this to learn decorators. I'm not taking this any farther than "if actual equals expected, do nothing, else print params".

I found this function which prints out all of a function's parameters:

def dumpArgs(func): '''Decorator to print function call details - parameters names and effective values''' def wrapper(*func_args, **func_kwargs): arg_names = func.__code__.co_varnames[:func.__code__.co_argcount] args = func_args[:len(arg_names)] defaults = func.__defaults__ or () args = args + defaults[len(defaults) - (func.__code__.co_argcount - len(args)):] params = list(zip(arg_names, args)) args = func_args[len(arg_names):] if args: params.append(('args', args)) if func_kwargs: params.append(('kwargs', func_kwargs)) print(func.__name__ + ' (' + ', '.join('%s = %r' % p for p in params) + ' )') return func(*func_args, **func_kwargs) return wrapper @dumpArgs def test(a, b = 4, c = 'blah-blah', *args, **kwargs): pass test(1) test(1, 3) test(1, d = 5) test(1, 2, 3, 4, 5, d = 6, g = 12.9) 

Output:

test (a = 1, b = 4, c = 'blah-blah' ) test (a = 1, b = 3, c = 'blah-blah' ) test (a = 1, b = 4, c = 'blah-blah', kwargs = {'d': 5} ) test (a = 1, b = 2, c = 3, args = (4, 5), kwargs = {'g': 12.9, 'd': 6} ) 

I changed it to this, which prints out the parameters only if the function does not equal 4 (implemented without a decorator param):

def get_all_func_param_name_values(func, *func_args, **func_kwargs): arg_names = func.__code__.co_varnames[:func.__code__.co_argcount] args = func_args[:len(arg_names)] defaults = func.__defaults__ or () args = args + defaults[len(defaults) - (func.__code__.co_argcount - len(args)):] params = list(zip(arg_names, args)) args = func_args[len(arg_names):] if args: params.append(('args', args)) if func_kwargs: params.append(('kwargs', func_kwargs)) return '(' + ', '.join('%s = %r' % p for p in params) + ')' def dumpArgs(func): '''Decorator to print function call details - parameters names and effective values''' def wrapper(*func_args, **func_kwargs): a = func(*func_args, **func_kwargs) if(a != 4): return a print("FAILURE: " + func.__name__ + get_all_func_param_name_values(func, *func_args, **func_kwargs)) return a return wrapper @dumpArgs def getA(a, b = 4, c = 'blah-blah', *args, **kwargs): return a getA(1) getA(1, 3) getA(4, d = 5) getA(1, 2, 3, 4, 5, d = 6, g = 12.9) 

Output:

FAILURE: getA(a = 4, b = 4, c = 'blah-blah', kwargs = {'d': 5}) Out[21]: 1 

(I don't understand why the 1 is printed in the second line.)

I then changed it to pass in the expected value, 4, as decorator parameter. As described in this answer, it requires that the original decorator be a nested function:

def get_all_func_param_name_values(func, *func_args, **func_kwargs): arg_names = func.__code__.co_varnames[:func.__code__.co_argcount] args = func_args[:len(arg_names)] defaults = func.__defaults__ or () args = args + defaults[len(defaults) - (func.__code__.co_argcount - len(args)):] params = list(zip(arg_names, args)) args = func_args[len(arg_names):] if args: params.append(('args', args)) if func_kwargs: params.append(('kwargs', func_kwargs)) return '(' + ', '.join('%s = %r' % p for p in params) + ')' def dumpArgs(expected_value): def dumpArgs2(func): '''Decorator to print function call details - parameters names and effective values''' def wrapper(*func_args, **func_kwargs): a = func(*func_args, **func_kwargs) if(a == expected_value): return a print("FAILURE: " + func.__name__ + get_all_func_param_name_values(func, *func_args, **func_kwargs)) return a return wrapper return dumpArgs2 @dumpArgs(4) def getA(a, b = 4, c = 'blah-blah', *args, **kwargs): return a getA(1) getA(1, 3) getA(4, d = 5) getA(1, 2, 3, 4, 5, d = 6, g = 12.9) 

Output:

FAILURE: getA(a = 1, b = 4, c = 'blah-blah') FAILURE: getA(a = 1, b = 3, c = 'blah-blah') FAILURE: getA(a = 1, b = 2, c = 3, args = (4, 5), kwargs = {'g': 12.9, 'd': 6}) Out[31]: 1 

(Again, that 1...)

I'm not clear on how to change this hard-coded 4 to an expected_value parameter, that is passed through at every function call. All the examples I've seen (like this one) have hard-coded parameters.

I currently experimenting with

assert_expected_func_params(4, getA, 1) assert_expected_func_params(4, getA, 1, 3) assert_expected_func_params(4, getA, 4, d = 5) assert_expected_func_params(4, getA, 1, 2, 3, 4, 5, d = 6, g = 12.9) 

But it's far from working.

How do I implement a decorator parameter that I can pass in to every function call?

16
  • 2
    Remember The Zen of Python: Readability counts. Commented Aug 8, 2014 at 16:26
  • 1
    I don't know if you can decorate with a run-time value, since the decorator is evaluated at the time of the function definition, and then not again after that. Commented Aug 8, 2014 at 16:27
  • if you have a function assert_expected_func_params, why do you need a decorator? Commented Aug 8, 2014 at 16:28
  • 2
    You can't do this with @decorator(arg) syntax for the reason @TheSoundDefense provides, but you can do the manual equivalent, i.e. func = decorator(arg)(func). Commented Aug 8, 2014 at 16:29
  • 1
    The reason why the 1 is appearing is because that's the output of your getA function (which just returns whatever param a is). You'd get the same result regardless or not if you applied the decorator. Commented Aug 8, 2014 at 16:30

1 Answer 1

2

Since a decorator wraps the function, you can intercept the input and output of the function when it is called. In this way, you could look for an _expected keyword, strip it out, call the function, then test the return value of the function against the passed in expected value.

from functools import wraps _empty = object() # sentinel value used to control testing def dump_ne(func): @wraps(func) def decorated(*args, **kwargs): # remove the expected value from the actual call kwargs expected = kwargs.pop('_expected', _empty) # call the function with rest of args and kwargs result = func(*args, **kwargs) # only test when _expected was passed in the kwargs # only print when the result didn't equal expected if expected is not _empty and expected != result: print('FAIL: func={}, args={}, kwargs={}'.format(func.__name__, args, kwargs)) return result return decorated @dump_ne def cool(thing): return thing.upper() print(cool('cat')) # prints 'CAT', test isn't run for thing in ('cat', 'ice', 'cucumber'): print(cool(thing, _expected='CUCUMBER')) # dumps info for first 2 calls (cat, ice) 
Sign up to request clarification or add additional context in comments.

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.