Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

deprecate non-cm raises,warns&deprecated call + add paramspec #13241

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ exclude_lines =
^\s*assert False(,|$)
^\s*assert_never\(

^\s*if TYPE_CHECKING:
^\s*(el)?if TYPE_CHECKING:
^\s*@overload( |$)

^\s*@pytest\.mark\.xfail
1 change: 1 addition & 0 deletions changelog/13241.deprecation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The legacy callable form of :func:`pytest.raises`, :func:`pytest.warns` and :func:`pytest.deprecated_call` has been deprecated. Use the context-manager form instead.
2 changes: 2 additions & 0 deletions changelog/13241.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:func:`pytest.raises`, :func:`pytest.warns` and :func:`pytest.deprecated_call` now uses :class:`ParamSpec` for the type hint to the (now-deprecated) callable overload, instead of :class:`Any`. This allows type checkers to raise errors when passing incorrect function parameters.
``func`` can now also be passed as a kwarg, which the type hint previously showed as possible but didn't accept.
4 changes: 4 additions & 0 deletions doc/en/conf.py
Original file line number Diff line number Diff line change
@@ -97,6 +97,10 @@
# TypeVars
("py:class", "_pytest._code.code.E"),
("py:class", "E"), # due to delayed annotation
("py:class", "T"),
("py:class", "P"),
("py:class", "P.args"),
("py:class", "P.kwargs"),
("py:class", "_pytest.fixtures.FixtureFunction"),
("py:class", "_pytest.nodes._NodeType"),
("py:class", "_NodeType"), # due to delayed annotation
35 changes: 35 additions & 0 deletions doc/en/deprecations.rst
Original file line number Diff line number Diff line change
@@ -14,6 +14,41 @@ 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>`.

Legacy callable form of :func:`raises <pytest.raises>`, :func:`warns <pytest.warns>` and :func:`deprecated_call <pytest.deprecated_call>`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. deprecated:: 8.4

Pytest created the callable form of :func:`pytest.raises`, :func:`pytest.warns` and :func:`pytest.deprecated_call` before
the ``with`` statement was added in :pep:`python 2.5 <343>`. It has been kept for a long time, but is considered harder
to read and doesn't allow passing `match` or other parameters.

.. code-block:: python

def my_warn(par1, par2, par3):
warnings.warn(DeprecationWarning(f"{par1}{par2}{par3}"))
return 6.28


# Deprecated form, using callable + arguments

excinfo = pytest.raises(ValueError, int, "hello")
ret1 = pytest.warns(DeprecationWarning, my_warns, "a", "b", "c")
ret2 = pytest.deprecated_call(my_warns, "d", "e", "f")

# The calls above can be upgraded to the context-manager form

with pytest.raises(ValueError) as excinfo:
int("hello")
with pytest.warns(DeprecationWarning):
ret1 = my_warns("a", "b", "c")
with pytest.deprecated_call():
ret2 = my_warns("d", "e", "f")


.. note::
This feature is not fully deprecated as of yet, awaiting the availability of an
automated tool to automatically fix code making extensive use of it.

.. _sync-test-async-fixture:

5 changes: 4 additions & 1 deletion doc/en/how-to/assert.rst
Original file line number Diff line number Diff line change
@@ -282,6 +282,10 @@ exception at a specific level; exceptions contained directly in the top
Alternate `pytest.raises` form (legacy)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. warning::
This will be deprecated and removed in a future release.


There is an alternate form of :func:`pytest.raises` where you pass
a function that will be executed, along with ``*args`` and ``**kwargs``. :func:`pytest.raises`
will then execute the function with those arguments and assert that the given exception is raised:
@@ -301,7 +305,6 @@ exception* or *wrong exception*.
This form was the original :func:`pytest.raises` API, developed before the ``with`` statement was
added to the Python language. Nowadays, this form is rarely used, with the context-manager form (using ``with``)
being considered more readable.
Nonetheless, this form is fully supported and not deprecated in any way.

xfail mark and pytest.raises
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7 changes: 0 additions & 7 deletions doc/en/how-to/capture-warnings.rst
Original file line number Diff line number Diff line change
@@ -337,13 +337,6 @@ Some examples:
... warnings.warn("issue with foo() func")
...

You can also call :func:`pytest.warns` on a function or code string:

.. code-block:: python

pytest.warns(expected_warning, func, *args, **kwargs)
pytest.warns(expected_warning, "func(*args, **kwargs)")

The function also returns a list of all raised warnings (as
``warnings.WarningMessage`` objects), which you can query for
additional information:
42 changes: 42 additions & 0 deletions src/_pytest/deprecated.py
Original file line number Diff line number Diff line change
@@ -11,13 +11,55 @@

from __future__ import annotations

from typing import TYPE_CHECKING
from warnings import warn

from _pytest.warning_types import PytestDeprecationWarning
from _pytest.warning_types import PytestPendingDeprecationWarning
from _pytest.warning_types import PytestRemovedIn9Warning
from _pytest.warning_types import UnformattedWarning


# the `as` indicates explicit re-export to type checkers
# mypy currently does not support overload+deprecated
if TYPE_CHECKING:
from typing_extensions import deprecated as deprecated
else:

def deprecated(reason: str = "") -> object:
# This decorator should only be used to indicate that overloads are deprecated
# once py<3.13 is no longer supported, or when somebody wants to use @deprecated
# for runtime warning, we can consider adapting this decorator to support that
def decorator(func: object) -> object:
return func

return decorator


CALLABLE_RAISES = PytestPendingDeprecationWarning(
"The callable form of pytest.raises will be deprecated in a future version.\n"
"Use `with pytest.raises(...):` instead.\n"
"Full deprecation will not be made until there's a tool to automatically update"
" code to use the context-manager form.\n"
"See https://docs.pytest.org/en/stable/reference/deprecations.html#legacy-callable-form-of-raises-warns-and-deprecated-call"
)

CALLABLE_WARNS = PytestPendingDeprecationWarning(
"The callable form of pytest.warns will be deprecated in a future version.\n"
"Use `with pytest.warns(...):` instead."
"Full deprecation will not be made until there's a tool to automatically update"
" code to use the context-manager form.\n"
"See https://docs.pytest.org/en/stable/reference/deprecations.html#legacy-callable-form-of-raises-warns-and-deprecated-call"
)
CALLABLE_DEPRECATED_CALL = PytestPendingDeprecationWarning(
"The callable form of pytest.deprecated_call will be deprecated in a future version.\n"
"Use `with pytest.deprecated_call():` instead."
"Full deprecation will not be made until there's a tool to automatically update"
" code to use the context-manager form.\n"
"See https://docs.pytest.org/en/stable/reference/deprecations.html#legacy-callable-form-of-raises-warns-and-deprecated-call"
)


# set of plugins which have been integrated into the core; we use this list to ignore
# them during registration to avoid conflicts
DEPRECATED_EXTERNAL_PLUGINS = {
3 changes: 3 additions & 0 deletions src/_pytest/python_api.py
Original file line number Diff line number Diff line change
@@ -16,6 +16,9 @@

if TYPE_CHECKING:
from numpy import ndarray
from typing_extensions import ParamSpec

P = ParamSpec("P")


def _compare_approx(
18 changes: 11 additions & 7 deletions src/_pytest/raises.py
Original file line number Diff line number Diff line change
@@ -19,6 +19,8 @@

from _pytest._code import ExceptionInfo
from _pytest._code.code import stringify_exception
from _pytest.deprecated import CALLABLE_RAISES
from _pytest.deprecated import deprecated
from _pytest.outcomes import fail
from _pytest.warning_types import PytestWarning

@@ -93,16 +95,18 @@ def raises(*, check: Callable[[BaseException], bool]) -> RaisesExc[BaseException


@overload
@deprecated("Use context-manager form instead")
def raises(
expected_exception: type[E] | tuple[type[E], ...],
func: Callable[..., Any],
*args: Any,
**kwargs: Any,
func: Callable[P, object],
*args: P.args,
**kwargs: P.kwargs,
) -> ExceptionInfo[E]: ...


def raises(
expected_exception: type[E] | tuple[type[E], ...] | None = None,
func: Callable[P, object] | None = None,
*args: Any,
**kwargs: Any,
) -> RaisesExc[BaseException] | ExceptionInfo[E]:
@@ -253,7 +257,7 @@ def raises(
>>> raises(ZeroDivisionError, f, x=0)
<ExceptionInfo ...>

The form above is fully supported but discouraged for new code because the
The form above is going to be deprecated in a future pytest release as the
context manager form is regarded as more readable and less error-prone.

.. note::
@@ -272,7 +276,7 @@ def raises(
"""
__tracebackhide__ = True

if not args:
if func is None and not args:
if set(kwargs) - {"match", "check", "expected_exception"}:
msg = "Unexpected keyword arguments passed to pytest.raises: "
msg += ", ".join(sorted(kwargs))
@@ -289,11 +293,11 @@ def raises(
f"Raising exceptions is already understood as failing the test, so you don't need "
f"any special code to say 'this should never raise an exception'."
)
func = args[0]
if not callable(func):
raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
warnings.warn(CALLABLE_RAISES, stacklevel=2)
with RaisesExc(expected_exception) as excinfo:
func(*args[1:], **kwargs)
func(*args, **kwargs)
try:
return excinfo
finally:
43 changes: 29 additions & 14 deletions src/_pytest/recwarn.py
Original file line number Diff line number Diff line change
@@ -17,11 +17,17 @@


if TYPE_CHECKING:
from typing_extensions import ParamSpec
from typing_extensions import Self

P = ParamSpec("P")

import warnings

from _pytest.deprecated import CALLABLE_DEPRECATED_CALL
from _pytest.deprecated import CALLABLE_WARNS
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import deprecated
from _pytest.fixtures import fixture
from _pytest.outcomes import Exit
from _pytest.outcomes import fail
@@ -49,7 +55,8 @@ def deprecated_call(


@overload
def deprecated_call(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: ...
@deprecated("Use context-manager form instead")
def deprecated_call(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: ...


def deprecated_call(
@@ -67,6 +74,8 @@ def deprecated_call(
>>> import pytest
>>> with pytest.deprecated_call():
... assert api_call_v2() == 200
>>> with pytest.deprecated_call(match="^use v3 of this api$") as warning_messages:
... assert api_call_v2() == 200

It can also be used by passing a function and ``*args`` and ``**kwargs``,
in which case it will ensure calling ``func(*args, **kwargs)`` produces one of
@@ -76,14 +85,18 @@ def deprecated_call(
that the warning matches a text or regex.

The context manager produces a list of :class:`warnings.WarningMessage` objects,
one for each warning raised.
one for each warning emitted
(regardless of whether it is an ``expected_warning`` or not).
"""
__tracebackhide__ = True
if func is not None:
args = (func, *args)
return warns(
(DeprecationWarning, PendingDeprecationWarning, FutureWarning), *args, **kwargs
)
# Potential QoL: allow `with deprecated_call:` - i.e. no parens
dep_warnings = (DeprecationWarning, PendingDeprecationWarning, FutureWarning)
if func is None:
return warns(dep_warnings, *args, **kwargs)

warnings.warn(CALLABLE_DEPRECATED_CALL, stacklevel=2)
with warns(dep_warnings):
return func(*args, **kwargs)


@overload
@@ -95,18 +108,19 @@ def warns(


@overload
@deprecated("Use context-manager form instead")
def warns(
expected_warning: type[Warning] | tuple[type[Warning], ...],
func: Callable[..., T],
*args: Any,
**kwargs: Any,
func: Callable[P, T],
*args: P.args,
**kwargs: P.kwargs,
) -> T: ...


def warns(
expected_warning: type[Warning] | tuple[type[Warning], ...] = Warning,
func: Callable[..., object] | None = None,
*args: Any,
match: str | re.Pattern[str] | None = None,
**kwargs: Any,
) -> WarningsChecker | Any:
r"""Assert that code raises a particular class of warning.
@@ -151,7 +165,8 @@ def warns(

"""
__tracebackhide__ = True
if not args:
if func is None and not args:
match: str | re.Pattern[str] | None = kwargs.pop("match", None)
if kwargs:
argnames = ", ".join(sorted(kwargs))
raise TypeError(
@@ -160,11 +175,11 @@ def warns(
)
return WarningsChecker(expected_warning, match_expr=match, _ispytest=True)
else:
func = args[0]
if not callable(func):
raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
warnings.warn(CALLABLE_WARNS, stacklevel=2)
with WarningsChecker(expected_warning, _ispytest=True):
return func(*args[1:], **kwargs)
return func(*args, **kwargs)


class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg]
6 changes: 6 additions & 0 deletions src/_pytest/warning_types.py
Original file line number Diff line number Diff line change
@@ -44,6 +44,12 @@ class PytestCollectionWarning(PytestWarning):
__module__ = "pytest"


class PytestPendingDeprecationWarning(PytestWarning, PendingDeprecationWarning):
"""Warning emitted for features that will be deprecated in a future version."""

__module__ = "pytest"


class PytestDeprecationWarning(PytestWarning, DeprecationWarning):
"""Warning class for features that will be removed in a future version."""

2 changes: 2 additions & 0 deletions src/pytest/__init__.py
Original file line number Diff line number Diff line change
@@ -81,6 +81,7 @@
from _pytest.warning_types import PytestDeprecationWarning
from _pytest.warning_types import PytestExperimentalApiWarning
from _pytest.warning_types import PytestFDWarning
from _pytest.warning_types import PytestPendingDeprecationWarning
from _pytest.warning_types import PytestRemovedIn9Warning
from _pytest.warning_types import PytestUnhandledThreadExceptionWarning
from _pytest.warning_types import PytestUnknownMarkWarning
@@ -130,6 +131,7 @@
"PytestDeprecationWarning",
"PytestExperimentalApiWarning",
"PytestFDWarning",
"PytestPendingDeprecationWarning",
"PytestPluginManager",
"PytestRemovedIn9Warning",
"PytestUnhandledThreadExceptionWarning",
Loading
Oops, something went wrong.