Skip to content
Open
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
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*def .+: \.\.\.$

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 (old and not recommended) 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
4 changes: 0 additions & 4 deletions doc/en/how-to/assert.rst
Original file line number Diff line number Diff line change
@@ -295,13 +295,9 @@ will then execute the function with those arguments and assert that the given ex

pytest.raises(ValueError, func, x=-1)

The reporter will provide you with helpful output in case of failures such as *no
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
@@ -338,13 +338,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:
31 changes: 6 additions & 25 deletions src/_pytest/raises.py
Original file line number Diff line number Diff line change
@@ -95,14 +95,15 @@ def raises(*, check: Callable[[BaseException], bool]) -> RaisesExc[BaseException
@overload
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]:
@@ -237,25 +238,6 @@ def raises(

:ref:`assertraises` for more examples and detailed discussion.

**Legacy form**

It is possible to specify a callable by passing a to-be-called lambda::

>>> raises(ZeroDivisionError, lambda: 1/0)
<ExceptionInfo ...>

or you can specify an arbitrary callable with arguments::

>>> def f(x): return 1/x
...
>>> raises(ZeroDivisionError, f, 0)
<ExceptionInfo ...>
>>> raises(ZeroDivisionError, f, x=0)
<ExceptionInfo ...>

The form above is fully supported but discouraged for new code because the
context manager form is regarded as more readable and less error-prone.

.. note::
Similar to caught exception objects in Python, explicitly clearing
local references to returned ``ExceptionInfo`` objects can
@@ -272,7 +254,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 +271,10 @@ 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")
with RaisesExc(expected_exception) as excinfo:
func(*args[1:], **kwargs)
func(*args, **kwargs)
try:
return excinfo
finally:
47 changes: 25 additions & 22 deletions src/_pytest/recwarn.py
Original file line number Diff line number Diff line change
@@ -17,8 +17,11 @@


if TYPE_CHECKING:
from typing_extensions import ParamSpec
from typing_extensions import Self

P = ParamSpec("P")

import warnings

from _pytest.deprecated import check_ispytest
@@ -49,7 +52,7 @@ def deprecated_call(


@overload
def deprecated_call(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: ...
def deprecated_call(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: ...


def deprecated_call(
@@ -67,23 +70,23 @@ 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
the warnings types above. The return value is the return value of the function.

In the context manager form you may use the keyword argument ``match`` to assert
You may use the keyword argument ``match`` to assert
that the warning matches a text or regex.

The context manager produces a list of :class:`warnings.WarningMessage` objects,
one for each warning raised.
The return value is a list of :class:`warnings.WarningMessage` objects,
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
)
dep_warnings = (DeprecationWarning, PendingDeprecationWarning, FutureWarning)
if func is None:
return warns(dep_warnings, *args, **kwargs)

with warns(dep_warnings):
return func(*args, **kwargs)


@overload
@@ -97,16 +100,16 @@ def warns(
@overload
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.
@@ -119,13 +122,13 @@ def warns(
each warning emitted (regardless of whether it is an ``expected_warning`` or not).
Since pytest 8.0, unmatched warnings are also re-emitted when the context closes.

This function can be used as a context manager::
This function should be used as a context manager::

>>> import pytest
>>> with pytest.warns(RuntimeWarning):
... warnings.warn("my warning", RuntimeWarning)

In the context manager form you may use the keyword argument ``match`` to assert
The ``match`` keyword argument can be used to assert
that the warning matches a text or regex::

>>> with pytest.warns(UserWarning, match='must be 0 or None'):
@@ -151,7 +154,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 +164,10 @@ 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")
with WarningsChecker(expected_warning, _ispytest=True):
return func(*args[1:], **kwargs)
return func(*args, **kwargs)


class WarningsRecorder(warnings.catch_warnings):
15 changes: 10 additions & 5 deletions testing/_py/test_local.py
Original file line number Diff line number Diff line change
@@ -625,7 +625,8 @@ def test_chdir_gone(self, path1):
p = path1.ensure("dir_to_be_removed", dir=1)
p.chdir()
p.remove()
pytest.raises(error.ENOENT, local)
with pytest.raises(error.ENOENT):
local()
assert path1.chdir() is None
assert os.getcwd() == str(path1)

@@ -998,8 +999,10 @@ def test_locked_make_numbered_dir(self, tmpdir):
assert numdir.new(ext=str(j)).check()

def test_error_preservation(self, path1):
pytest.raises(EnvironmentError, path1.join("qwoeqiwe").mtime)
pytest.raises(EnvironmentError, path1.join("qwoeqiwe").read)
with pytest.raises(EnvironmentError):
path1.join("qwoeqiwe").mtime()
with pytest.raises(EnvironmentError):
path1.join("qwoeqiwe").read()

# def test_parentdirmatch(self):
# local.parentdirmatch('std', startmodule=__name__)
@@ -1099,7 +1102,8 @@ def test_pyimport_check_filepath_consistency(self, monkeypatch, tmpdir):
pseudopath = tmpdir.ensure(name + "123.py")
mod.__file__ = str(pseudopath)
monkeypatch.setitem(sys.modules, name, mod)
excinfo = pytest.raises(pseudopath.ImportMismatchError, p.pyimport)
with pytest.raises(pseudopath.ImportMismatchError) as excinfo:
p.pyimport()
modname, modfile, orig = excinfo.value.args
assert modname == name
assert modfile == pseudopath
@@ -1397,7 +1401,8 @@ def test_stat_helpers(self, tmpdir, monkeypatch):

def test_stat_non_raising(self, tmpdir):
path1 = tmpdir.join("file")
pytest.raises(error.ENOENT, lambda: path1.stat())
with pytest.raises(error.ENOENT):
path1.stat()
res = path1.stat(raising=False)
assert res is None

4 changes: 1 addition & 3 deletions testing/code/test_code.py
Original file line number Diff line number Diff line change
@@ -85,10 +85,8 @@ def test_code_from_func() -> None:
def test_unicode_handling() -> None:
value = "ąć".encode()

def f() -> None:
with pytest.raises(Exception) as excinfo:
raise Exception(value)

excinfo = pytest.raises(Exception, f)
str(excinfo)


Loading
Oops, something went wrong.
Loading
Oops, something went wrong.