Skip to content

Commit 3bf2bc5

Browse files
authoredOct 9, 2022
Add deprecations for tests written for nose (#9907)
Fixes #9886
1 parent 571dc6b commit 3bf2bc5

File tree

10 files changed

+230
-6
lines changed

10 files changed

+230
-6
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ coverage.xml
5050
.project
5151
.settings
5252
.vscode
53+
__pycache__/
5354

5455
# generated by pip
5556
pip-wheel-metadata/

‎changelog/9886.deprecation.rst

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
The functionality for running tests written for ``nose`` has been officially deprecated.
2+
3+
This includes:
4+
5+
* Plain ``setup`` and ``teardown`` functions and methods: this might catch users by surprise, as ``setup()`` and ``teardown()`` are not pytest idioms, but part of the ``nose`` support.
6+
* Setup/teardown using the `@with_setup <with-setup-nose>`_ decorator.
7+
8+
For more details, consult the :ref:`deprecation docs <nose-deprecation>`.
9+
10+
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup

‎doc/en/deprecations.rst

+107
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,113 @@ Deprecated Features
1818
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
1919
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
2020

21+
22+
.. _nose-deprecation:
23+
24+
Support for tests written for nose
25+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
26+
27+
.. deprecated:: 7.2
28+
29+
Support for running tests written for `nose <https://nose.readthedocs.io/en/latest/>`__ is now deprecated.
30+
31+
``nose`` has been in maintenance mode-only for years, and maintaining the plugin is not trivial as it spills
32+
over the code base (see :issue:`9886` for more details).
33+
34+
setup/teardown
35+
^^^^^^^^^^^^^^
36+
37+
One thing that might catch users by surprise is that plain ``setup`` and ``teardown`` methods are not pytest native,
38+
they are in fact part of the ``nose`` support.
39+
40+
41+
.. code-block:: python
42+
43+
class Test:
44+
def setup(self):
45+
self.resource = make_resource()
46+
47+
def teardown(self):
48+
self.resource.close()
49+
50+
def test_foo(self):
51+
...
52+
53+
def test_bar(self):
54+
...
55+
56+
57+
58+
Native pytest support uses ``setup_method`` and ``teardown_method`` (see :ref:`xunit-method-setup`), so the above should be changed to:
59+
60+
.. code-block:: python
61+
62+
class Test:
63+
def setup_method(self):
64+
self.resource = make_resource()
65+
66+
def teardown_method(self):
67+
self.resource.close()
68+
69+
def test_foo(self):
70+
...
71+
72+
def test_bar(self):
73+
...
74+
75+
76+
This is easy to do in an entire code base by doing a simple find/replace.
77+
78+
@with_setup
79+
^^^^^^^^^^^
80+
81+
Code using `@with_setup <with-setup-nose>`_ such as this:
82+
83+
.. code-block:: python
84+
85+
from nose.tools import with_setup
86+
87+
88+
def setup_some_resource():
89+
...
90+
91+
92+
def teardown_some_resource():
93+
...
94+
95+
96+
@with_setup(setup_some_resource, teardown_some_resource)
97+
def test_foo():
98+
...
99+
100+
Will also need to be ported to a supported pytest style. One way to do it is using a fixture:
101+
102+
.. code-block:: python
103+
104+
import pytest
105+
106+
107+
def setup_some_resource():
108+
...
109+
110+
111+
def teardown_some_resource():
112+
...
113+
114+
115+
@pytest.fixture
116+
def some_resource():
117+
setup_some_resource()
118+
yield
119+
teardown_some_resource()
120+
121+
122+
def test_foo(some_resource):
123+
...
124+
125+
126+
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
127+
21128
.. _instance-collector-deprecation:
22129

23130
The ``pytest.Instance`` collector

‎doc/en/how-to/nose.rst

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ How to run tests written for nose
55

66
``pytest`` has basic support for running tests written for nose_.
77

8+
.. warning::
9+
This functionality has been deprecated and is likely to be removed in ``pytest 8.x``.
10+
811
.. _nosestyle:
912

1013
Usage

‎doc/en/how-to/xunit_setup.rst

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ and after all test methods of the class are called:
6363
setup_class.
6464
"""
6565
66+
.. _xunit-method-setup:
67+
6668
Method and function level setup/teardown
6769
-----------------------------------------------
6870

‎src/_pytest/deprecated.py

+15
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@
2222
"pytest_faulthandler",
2323
}
2424

25+
NOSE_SUPPORT = UnformattedWarning(
26+
PytestRemovedIn8Warning,
27+
"Support for nose tests is deprecated and will be removed in a future release.\n"
28+
"{nodeid} is using nose method: `{method}` ({stage})\n"
29+
"See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
30+
)
31+
32+
NOSE_SUPPORT_METHOD = UnformattedWarning(
33+
PytestRemovedIn8Warning,
34+
"Support for nose tests is deprecated and will be removed in a future release.\n"
35+
"{nodeid} is using nose-specific method: `{method}(self)`\n"
36+
"To remove this warning, rename it to `{method}_method(self)`\n"
37+
"See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
38+
)
39+
2540

2641
# This can be* removed pytest 8, but it's harmless and common, so no rush to remove.
2742
# * If you're in the future: "could have been".

‎src/_pytest/nose.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""Run testsuites written for nose."""
2+
import warnings
3+
24
from _pytest.config import hookimpl
5+
from _pytest.deprecated import NOSE_SUPPORT
36
from _pytest.fixtures import getfixturemarker
47
from _pytest.nodes import Item
58
from _pytest.python import Function
@@ -18,16 +21,16 @@ def pytest_runtest_setup(item: Item) -> None:
1821
# see https://github.com/python/mypy/issues/2608
1922
func = item
2023

21-
call_optional(func.obj, "setup")
22-
func.addfinalizer(lambda: call_optional(func.obj, "teardown"))
24+
call_optional(func.obj, "setup", func.nodeid)
25+
func.addfinalizer(lambda: call_optional(func.obj, "teardown", func.nodeid))
2326

2427
# NOTE: Module- and class-level fixtures are handled in python.py
2528
# with `pluginmanager.has_plugin("nose")` checks.
2629
# It would have been nicer to implement them outside of core, but
2730
# it's not straightforward.
2831

2932

30-
def call_optional(obj: object, name: str) -> bool:
33+
def call_optional(obj: object, name: str, nodeid: str) -> bool:
3134
method = getattr(obj, name, None)
3235
if method is None:
3336
return False
@@ -36,6 +39,11 @@ def call_optional(obj: object, name: str) -> bool:
3639
return False
3740
if not callable(method):
3841
return False
42+
# Warn about deprecation of this plugin.
43+
method_name = getattr(method, "__name__", str(method))
44+
warnings.warn(
45+
NOSE_SUPPORT.format(nodeid=nodeid, method=method_name, stage=name), stacklevel=2
46+
)
3947
# If there are any problems allow the exception to raise rather than
4048
# silently ignoring it.
4149
method()

‎src/_pytest/python.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
from _pytest.deprecated import check_ispytest
6060
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
6161
from _pytest.deprecated import INSTANCE_COLLECTOR
62+
from _pytest.deprecated import NOSE_SUPPORT_METHOD
6263
from _pytest.fixtures import FuncFixtureInfo
6364
from _pytest.main import Session
6465
from _pytest.mark import MARK_GEN
@@ -872,19 +873,23 @@ def _inject_setup_method_fixture(self) -> None:
872873
"""Inject a hidden autouse, function scoped fixture into the collected class object
873874
that invokes setup_method/teardown_method if either or both are available.
874875
875-
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
876+
Using a fixture to invoke these methods ensures we play nicely and unsurprisingly with
876877
other fixtures (#517).
877878
"""
878879
has_nose = self.config.pluginmanager.has_plugin("nose")
879880
setup_name = "setup_method"
880881
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
882+
emit_nose_setup_warning = False
881883
if setup_method is None and has_nose:
882884
setup_name = "setup"
885+
emit_nose_setup_warning = True
883886
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
884887
teardown_name = "teardown_method"
885888
teardown_method = getattr(self.obj, teardown_name, None)
889+
emit_nose_teardown_warning = False
886890
if teardown_method is None and has_nose:
887891
teardown_name = "teardown"
892+
emit_nose_teardown_warning = True
888893
teardown_method = getattr(self.obj, teardown_name, None)
889894
if setup_method is None and teardown_method is None:
890895
return
@@ -900,10 +905,24 @@ def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
900905
if setup_method is not None:
901906
func = getattr(self, setup_name)
902907
_call_with_optional_argument(func, method)
908+
if emit_nose_setup_warning:
909+
warnings.warn(
910+
NOSE_SUPPORT_METHOD.format(
911+
nodeid=request.node.nodeid, method="setup"
912+
),
913+
stacklevel=2,
914+
)
903915
yield
904916
if teardown_method is not None:
905917
func = getattr(self, teardown_name)
906918
_call_with_optional_argument(func, method)
919+
if emit_nose_teardown_warning:
920+
warnings.warn(
921+
NOSE_SUPPORT_METHOD.format(
922+
nodeid=request.node.nodeid, method="teardown"
923+
),
924+
stacklevel=2,
925+
)
907926

908927
self.obj.__pytest_setup_method = xunit_setup_method_fixture
909928

‎testing/deprecated_test.py

+59
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,62 @@ def test_importing_instance_is_deprecated(pytester: Pytester) -> None:
279279
match=re.escape("The pytest.Instance collector type is deprecated"),
280280
):
281281
from _pytest.python import Instance # noqa: F401
282+
283+
284+
@pytest.mark.filterwarnings("default")
285+
def test_nose_deprecated_with_setup(pytester: Pytester) -> None:
286+
pytest.importorskip("nose")
287+
pytester.makepyfile(
288+
"""
289+
from nose.tools import with_setup
290+
291+
def setup_fn_no_op():
292+
...
293+
294+
def teardown_fn_no_op():
295+
...
296+
297+
@with_setup(setup_fn_no_op, teardown_fn_no_op)
298+
def test_omits_warnings():
299+
...
300+
"""
301+
)
302+
output = pytester.runpytest()
303+
message = [
304+
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
305+
"*test_nose_deprecated_with_setup.py::test_omits_warnings is using nose method: `setup_fn_no_op` (setup)",
306+
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
307+
"*test_nose_deprecated_with_setup.py::test_omits_warnings is using nose method: `teardown_fn_no_op` (teardown)",
308+
]
309+
output.stdout.fnmatch_lines(message)
310+
output.assert_outcomes(passed=1)
311+
312+
313+
@pytest.mark.filterwarnings("default")
314+
def test_nose_deprecated_setup_teardown(pytester: Pytester) -> None:
315+
pytest.importorskip("nose")
316+
pytester.makepyfile(
317+
"""
318+
class Test:
319+
320+
def setup(self):
321+
...
322+
323+
def teardown(self):
324+
...
325+
326+
def test(self):
327+
...
328+
"""
329+
)
330+
output = pytester.runpytest()
331+
message = [
332+
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
333+
"*test_nose_deprecated_setup_teardown.py::Test::test is using nose-specific method: `setup(self)`",
334+
"*To remove this warning, rename it to `setup_method(self)`",
335+
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
336+
"*test_nose_deprecated_setup_teardown.py::Test::test is using nose-specific method: `teardown(self)`",
337+
"*To remove this warning, rename it to `teardown_method(self)`",
338+
]
339+
output.stdout.fnmatch_lines(message)
340+
output.assert_outcomes(passed=1)

‎testing/test_nose.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class A:
3737
def f(self):
3838
values.append(1)
3939

40-
call_optional(A(), "f")
40+
call_optional(A(), "f", "A.f")
4141
assert not values
4242

4343

@@ -47,7 +47,7 @@ def test_setup_func_not_callable() -> None:
4747
class A:
4848
f = 1
4949

50-
call_optional(A(), "f")
50+
call_optional(A(), "f", "A.f")
5151

5252

5353
def test_nose_setup_func(pytester: Pytester) -> None:

0 commit comments

Comments
 (0)
Please sign in to comment.