Merge pull request #9273 from bluetech/nose-fixtures2
nose: fix class- and module-level fixture behavior
This commit is contained in:
		
						commit
						86446edc86
					
				| 
						 | 
				
			
			@ -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.
 | 
			
		||||
| 
						 | 
				
			
			@ -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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -514,12 +514,17 @@ class Module(nodes.File, PyCollector):
 | 
			
		|||
        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 @@ class Class(PyCollector):
 | 
			
		|||
        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 @@ class Class(PyCollector):
 | 
			
		|||
        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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue