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?

Community
  • 1
  • 1
aliteralmind
  • 19,847
  • 17
  • 77
  • 108
  • 2
    Remember The Zen of Python: Readability counts. – Mauro Baraldi Aug 08 '14 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. – TheSoundDefense Aug 08 '14 at 16:27
  • if you have a function `assert_expected_func_params`, why do you need a decorator? – Daniel Aug 08 '14 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)`. – jonrsharpe Aug 08 '14 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. – Michael0x2a Aug 08 '14 at 16:30
  • @Daniel I don't have that `assert_expected_func_params` function. I was just dreaming of what could make this work. Seems I'm trying to do something that's not possible. – aliteralmind Aug 08 '14 at 16:32
  • @jonsharpe First time I'm seeing that syntax. Will give it a try. – aliteralmind Aug 08 '14 at 16:34
  • Also, you should be aware that `dumpArgs` doesn't work correctly in all circumstances - testing it on a function with spec `(x, y, z=13, a=12)` and calling with `(2, 4, a=6)` I got `(x = 2, y = 4, z = 13, a = 12, kwargs = {'a': 6} )` from `dumpArgs` (actually, `a == 6` and there are no kwargs). – jonrsharpe Aug 08 '14 at 16:43
  • @aliteralmind: Note that if you use `func = decorator(arg)(func)` you will have to save and (re)decorate the original function every time you want a different value to be the expected value. – martineau Aug 08 '14 at 16:48
  • @aliteralmind: Also note that the `assert_expected_func_params()` you're now dreaming of isn't a decorator. – martineau Aug 08 '14 at 16:51
  • @martineau Not sure what you mean by having to re-decorate. – aliteralmind Aug 08 '14 at 16:57
  • @jonrsharpe That was an [already existing answer](http://stackoverflow.com/a/25206079/2736496), so I assumed it was solid. I'll try and fix it, or change it over to use [`inspect.getargvalues`](https://docs.python.org/3.4/library/inspect.html#inspect.getargvalues), as in [this question](http://stackoverflow.com/questions/2912615/how-to-iterate-over-function-arguments). – aliteralmind Aug 08 '14 at 17:01
  • @aliteralmind: I only meant you'd need to save the value of the original _completely undecorated_ function, and not be able to redecorate the results from a prior `decorator(arg)(func)` call. – martineau Aug 08 '14 at 17:05
  • @martineau Ah. So you have to keep referring the original function, otherwise the decorator-parameter from the previous call will be used. – aliteralmind Aug 08 '14 at 17:08
  • @jonrsharpe I mean to say [this *answer*](http://stackoverflow.com/a/2913020/2736496) in that question. – aliteralmind Aug 08 '14 at 17:11
  • @aliteralmind: Well, sort of, actually previous decorations would become nested which would mean earlier expected values would accumulate I believe. – martineau Aug 08 '14 at 17:13

1 Answers1

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)
davidism
  • 121,510
  • 29
  • 395
  • 339