Merge branch 'main' into warn-when-a-mark-is-applied-to-a-fixture

This commit is contained in:
Thomas Grainger 2022-10-10 13:55:24 +01:00 committed by GitHub
commit 2fd160110c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 315 additions and 37 deletions

1
.gitignore vendored
View File

@ -50,6 +50,7 @@ coverage.xml
.project .project
.settings .settings
.vscode .vscode
__pycache__/
# generated by pip # generated by pip
pip-wheel-metadata/ pip-wheel-metadata/

View File

@ -154,6 +154,7 @@ Ian Bicking
Ian Lesperance Ian Lesperance
Ilya Konstantinov Ilya Konstantinov
Ionuț Turturică Ionuț Turturică
Itxaso Aizpurua
Iwan Briquemont Iwan Briquemont
Jaap Broekhuizen Jaap Broekhuizen
Jakob van Santen Jakob van Santen
@ -356,6 +357,7 @@ Victor Uriarte
Vidar T. Fauske Vidar T. Fauske
Virgil Dupras Virgil Dupras
Vitaly Lashmanov Vitaly Lashmanov
Vivaan Verma
Vlad Dragos Vlad Dragos
Vlad Radziuk Vlad Radziuk
Vladyslav Rachek Vladyslav Rachek

View File

@ -0,0 +1 @@
:class:`~pytest.PytestReturnNotNoneWarning` is now a subclass of :class:`~pytest.PytestRemovedIn8Warning`: the plan is to make returning non-``None`` from tests an error in the future.

1
changelog/10344.doc.rst Normal file
View File

@ -0,0 +1 @@
Update information on writing plugins to use ``pyproject.toml`` instead of ``setup.py``.

View File

@ -0,0 +1 @@
Assertion failures with strings in NFC and NFD forms that normalize to the same string now have a dedicated error message detailing the issue, and their utf-8 representation is expresed instead.

View File

@ -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

View File

@ -18,6 +18,113 @@ Deprecated Features
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue 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>`. :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: .. _instance-collector-deprecation:
The ``pytest.Instance`` collector The ``pytest.Instance`` collector

View File

@ -5,6 +5,9 @@ How to run tests written for nose
``pytest`` has basic support for running 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: .. _nosestyle:
Usage Usage

View File

@ -147,27 +147,33 @@ Making your plugin installable by others
If you want to make your plugin externally available, you If you want to make your plugin externally available, you
may define a so-called entry point for your distribution so may define a so-called entry point for your distribution so
that ``pytest`` finds your plugin module. Entry points are that ``pytest`` finds your plugin module. Entry points are
a feature that is provided by :std:doc:`setuptools:index`. pytest looks up a feature that is provided by :std:doc:`setuptools <setuptools:index>`.
the ``pytest11`` entrypoint to discover its
plugins and you can thus make your plugin available by defining
it in your setuptools-invocation:
.. sourcecode:: python pytest looks up the ``pytest11`` entrypoint to discover its
plugins, thus you can make your plugin available by defining
it in your ``pyproject.toml`` file.
# sample ./setup.py file .. sourcecode:: toml
from setuptools import setup
# sample ./pyproject.toml file
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
name_of_plugin = "myproject" # register plugin with this name [project]
setup( name = "myproject"
name="myproject", classifiers = [
packages=["myproject"], "Framework :: Pytest",
# the following makes a plugin available to pytest ]
entry_points={"pytest11": [f"{name_of_plugin} = myproject.pluginmodule"]},
# custom PyPI classifier for pytest plugins [tool.setuptools]
classifiers=["Framework :: Pytest"], packages = ["myproject"]
)
[project.entry_points]
pytest11 = [
"myproject = myproject.pluginmodule",
]
If a package is installed this way, ``pytest`` will load If a package is installed this way, ``pytest`` will load
``myproject.pluginmodule`` as a plugin which can define ``myproject.pluginmodule`` as a plugin which can define

View File

@ -63,6 +63,8 @@ and after all test methods of the class are called:
setup_class. setup_class.
""" """
.. _xunit-method-setup:
Method and function level setup/teardown Method and function level setup/teardown
----------------------------------------------- -----------------------------------------------

View File

@ -1136,6 +1136,9 @@ Custom warnings generated in some situations such as improper usage or deprecate
.. autoclass:: pytest.PytestReturnNotNoneWarning .. autoclass:: pytest.PytestReturnNotNoneWarning
:show-inheritance: :show-inheritance:
.. autoclass:: pytest.PytestRemovedIn8Warning
:show-inheritance:
.. autoclass:: pytest.PytestUnhandledCoroutineWarning .. autoclass:: pytest.PytestUnhandledCoroutineWarning
:show-inheritance: :show-inheritance:

View File

@ -41,7 +41,7 @@ class SafeRepr(reprlib.Repr):
information on exceptions raised during the call. information on exceptions raised during the call.
""" """
def __init__(self, maxsize: Optional[int]) -> None: def __init__(self, maxsize: Optional[int], use_ascii: bool = False) -> None:
""" """
:param maxsize: :param maxsize:
If not None, will truncate the resulting repr to that specific size, using ellipsis If not None, will truncate the resulting repr to that specific size, using ellipsis
@ -54,10 +54,15 @@ class SafeRepr(reprlib.Repr):
# truncation. # truncation.
self.maxstring = maxsize if maxsize is not None else 1_000_000_000 self.maxstring = maxsize if maxsize is not None else 1_000_000_000
self.maxsize = maxsize self.maxsize = maxsize
self.use_ascii = use_ascii
def repr(self, x: object) -> str: def repr(self, x: object) -> str:
try: try:
s = super().repr(x) if self.use_ascii:
s = ascii(x)
else:
s = super().repr(x)
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
raise raise
except BaseException as exc: except BaseException as exc:
@ -94,7 +99,9 @@ def safeformat(obj: object) -> str:
DEFAULT_REPR_MAX_SIZE = 240 DEFAULT_REPR_MAX_SIZE = 240
def saferepr(obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE) -> str: def saferepr(
obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False
) -> str:
"""Return a size-limited safe repr-string for the given object. """Return a size-limited safe repr-string for the given object.
Failing __repr__ functions of user instances will be represented Failing __repr__ functions of user instances will be represented
@ -104,10 +111,11 @@ def saferepr(obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE) -> str
This function is a wrapper around the Repr/reprlib functionality of the This function is a wrapper around the Repr/reprlib functionality of the
stdlib. stdlib.
""" """
return SafeRepr(maxsize).repr(obj)
return SafeRepr(maxsize, use_ascii).repr(obj)
def saferepr_unlimited(obj: object) -> str: def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
"""Return an unlimited-size safe repr-string for the given object. """Return an unlimited-size safe repr-string for the given object.
As with saferepr, failing __repr__ functions of user instances As with saferepr, failing __repr__ functions of user instances
@ -119,6 +127,8 @@ def saferepr_unlimited(obj: object) -> str:
when maxsize=None, but that might affect some other code. when maxsize=None, but that might affect some other code.
""" """
try: try:
if use_ascii:
return ascii(obj)
return repr(obj) return repr(obj)
except Exception as exc: except Exception as exc:
return _format_repr_exception(exc, obj) return _format_repr_exception(exc, obj)

View File

@ -10,6 +10,7 @@ from typing import List
from typing import Mapping from typing import Mapping
from typing import Optional from typing import Optional
from typing import Sequence from typing import Sequence
from unicodedata import normalize
import _pytest._code import _pytest._code
from _pytest import outcomes from _pytest import outcomes
@ -156,20 +157,32 @@ def has_default_eq(
return True return True
def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]: def assertrepr_compare(
config, op: str, left: Any, right: Any, use_ascii: bool = False
) -> Optional[List[str]]:
"""Return specialised explanations for some operators/operands.""" """Return specialised explanations for some operators/operands."""
verbose = config.getoption("verbose") verbose = config.getoption("verbose")
# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
# See issue #3246.
use_ascii = (
isinstance(left, str)
and isinstance(right, str)
and normalize("NFD", left) == normalize("NFD", right)
)
if verbose > 1: if verbose > 1:
left_repr = saferepr_unlimited(left) left_repr = saferepr_unlimited(left, use_ascii=use_ascii)
right_repr = saferepr_unlimited(right) right_repr = saferepr_unlimited(right, use_ascii=use_ascii)
else: else:
# XXX: "15 chars indentation" is wrong # XXX: "15 chars indentation" is wrong
# ("E AssertionError: assert "); should use term width. # ("E AssertionError: assert "); should use term width.
maxsize = ( maxsize = (
80 - 15 - len(op) - 2 80 - 15 - len(op) - 2
) // 2 # 15 chars indentation, 1 space around op ) // 2 # 15 chars indentation, 1 space around op
left_repr = saferepr(left, maxsize=maxsize)
right_repr = saferepr(right, maxsize=maxsize) left_repr = saferepr(left, maxsize=maxsize, use_ascii=use_ascii)
right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii)
summary = f"{left_repr} {op} {right_repr}" summary = f"{left_repr} {op} {right_repr}"

View File

@ -22,6 +22,21 @@ DEPRECATED_EXTERNAL_PLUGINS = {
"pytest_faulthandler", "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. # 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". # * If you're in the future: "could have been".

View File

@ -1,5 +1,8 @@
"""Run testsuites written for nose.""" """Run testsuites written for nose."""
import warnings
from _pytest.config import hookimpl from _pytest.config import hookimpl
from _pytest.deprecated import NOSE_SUPPORT
from _pytest.fixtures import getfixturemarker from _pytest.fixtures import getfixturemarker
from _pytest.nodes import Item from _pytest.nodes import Item
from _pytest.python import Function from _pytest.python import Function
@ -18,8 +21,8 @@ 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
call_optional(func.obj, "setup") call_optional(func.obj, "setup", func.nodeid)
func.addfinalizer(lambda: call_optional(func.obj, "teardown")) func.addfinalizer(lambda: call_optional(func.obj, "teardown", func.nodeid))
# NOTE: Module- and class-level fixtures are handled in python.py # NOTE: Module- and class-level fixtures are handled in python.py
# with `pluginmanager.has_plugin("nose")` checks. # with `pluginmanager.has_plugin("nose")` checks.
@ -27,7 +30,7 @@ def pytest_runtest_setup(item: Item) -> None:
# it's not straightforward. # 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) method = getattr(obj, name, None)
if method is None: if method is None:
return False return False
@ -36,6 +39,11 @@ def call_optional(obj: object, name: str) -> bool:
return False return False
if not callable(method): if not callable(method):
return False 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 # If there are any problems allow the exception to raise rather than
# silently ignoring it. # silently ignoring it.
method() method()

View File

@ -59,6 +59,7 @@ from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.deprecated import INSTANCE_COLLECTOR from _pytest.deprecated import INSTANCE_COLLECTOR
from _pytest.deprecated import NOSE_SUPPORT_METHOD
from _pytest.fixtures import FuncFixtureInfo from _pytest.fixtures import FuncFixtureInfo
from _pytest.main import Session from _pytest.main import Session
from _pytest.mark import MARK_GEN from _pytest.mark import MARK_GEN
@ -872,19 +873,23 @@ class Class(PyCollector):
"""Inject a hidden autouse, function scoped fixture into the collected class object """Inject a hidden autouse, function scoped fixture into the collected class object
that invokes setup_method/teardown_method if either or both are available. 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). other fixtures (#517).
""" """
has_nose = self.config.pluginmanager.has_plugin("nose") has_nose = self.config.pluginmanager.has_plugin("nose")
setup_name = "setup_method" setup_name = "setup_method"
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,)) setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
emit_nose_setup_warning = False
if setup_method is None and has_nose: if setup_method is None and has_nose:
setup_name = "setup" setup_name = "setup"
emit_nose_setup_warning = True
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,)) setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
teardown_name = "teardown_method" teardown_name = "teardown_method"
teardown_method = getattr(self.obj, teardown_name, None) teardown_method = getattr(self.obj, teardown_name, None)
emit_nose_teardown_warning = False
if teardown_method is None and has_nose: if teardown_method is None and has_nose:
teardown_name = "teardown" teardown_name = "teardown"
emit_nose_teardown_warning = True
teardown_method = getattr(self.obj, teardown_name, None) 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
@ -900,10 +905,24 @@ class Class(PyCollector):
if setup_method is not None: if setup_method is not None:
func = getattr(self, setup_name) func = getattr(self, setup_name)
_call_with_optional_argument(func, method) _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 yield
if teardown_method is not None: if teardown_method is not None:
func = getattr(self, teardown_name) func = getattr(self, teardown_name)
_call_with_optional_argument(func, method) _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 self.obj.__pytest_setup_method = xunit_setup_method_fixture

View File

@ -51,14 +51,13 @@ class PytestDeprecationWarning(PytestWarning, DeprecationWarning):
__module__ = "pytest" __module__ = "pytest"
@final
class PytestRemovedIn8Warning(PytestDeprecationWarning): class PytestRemovedIn8Warning(PytestDeprecationWarning):
"""Warning class for features that will be removed in pytest 8.""" """Warning class for features that will be removed in pytest 8."""
__module__ = "pytest" __module__ = "pytest"
class PytestReturnNotNoneWarning(PytestDeprecationWarning): class PytestReturnNotNoneWarning(PytestRemovedIn8Warning):
"""Warning emitted when a test function is returning value other than None.""" """Warning emitted when a test function is returning value other than None."""
__module__ = "pytest" __module__ = "pytest"

View File

@ -330,3 +330,62 @@ def test_fixture_disallowed_between_marks():
raise NotImplementedError() raise NotImplementedError()
assert len(record) == 2 # one for each mark decorator assert len(record) == 2 # one for each mark decorator
@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)

View File

@ -1,12 +1,12 @@
anyio[curio,trio]==3.6.1 anyio[curio,trio]==3.6.1
django==4.1.1 django==4.1.2
pytest-asyncio==0.19.0 pytest-asyncio==0.19.0
pytest-bdd==6.0.1 pytest-bdd==6.0.1
pytest-cov==4.0.0 pytest-cov==4.0.0
pytest-django==4.5.2 pytest-django==4.5.2
pytest-flakes==4.0.5 pytest-flakes==4.0.5
pytest-html==3.1.1 pytest-html==3.1.1
pytest-mock==3.9.0 pytest-mock==3.10.0
pytest-rerunfailures==10.2 pytest-rerunfailures==10.2
pytest-sugar==0.9.5 pytest-sugar==0.9.5
pytest-trio==0.7.0 pytest-trio==0.7.0

View File

@ -776,6 +776,24 @@ class TestAssert_reprcompare:
msg = "\n".join(expl) msg = "\n".join(expl)
assert msg assert msg
def test_nfc_nfd_same_string(self) -> None:
# issue 3426
left = "hyv\xe4"
right = "hyva\u0308"
expl = callequal(left, right)
assert expl == [
r"'hyv\xe4' == 'hyva\u0308'",
f"- {str(right)}",
f"+ {str(left)}",
]
expl = callequal(left, right, verbose=2)
assert expl == [
r"'hyv\xe4' == 'hyva\u0308'",
f"- {str(right)}",
f"+ {str(left)}",
]
class TestAssert_reprcompare_dataclass: class TestAssert_reprcompare_dataclass:
def test_dataclasses(self, pytester: Pytester) -> None: def test_dataclasses(self, pytester: Pytester) -> None:

View File

@ -37,7 +37,7 @@ def test_setup_func_with_setup_decorator() -> None:
def f(self): def f(self):
values.append(1) values.append(1)
call_optional(A(), "f") call_optional(A(), "f", "A.f")
assert not values assert not values
@ -47,7 +47,7 @@ def test_setup_func_not_callable() -> None:
class A: class A:
f = 1 f = 1
call_optional(A(), "f") call_optional(A(), "f", "A.f")
def test_nose_setup_func(pytester: Pytester) -> None: def test_nose_setup_func(pytester: Pytester) -> None: