Skip to content

Commit 89f0b5b

Browse files
committed
nose: fix class- and module-level fixture behavior
Fixes pytest-dev#9272. Fixing the issue directly in the plugin is somewhat hard, so do it in core. Since the plugin is going to be deprecated, I figure it's OK to cheat a bit.
1 parent 842814c commit 89f0b5b

File tree

4 files changed

+45
-25
lines changed

4 files changed

+45
-25
lines changed

changelog/9272.bugfix.rst

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The nose compatibility module-level fixtures `setup()` and `teardown()` are now only called once per module, instead of for each test function.
2+
They are now called even if object-level `setup`/`teardown` is defined.

src/_pytest/nose.py

+6-11
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,13 @@ def pytest_runtest_setup(item: Item) -> None:
1818
# see https://github.com/python/mypy/issues/2608
1919
func = item
2020

21-
if not call_optional(func.obj, "setup"):
22-
# Call module level setup if there is no object level one.
23-
assert func.parent is not None
24-
call_optional(func.parent.obj, "setup") # type: ignore[attr-defined]
21+
call_optional(func.obj, "setup")
22+
func.addfinalizer(lambda: call_optional(func.obj, "teardown"))
2523

26-
def teardown_nose() -> None:
27-
if not call_optional(func.obj, "teardown"):
28-
assert func.parent is not None
29-
call_optional(func.parent.obj, "teardown") # type: ignore[attr-defined]
30-
31-
# XXX This implies we only call teardown when setup worked.
32-
func.addfinalizer(teardown_nose)
24+
# NOTE: Module- and class-level fixtures are handled in python.py
25+
# with `pluginmanager.has_plugin("nose")` checks.
26+
# It would have been nicer to implement them outside of core, but
27+
# it's not straightforward.
3328

3429

3530
def call_optional(obj: object, name: str) -> bool:

src/_pytest/python.py

+21-6
Original file line numberDiff line numberDiff line change
@@ -514,12 +514,17 @@ def _inject_setup_module_fixture(self) -> None:
514514
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
515515
other fixtures (#517).
516516
"""
517+
has_nose = self.config.pluginmanager.has_plugin("nose")
517518
setup_module = _get_first_non_fixture_func(
518519
self.obj, ("setUpModule", "setup_module")
519520
)
521+
if setup_module is None and has_nose:
522+
setup_module = _get_first_non_fixture_func(self.obj, ("setup",))
520523
teardown_module = _get_first_non_fixture_func(
521524
self.obj, ("tearDownModule", "teardown_module")
522525
)
526+
if teardown_module is None and has_nose:
527+
teardown_module = _get_first_non_fixture_func(self.obj, ("teardown",))
523528

524529
if setup_module is None and teardown_module is None:
525530
return
@@ -750,13 +755,14 @@ def _call_with_optional_argument(func, arg) -> None:
750755
func()
751756

752757

753-
def _get_first_non_fixture_func(obj: object, names: Iterable[str]):
758+
def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[object]:
754759
"""Return the attribute from the given object to be used as a setup/teardown
755760
xunit-style function, but only if not marked as a fixture to avoid calling it twice."""
756761
for name in names:
757-
meth = getattr(obj, name, None)
762+
meth: Optional[object] = getattr(obj, name, None)
758763
if meth is not None and fixtures.getfixturemarker(meth) is None:
759764
return meth
765+
return None
760766

761767

762768
class Class(PyCollector):
@@ -832,8 +838,17 @@ def _inject_setup_method_fixture(self) -> None:
832838
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
833839
other fixtures (#517).
834840
"""
835-
setup_method = _get_first_non_fixture_func(self.obj, ("setup_method",))
836-
teardown_method = getattr(self.obj, "teardown_method", None)
841+
has_nose = self.config.pluginmanager.has_plugin("nose")
842+
setup_name = "setup_method"
843+
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
844+
if setup_method is None and has_nose:
845+
setup_name = "setup"
846+
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
847+
teardown_name = "teardown_method"
848+
teardown_method = getattr(self.obj, teardown_name, None)
849+
if teardown_method is None and has_nose:
850+
teardown_name = "teardown"
851+
teardown_method = getattr(self.obj, teardown_name, None)
837852
if setup_method is None and teardown_method is None:
838853
return
839854

@@ -846,11 +861,11 @@ def _inject_setup_method_fixture(self) -> None:
846861
def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
847862
method = request.function
848863
if setup_method is not None:
849-
func = getattr(self, "setup_method")
864+
func = getattr(self, setup_name)
850865
_call_with_optional_argument(func, method)
851866
yield
852867
if teardown_method is not None:
853-
func = getattr(self, "teardown_method")
868+
func = getattr(self, teardown_name)
854869
_call_with_optional_argument(func, method)
855870

856871
self.obj.__pytest_setup_method = xunit_setup_method_fixture

testing/test_nose.py

+16-8
Original file line numberDiff line numberDiff line change
@@ -165,28 +165,36 @@ def test_module_level_setup(pytester: Pytester) -> None:
165165
items = {}
166166
167167
def setup():
168-
items[1]=1
168+
items.setdefault("setup", []).append("up")
169169
170170
def teardown():
171-
del items[1]
171+
items.setdefault("setup", []).append("down")
172172
173173
def setup2():
174-
items[2] = 2
174+
items.setdefault("setup2", []).append("up")
175175
176176
def teardown2():
177-
del items[2]
177+
items.setdefault("setup2", []).append("down")
178178
179179
def test_setup_module_setup():
180-
assert items[1] == 1
180+
assert items["setup"] == ["up"]
181+
182+
def test_setup_module_setup_again():
183+
assert items["setup"] == ["up"]
181184
182185
@with_setup(setup2, teardown2)
183186
def test_local_setup():
184-
assert items[2] == 2
185-
assert 1 not in items
187+
assert items["setup"] == ["up"]
188+
assert items["setup2"] == ["up"]
189+
190+
@with_setup(setup2, teardown2)
191+
def test_local_setup_again():
192+
assert items["setup"] == ["up"]
193+
assert items["setup2"] == ["up", "down", "up"]
186194
"""
187195
)
188196
result = pytester.runpytest("-p", "nose")
189-
result.stdout.fnmatch_lines(["*2 passed*"])
197+
result.stdout.fnmatch_lines(["*4 passed*"])
190198

191199

192200
def test_nose_style_setup_teardown(pytester: Pytester) -> None:

0 commit comments

Comments
 (0)