Skip to content

Use fixtures to invoke xunit-style fixtures #4091

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/3094.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
`Class xunit-style <https://docs.pytest.org/en/latest/xunit_setup.html>`__ functions and methods
now obey the scope of *autouse* fixtures.

This fixes a number of surprising issues like ``setup_method`` being called before session-scoped
autouse fixtures (see `#517 <https://github.com/pytest-dev/pytest/issues/517>`__ for an example).
8 changes: 8 additions & 0 deletions doc/en/xunit_setup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,15 @@ Remarks:

* It is possible for setup/teardown pairs to be invoked multiple times
per testing process.

* teardown functions are not called if the corresponding setup function existed
and failed/was skipped.

* Prior to pytest-4.2, xunit-style functions did not obey the scope rules of fixtures, so
it was possible, for example, for a ``setup_method`` to be called before a
session-scoped autouse fixture.

Now the xunit-style functions are integrated with the fixture mechanism and obey the proper
scope rules of fixtures involved in the call.

.. _`unittest.py module`: http://docs.python.org/library/unittest.html
188 changes: 142 additions & 46 deletions src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import os
import sys
import warnings
from functools import partial
from textwrap import dedent

import py
Expand Down Expand Up @@ -435,9 +436,66 @@ def _getobj(self):
return self._importtestmodule()

def collect(self):
self._inject_setup_module_fixture()
self._inject_setup_function_fixture()
self.session._fixturemanager.parsefactories(self)
return super(Module, self).collect()

def _inject_setup_module_fixture(self):
"""Injects a hidden autouse, module scoped fixture into the collected module object
that invokes setUpModule/tearDownModule if either or both are available.

Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
other fixtures (#517).
"""
setup_module = _get_non_fixture_func(self.obj, "setUpModule")
if setup_module is None:
setup_module = _get_non_fixture_func(self.obj, "setup_module")

teardown_module = _get_non_fixture_func(self.obj, "tearDownModule")
if teardown_module is None:
teardown_module = _get_non_fixture_func(self.obj, "teardown_module")

if setup_module is None and teardown_module is None:
return

@fixtures.fixture(autouse=True, scope="module")
def xunit_setup_module_fixture(request):
if setup_module is not None:
_call_with_optional_argument(setup_module, request.module)
yield
if teardown_module is not None:
_call_with_optional_argument(teardown_module, request.module)

self.obj.__pytest_setup_module = xunit_setup_module_fixture

def _inject_setup_function_fixture(self):
"""Injects a hidden autouse, function scoped fixture into the collected module object
that invokes setup_function/teardown_function if either or both are available.

Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
other fixtures (#517).
"""
setup_function = _get_non_fixture_func(self.obj, "setup_function")
teardown_function = _get_non_fixture_func(self.obj, "teardown_function")
if setup_function is None and teardown_function is None:
return

@fixtures.fixture(autouse=True, scope="function")
def xunit_setup_function_fixture(request):
if request.instance is not None:
# in this case we are bound to an instance, so we need to let
# setup_method handle this
yield
return
if setup_function is not None:
_call_with_optional_argument(setup_function, request.function)
yield
if teardown_function is not None:
_call_with_optional_argument(teardown_function, request.function)

self.obj.__pytest_setup_function = xunit_setup_function_fixture

def _importtestmodule(self):
# we assume we are only called once per module
importmode = self.config.getoption("--import-mode")
Expand Down Expand Up @@ -488,19 +546,6 @@ def _importtestmodule(self):
self.config.pluginmanager.consider_module(mod)
return mod

def setup(self):
setup_module = _get_xunit_setup_teardown(self.obj, "setUpModule")
if setup_module is None:
setup_module = _get_xunit_setup_teardown(self.obj, "setup_module")
if setup_module is not None:
setup_module()

teardown_module = _get_xunit_setup_teardown(self.obj, "tearDownModule")
if teardown_module is None:
teardown_module = _get_xunit_setup_teardown(self.obj, "teardown_module")
if teardown_module is not None:
self.addfinalizer(teardown_module)


class Package(Module):
def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None):
Expand All @@ -513,6 +558,22 @@ def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None):
self._norecursepatterns = session._norecursepatterns
self.fspath = fspath

