BlueTrin
BlueTrin

Reputation: 10113

Problem when calling decorator with arguments

I am trying to test the decorator test_params below for an unittest, but when I try to call it in the function test_testparams, I get an error:

TypeError: wrapper() takes exactly 1 argument (0 given)

Is it because it is expecting self ?

import unittest
import functools


def test_params(kwargs_lst):
    '''allows to parametrise tests easily using a decorator'''

    def decorator(fn):
        @functools.wraps(fn)
        def wrapper(self):
            for kwargs in kwargs_lst:
                fn(self, **kwargs)

        return wrapper

    return decorator


TEST_PARAMS_ARGS = [
    {'arg1': 1, 'arg2': None, 'arg3': "test"},
]

RECORDER = []


class TestParamsClass(object):
    def record_args(self, arg1, arg2, arg3):
        RECORDER.append({'arg1': arg1, 'arg2': arg2, 'arg3': arg3})


class DecoratorTest(unittest.TestCase):
    def setUp(self):
        pass

    def test_testparams(self):
        obj = TestParamsClass()

        decorated_fn = test_params(TEST_PARAMS_ARGS)(obj.record_args)
        decorated_fn()
        print("RECORDED:{}".format(RECORDER))

For full disclosure, I have already tried passing the self argument:

decorated_fn(obj)

but this will give this error:

TypeError: record_args() got multiple values for keyword argument 'arg1'

I tried as well using:

from functools import partial
decorated_fn = test_params(TEST_PARAMS_ARGS)(partial(obj.record_args, self=obj))

This gives me the error:

AttributeError: 'functools.partial' object has no attribute '__module__'

The reason there is a self argument is that it is normally used in test functions to pass arguments and the test function is inside an unittest.TestCase.

Upvotes: 0

Views: 92

Answers (1)

bruno desthuilliers
bruno desthuilliers

Reputation: 77942

This has to do with what Python methods really are.

Normally you would apply the decorator to the function in the class declaration, ie:

class TestParamsClass(object):
    @test_params(TEST_PARAMS_ARGS)
    def record_args(self, arg1, arg2, arg3):
        RECORDER.append({'arg1': arg1, 'arg2': arg2, 'arg3': arg3})

in which case test_params (or more exactly the inner decorator function) will receive the record_args function as argument. But here:

decorated_fn = test_params(TEST_PARAMS_ARGS)(obj.record_args)

what you're passing is a Method object (cf the link above), which __call__ method will already inject obj as the self argument when calling record_args() function.

If your test_param decorator is supposed to be used exclusively on methods (or, more exactly, on functions that will only be used as methods), you can change you test to decorate the method's function instead:

decorated_fn = test_params(TEST_PARAMS_ARGS)(obj.record_args.__func__)

and of course manually pass obj as argument:

decorated_fn(obj)

Else if you want to be able to use this decorator on both functions and methods, you can change the inner wrapper function signature from self to *args (and pass it along to fn so it can accomodate both situations.

Upvotes: 1

Related Questions