Skip to content

Commit 0520d35

Browse files
committed
add paramspec to raises
1 parent b0caf3d commit 0520d35

File tree

4 files changed

+26
-12
lines changed

4 files changed

+26
-12
lines changed

changelog/12141.improvement.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:func:`pytest.raises` now uses :class:`ParamSpec` for the type hint to the legacy callable overload, instead of :class:`Any`. Also ``func`` can now be passed as a kwarg, which the type hint previously showed as possible but didn't accept.

src/_pytest/python_api.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333

3434
if TYPE_CHECKING:
3535
from numpy import ndarray
36+
from typing_extensions import ParamSpec
37+
38+
P = ParamSpec("P")
3639

3740

3841
def _compare_approx(
@@ -805,14 +808,17 @@ def raises(
805808
@overload
806809
def raises(
807810
expected_exception: type[E] | tuple[type[E], ...],
808-
func: Callable[..., Any],
809-
*args: Any,
810-
**kwargs: Any,
811+
func: Callable[P, object],
812+
*args: P.args,
813+
**kwargs: P.kwargs,
811814
) -> _pytest._code.ExceptionInfo[E]: ...
812815

813816

814817
def raises(
815-
expected_exception: type[E] | tuple[type[E], ...], *args: Any, **kwargs: Any
818+
expected_exception: type[E] | tuple[type[E], ...],
819+
func: Callable[P, object] | None = None,
820+
*args: Any,
821+
**kwargs: Any,
816822
) -> RaisesContext[E] | _pytest._code.ExceptionInfo[E]:
817823
r"""Assert that a code block/function call raises an exception type, or one of its subclasses.
818824
@@ -1003,7 +1009,7 @@ def validate_exc(exc: type[E]) -> type[E]:
10031009

10041010
message = f"DID NOT RAISE {expected_exception}"
10051011

1006-
if not args:
1012+
if func is None and not args:
10071013
match: str | re.Pattern[str] | None = kwargs.pop("match", None)
10081014
if kwargs:
10091015
msg = "Unexpected keyword arguments passed to pytest.raises: "
@@ -1012,11 +1018,10 @@ def validate_exc(exc: type[E]) -> type[E]:
10121018
raise TypeError(msg)
10131019
return RaisesContext(expected_exceptions, message, match)
10141020
else:
1015-
func = args[0]
10161021
if not callable(func):
10171022
raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
10181023
try:
1019-
func(*args[1:], **kwargs)
1024+
func(*args, **kwargs)
10201025
except expected_exceptions as e:
10211026
return _pytest._code.ExceptionInfo.from_exception(e)
10221027
fail(message)

testing/python/raises.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ def test_raises_match(self) -> None:
231231
pytest.raises(ValueError, int, "asdf").match(msg)
232232
assert str(excinfo.value) == expr
233233

234-
pytest.raises(TypeError, int, match="invalid")
234+
pytest.raises(TypeError, int, match="invalid") # type: ignore[call-overload]
235235

236236
def tfunc(match):
237237
raise ValueError(f"match={match}")
@@ -337,3 +337,11 @@ def test_issue_11872(self) -> None:
337337

338338
with pytest.raises(HTTPError, match="Not Found"):
339339
raise HTTPError(code=404, msg="Not Found", fp=None, hdrs=None, url="") # type: ignore [arg-type]
340+
341+
def test_callable_func_kwarg(self) -> None:
342+
# raises previously assumed that `func` was passed as positional, but
343+
# the type hint indicated it could be a keyword parameter
344+
def my_raise() -> None:
345+
raise ValueError
346+
347+
pytest.raises(expected_exception=ValueError, func=my_raise)

testing/test_capture.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -853,7 +853,7 @@ def test_text(self) -> None:
853853
def test_unicode_and_str_mixture(self) -> None:
854854
f = capture.CaptureIO()
855855
f.write("\u00f6")
856-
pytest.raises(TypeError, f.write, b"hello")
856+
pytest.raises(TypeError, f.write, b"hello") # type: ignore[call-overload]
857857

858858
def test_write_bytes_to_buffer(self) -> None:
859859
"""In python3, stdout / stderr are text io wrappers (exposing a buffer
@@ -880,7 +880,7 @@ def test_unicode_and_str_mixture(self) -> None:
880880
sio = io.StringIO()
881881
f = capture.TeeCaptureIO(sio)
882882
f.write("\u00f6")
883-
pytest.raises(TypeError, f.write, b"hello")
883+
pytest.raises(TypeError, f.write, b"hello") # type: ignore[call-overload]
884884

885885

886886
def test_dontreadfrominput() -> None:
@@ -900,7 +900,7 @@ def test_dontreadfrominput() -> None:
900900
assert not f.seekable()
901901
pytest.raises(UnsupportedOperation, f.tell)
902902
pytest.raises(UnsupportedOperation, f.truncate, 0)
903-
pytest.raises(UnsupportedOperation, f.write, b"")
903+
pytest.raises(UnsupportedOperation, f.write, b"") # type: ignore[call-overload]
904904
pytest.raises(UnsupportedOperation, f.writelines, [])
905905
assert not f.writable()
906906
assert isinstance(f.encoding, str)
@@ -1635,7 +1635,7 @@ def test_encodedfile_writelines(tmpfile: BinaryIO) -> None:
16351635

16361636
def test__get_multicapture() -> None:
16371637
assert isinstance(_get_multicapture("no"), MultiCapture)
1638-
pytest.raises(ValueError, _get_multicapture, "unknown").match(
1638+
pytest.raises(ValueError, _get_multicapture, "unknown").match( # type: ignore[call-overload]
16391639
r"^unknown capturing method: 'unknown'"
16401640
)
16411641

0 commit comments

Comments
 (0)