diff --git a/changelog/9272.bugfix.rst b/changelog/9272.bugfix.rst new file mode 100644 index 00000000000..0f242d40891 --- /dev/null +++ b/changelog/9272.bugfix.rst @@ -0,0 +1,2 @@ +The nose compatibility module-level fixtures `setup()` and `teardown()` are now only called once per module, instead of for each test function. +They are now called even if object-level `setup`/`teardown` is defined. diff --git a/src/_pytest/nose.py b/src/_pytest/nose.py index 16d5224e9fa..b0699d22bd8 100644 --- a/src/_pytest/nose.py +++ b/src/_pytest/nose.py @@ -18,18 +18,13 @@ def pytest_runtest_setup(item: Item) -> None: # see https://github.com/python/mypy/issues/2608 func = item - if not call_optional(func.obj, "setup"): - # Call module level setup if there is no object level one. - assert func.parent is not None - call_optional(func.parent.obj, "setup") # type: ignore[attr-defined] + call_optional(func.obj, "setup") + func.addfinalizer(lambda: call_optional(func.obj, "teardown")) - def teardown_nose() -> None: - if not call_optional(func.obj, "teardown"): - assert func.parent is not None - call_optional(func.parent.obj, "teardown") # type: ignore[attr-defined] - - # XXX This implies we only call teardown when setup worked. - func.addfinalizer(teardown_nose) + # NOTE: Module- and class-level fixtures are handled in python.py + # with `pluginmanager.has_plugin("nose")` checks. + # It would have been nicer to implement them outside of core, but + # it's not straightforward. def call_optional(obj: object, name: str) -> bool: diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 8acef2539c4..184c22143db 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -514,12 +514,17 @@ def _inject_setup_module_fixture(self) -> None: Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with other fixtures (#517). """ + has_nose = self.config.pluginmanager.has_plugin("nose") setup_module = _get_first_non_fixture_func( self.obj, ("setUpModule", "setup_module") ) + if setup_module is None and has_nose: + setup_module = _get_first_non_fixture_func(self.obj, ("setup",)) teardown_module = _get_first_non_fixture_func( self.obj, ("tearDownModule", "teardown_module") ) + if teardown_module is None and has_nose: + teardown_module = _get_first_non_fixture_func(self.obj, ("teardown",)) if setup_module is None and teardown_module is None: return @@ -750,13 +755,14 @@ def _call_with_optional_argument(func, arg) -> None: func() -def _get_first_non_fixture_func(obj: object, names: Iterable[str]): +def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[object]: """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.""" for name in names: - meth = getattr(obj, name, None) + meth: Optional[object] = getattr(obj, name, None) if meth is not None and fixtures.getfixturemarker(meth) is None: return meth + return None class Class(PyCollector): @@ -832,8 +838,17 @@ def _inject_setup_method_fixture(self) -> None: Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with other fixtures (#517). """ - setup_method = _get_first_non_fixture_func(self.obj, ("setup_method",)) - teardown_method = getattr(self.obj, "teardown_method", None) + has_nose = self.config.pluginmanager.has_plugin("nose") + setup_name = "setup_method" + setup_method = _get_first_non_fixture_func(self.obj, (setup_name,)) + if setup_method is None and has_nose: + setup_name = "setup" + setup_method = _get_first_non_fixture_func(self.obj, (setup_name,)) + teardown_name = "teardown_method" + teardown_method = getattr(self.obj, teardown_name, None) + if teardown_method is None and has_nose: + teardown_name = "teardown" + teardown_method = getattr(self.obj, teardown_name, None) if setup_method is None and teardown_method is None: return @@ -846,11 +861,11 @@ def _inject_setup_method_fixture(self) -> None: def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]: method = request.function if setup_method is not None: - func = getattr(self, "setup_method") + func = getattr(self, setup_name) _call_with_optional_argument(func, method) yield if teardown_method is not None: - func = getattr(self, "teardown_method") + func = getattr(self, teardown_name) _call_with_optional_argument(func, method) self.obj.__pytest_setup_method = xunit_setup_method_fixture diff --git a/testing/test_nose.py b/testing/test_nose.py index 3b3d3d2f5fb..a000b386ddc 100644 --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -165,28 +165,36 @@ def test_module_level_setup(pytester: Pytester) -> None: items = {} def setup(): - items[1]=1 + items.setdefault("setup", []).append("up") def teardown(): - del items[1] + items.setdefault("setup", []).append("down") def setup2(): - items[2] = 2 + items.setdefault("setup2", []).append("up") def teardown2(): - del items[2] + items.setdefault("setup2", []).append("down") def test_setup_module_setup(): - assert items[1] == 1 + assert items["setup"] == ["up"] + + def test_setup_module_setup_again(): + assert items["setup"] == ["up"] @with_setup(setup2, teardown2) def test_local_setup(): - assert items[2] == 2 - assert 1 not in items + assert items["setup"] == ["up"] + assert items["setup2"] == ["up"] + + @with_setup(setup2, teardown2) + def test_local_setup_again(): + assert items["setup"] == ["up"] + assert items["setup2"] == ["up", "down", "up"] """ ) result = pytester.runpytest("-p", "nose") - result.stdout.fnmatch_lines(["*2 passed*"]) + result.stdout.fnmatch_lines(["*4 passed*"]) def test_nose_style_setup_teardown(pytester: Pytester) -> None: