50

py.test seems to fail when I decorate test functions which has a fixture as an argument.

def deco(func):

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)

    return wrapper


@pytest.fixture
def x():
    return 0

@deco
def test_something(x):
    assert x == 0

In this simple example, I get the following error:

TypeError: test_something() takes exactly 1 argument (0 given).

Is there a way to fix this, preferably without modifying the decorator too much? (Since the decorator is used outside testing code too.)

falsetru
  • 357,413
  • 63
  • 732
  • 636
jck
  • 1,910
  • 4
  • 18
  • 25

3 Answers3

49

It looks like functools.wraps does not do the job well enough, so it breaks py.test's introspection.

Creating the decorator using the decorator package seems to do the trick.

import decorator

def deco(func):
    def wrapper(func, *args, **kwargs):
        return func(*args, **kwargs)
    return decorator.decorator(wrapper, func)
falsetru
  • 357,413
  • 63
  • 732
  • 636
jck
  • 1,910
  • 4
  • 18
  • 25
  • 12
    It could be useful to know that pytest depends on decorator, so you don't need to pull in any new dependency. – zneak Jan 10 '18 at 23:51
  • How shall we handle passing an argument to deco when using this e.g. @deco(3) – user1065000 Apr 29 '20 at 17:15
  • 4
    why are there two uses of `func` in your answer? is that intentional? – joel May 27 '20 at 11:39
  • @joel it seems intentional. Without second `func` decorator doesn't work, though even PyCharm warns about shadowing name from outer scope. – paveldroo Sep 14 '21 at 15:09
  • @zneak Do you recall your env? Because `import decorator` does not work for me without a separate package install on pytest 7.2 (2022) Mac or pytest 2.8.7 (2016) Linux. And pip3 does not list `decorator` as among the requirements for either one. – Noah Dec 13 '22 at 22:45
  • @Noah this question is about a decade old. Today, I'd recommend [wrapt](https://pypi.org/project/wrapt/) over decorator. – jck Dec 25 '22 at 14:32
  • @jck Ha, well, appreciate your responding after all this time. (Can't install anything new in the product I'm working on, but `functools.wraps` ended up working for me.) – Noah Dec 26 '22 at 01:26
7

Fixture feature depends on test function signature.

If you can change wrapper signature as follow, it will works.

def deco(func):
    @functools.wraps(func)
    def wrapper(x):
        return func(x)
    return wrapper

If you can't change it, make another decorator:

def deco(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

def deco_x(func):
    @functools.wraps(func)
    def wrapper(x):
        return func(x)
    return wrapper

And decorate test_somthing with deco_x:

@deco_x
@deco
def test_something(x):
    assert x == 0
falsetru
  • 357,413
  • 63
  • 732
  • 636
0

In the case where you are using a pytest fixture with a decorator while passing data to the decorator, you can do it as follows:

import pytest

@pytest.fixture(scope="session")
def data(request):
    data = list(range(10))
    yield data

def decorator_with_variable_and_fixture(whatever):
    def wrap(test_func):
        def wrapper(data):
            mod = whatever.lower()[::-1]
            test_func(data, mod)
        return wrapper
    return wrap

@decorator_with_variable_and_fixture(whatever='SOMETHING!')
def test_decorator_with_variable_and_fixture(data, whatever):
    for i in data:
        print(whatever[i]*(i+1))
    assert whatever == '!gnihtemos'
    assert data == list(range(10))

Note how data is passed to the wrapper function - this way pytest's fixture approach still works correctly.

This is quite handy for example if you need to pass a PySpark session around, while using a decorator to perform other functionality.

Jurgen Strydom
  • 3,540
  • 1
  • 23
  • 30