Deprecation of `msg=` for both `pytest.skip()` and `pytest.fail()`. (#8950)

* porting pytest.skip() to use reason=, adding tests

* avoid adding **kwargs, it breaks other functionality, use optional msg= instead

* deprecation of `pytest.fail(msg=...)`

* fix bug with not capturing the returned reason value

* pass reason= in acceptance async tests instead of msg=

* finalising deprecations of `msg` in `pytest.skip()` and `pytest.fail()`

* Update doc/en/deprecations.rst

Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>

* Update doc/en/deprecations.rst

Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>

* fix failing test after upstream merge

* adding deprecation to `pytest.exit(msg=...)`

* add docs for pytest.exit deprecations

* finalising deprecation of msg for pytest.skip, pytest.exit and pytest.fail

* hold a reference to the Scope instance to please mypy

Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
This commit is contained in:
Simon K 2021-11-08 14:31:14 +00:00 committed by GitHub
parent b7603fa730
commit eb6c4493b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 288 additions and 20 deletions

View File

@ -0,0 +1,5 @@
:func:`pytest.skip(msg=...) <pytest.skip>`, :func:`pytest.fail(msg=...) <pytest.fail>` and :func:`pytest.exit(msg=...) <pytest.exit>`
signatures now accept a ``reason`` argument instead of ``msg``. Using ``msg`` still works, but is deprecated and will be removed in a future release.
This was changed for consistency with :func:`pytest.mark.skip <pytest.mark.skip>` and :func:`pytest.mark.xfail <pytest.mark.xfail>` which both accept
``reason`` as an argument.

View File

@ -56,6 +56,38 @@ In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the
The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments. The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments.
Passing ``msg=`` to ``pytest.skip``, ``pytest.fail`` or ``pytest.exit``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
Passing the keyword argument ``msg`` to :func:`pytest.skip`, :func:`pytest.fail` or :func:`pytest.exit`
is now deprecated and ``reason`` should be used instead. This change is to bring consistency between these
functions and the``@pytest.mark.skip`` and ``@pytest.mark.xfail`` markers which already accept a ``reason`` argument.
.. code-block:: python
def test_fail_example():
# old
pytest.fail(msg="foo")
# new
pytest.fail(reason="bar")
def test_skip_example():
# old
pytest.skip(msg="foo")
# new
pytest.skip(reason="bar")
def test_exit_example():
# old
pytest.exit(msg="foo")
# new
pytest.exit(reason="bar")
Implementing the ``pytest_cmdline_preparse`` hook Implementing the ``pytest_cmdline_preparse`` hook
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -22,12 +22,12 @@ pytest.fail
**Tutorial**: :ref:`skipping` **Tutorial**: :ref:`skipping`
.. autofunction:: pytest.fail .. autofunction:: pytest.fail(reason, [pytrace=True, msg=None])
pytest.skip pytest.skip
~~~~~~~~~~~ ~~~~~~~~~~~
.. autofunction:: pytest.skip(msg, [allow_module_level=False]) .. autofunction:: pytest.skip(reason, [allow_module_level=False, msg=None])
.. _`pytest.importorskip ref`: .. _`pytest.importorskip ref`:
@ -44,7 +44,7 @@ pytest.xfail
pytest.exit pytest.exit
~~~~~~~~~~~ ~~~~~~~~~~~
.. autofunction:: pytest.exit .. autofunction:: pytest.exit(reason, [returncode=False, msg=None])
pytest.main pytest.main
~~~~~~~~~~~ ~~~~~~~~~~~

View File

@ -20,9 +20,6 @@ from typing import Union
import attr import attr
import py import py
from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import NoReturn from typing import NoReturn
from typing_extensions import Final from typing_extensions import Final
@ -152,6 +149,8 @@ def getfuncargnames(
try: try:
parameters = signature(function).parameters parameters = signature(function).parameters
except (ValueError, TypeError) as e: except (ValueError, TypeError) as e:
from _pytest.outcomes import fail
fail( fail(
f"Could not determine arguments of {function!r}: {e}", f"Could not determine arguments of {function!r}: {e}",
pytrace=False, pytrace=False,
@ -324,6 +323,8 @@ def safe_getattr(object: Any, name: str, default: Any) -> Any:
are derived from BaseException instead of Exception (for more details are derived from BaseException instead of Exception (for more details
check #2707). check #2707).
""" """
from _pytest.outcomes import TEST_OUTCOME
try: try:
return getattr(object, name, default) return getattr(object, name, default)
except TEST_OUTCOME: except TEST_OUTCOME:

View File

@ -114,6 +114,11 @@ WARNS_NONE_ARG = PytestDeprecationWarning(
" Replace pytest.warns(None) by simply pytest.warns()." " Replace pytest.warns(None) by simply pytest.warns()."
) )
KEYWORD_MSG_ARG = UnformattedWarning(
PytestDeprecationWarning,
"pytest.{func}(msg=...) is now deprecated, use pytest.{func}(reason=...) instead",
)
# You want to make some `__init__` or function "private". # You want to make some `__init__` or function "private".
# #
# def my_private_function(some, args): # def my_private_function(some, args):

View File

@ -1,6 +1,7 @@
"""Exception classes and constants handling test outcomes as well as """Exception classes and constants handling test outcomes as well as
functions creating them.""" functions creating them."""
import sys import sys
import warnings
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
@ -8,6 +9,8 @@ from typing import Optional
from typing import Type from typing import Type
from typing import TypeVar from typing import TypeVar
from _pytest.deprecated import KEYWORD_MSG_ARG
TYPE_CHECKING = False # Avoid circular import through compat. TYPE_CHECKING = False # Avoid circular import through compat.
if TYPE_CHECKING: if TYPE_CHECKING:
@ -110,28 +113,56 @@ def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _E
@_with_exception(Exit) @_with_exception(Exit)
def exit(msg: str, returncode: Optional[int] = None) -> "NoReturn": def exit(
reason: str = "", returncode: Optional[int] = None, *, msg: Optional[str] = None
) -> "NoReturn":
"""Exit testing process. """Exit testing process.
:param str msg: Message to display upon exit. :param reason:
:param int returncode: Return code to be used when exiting pytest. The message to show as the reason for exiting pytest. reason has a default value
only because `msg` is deprecated.
:param returncode:
Return code to be used when exiting pytest.
:param msg:
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
""" """
__tracebackhide__ = True __tracebackhide__ = True
raise Exit(msg, returncode) from _pytest.config import UsageError
if reason and msg:
raise UsageError(
"cannot pass reason and msg to exit(), `msg` is deprecated, use `reason`."
)
if not reason:
if msg is None:
raise UsageError("exit() requires a reason argument")
warnings.warn(KEYWORD_MSG_ARG.format(func="exit"), stacklevel=2)
reason = msg
raise Exit(reason, returncode)
@_with_exception(Skipped) @_with_exception(Skipped)
def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn": def skip(
reason: str = "", *, allow_module_level: bool = False, msg: Optional[str] = None
) -> "NoReturn":
"""Skip an executing test with the given message. """Skip an executing test with the given message.
This function should be called only during testing (setup, call or teardown) or This function should be called only during testing (setup, call or teardown) or
during collection by using the ``allow_module_level`` flag. This function can during collection by using the ``allow_module_level`` flag. This function can
be called in doctests as well. be called in doctests as well.
:param bool allow_module_level: :param reason:
The message to show the user as reason for the skip.
:param allow_module_level:
Allows this function to be called at module level, skipping the rest Allows this function to be called at module level, skipping the rest
of the module. Defaults to False. of the module. Defaults to False.
:param msg:
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
.. note:: .. note::
It is better to use the :ref:`pytest.mark.skipif ref` marker when It is better to use the :ref:`pytest.mark.skipif ref` marker when
possible to declare a test to be skipped under certain conditions possible to declare a test to be skipped under certain conditions
@ -140,21 +171,66 @@ def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn":
to skip a doctest statically. to skip a doctest statically.
""" """
__tracebackhide__ = True __tracebackhide__ = True
raise Skipped(msg=msg, allow_module_level=allow_module_level) reason = _resolve_msg_to_reason("skip", reason, msg)
raise Skipped(msg=reason, allow_module_level=allow_module_level)
@_with_exception(Failed) @_with_exception(Failed)
def fail(msg: str = "", pytrace: bool = True) -> "NoReturn": def fail(
reason: str = "", pytrace: bool = True, msg: Optional[str] = None
) -> "NoReturn":
"""Explicitly fail an executing test with the given message. """Explicitly fail an executing test with the given message.
:param str msg: :param reason:
The message to show the user as reason for the failure. The message to show the user as reason for the failure.
:param bool pytrace:
:param pytrace:
If False, msg represents the full failure information and no If False, msg represents the full failure information and no
python traceback will be reported. python traceback will be reported.
:param msg:
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
""" """
__tracebackhide__ = True __tracebackhide__ = True
raise Failed(msg=msg, pytrace=pytrace) reason = _resolve_msg_to_reason("fail", reason, msg)
raise Failed(msg=reason, pytrace=pytrace)
def _resolve_msg_to_reason(
func_name: str, reason: str, msg: Optional[str] = None
) -> str:
"""
Handles converting the deprecated msg parameter if provided into
reason, raising a deprecation warning. This function will be removed
when the optional msg argument is removed from here in future.
:param str func_name:
The name of the offending function, this is formatted into the deprecation message.
:param str reason:
The reason= passed into either pytest.fail() or pytest.skip()
:param str msg:
The msg= passed into either pytest.fail() or pytest.skip(). This will
be converted into reason if it is provided to allow pytest.skip(msg=) or
pytest.fail(msg=) to continue working in the interim period.
:returns:
The value to use as reason.
"""
__tracebackhide__ = True
if msg is not None:
if reason:
from pytest import UsageError
raise UsageError(
f"Passing both ``reason`` and ``msg`` to pytest.{func_name}(...) is not permitted."
)
warnings.warn(KEYWORD_MSG_ARG.format(func=func_name), stacklevel=3)
reason = msg
return reason
class XFailed(Failed): class XFailed(Failed):

View File

@ -175,7 +175,7 @@ def async_warn_and_skip(nodeid: str) -> None:
msg += " - pytest-trio\n" msg += " - pytest-trio\n"
msg += " - pytest-twisted" msg += " - pytest-twisted"
warnings.warn(PytestUnhandledCoroutineWarning(msg.format(nodeid))) warnings.warn(PytestUnhandledCoroutineWarning(msg.format(nodeid)))
skip(msg="async def function and no async plugin installed (see warnings)") skip(reason="async def function and no async plugin installed (see warnings)")
@hookimpl(trylast=True) @hookimpl(trylast=True)

View File

@ -71,7 +71,8 @@ class Scope(Enum):
from _pytest.outcomes import fail from _pytest.outcomes import fail
try: try:
return Scope(scope_name) # Holding this reference is necessary for mypy at the moment.
scope = Scope(scope_name)
except ValueError: except ValueError:
fail( fail(
"{} {}got an unexpected scope value '{}'".format( "{} {}got an unexpected scope value '{}'".format(
@ -79,6 +80,7 @@ class Scope(Enum):
), ),
pytrace=False, pytrace=False,
) )
return scope
_ALL_SCOPES = list(Scope) _ALL_SCOPES = list(Scope)

View File

@ -200,6 +200,64 @@ def test_warns_none_is_deprecated():
pass pass
class TestSkipMsgArgumentDeprecated:
def test_skip_with_msg_is_deprecated(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import pytest
def test_skipping_msg():
pytest.skip(msg="skippedmsg")
"""
)
result = pytester.runpytest(p)
result.stdout.fnmatch_lines(
[
"*PytestDeprecationWarning: pytest.skip(msg=...) is now deprecated, "
"use pytest.skip(reason=...) instead",
'*pytest.skip(msg="skippedmsg")*',
]
)
result.assert_outcomes(skipped=1, warnings=1)
def test_fail_with_msg_is_deprecated(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import pytest
def test_failing_msg():
pytest.fail(msg="failedmsg")
"""
)
result = pytester.runpytest(p)
result.stdout.fnmatch_lines(
[
"*PytestDeprecationWarning: pytest.fail(msg=...) is now deprecated, "
"use pytest.fail(reason=...) instead",
'*pytest.fail(msg="failedmsg")',
]
)
result.assert_outcomes(failed=1, warnings=1)
def test_exit_with_msg_is_deprecated(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import pytest
def test_exit_msg():
pytest.exit(msg="exitmsg")
"""
)
result = pytester.runpytest(p)
result.stdout.fnmatch_lines(
[
"*PytestDeprecationWarning: pytest.exit(msg=...) is now deprecated, "
"use pytest.exit(reason=...) instead",
]
)
result.assert_outcomes(warnings=1)
def test_deprecation_of_cmdline_preparse(pytester: Pytester) -> None: def test_deprecation_of_cmdline_preparse(pytester: Pytester) -> None:
pytester.makeconftest( pytester.makeconftest(
""" """

View File

@ -70,7 +70,7 @@ def test_wrap_session_exit_sessionfinish(
""" """
import pytest import pytest
def pytest_sessionfinish(): def pytest_sessionfinish():
pytest.exit(msg="exit_pytest_sessionfinish", returncode={returncode}) pytest.exit(reason="exit_pytest_sessionfinish", returncode={returncode})
""".format( """.format(
returncode=returncode returncode=returncode
) )

View File

@ -1444,3 +1444,92 @@ def test_relpath_rootdir(pytester: Pytester) -> None:
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
["SKIPPED [[]1[]] tests/test_1.py:2: unconditional skip"] ["SKIPPED [[]1[]] tests/test_1.py:2: unconditional skip"]
) )
def test_skip_using_reason_works_ok(pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import pytest
def test_skipping_reason():
pytest.skip(reason="skippedreason")
"""
)
result = pytester.runpytest(p)
result.stdout.no_fnmatch_line("*PytestDeprecationWarning*")
result.assert_outcomes(skipped=1)
def test_fail_using_reason_works_ok(pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import pytest
def test_failing_reason():
pytest.fail(reason="failedreason")
"""
)
result = pytester.runpytest(p)
result.stdout.no_fnmatch_line("*PytestDeprecationWarning*")
result.assert_outcomes(failed=1)
def test_fail_fails_with_msg_and_reason(pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import pytest
def test_fail_both_arguments():
pytest.fail(reason="foo", msg="bar")
"""
)
result = pytester.runpytest(p)
result.stdout.fnmatch_lines(
"*UsageError: Passing both ``reason`` and ``msg`` to pytest.fail(...) is not permitted.*"
)
result.assert_outcomes(failed=1)
def test_skip_fails_with_msg_and_reason(pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import pytest
def test_skip_both_arguments():
pytest.skip(reason="foo", msg="bar")
"""
)
result = pytester.runpytest(p)
result.stdout.fnmatch_lines(
"*UsageError: Passing both ``reason`` and ``msg`` to pytest.skip(...) is not permitted.*"
)
result.assert_outcomes(failed=1)
def test_exit_with_msg_and_reason_fails(pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import pytest
def test_exit_both_arguments():
pytest.exit(reason="foo", msg="bar")
"""
)
result = pytester.runpytest(p)
result.stdout.fnmatch_lines(
"*UsageError: cannot pass reason and msg to exit(), `msg` is deprecated, use `reason`.*"
)
result.assert_outcomes(failed=1)
def test_exit_with_reason_works_ok(pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import pytest
def test_exit_reason_only():
pytest.exit(reason="foo")
"""
)
result = pytester.runpytest(p)
result.stdout.fnmatch_lines("*_pytest.outcomes.Exit: foo*")