Skip to content

Commit 4cbef64

Browse files
authored
Merge pull request #13241 from jakkdl/raises_paramspec
add paramspec to callable forms of raises/warns/deprecated_call, rewrite tests to use CM form
2 parents fa643de + f7f0889 commit 4cbef64

28 files changed

Lines changed: 290 additions & 202 deletions

changelog/13241.improvement.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
: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.
2+
``func`` can now also be passed as a kwarg, which the type hint previously showed as possible but didn't accept.

doc/en/conf.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@
102102
# TypeVars
103103
("py:class", "_pytest._code.code.E"),
104104
("py:class", "E"), # due to delayed annotation
105+
("py:class", "T"),
106+
("py:class", "P"),
107+
("py:class", "P.args"),
108+
("py:class", "P.kwargs"),
105109
("py:class", "_pytest.fixtures.FixtureFunction"),
106110
("py:class", "_pytest.nodes._NodeType"),
107111
("py:class", "_NodeType"), # due to delayed annotation

doc/en/how-to/assert.rst

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -322,13 +322,9 @@ will then execute the function with those arguments and assert that the given ex
322322
323323
pytest.raises(ValueError, func, x=-1)
324324
325-
The reporter will provide you with helpful output in case of failures such as *no
326-
exception* or *wrong exception*.
327-
328325
This form was the original :func:`pytest.raises` API, developed before the ``with`` statement was
329326
added to the Python language. Nowadays, this form is rarely used, with the context-manager form (using ``with``)
330327
being considered more readable.
331-
Nonetheless, this form is fully supported and not deprecated in any way.
332328

333329
xfail mark and pytest.raises
334330
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

doc/en/how-to/capture-warnings.rst

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -371,13 +371,6 @@ Some examples:
371371
... warnings.warn("issue with foo() func")
372372
...
373373
374-
You can also call :func:`pytest.warns` on a function or code string:
375-
376-
.. code-block:: python
377-
378-
pytest.warns(expected_warning, func, *args, **kwargs)
379-
pytest.warns(expected_warning, "func(*args, **kwargs)")
380-
381374
The function also returns a list of all raised warnings (as
382375
``warnings.WarningMessage`` objects), which you can query for
383376
additional information:

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,7 @@ exclude_lines = [
453453
'^\s*case unreachable:',
454454
'^\s*assert_never\(',
455455
'^\s*if TYPE_CHECKING:',
456+
'^\s*(el)?if TYPE_CHECKING:',
456457
'^\s*@overload( |$)',
457458
'^\s*def .+: \.\.\.$',
458459
'^\s*@pytest\.mark\.xfail',

src/_pytest/raises.py

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,15 @@ def raises(*, check: Callable[[BaseException], bool]) -> RaisesExc[BaseException
9595
@overload
9696
def raises(
9797
expected_exception: type[E] | tuple[type[E], ...],
98-
func: Callable[..., Any],
99-
*args: Any,
100-
**kwargs: Any,
98+
func: Callable[P, object],
99+
*args: P.args,
100+
**kwargs: P.kwargs,
101101
) -> ExceptionInfo[E]: ...
102102

103103

104104
def raises(
105105
expected_exception: type[E] | tuple[type[E], ...] | None = None,
106+
func: Callable[P, object] | None = None,
106107
*args: Any,
107108
**kwargs: Any,
108109
) -> RaisesExc[BaseException] | ExceptionInfo[E]:
@@ -237,25 +238,6 @@ def raises(
237238
238239
:ref:`assertraises` for more examples and detailed discussion.
239240
240-
**Legacy form**
241-
242-
It is possible to specify a callable by passing a to-be-called lambda::
243-
244-
>>> raises(ZeroDivisionError, lambda: 1/0)
245-
<ExceptionInfo ...>
246-
247-
or you can specify an arbitrary callable with arguments::
248-
249-
>>> def f(x): return 1/x
250-
...
251-
>>> raises(ZeroDivisionError, f, 0)
252-
<ExceptionInfo ...>
253-
>>> raises(ZeroDivisionError, f, x=0)
254-
<ExceptionInfo ...>
255-
256-
The form above is fully supported but discouraged for new code because the
257-
context manager form is regarded as more readable and less error-prone.
258-
259241
.. note::
260242
Similar to caught exception objects in Python, explicitly clearing
261243
local references to returned ``ExceptionInfo`` objects can
@@ -272,7 +254,7 @@ def raises(
272254
"""
273255
__tracebackhide__ = True
274256

275-
if not args:
257+
if func is None and not args:
276258
if set(kwargs) - {"match", "check", "expected_exception"}:
277259
msg = "Unexpected keyword arguments passed to pytest.raises: "
278260
msg += ", ".join(sorted(kwargs))
@@ -289,11 +271,10 @@ def raises(
289271
f"Raising exceptions is already understood as failing the test, so you don't need "
290272
f"any special code to say 'this should never raise an exception'."
291273
)
292-
func = args[0]
293274
if not callable(func):
294275
raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
295276
with RaisesExc(expected_exception) as excinfo:
296-
func(*args[1:], **kwargs)
277+
func(*args, **kwargs)
297278
try:
298279
return excinfo
299280
finally:

src/_pytest/recwarn.py

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717

1818

1919
if TYPE_CHECKING:
20+
from typing_extensions import ParamSpec
2021
from typing_extensions import Self
2122

23+
P = ParamSpec("P")
24+
2225
import warnings
2326

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

5053

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

5457

5558
def deprecated_call(
@@ -67,23 +70,23 @@ def deprecated_call(
6770
>>> import pytest
6871
>>> with pytest.deprecated_call():
6972
... assert api_call_v2() == 200
73+
>>> with pytest.deprecated_call(match="^use v3 of this api$") as warning_messages:
74+
... assert api_call_v2() == 200
7075
71-
It can also be used by passing a function and ``*args`` and ``**kwargs``,
72-
in which case it will ensure calling ``func(*args, **kwargs)`` produces one of
73-
the warnings types above. The return value is the return value of the function.
74-
75-
In the context manager form you may use the keyword argument ``match`` to assert
76+
You may use the keyword argument ``match`` to assert
7677
that the warning matches a text or regex.
7778
78-
The context manager produces a list of :class:`warnings.WarningMessage` objects,
79-
one for each warning raised.
79+
The return value is a list of :class:`warnings.WarningMessage` objects,
80+
one for each warning emitted
81+
(regardless of whether it is an ``expected_warning`` or not).
8082
"""
8183
__tracebackhide__ = True
82-
if func is not None:
83-
args = (func, *args)
84-
return warns(
85-
(DeprecationWarning, PendingDeprecationWarning, FutureWarning), *args, **kwargs
86-
)
84+
dep_warnings = (DeprecationWarning, PendingDeprecationWarning, FutureWarning)
85+
if func is None:
86+
return warns(dep_warnings, *args, **kwargs)
87+
88+
with warns(dep_warnings):
89+
return func(*args, **kwargs)
8790

8891

8992
@overload
@@ -97,16 +100,16 @@ def warns(
97100
@overload
98101
def warns(
99102
expected_warning: type[Warning] | tuple[type[Warning], ...],
100-
func: Callable[..., T],
101-
*args: Any,
102-
**kwargs: Any,
103+
func: Callable[P, T],
104+
*args: P.args,
105+
**kwargs: P.kwargs,
103106
) -> T: ...
104107

105108

106109
def warns(
107110
expected_warning: type[Warning] | tuple[type[Warning], ...] = Warning,
111+
func: Callable[..., object] | None = None,
108112
*args: Any,
109-
match: str | re.Pattern[str] | None = None,
110113
**kwargs: Any,
111114
) -> WarningsChecker | Any:
112115
r"""Assert that code raises a particular class of warning.
@@ -119,13 +122,13 @@ def warns(
119122
each warning emitted (regardless of whether it is an ``expected_warning`` or not).
120123
Since pytest 8.0, unmatched warnings are also re-emitted when the context closes.
121124
122-
This function can be used as a context manager::
125+
This function should be used as a context manager::
123126
124127
>>> import pytest
125128
>>> with pytest.warns(RuntimeWarning):
126129
... warnings.warn("my warning", RuntimeWarning)
127130
128-
In the context manager form you may use the keyword argument ``match`` to assert
131+
The ``match`` keyword argument can be used to assert
129132
that the warning matches a text or regex::
130133
131134
>>> with pytest.warns(UserWarning, match='must be 0 or None'):
@@ -153,7 +156,8 @@ def warns(
153156
154157
"""
155158
__tracebackhide__ = True
156-
if not args:
159+
if func is None and not args:
160+
match: str | re.Pattern[str] | None = kwargs.pop("match", None)
157161
if kwargs:
158162
argnames = ", ".join(sorted(kwargs))
159163
raise TypeError(
@@ -162,11 +166,10 @@ def warns(
162166
)
163167
return WarningsChecker(expected_warning, match_expr=match, _ispytest=True)
164168
else:
165-
func = args[0]
166169
if not callable(func):
167170
raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
168171
with WarningsChecker(expected_warning, _ispytest=True):
169-
return func(*args[1:], **kwargs)
172+
return func(*args, **kwargs)
170173

171174

172175
class WarningsRecorder(warnings.catch_warnings):

testing/_py/test_local.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,8 @@ def test_chdir_gone(self, path1):
620620
p = path1.ensure("dir_to_be_removed", dir=1)
621621
p.chdir()
622622
p.remove()
623-
pytest.raises(error.ENOENT, local)
623+
with pytest.raises(error.ENOENT):
624+
local()
624625
assert path1.chdir() is None
625626
assert os.getcwd() == str(path1)
626627

@@ -989,8 +990,10 @@ def test_locked_make_numbered_dir(self, tmpdir):
989990
assert numdir.new(ext=str(j)).check()
990991

991992
def test_error_preservation(self, path1):
992-
pytest.raises(EnvironmentError, path1.join("qwoeqiwe").mtime)
993-
pytest.raises(EnvironmentError, path1.join("qwoeqiwe").read)
993+
with pytest.raises(EnvironmentError):
994+
path1.join("qwoeqiwe").mtime()
995+
with pytest.raises(EnvironmentError):
996+
path1.join("qwoeqiwe").read()
994997

995998
# def test_parentdirmatch(self):
996999
# local.parentdirmatch('std', startmodule=__name__)
@@ -1090,7 +1093,8 @@ def test_pyimport_check_filepath_consistency(self, monkeypatch, tmpdir):
10901093
pseudopath = tmpdir.ensure(name + "123.py")
10911094
mod.__file__ = str(pseudopath)
10921095
monkeypatch.setitem(sys.modules, name, mod)
1093-
excinfo = pytest.raises(pseudopath.ImportMismatchError, p.pyimport)
1096+
with pytest.raises(pseudopath.ImportMismatchError) as excinfo:
1097+
p.pyimport()
10941098
modname, modfile, orig = excinfo.value.args
10951099
assert modname == name
10961100
assert modfile == pseudopath
@@ -1388,7 +1392,8 @@ def test_stat_helpers(self, tmpdir, monkeypatch):
13881392

13891393
def test_stat_non_raising(self, tmpdir):
13901394
path1 = tmpdir.join("file")
1391-
pytest.raises(error.ENOENT, path1.stat)
1395+
with pytest.raises(error.ENOENT):
1396+
path1.stat()
13921397
res = path1.stat(raising=False)
13931398
assert res is None
13941399

testing/code/test_code.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,8 @@ def test_code_from_func() -> None:
8585
def test_unicode_handling() -> None:
8686
value = "ąć".encode()
8787

88-
def f() -> None:
89-
raise ValueError(value)
90-
91-
excinfo = pytest.raises(ValueError, f)
88+
with pytest.raises(Exception) as excinfo:
89+
raise Exception(value)
9290
str(excinfo)
9391

9492

0 commit comments

Comments
 (0)