Skip to content

Commit

Permalink
Add deprecations for tests written for nose (#9907)
Browse files Browse the repository at this point in the history
Fixes #9886
  • Loading branch information
symonk committed Oct 9, 2022
1 parent 571dc6b commit 3bf2bc5
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 6 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -50,6 +50,7 @@ coverage.xml
.project
.settings
.vscode
__pycache__/

# generated by pip
pip-wheel-metadata/
Expand Down
10 changes: 10 additions & 0 deletions changelog/9886.deprecation.rst
@@ -0,0 +1,10 @@
The functionality for running tests written for ``nose`` has been officially deprecated.

This includes:

* 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.
* Setup/teardown using the `@with_setup <with-setup-nose>`_ decorator.

For more details, consult the :ref:`deprecation docs <nose-deprecation>`.

.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
107 changes: 107 additions & 0 deletions doc/en/deprecations.rst
Expand Up @@ -18,6 +18,113 @@ Deprecated Features
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.


.. _nose-deprecation:

Support for tests written for nose
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. deprecated:: 7.2

Support for running tests written for `nose <https://nose.readthedocs.io/en/latest/>`__ is now deprecated.

``nose`` has been in maintenance mode-only for years, and maintaining the plugin is not trivial as it spills
over the code base (see :issue:`9886` for more details).

setup/teardown
^^^^^^^^^^^^^^

One thing that might catch users by surprise is that plain ``setup`` and ``teardown`` methods are not pytest native,
they are in fact part of the ``nose`` support.


.. code-block:: python
class Test:
def setup(self):
self.resource = make_resource()
def teardown(self):
self.resource.close()
def test_foo(self):
...
def test_bar(self):
...
Native pytest support uses ``setup_method`` and ``teardown_method`` (see :ref:`xunit-method-setup`), so the above should be changed to:

.. code-block:: python
class Test:
def setup_method(self):
self.resource = make_resource()
def teardown_method(self):
self.resource.close()
def test_foo(self):
...
def test_bar(self):
...
This is easy to do in an entire code base by doing a simple find/replace.

@with_setup
^^^^^^^^^^^

Code using `@with_setup <with-setup-nose>`_ such as this:

.. code-block:: python
from nose.tools import with_setup
def setup_some_resource():
...
def teardown_some_resource():
...
@with_setup(setup_some_resource, teardown_some_resource)
def test_foo():
...
Will also need to be ported to a supported pytest style. One way to do it is using a fixture:

.. code-block:: python
import pytest
def setup_some_resource():
...
def teardown_some_resource():
...
@pytest.fixture
def some_resource():
setup_some_resource()
yield
teardown_some_resource()
def test_foo(some_resource):
...
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup

.. _instance-collector-deprecation:

The ``pytest.Instance`` collector
Expand Down
3 changes: 3 additions & 0 deletions doc/en/how-to/nose.rst
Expand Up @@ -5,6 +5,9 @@ How to run tests written for nose

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

.. warning::
This functionality has been deprecated and is likely to be removed in ``pytest 8.x``.

.. _nosestyle:

Usage
Expand Down
2 changes: 2 additions & 0 deletions doc/en/how-to/xunit_setup.rst
Expand Up @@ -63,6 +63,8 @@ and after all test methods of the class are called:
setup_class.
"""
.. _xunit-method-setup:

Method and function level setup/teardown
-----------------------------------------------

Expand Down
15 changes: 15 additions & 0 deletions src/_pytest/deprecated.py
Expand Up @@ -22,6 +22,21 @@
"pytest_faulthandler",
}

NOSE_SUPPORT = UnformattedWarning(
PytestRemovedIn8Warning,
"Support for nose tests is deprecated and will be removed in a future release.\n"
"{nodeid} is using nose method: `{method}` ({stage})\n"
"See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
)

NOSE_SUPPORT_METHOD = UnformattedWarning(
PytestRemovedIn8Warning,
"Support for nose tests is deprecated and will be removed in a future release.\n"
"{nodeid} is using nose-specific method: `{method}(self)`\n"
"To remove this warning, rename it to `{method}_method(self)`\n"
"See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
)


# This can be* removed pytest 8, but it's harmless and common, so no rush to remove.
# * If you're in the future: "could have been".
Expand Down
14 changes: 11 additions & 3 deletions src/_pytest/nose.py
@@ -1,5 +1,8 @@
"""Run testsuites written for nose."""
import warnings

from _pytest.config import hookimpl
from _pytest.deprecated import NOSE_SUPPORT
from _pytest.fixtures import getfixturemarker
from _pytest.nodes import Item
from _pytest.python import Function
Expand All @@ -18,16 +21,16 @@ def pytest_runtest_setup(item: Item) -> None:
# see https://github.com/python/mypy/issues/2608
func = item

call_optional(func.obj, "setup")
func.addfinalizer(lambda: call_optional(func.obj, "teardown"))
call_optional(func.obj, "setup", func.nodeid)
func.addfinalizer(lambda: call_optional(func.obj, "teardown", func.nodeid))

# 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:
def call_optional(obj: object, name: str, nodeid: str) -> bool:
method = getattr(obj, name, None)
if method is None:
return False
Expand All @@ -36,6 +39,11 @@ def call_optional(obj: object, name: str) -> bool:
return False
if not callable(method):
return False
# Warn about deprecation of this plugin.
method_name = getattr(method, "__name__", str(method))
warnings.warn(
NOSE_SUPPORT.format(nodeid=nodeid, method=method_name, stage=name), stacklevel=2
)
# If there are any problems allow the exception to raise rather than
# silently ignoring it.
method()
Expand Down
21 changes: 20 additions & 1 deletion src/_pytest/python.py
Expand Up @@ -59,6 +59,7 @@
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.deprecated import INSTANCE_COLLECTOR
from _pytest.deprecated import NOSE_SUPPORT_METHOD
from _pytest.fixtures import FuncFixtureInfo
from _pytest.main import Session
from _pytest.mark import MARK_GEN
Expand Down Expand Up @@ -872,19 +873,23 @@ def _inject_setup_method_fixture(self) -> None:
"""Inject a hidden autouse, function scoped fixture into the collected class object
that invokes setup_method/teardown_method if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
Using a fixture to invoke these methods ensures we play nicely and unsurprisingly with
other fixtures (#517).
"""
has_nose = self.config.pluginmanager.has_plugin("nose")
setup_name = "setup_method"
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
emit_nose_setup_warning = False
if setup_method is None and has_nose:
setup_name = "setup"
emit_nose_setup_warning = True
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
teardown_name = "teardown_method"
teardown_method = getattr(self.obj, teardown_name, None)
emit_nose_teardown_warning = False
if teardown_method is None and has_nose:
teardown_name = "teardown"
emit_nose_teardown_warning = True
teardown_method = getattr(self.obj, teardown_name, None)
if setup_method is None and teardown_method is None:
return
Expand All @@ -900,10 +905,24 @@ def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
if setup_method is not None:
func = getattr(self, setup_name)
_call_with_optional_argument(func, method)
if emit_nose_setup_warning:
warnings.warn(
NOSE_SUPPORT_METHOD.format(
nodeid=request.node.nodeid, method="setup"
),
stacklevel=2,
)
yield
if teardown_method is not None:
func = getattr(self, teardown_name)
_call_with_optional_argument(func, method)
if emit_nose_teardown_warning:
warnings.warn(
NOSE_SUPPORT_METHOD.format(
nodeid=request.node.nodeid, method="teardown"
),
stacklevel=2,
)

self.obj.__pytest_setup_method = xunit_setup_method_fixture

Expand Down
59 changes: 59 additions & 0 deletions testing/deprecated_test.py
Expand Up @@ -279,3 +279,62 @@ def test_importing_instance_is_deprecated(pytester: Pytester) -> None:
match=re.escape("The pytest.Instance collector type is deprecated"),
):
from _pytest.python import Instance # noqa: F401


@pytest.mark.filterwarnings("default")
def test_nose_deprecated_with_setup(pytester: Pytester) -> None:
pytest.importorskip("nose")
pytester.makepyfile(
"""
from nose.tools import with_setup
def setup_fn_no_op():
...
def teardown_fn_no_op():
...
@with_setup(setup_fn_no_op, teardown_fn_no_op)
def test_omits_warnings():
...
"""
)
output = pytester.runpytest()
message = [
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
"*test_nose_deprecated_with_setup.py::test_omits_warnings is using nose method: `setup_fn_no_op` (setup)",
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
"*test_nose_deprecated_with_setup.py::test_omits_warnings is using nose method: `teardown_fn_no_op` (teardown)",
]
output.stdout.fnmatch_lines(message)
output.assert_outcomes(passed=1)


@pytest.mark.filterwarnings("default")
def test_nose_deprecated_setup_teardown(pytester: Pytester) -> None:
pytest.importorskip("nose")
pytester.makepyfile(
"""
class Test:
def setup(self):
...
def teardown(self):
...
def test(self):
...
"""
)
output = pytester.runpytest()
message = [
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
"*test_nose_deprecated_setup_teardown.py::Test::test is using nose-specific method: `setup(self)`",
"*To remove this warning, rename it to `setup_method(self)`",
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
"*test_nose_deprecated_setup_teardown.py::Test::test is using nose-specific method: `teardown(self)`",
"*To remove this warning, rename it to `teardown_method(self)`",
]
output.stdout.fnmatch_lines(message)
output.assert_outcomes(passed=1)
4 changes: 2 additions & 2 deletions testing/test_nose.py
Expand Up @@ -37,7 +37,7 @@ class A:
def f(self):
values.append(1)

call_optional(A(), "f")
call_optional(A(), "f", "A.f")
assert not values


Expand All @@ -47,7 +47,7 @@ def test_setup_func_not_callable() -> None:
class A:
f = 1

call_optional(A(), "f")
call_optional(A(), "f", "A.f")


def test_nose_setup_func(pytester: Pytester) -> None:
Expand Down

0 comments on commit 3bf2bc5

Please sign in to comment.