Skip to content

Commit daf3911

Browse files
authored
Merge pull request #4091 from nicoddemus/setup-methods-as-fixtures-3094
Use fixtures to invoke xunit-style fixtures
2 parents 4947eb8 + 0f918b1 commit daf3911

File tree

6 files changed

+239
-68
lines changed

6 files changed

+239
-68
lines changed

changelog/3094.feature.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
`Class xunit-style <https://docs.pytest.org/en/latest/xunit_setup.html>`__ functions and methods
2+
now obey the scope of *autouse* fixtures.
3+
4+
This fixes a number of surprising issues like ``setup_method`` being called before session-scoped
5+
autouse fixtures (see `#517 <https://github.com/pytest-dev/pytest/issues/517>`__ for an example).

doc/en/xunit_setup.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,15 @@ Remarks:
9393

9494
* It is possible for setup/teardown pairs to be invoked multiple times
9595
per testing process.
96+
9697
* teardown functions are not called if the corresponding setup function existed
9798
and failed/was skipped.
9899

100+
* Prior to pytest-4.2, xunit-style functions did not obey the scope rules of fixtures, so
101+
it was possible, for example, for a ``setup_method`` to be called before a
102+
session-scoped autouse fixture.
103+
104+
Now the xunit-style functions are integrated with the fixture mechanism and obey the proper
105+
scope rules of fixtures involved in the call.
106+
99107
.. _`unittest.py module`: http://docs.python.org/library/unittest.html

src/_pytest/python.py

Lines changed: 142 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import os
1010
import sys
1111
import warnings
12+
from functools import partial
1213
from textwrap import dedent
1314

1415
import py
@@ -432,9 +433,66 @@ def _getobj(self):
432433
return self._importtestmodule()
433434

434435
def collect(self):
436+
self._inject_setup_module_fixture()
437+
self._inject_setup_function_fixture()
435438
self.session._fixturemanager.parsefactories(self)
436439
return super(Module, self).collect()
437440

441+
def _inject_setup_module_fixture(self):
442+
"""Injects a hidden autouse, module scoped fixture into the collected module object
443+
that invokes setUpModule/tearDownModule if either or both are available.
444+
445+
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
446+
other fixtures (#517).
447+
"""
448+
setup_module = _get_non_fixture_func(self.obj, "setUpModule")
449+
if setup_module is None:
450+
setup_module = _get_non_fixture_func(self.obj, "setup_module")
451+
452+
teardown_module = _get_non_fixture_func(self.obj, "tearDownModule")
453+
if teardown_module is None:
454+
teardown_module = _get_non_fixture_func(self.obj, "teardown_module")
455+
456+
if setup_module is None and teardown_module is None:
457+
return
458+
459+
@fixtures.fixture(autouse=True, scope="module")
460+
def xunit_setup_module_fixture(request):
461+
if setup_module is not None:
462+
_call_with_optional_argument(setup_module, request.module)
463+
yield
464+
if teardown_module is not None:
465+
_call_with_optional_argument(teardown_module, request.module)
466+
467+
self.obj.__pytest_setup_module = xunit_setup_module_fixture
468+
469+
def _inject_setup_function_fixture(self):
470+
"""Injects a hidden autouse, function scoped fixture into the collected module object
471+
that invokes setup_function/teardown_function if either or both are available.
472+
473+
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
474+
other fixtures (#517).
475+
"""
476+
setup_function = _get_non_fixture_func(self.obj, "setup_function")
477+
teardown_function = _get_non_fixture_func(self.obj, "teardown_function")
478+
if setup_function is None and teardown_function is None:
479+
return
480+
481+
@fixtures.fixture(autouse=True, scope="function")
482+
def xunit_setup_function_fixture(request):
483+
if request.instance is not None:
484+
# in this case we are bound to an instance, so we need to let
485+
# setup_method handle this
486+
yield
487+
return
488+
if setup_function is not None:
489+
_call_with_optional_argument(setup_function, request.function)
490+
yield
491+
if teardown_function is not None:
492+
_call_with_optional_argument(teardown_function, request.function)
493+
494+
self.obj.__pytest_setup_function = xunit_setup_function_fixture
495+
438496
def _importtestmodule(self):
439497
# we assume we are only called once per module
440498
importmode = self.config.getoption("--import-mode")
@@ -485,19 +543,6 @@ def _importtestmodule(self):
485543
self.config.pluginmanager.consider_module(mod)
486544
return mod
487545

488-
def setup(self):
489-
setup_module = _get_xunit_setup_teardown(self.obj, "setUpModule")
490-
if setup_module is None:
491-
setup_module = _get_xunit_setup_teardown(self.obj, "setup_module")
492-
if setup_module is not None:
493-
setup_module()
494-
495-
teardown_module = _get_xunit_setup_teardown(self.obj, "tearDownModule")
496-
if teardown_module is None:
497-
teardown_module = _get_xunit_setup_teardown(self.obj, "teardown_module")
498-
if teardown_module is not None:
499-
self.addfinalizer(teardown_module)
500-
501546

502547
class Package(Module):
503548
def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None):
@@ -510,6 +555,22 @@ def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None):
510555
self._norecursepatterns = session._norecursepatterns
511556
self.fspath = fspath
512557

558+
def setup(self):
559+
# not using fixtures to call setup_module here because autouse fixtures
560+
# from packages are not called automatically (#4085)
561+
setup_module = _get_non_fixture_func(self.obj, "setUpModule")
562+
if setup_module is None:
563+
setup_module = _get_non_fixture_func(self.obj, "setup_module")
564+
if setup_module is not None:
565+
_call_with_optional_argument(setup_module, self.obj)
566+
567+
teardown_module = _get_non_fixture_func(self.obj, "tearDownModule")
568+
if teardown_module is None:
569+
teardown_module = _get_non_fixture_func(self.obj, "teardown_module")
570+
if teardown_module is not None:
571+
func = partial(_call_with_optional_argument, teardown_module, self.obj)
572+
self.addfinalizer(func)
573+
513574
def _recurse(self, dirpath):
514575
if dirpath.basename == "__pycache__":
515576
return False
@@ -596,8 +657,9 @@ def _get_xunit_setup_teardown(holder, attr_name, param_obj=None):
596657
when the callable is called without arguments, defaults to the ``holder`` object.
597658
Return ``None`` if a suitable callable is not found.
598659
"""
660+
# TODO: only needed because of Package!
599661
param_obj = param_obj if param_obj is not None else holder
600-
result = _get_xunit_func(holder, attr_name)
662+
result = _get_non_fixture_func(holder, attr_name)
601663
if result is not None:
602664
arg_count = result.__code__.co_argcount
603665
if inspect.ismethod(result):
@@ -608,7 +670,19 @@ def _get_xunit_setup_teardown(holder, attr_name, param_obj=None):
608670
return result
609671

610672

611-
def _get_xunit_func(obj, name):
673+
def _call_with_optional_argument(func, arg):
674+
"""Call the given function with the given argument if func accepts one argument, otherwise
675+
calls func without arguments"""
676+
arg_count = func.__code__.co_argcount
677+
if inspect.ismethod(func):
678+
arg_count -= 1
679+
if arg_count:
680+
func(arg)
681+
else:
682+
func()
683+
684+
685+
def _get_non_fixture_func(obj, name):
612686
"""Return the attribute from the given object to be used as a setup/teardown
613687
xunit-style function, but only if not marked as a fixture to
614688
avoid calling it twice.
@@ -640,18 +714,60 @@ def collect(self):
640714
)
641715
)
642716
return []
717+
718+
self._inject_setup_class_fixture()
719+
self._inject_setup_method_fixture()
720+
643721
return [Instance(name="()", parent=self)]
644722

645-
def setup(self):
646-
setup_class = _get_xunit_func(self.obj, "setup_class")
647-
if setup_class is not None:
648-
setup_class = getimfunc(setup_class)
649-
setup_class(self.obj)
723+
def _inject_setup_class_fixture(self):
724+
"""Injects a hidden autouse, class scoped fixture into the collected class object
725+
that invokes setup_class/teardown_class if either or both are available.
650726
651-
fin_class = getattr(self.obj, "teardown_class", None)
652-
if fin_class is not None:
653-
fin_class = getimfunc(fin_class)
654-
self.addfinalizer(lambda: fin_class(self.obj))
727+
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
728+
other fixtures (#517).
729+
"""
730+
setup_class = _get_non_fixture_func(self.obj, "setup_class")
731+
teardown_class = getattr(self.obj, "teardown_class", None)
732+
if setup_class is None and teardown_class is None:
733+
return
734+
735+
@fixtures.fixture(autouse=True, scope="class")
736+
def xunit_setup_class_fixture(cls):
737+
if setup_class is not None:
738+
func = getimfunc(setup_class)
739+
_call_with_optional_argument(func, self.obj)
740+
yield
741+
if teardown_class is not None:
742+
func = getimfunc(teardown_class)
743+
_call_with_optional_argument(func, self.obj)
744+
745+
self.obj.__pytest_setup_class = xunit_setup_class_fixture
746+
747+
def _inject_setup_method_fixture(self):
748+
"""Injects a hidden autouse, function scoped fixture into the collected class object
749+
that invokes setup_method/teardown_method if either or both are available.
750+
751+
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
752+
other fixtures (#517).
753+
"""
754+
setup_method = _get_non_fixture_func(self.obj, "setup_method")
755+
teardown_method = getattr(self.obj, "teardown_method", None)
756+
if setup_method is None and teardown_method is None:
757+
return
758+
759+
@fixtures.fixture(autouse=True, scope="function")
760+
def xunit_setup_method_fixture(self, request):
761+
method = request.function
762+
if setup_method is not None:
763+
func = getattr(self, "setup_method")
764+
_call_with_optional_argument(func, method)
765+
yield
766+
if teardown_method is not None:
767+
func = getattr(self, "teardown_method")
768+
_call_with_optional_argument(func, method)
769+
770+
self.obj.__pytest_setup_method = xunit_setup_method_fixture
655771

656772

657773
class Instance(PyCollector):
@@ -678,29 +794,9 @@ class FunctionMixin(PyobjMixin):
678794

679795
def setup(self):
680796
""" perform setup for this test function. """
681-
if hasattr(self, "_preservedparent"):
682-
obj = self._preservedparent
683-
elif isinstance(self.parent, Instance):
684-
obj = self.parent.newinstance()
797+
if isinstance(self.parent, Instance):
798+
self.parent.newinstance()
685799
self.obj = self._getobj()
686-
else:
687-
obj = self.parent.obj
688-
if inspect.ismethod(self.obj):
689-
setup_name = "setup_method"
690-
teardown_name = "teardown_method"
691-
else:
692-
setup_name = "setup_function"
693-
teardown_name = "teardown_function"
694-
setup_func_or_method = _get_xunit_setup_teardown(
695-
obj, setup_name, param_obj=self.obj
696-
)
697-
if setup_func_or_method is not None:
698-
setup_func_or_method()
699-
teardown_func_or_method = _get_xunit_setup_teardown(
700-
obj, teardown_name, param_obj=self.obj
701-
)
702-
if teardown_func_or_method is not None:
703-
self.addfinalizer(teardown_func_or_method)
704800

705801
def _prunetraceback(self, excinfo):
706802
if hasattr(self, "_obj") and not self.config.option.fulltrace:

src/_pytest/unittest.py

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import traceback
88

