nose: fix class- and module-level fixture behavior
Fixes #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.
This commit is contained in:
parent
842814c969
commit
89f0b5b5a2
|
@ -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
|
# see https://github.com/python/mypy/issues/2608
|
||||||
func = item
|
func = item
|
||||||
|
|
||||||
if not call_optional(func.obj, "setup"):
|
call_optional(func.obj, "setup")
|
||||||
# Call module level setup if there is no object level one.
|
func.addfinalizer(lambda: call_optional(func.obj, "teardown"))
|
||||||
assert func.parent is not None
|
|
||||||
call_optional(func.parent.obj, "setup") # type: ignore[attr-defined]
|
|
||||||
|
|
||||||
def teardown_nose() -> None:
|
# NOTE: Module- and class-level fixtures are handled in python.py
|
||||||
if not call_optional(func.obj, "teardown"):
|
# with `pluginmanager.has_plugin("nose")` checks.
|
||||||
assert func.parent is not None
|
# It would have been nicer to implement them outside of core, but
|
||||||
call_optional(func.parent.obj, "teardown") # type: ignore[attr-defined]
|
# it's not straightforward.
|
||||||
|
|
||||||
# XXX This implies we only call teardown when setup worked.
|
|
||||||
func.addfinalizer(teardown_nose)
|
|
||||||
|
|
||||||
|
|
||||||
def call_optional(obj: object, name: str) -> bool:
|
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
|
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
|
||||||
other fixtures (#517).
|
other fixtures (#517).
|
||||||
"""
|
"""
|
||||||
|
has_nose = self.config.pluginmanager.has_plugin("nose")
|
||||||
setup_module = _get_first_non_fixture_func(
|
setup_module = _get_first_non_fixture_func(
|
||||||
self.obj, ("setUpModule", "setup_module")
|
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(
|
teardown_module = _get_first_non_fixture_func(
|
||||||
self.obj, ("tearDownModule", "teardown_module")
|
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:
|
if setup_module is None and teardown_module is None:
|
||||||
return
|
return
|
||||||
|
@ -750,13 +755,14 @@ def _call_with_optional_argument(func, arg) -> None:
|
||||||
func()
|
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
|
"""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."""
|
xunit-style function, but only if not marked as a fixture to avoid calling it twice."""
|
||||||
for name in names:
|
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:
|
if meth is not None and fixtures.getfixturemarker(meth) is None:
|
||||||
return meth
|
return meth
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class Class(PyCollector):
|
class Class(PyCollector):
|
||||||
|
@ -832,8 +838,17 @@ class Class(PyCollector):
|
||||||
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
|
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
|
||||||
other fixtures (#517).
|
other fixtures (#517).
|
||||||
"""
|
"""
|
||||||
setup_method = _get_first_non_fixture_func(self.obj, ("setup_method",))
|
has_nose = self.config.pluginmanager.has_plugin("nose")
|
||||||
teardown_method = getattr(self.obj, "teardown_method", None)
|
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:
|
if setup_method is None and teardown_method is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -846,11 +861,11 @@ class Class(PyCollector):
|
||||||
def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
|
def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
|
||||||
method = request.function
|
method = request.function
|
||||||
if setup_method is not None:
|
if setup_method is not None:
|
||||||
func = getattr(self, "setup_method")
|
func = getattr(self, setup_name)
|
||||||
_call_with_optional_argument(func, method)
|
_call_with_optional_argument(func, method)
|
||||||
yield
|
yield
|
||||||
if teardown_method is not None:
|
if teardown_method is not None:
|
||||||
func = getattr(self, "teardown_method")
|
func = getattr(self, teardown_name)
|
||||||
_call_with_optional_argument(func, method)
|
_call_with_optional_argument(func, method)
|
||||||
|
|
||||||
self.obj.__pytest_setup_method = xunit_setup_method_fixture
|
self.obj.__pytest_setup_method = xunit_setup_method_fixture
|
||||||
|
|
|
@ -165,28 +165,36 @@ def test_module_level_setup(pytester: Pytester) -> None:
|
||||||
items = {}
|
items = {}
|
||||||
|
|
||||||
def setup():
|
def setup():
|
||||||
items[1]=1
|
items.setdefault("setup", []).append("up")
|
||||||
|
|
||||||
def teardown():
|
def teardown():
|
||||||
del items[1]
|
items.setdefault("setup", []).append("down")
|
||||||
|
|
||||||
def setup2():
|
def setup2():
|
||||||
items[2] = 2
|
items.setdefault("setup2", []).append("up")
|
||||||
|
|
||||||
def teardown2():
|
def teardown2():
|
||||||
del items[2]
|
items.setdefault("setup2", []).append("down")
|
||||||
|
|
||||||
def test_setup_module_setup():
|
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)
|
@with_setup(setup2, teardown2)
|
||||||
def test_local_setup():
|
def test_local_setup():
|
||||||
assert items[2] == 2
|
assert items["setup"] == ["up"]
|
||||||
assert 1 not in items
|
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 = 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:
|
def test_nose_style_setup_teardown(pytester: Pytester) -> None:
|
||||||
|
|
Loading…
Reference in New Issue