Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/8948.deprecation.rst
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 32 additions & 0 deletions doc/en/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,38 @@ In order to support the transition to :mod:`pathlib`, the following hooks now re
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
6 changes: 3 additions & 3 deletions doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ pytest.fail

**Tutorial**: :ref:`skipping`

.. autofunction:: pytest.fail
.. autofunction:: pytest.fail(reason, [pytrace=True, msg=None])

pytest.skip
~~~~~~~~~~~

.. autofunction:: pytest.skip(msg, [allow_module_level=False])
.. autofunction:: pytest.skip(reason, [allow_module_level=False, msg=None])

.. _`pytest.importorskip ref`:

Expand All @@ -44,7 +44,7 @@ pytest.xfail
pytest.exit
~~~~~~~~~~~

.. autofunction:: pytest.exit
.. autofunction:: pytest.exit(reason, [returncode=False, msg=None])

pytest.main
~~~~~~~~~~~
Expand Down
7 changes: 4 additions & 3 deletions src/_pytest/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@
import attr
import py

from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME

if TYPE_CHECKING:
from typing import NoReturn
from typing_extensions import Final
Expand Down Expand Up @@ -157,6 +154,8 @@ def getfuncargnames(
try:
parameters = signature(function).parameters
except (ValueError, TypeError) as e:
from _pytest.outcomes import fail

fail(
f"Could not determine arguments of {function!r}: {e}",
pytrace=False,
Expand Down Expand Up @@ -329,6 +328,8 @@ def safe_getattr(object: Any, name: str, default: Any) -> Any:
are derived from BaseException instead of Exception (for more details
check #2707).
"""
from _pytest.outcomes import TEST_OUTCOME

try:
return getattr(object, name, default)
except TEST_OUTCOME:
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@
" 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".
#
# def my_private_function(some, args):
Expand Down
102 changes: 89 additions & 13 deletions src/_pytest/outcomes.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
"""Exception classes and constants handling test outcomes as well as
functions creating them."""
import sys
import warnings
from typing import Any
from typing import Callable
from typing import cast
from typing import Optional
from typing import Type
from typing import TypeVar

from _pytest.deprecated import KEYWORD_MSG_ARG

TYPE_CHECKING = False # Avoid circular import through compat.

if TYPE_CHECKING:
Expand All @@ -33,7 +36,7 @@ def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None:
"Perhaps you meant to use a mark?"
)
raise TypeError(error_msg.format(type(self).__name__, type(msg).__name__))
BaseException.__init__(self, msg)
super().__init__(msg)
self.msg = msg
self.pytrace = pytrace

Expand Down Expand Up @@ -61,7 +64,7 @@ def __init__(
*,
_use_item_location: bool = False,
) -> None:
OutcomeException.__init__(self, msg=msg, pytrace=pytrace)
super().__init__(msg=msg, pytrace=pytrace)
self.allow_module_level = allow_module_level
# If true, the skip location is reported as the item's location,
# instead of the place that raises the exception/calls skip().
Expand Down Expand Up @@ -110,28 +113,56 @@ def decorate(func: _F) -> _WithException[_F, _ET]:


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

:param str msg: Message to display upon exit.
:param int returncode: Return code to be used when exiting pytest.
:param reason:
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
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)
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.

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
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
of the module. Defaults to False.

:param msg:
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.

.. note::
It is better to use the :ref:`pytest.mark.skipif ref` marker when
possible to declare a test to be skipped under certain conditions
Expand All @@ -141,21 +172,66 @@ def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn":
to skip a doctest statically.
"""
__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)
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.

:param str msg:
:param reason:
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
python traceback will be reported.

:param msg:
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
"""
__tracebackhide__ = True
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
raise Failed(msg=msg, pytrace=pytrace)
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):
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def async_warn_and_skip(nodeid: str) -> None:
msg += " - pytest-trio\n"
msg += " - pytest-twisted"
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)
Expand Down
4 changes: 3 additions & 1 deletion src/_pytest/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,16 @@ def from_user(
from _pytest.outcomes import fail

try:
return Scope(scope_name)
# Holding this reference is necessary for mypy at the moment.
scope = Scope(scope_name)
except ValueError:
fail(
"{} {}got an unexpected scope value '{}'".format(
descr, f"from {where} " if where else "", scope_name
),
pytrace=False,
)
return scope


_ALL_SCOPES = list(Scope)
Expand Down
58 changes: 58 additions & 0 deletions testing/deprecated_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,64 @@ def test_warns_none_is_deprecated():
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:
pytester.makeconftest(
"""
Expand Down
2 changes: 1 addition & 1 deletion testing/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def test_wrap_session_exit_sessionfinish(
"""
import pytest
def pytest_sessionfinish():
pytest.exit(msg="exit_pytest_sessionfinish", returncode={returncode})
pytest.exit(reason="exit_pytest_sessionfinish", returncode={returncode})
""".format(
returncode=returncode
)
Expand Down
Loading