def setup(self):
# not using fixtures to call setup_module here because autouse fixtures
# from packages are not called automatically (#4085)
setup_module = _get_non_fixture_func(self.obj, "setUpModule")
if setup_module is None:
setup_module = _get_non_fixture_func(self.obj, "setup_module")
if setup_module is not None:
_call_with_optional_argument(setup_module, self.obj)

teardown_module = _get_non_fixture_func(self.obj, "tearDownModule")
if teardown_module is None:
teardown_module = _get_non_fixture_func(self.obj, "teardown_module")
if teardown_module is not None:
func = partial(_call_with_optional_argument, teardown_module, self.obj)
self.addfinalizer(func)

def _recurse(self, dirpath):
if dirpath.basename == "__pycache__":
return False
Expand Down Expand Up @@ -599,8 +660,9 @@ def _get_xunit_setup_teardown(holder, attr_name, param_obj=None):
when the callable is called without arguments, defaults to the ``holder`` object.
Return ``None`` if a suitable callable is not found.
"""
# TODO: only needed because of Package!
param_obj = param_obj if param_obj is not None else holder
result = _get_xunit_func(holder, attr_name)
result = _get_non_fixture_func(holder, attr_name)
if result is not None:
arg_count = result.__code__.co_argcount
if inspect.ismethod(result):
Expand All @@ -611,7 +673,19 @@ def _get_xunit_setup_teardown(holder, attr_name, param_obj=None):
return result


def _get_xunit_func(obj, name):
def _call_with_optional_argument(func, arg):
"""Call the given function with the given argument if func accepts one argument, otherwise
calls func without arguments"""
arg_count = func.__code__.co_argcount
if inspect.ismethod(func):
arg_count -= 1
if arg_count:
func(arg)
else:
func()


def _get_non_fixture_func(obj, name):
"""Return the attribute from the given object to be used as a setup/teardown
xunit-style function, but only if not marked as a fixture to
avoid calling it twice.
Expand Down Expand Up @@ -643,18 +717,60 @@ def collect(self):
)
)
return []

self._inject_setup_class_fixture()
self._inject_setup_method_fixture()

return [Instance(name="()", parent=self)]

def setup(self):
setup_class = _get_xunit_func(self.obj, "setup_class")
if setup_class is not None:
setup_class = getimfunc(setup_class)
setup_class(self.obj)
def _inject_setup_class_fixture(self):
"""Injects a hidden autouse, class scoped fixture into the collected class object
that invokes setup_class/teardown_class if either or both are available.

fin_class = getattr(self.obj, "teardown_class", None)
if fin_class is not None:
fin_class = getimfunc(fin_class)
self.addfinalizer(lambda: fin_class(self.obj))
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
other fixtures (#517).
"""
setup_class = _get_non_fixture_func(self.obj, "setup_class")
teardown_class = getattr(self.obj, "teardown_class", None)
if setup_class is None and teardown_class is None:
return

@fixtures.fixture(autouse=True, scope="class")
def xunit_setup_class_fixture(cls):
if setup_class is not None:
func = getimfunc(setup_class)
_call_with_optional_argument(func, self.obj)
yield
if teardown_class is not None:
func = getimfunc(teardown_class)
_call_with_optional_argument(func, self.obj)

self.obj.__pytest_setup_class = xunit_setup_class_fixture

def _inject_setup_method_fixture(self):
"""Injects a hidden autouse, function scoped fixture into the collected class object
that invokes setup_method/teardown_method if either or both are available.

Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
other fixtures (#517).
"""
setup_method = _get_non_fixture_func(self.obj, "setup_method")
teardown_method = getattr(self.obj, "teardown_method", None)
if setup_method is None and teardown_method is None:
return

@fixtures.fixture(autouse=True, scope="function")
def xunit_setup_method_fixture(self, request):
method = request.function
if setup_method is not None:
func = getattr(self, "setup_method")
_call_with_optional_argument(func, method)
yield
if teardown_method is not None:
func = getattr(self, "teardown_method")
_call_with_optional_argument(func, method)

self.obj.__pytest_setup_method = xunit_setup_method_fixture


class Instance(PyCollector):
Expand All @@ -681,29 +797,9 @@ class FunctionMixin(PyobjMixin):

def setup(self):
""" perform setup for this test function. """
if hasattr(self, "_preservedparent"):
obj = self._preservedparent
elif isinstance(self.parent, Instance):
obj = self.parent.newinstance()
if isinstance(self.parent, Instance):
self.parent.newinstance()
self.obj = self._getobj()
else:
obj = self.parent.obj
if inspect.ismethod(self.obj):
setup_name = "setup_method"
teardown_name = "teardown_method"
else:
setup_name = "setup_function"
teardown_name = "teardown_function"
setup_func_or_method = _get_xunit_setup_teardown(
obj, setup_name, param_obj=self.obj
)
if setup_func_or_method is not None:
setup_func_or_method()
teardown_func_or_method = _get_xunit_setup_teardown(
obj, teardown_name, param_obj=self.obj
)
if teardown_func_or_method is not None:
self.addfinalizer(teardown_func_or_method)

def _prunetraceback(self, excinfo):
if hasattr(self, "_obj") and not self.config.option.fulltrace:
Expand Down
64 changes: 45 additions & 19 deletions src/_pytest/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import traceback

import _pytest._code
import pytest
from _pytest.compat import getimfunc
from _pytest.config import hookimpl
from _pytest.outcomes import fail
Expand All @@ -32,24 +33,18 @@ class UnitTestCase(Class):
# to declare that our children do not support funcargs
nofuncargs = True

def setup(self):
cls = self.obj
if getattr(cls, "__unittest_skip__", False):
return # skipped
setup = getattr(cls, "setUpClass", None)
if setup is not None:
setup()
teardown = getattr(cls, "tearDownClass", None)
if teardown is not None:
self.addfinalizer(teardown)
super(UnitTestCase, self).setup()

def collect(self):
from unittest import TestLoader

cls = self.obj
if not getattr(cls, "__test__", True):
return

skipped = getattr(cls, "__unittest_skip__", False)
if not skipped:
self._inject_setup_teardown_fixtures(cls)
self._inject_setup_class_fixture()

self.session._fixturemanager.parsefactories(self, unittest=True)
loader = TestLoader()
foundsomething = False
Expand All @@ -68,6 +63,44 @@ def collect(self):
if ut is None or runtest != ut.TestCase.runTest:
yield TestCaseFunction("runTest", parent=self)

def _inject_setup_teardown_fixtures(self, cls):
"""Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding
teardown functions (#517)"""
class_fixture = _make_xunit_fixture(
cls, "setUpClass", "tearDownClass", scope="class", pass_self=False
)
if class_fixture:
cls.__pytest_class_setup = class_fixture

method_fixture = _make_xunit_fixture(
cls, "setup_method", "teardown_method", scope="function", pass_self=True
)
if method_fixture:
cls.__pytest_method_setup = method_fixture


def _make_xunit_fixture(obj, setup_name, teardown_name, scope, pass_self):
setup = getattr(obj, setup_name, None)
teardown = getattr(obj, teardown_name, None)
if setup is None and teardown is None:
return None

@pytest.fixture(scope=scope, autouse=True)
def fixture(self, request):
if setup is not None:
if pass_self:
setup(self, request.function)
else:
setup()
yield
if teardown is not None:
if pass_self:
teardown(self, request.function)
else:
teardown()

return fixture


class TestCaseFunction(Function):
nofuncargs = True
Expand All @@ -77,9 +110,6 @@ class TestCaseFunction(Function):
def setup(self):
self._testcase = self.parent.obj(self.name)
self._fix_unittest_skip_decorator()
self._obj = getattr(self._testcase, self.name)
if hasattr(self._testcase, "setup_method"):
self._testcase.setup_method(self._obj)
if hasattr(self, "_request"):
self._request._fillfixtures()

Expand All @@ -97,11 +127,7 @@ def _fix_unittest_skip_decorator(self):
setattr(self._testcase, "__name__", self.name)

def teardown(self):
if hasattr(self._testcase, "teardown_method"):
self._testcase.teardown_method(self._obj)
# Allow garbage collection on TestCase instance attributes.
self._testcase = None
self._obj = None

def startTest(self, testcase):
pass
Expand Down
3 changes: 0 additions & 3 deletions testing/python/collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,6 @@ def prop(self):
assert result.ret == EXIT_NOTESTSCOLLECTED


@pytest.mark.filterwarnings(
"ignore:usage of Generator.Function is deprecated, please use pytest.Function instead"
)
class TestFunction(object):
def test_getmodulecollector(self, testdir):
item = testdir.getitem("def test_func(): pass")
Expand Down
Loading