99
import _pytest._code
10+
import pytest
1011
from _pytest.compat import getimfunc
1112
from _pytest.config import hookimpl
1213
from _pytest.outcomes import fail
@@ -32,24 +33,18 @@ class UnitTestCase(Class):
3233
# to declare that our children do not support funcargs
3334
nofuncargs = True
3435

35-
def setup(self):
36-
cls = self.obj
37-
if getattr(cls, "__unittest_skip__", False):
38-
return # skipped
39-
setup = getattr(cls, "setUpClass", None)
40-
if setup is not None:
41-
setup()
42-
teardown = getattr(cls, "tearDownClass", None)
43-
if teardown is not None:
44-
self.addfinalizer(teardown)
45-
super(UnitTestCase, self).setup()
46-
4736
def collect(self):
4837
from unittest import TestLoader
4938

5039
cls = self.obj
5140
if not getattr(cls, "__test__", True):
5241
return
42+
43+
skipped = getattr(cls, "__unittest_skip__", False)
44+
if not skipped:
45+
self._inject_setup_teardown_fixtures(cls)
46+
self._inject_setup_class_fixture()
47+
5348
self.session._fixturemanager.parsefactories(self, unittest=True)
5449
loader = TestLoader()
5550
foundsomething = False
@@ -68,6 +63,44 @@ def collect(self):
6863
if ut is None or runtest != ut.TestCase.runTest:
6964
yield TestCaseFunction("runTest", parent=self)
7065

66+
def _inject_setup_teardown_fixtures(self, cls):
67+
"""Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding
68+
teardown functions (#517)"""
69+
class_fixture = _make_xunit_fixture(
70+
cls, "setUpClass", "tearDownClass", scope="class", pass_self=False
71+
)
72+
if class_fixture:
73+
cls.__pytest_class_setup = class_fixture
74+
75+
method_fixture = _make_xunit_fixture(
76+
cls, "setup_method", "teardown_method", scope="function", pass_self=True
77+
)
78+
if method_fixture:
79+
cls.__pytest_method_setup = method_fixture
80+
81+
82+
def _make_xunit_fixture(obj, setup_name, teardown_name, scope, pass_self):
83+
setup = getattr(obj, setup_name, None)
84+
teardown = getattr(obj, teardown_name, None)
85+
if setup is None and teardown is None:
86+
return None
87+
88+
@pytest.fixture(scope=scope, autouse=True)
89+
def fixture(self, request):
90+
if setup is not None:
91+
if pass_self:
92+
setup(self, request.function)
93+
else:
94+
setup()
95+
yield
96+
if teardown is not None:
97+
if pass_self:
98+
teardown(self, request.function)
99+
else:
100+
teardown()
101+
102+
return fixture
103+
71104

72105
class TestCaseFunction(Function):
73106
nofuncargs = True
@@ -77,9 +110,6 @@ class TestCaseFunction(Function):
77110
def setup(self):
78111
self._testcase = self.parent.obj(self.name)
79112
self._fix_unittest_skip_decorator()
80-
self._obj = getattr(self._testcase, self.name)
81-
if hasattr(self._testcase, "setup_method"):
82-
self._testcase.setup_method(self._obj)
83113
if hasattr(self, "_request"):
84114
self._request._fillfixtures()
85115

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

99129
def teardown(self):
100-
if hasattr(self._testcase, "teardown_method"):
101-
self._testcase.teardown_method(self._obj)
102-
# Allow garbage collection on TestCase instance attributes.
103130
self._testcase = None
104-
self._obj = None
105131

106132
def startTest(self, testcase):
107133
pass

testing/python/collect.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,6 @@ def prop(self):
240240
assert result.ret == EXIT_NOTESTSCOLLECTED
241241

242242

243-
@pytest.mark.filterwarnings(
244-
"ignore:usage of Generator.Function is deprecated, please use pytest.Function instead"
245-
)
246243
class TestFunction(object):
247244
def test_getmodulecollector(self, testdir):
248245
item = testdir.getitem("def test_func(): pass")

0 commit comments

Comments
 (0)