From 0520d3566566a88f1f130949d715dc2fa95bddfa Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Thu, 20 Feb 2025 10:52:38 +0100
Subject: [PATCH 01/15] add paramspec to raises
---
changelog/12141.improvement.rst | 1 +
src/_pytest/python_api.py | 19 ++++++++++++-------
testing/python/raises.py | 10 +++++++++-
testing/test_capture.py | 8 ++++----
4 files changed, 26 insertions(+), 12 deletions(-)
create mode 100644 changelog/12141.improvement.rst
diff --git a/changelog/12141.improvement.rst b/changelog/12141.improvement.rst
new file mode 100644
index 00000000000..da8f6bbbfda
--- /dev/null
+++ b/changelog/12141.improvement.rst
@@ -0,0 +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.
diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py
index ddbf9b87251..552612e205e 100644
--- a/src/_pytest/python_api.py
+++ b/src/_pytest/python_api.py
@@ -33,6 +33,9 @@
if TYPE_CHECKING:
from numpy import ndarray
+ from typing_extensions import ParamSpec
+
+ P = ParamSpec("P")
def _compare_approx(
@@ -805,14 +808,17 @@ def raises(
@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,
) -> _pytest._code.ExceptionInfo[E]: ...
def raises(
- expected_exception: type[E] | tuple[type[E], ...], *args: Any, **kwargs: Any
+ expected_exception: type[E] | tuple[type[E], ...],
+ func: Callable[P, object] | None = None,
+ *args: Any,
+ **kwargs: Any,
) -> RaisesContext[E] | _pytest._code.ExceptionInfo[E]:
r"""Assert that a code block/function call raises an exception type, or one of its subclasses.
@@ -1003,7 +1009,7 @@ def validate_exc(exc: type[E]) -> type[E]:
message = f"DID NOT RAISE {expected_exception}"
- if not args:
+ if func is None and not args:
match: str | re.Pattern[str] | None = kwargs.pop("match", None)
if kwargs:
msg = "Unexpected keyword arguments passed to pytest.raises: "
@@ -1012,11 +1018,10 @@ def validate_exc(exc: type[E]) -> type[E]:
raise TypeError(msg)
return RaisesContext(expected_exceptions, message, match)
else:
- func = args[0]
if not callable(func):
raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
try:
- func(*args[1:], **kwargs)
+ func(*args, **kwargs)
except expected_exceptions as e:
return _pytest._code.ExceptionInfo.from_exception(e)
fail(message)
diff --git a/testing/python/raises.py b/testing/python/raises.py
index 2011c81615e..e75115ade67 100644
--- a/testing/python/raises.py
+++ b/testing/python/raises.py
@@ -231,7 +231,7 @@ def test_raises_match(self) -> None:
pytest.raises(ValueError, int, "asdf").match(msg)
assert str(excinfo.value) == expr
- pytest.raises(TypeError, int, match="invalid")
+ pytest.raises(TypeError, int, match="invalid") # type: ignore[call-overload]
def tfunc(match):
raise ValueError(f"match={match}")
@@ -337,3 +337,11 @@ def test_issue_11872(self) -> None:
with pytest.raises(HTTPError, match="Not Found"):
raise HTTPError(code=404, msg="Not Found", fp=None, hdrs=None, url="") # type: ignore [arg-type]
+
+ def test_callable_func_kwarg(self) -> None:
+ # raises previously assumed that `func` was passed as positional, but
+ # the type hint indicated it could be a keyword parameter
+ def my_raise() -> None:
+ raise ValueError
+
+ pytest.raises(expected_exception=ValueError, func=my_raise)
diff --git a/testing/test_capture.py b/testing/test_capture.py
index a59273734c4..f2da428e303 100644
--- a/testing/test_capture.py
+++ b/testing/test_capture.py
@@ -853,7 +853,7 @@ def test_text(self) -> None:
def test_unicode_and_str_mixture(self) -> None:
f = capture.CaptureIO()
f.write("\u00f6")
- pytest.raises(TypeError, f.write, b"hello")
+ pytest.raises(TypeError, f.write, b"hello") # type: ignore[call-overload]
def test_write_bytes_to_buffer(self) -> None:
"""In python3, stdout / stderr are text io wrappers (exposing a buffer
@@ -880,7 +880,7 @@ def test_unicode_and_str_mixture(self) -> None:
sio = io.StringIO()
f = capture.TeeCaptureIO(sio)
f.write("\u00f6")
- pytest.raises(TypeError, f.write, b"hello")
+ pytest.raises(TypeError, f.write, b"hello") # type: ignore[call-overload]
def test_dontreadfrominput() -> None:
@@ -900,7 +900,7 @@ def test_dontreadfrominput() -> None:
assert not f.seekable()
pytest.raises(UnsupportedOperation, f.tell)
pytest.raises(UnsupportedOperation, f.truncate, 0)
- pytest.raises(UnsupportedOperation, f.write, b"")
+ pytest.raises(UnsupportedOperation, f.write, b"") # type: ignore[call-overload]
pytest.raises(UnsupportedOperation, f.writelines, [])
assert not f.writable()
assert isinstance(f.encoding, str)
@@ -1635,7 +1635,7 @@ def test_encodedfile_writelines(tmpfile: BinaryIO) -> None:
def test__get_multicapture() -> None:
assert isinstance(_get_multicapture("no"), MultiCapture)
- pytest.raises(ValueError, _get_multicapture, "unknown").match(
+ pytest.raises(ValueError, _get_multicapture, "unknown").match( # type: ignore[call-overload]
r"^unknown capturing method: 'unknown'"
)
From d1e7d9dc9f544968385cf7def0213e87f55819e5 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Thu, 20 Feb 2025 11:38:49 +0100
Subject: [PATCH 02/15] ignore RTD warning for paramspec
---
doc/en/conf.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/doc/en/conf.py b/doc/en/conf.py
index 47fc70dce85..f43e3b3b951 100644
--- a/doc/en/conf.py
+++ b/doc/en/conf.py
@@ -97,6 +97,7 @@
# TypeVars
("py:class", "_pytest._code.code.E"),
("py:class", "E"), # due to delayed annotation
+ ("py:class", "P"),
("py:class", "_pytest.fixtures.FixtureFunction"),
("py:class", "_pytest.nodes._NodeType"),
("py:class", "_NodeType"), # due to delayed annotation
From ab69076bf1e0ad9161b8d81b27c91f3f1cfbedf0 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Fri, 21 Feb 2025 18:03:11 +0100
Subject: [PATCH 03/15] well, let's just deprecate these then... ezpzzzz
---
src/_pytest/deprecated.py | 29 ++++
src/_pytest/python_api.py | 5 +
src/_pytest/recwarn.py | 35 +++--
testing/_py/test_local.py | 15 ++-
testing/code/test_code.py | 4 +-
testing/code/test_excinfo.py | 126 ++++++++++++------
testing/code/test_source.py | 9 +-
.../sub2/conftest.py | 3 +-
testing/python/collect.py | 12 +-
testing/python/metafunc.py | 12 +-
testing/python/raises.py | 42 ++++--
testing/test_capture.py | 59 +++++---
testing/test_config.py | 15 ++-
testing/test_debugging.py | 3 +-
testing/test_legacypath.py | 3 +-
testing/test_monkeypatch.py | 12 +-
testing/test_parseopt.py | 3 +-
testing/test_pluginmanager.py | 23 ++--
testing/test_pytester.py | 6 +-
testing/test_recwarn.py | 90 +++++++++----
testing/test_runner.py | 9 +-
testing/test_session.py | 3 +-
22 files changed, 356 insertions(+), 162 deletions(-)
diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py
index a605c24e58f..f10da3ad2ad 100644
--- a/src/_pytest/deprecated.py
+++ b/src/_pytest/deprecated.py
@@ -11,6 +11,8 @@
from __future__ import annotations
+import sys
+from typing import TYPE_CHECKING
from warnings import warn
from _pytest.warning_types import PytestDeprecationWarning
@@ -18,6 +20,33 @@
from _pytest.warning_types import UnformattedWarning
+# the `as` indicates explicit re-export to type checkers
+# mypy currently does not support overload+deprecated
+if sys.version_info >= (3, 13):
+ from warnings import deprecated as deprecated
+elif TYPE_CHECKING:
+ from typing_extensions import deprecated as deprecated
+else:
+
+ def deprecated(func: object) -> object:
+ return func
+
+
+CALLABLE_RAISES = PytestDeprecationWarning(
+ "The callable form of pytest.raises is deprecated.\n"
+ "Use `with pytest.raises(...):` instead."
+)
+
+CALLABLE_WARNS = PytestDeprecationWarning(
+ "The callable form of pytest.warns is deprecated.\n"
+ "Use `with pytest.warns(...):` instead."
+)
+CALLABLE_DEPRECATED_CALL = PytestDeprecationWarning(
+ "The callable form of pytest.deprecated_call is deprecated.\n"
+ "Use `with pytest.deprecated_call():` instead."
+)
+
+
# 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 = {
diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py
index 552612e205e..4756d3ccb1f 100644
--- a/src/_pytest/python_api.py
+++ b/src/_pytest/python_api.py
@@ -22,8 +22,11 @@
from typing import overload
from typing import TYPE_CHECKING
from typing import TypeVar
+import warnings
import _pytest._code
+from _pytest.deprecated import CALLABLE_RAISES
+from _pytest.deprecated import deprecated
from _pytest.outcomes import fail
@@ -806,6 +809,7 @@ def raises(
@overload
+@deprecated("Use context-manager form instead")
def raises(
expected_exception: type[E] | tuple[type[E], ...],
func: Callable[P, object],
@@ -1020,6 +1024,7 @@ def validate_exc(exc: type[E]) -> type[E]:
else:
if not callable(func):
raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
+ warnings.warn(CALLABLE_RAISES, stacklevel=2)
try:
func(*args, **kwargs)
except expected_exceptions as e:
diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py
index 440e3efac8a..881e492da25 100644
--- a/src/_pytest/recwarn.py
+++ b/src/_pytest/recwarn.py
@@ -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(
@@ -79,11 +86,13 @@ def deprecated_call(
one for each warning raised.
"""
__tracebackhide__ = True
+ # potential QoL: allow `with deprecated_call:` - i.e. no parens
+ dep_warnings = (DeprecationWarning, PendingDeprecationWarning, FutureWarning)
if func is not None:
- args = (func, *args)
- return warns(
- (DeprecationWarning, PendingDeprecationWarning, FutureWarning), *args, **kwargs
- )
+ warnings.warn(CALLABLE_DEPRECATED_CALL, stacklevel=2)
+ with warns(dep_warnings):
+ return func(*args, **kwargs)
+ return warns(dep_warnings, *args, **kwargs)
@overload
@@ -95,18 +104,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 +161,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 +171,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]
diff --git a/testing/_py/test_local.py b/testing/_py/test_local.py
index 461b0b599c1..d531f159683 100644
--- a/testing/_py/test_local.py
+++ b/testing/_py/test_local.py
@@ -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
diff --git a/testing/code/test_code.py b/testing/code/test_code.py
index 7ae5ad46100..d1e7efdc678 100644
--- a/testing/code/test_code.py
+++ b/testing/code/test_code.py
@@ -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)
diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py
index 89088576980..3272a24ef75 100644
--- a/testing/code/test_excinfo.py
+++ b/testing/code/test_excinfo.py
@@ -222,17 +222,18 @@ def h():
g()
#
- excinfo = pytest.raises(ValueError, h)
+ with pytest.raises(ValueError) as excinfo:
+ h()
traceback = excinfo.traceback
ntraceback = traceback.filter(excinfo)
print(f"old: {traceback!r}")
print(f"new: {ntraceback!r}")
if matching:
- assert len(ntraceback) == len(traceback) - 2
- else:
# -1 because of the __tracebackhide__ in pytest.raises
assert len(ntraceback) == len(traceback) - 1
+ else:
+ assert len(ntraceback) == len(traceback)
def test_traceback_recursion_index(self):
def f(n):
@@ -240,7 +241,8 @@ def f(n):
n += 1
f(n)
- excinfo = pytest.raises(RecursionError, f, 8)
+ with pytest.raises(RecursionError) as excinfo:
+ f(8)
traceback = excinfo.traceback
recindex = traceback.recursionindex()
assert recindex == 3
@@ -251,7 +253,8 @@ def f(n):
raise RuntimeError("hello")
f(n - 1)
- excinfo = pytest.raises(RuntimeError, f, 25)
+ with pytest.raises(RuntimeError) as excinfo:
+ f(25)
monkeypatch.delattr(excinfo.traceback.__class__, "recursionindex")
repr = excinfo.getrepr()
assert "RuntimeError: hello" in str(repr.reprcrash)
@@ -273,8 +276,8 @@ def f(n: int) -> None:
except BaseException:
reraise_me()
- excinfo = pytest.raises(RuntimeError, f, 8)
- assert excinfo is not None
+ with pytest.raises(RuntimeError) as excinfo:
+ f(8)
traceback = excinfo.traceback
recindex = traceback.recursionindex()
assert recindex is None
@@ -294,7 +297,8 @@ def fail():
fail = log(log(fail))
- excinfo = pytest.raises(ValueError, fail)
+ with pytest.raises(ValueError) as excinfo:
+ fail()
assert excinfo.traceback.recursionindex() is None
def test_getreprcrash(self):
@@ -312,7 +316,8 @@ def g():
def f():
g()
- excinfo = pytest.raises(ValueError, f)
+ with pytest.raises(ValueError) as excinfo:
+ f()
reprcrash = excinfo._getreprcrash()
assert reprcrash is not None
co = _pytest._code.Code.from_function(h)
@@ -320,6 +325,8 @@ def f():
assert reprcrash.lineno == co.firstlineno + 1 + 1
def test_getreprcrash_empty(self):
+ __tracebackhide__ = True
+
def g():
__tracebackhide__ = True
raise ValueError
@@ -328,12 +335,14 @@ def f():
__tracebackhide__ = True
g()
- excinfo = pytest.raises(ValueError, f)
+ with pytest.raises(ValueError) as excinfo:
+ f()
assert excinfo._getreprcrash() is None
def test_excinfo_exconly():
- excinfo = pytest.raises(ValueError, h)
+ with pytest.raises(ValueError) as excinfo:
+ h()
assert excinfo.exconly().startswith("ValueError")
with pytest.raises(ValueError) as excinfo:
raise ValueError("hello\nworld")
@@ -343,7 +352,8 @@ def test_excinfo_exconly():
def test_excinfo_repr_str() -> None:
- excinfo1 = pytest.raises(ValueError, h)
+ with pytest.raises(ValueError) as excinfo1:
+ h()
assert repr(excinfo1) == ""
assert str(excinfo1) == ""
@@ -354,7 +364,8 @@ def __repr__(self):
def raises() -> None:
raise CustomException()
- excinfo2 = pytest.raises(CustomException, raises)
+ with pytest.raises(CustomException) as excinfo2:
+ raises()
assert repr(excinfo2) == ""
assert str(excinfo2) == ""
@@ -366,7 +377,8 @@ def test_excinfo_for_later() -> None:
def test_excinfo_errisinstance():
- excinfo = pytest.raises(ValueError, h)
+ with pytest.raises(ValueError) as excinfo:
+ h()
assert excinfo.errisinstance(ValueError)
@@ -390,7 +402,8 @@ def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None:
loader = jinja2.FileSystemLoader(str(tmp_path))
env = jinja2.Environment(loader=loader)
template = env.get_template("test.txt")
- excinfo = pytest.raises(ValueError, template.render, h=h)
+ with pytest.raises(ValueError) as excinfo:
+ template.render(h=h)
for item in excinfo.traceback:
print(item) # XXX: for some reason jinja.Template.render is printed in full
_ = item.source # shouldn't fail
@@ -754,7 +767,8 @@ def func1(m):
raise ValueError("hello\\nworld")
"""
)
- excinfo = pytest.raises(ValueError, mod.func1, "m" * 500)
+ with pytest.raises(ValueError) as excinfo:
+ mod.func1("m" * 500)
excinfo.traceback = excinfo.traceback.filter(excinfo)
entry = excinfo.traceback[-1]
p = FormattedExcinfo(funcargs=True, truncate_args=True)
@@ -777,7 +791,8 @@ def func1():
raise ValueError("hello\\nworld")
"""
)
- excinfo = pytest.raises(ValueError, mod.func1)
+ with pytest.raises(ValueError) as excinfo:
+ mod.func1()
excinfo.traceback = excinfo.traceback.filter(excinfo)
p = FormattedExcinfo()
reprtb = p.repr_traceback_entry(excinfo.traceback[-1])
@@ -810,7 +825,8 @@ def func1(m, x, y, z):
raise ValueError("hello\\nworld")
"""
)
- excinfo = pytest.raises(ValueError, mod.func1, "m" * 90, 5, 13, "z" * 120)
+ with pytest.raises(ValueError) as excinfo:
+ mod.func1("m" * 90, 5, 13, "z" * 120)
excinfo.traceback = excinfo.traceback.filter(excinfo)
entry = excinfo.traceback[-1]
p = FormattedExcinfo(funcargs=True)
@@ -837,7 +853,8 @@ def func1(x, *y, **z):
raise ValueError("hello\\nworld")
"""
)
- excinfo = pytest.raises(ValueError, mod.func1, "a", "b", c="d")
+ with pytest.raises(ValueError) as excinfo:
+ mod.func1("a", "b", c="d")
excinfo.traceback = excinfo.traceback.filter(excinfo)
entry = excinfo.traceback[-1]
p = FormattedExcinfo(funcargs=True)
@@ -863,7 +880,8 @@ def entry():
func1()
"""
)
- excinfo = pytest.raises(ValueError, mod.entry)
+ with pytest.raises(ValueError) as excinfo:
+ mod.entry()
p = FormattedExcinfo(style="short")
reprtb = p.repr_traceback_entry(excinfo.traceback[-2])
lines = reprtb.lines
@@ -898,7 +916,8 @@ def entry():
func1()
"""
)
- excinfo = pytest.raises(ZeroDivisionError, mod.entry)
+ with pytest.raises(ZeroDivisionError) as excinfo:
+ mod.entry()
p = FormattedExcinfo(style="short")
reprtb = p.repr_traceback_entry(excinfo.traceback[-3])
assert len(reprtb.lines) == 1
@@ -923,7 +942,8 @@ def entry():
func1()
"""
)
- excinfo = pytest.raises(ValueError, mod.entry)
+ with pytest.raises(ValueError) as excinfo:
+ mod.entry()
p = FormattedExcinfo(style="no")
p.repr_traceback_entry(excinfo.traceback[-2])
@@ -934,6 +954,7 @@ def entry():
assert not lines[1:]
def test_repr_traceback_tbfilter(self, importasmod):
+ __tracebackhide__ = True
mod = importasmod(
"""
def f(x):
@@ -942,7 +963,8 @@ def entry():
f(0)
"""
)
- excinfo = pytest.raises(ValueError, mod.entry)
+ with pytest.raises(ValueError) as excinfo:
+ mod.entry()
p = FormattedExcinfo(tbfilter=True)
reprtb = p.repr_traceback(excinfo)
assert len(reprtb.reprentries) == 2
@@ -963,7 +985,8 @@ def entry():
func1()
"""
)
- excinfo = pytest.raises(ValueError, mod.entry)
+ with pytest.raises(ValueError) as excinfo:
+ mod.entry()
from _pytest._code.code import Code
with monkeypatch.context() as mp:
@@ -980,6 +1003,7 @@ def entry():
assert last_lines[1] == "E ValueError: hello"
def test_repr_traceback_and_excinfo(self, importasmod) -> None:
+ __tracebackhide__ = True
mod = importasmod(
"""
def f(x):
@@ -988,7 +1012,8 @@ def entry():
f(0)
"""
)
- excinfo = pytest.raises(ValueError, mod.entry)
+ with pytest.raises(ValueError) as excinfo:
+ mod.entry()
styles: tuple[TracebackStyle, ...] = ("long", "short")
for style in styles:
@@ -1008,6 +1033,7 @@ def entry():
assert repr.reprcrash.message == "ValueError: 0"
def test_repr_traceback_with_invalid_cwd(self, importasmod, monkeypatch) -> None:
+ __tracebackhide__ = True
mod = importasmod(
"""
def f(x):
@@ -1016,7 +1042,8 @@ def entry():
f(0)
"""
)
- excinfo = pytest.raises(ValueError, mod.entry)
+ with pytest.raises(ValueError) as excinfo:
+ mod.entry()
p = FormattedExcinfo(abspath=False)
@@ -1065,7 +1092,8 @@ def entry():
raise ValueError()
"""
)
- excinfo = pytest.raises(ValueError, mod.entry)
+ with pytest.raises(ValueError) as excinfo:
+ mod.entry()
repr = excinfo.getrepr()
repr.addsection("title", "content")
repr.toterminal(tw_mock)
@@ -1079,7 +1107,8 @@ def entry():
raise ValueError()
"""
)
- excinfo = pytest.raises(ValueError, mod.entry)
+ with pytest.raises(ValueError) as excinfo:
+ mod.entry()
repr = excinfo.getrepr()
assert repr.reprcrash is not None
assert repr.reprcrash.path.endswith("mod.py")
@@ -1098,7 +1127,8 @@ def entry():
rec1(42)
"""
)
- excinfo = pytest.raises(RuntimeError, mod.entry)
+ with pytest.raises(RuntimeError) as excinfo:
+ mod.entry()
for style in ("short", "long", "no"):
p = FormattedExcinfo(style="short")
@@ -1115,7 +1145,8 @@ def entry():
f(0)
"""
)
- excinfo = pytest.raises(ValueError, mod.entry)
+ with pytest.raises(ValueError) as excinfo:
+ mod.entry()
styles: tuple[TracebackStyle, ...] = ("short", "long", "no")
for style in styles:
@@ -1138,6 +1169,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
assert x == "я"
def test_toterminal_long(self, importasmod, tw_mock):
+ __tracebackhide__ = True
mod = importasmod(
"""
def g(x):
@@ -1146,7 +1178,8 @@ def f():
g(3)
"""
)
- excinfo = pytest.raises(ValueError, mod.f)
+ with pytest.raises(ValueError) as excinfo:
+ mod.f()
excinfo.traceback = excinfo.traceback.filter(excinfo)
repr = excinfo.getrepr()
repr.toterminal(tw_mock)
@@ -1171,6 +1204,7 @@ def f():
def test_toterminal_long_missing_source(
self, importasmod, tmp_path: Path, tw_mock
) -> None:
+ __tracebackhide__ = True
mod = importasmod(
"""
def g(x):
@@ -1179,7 +1213,8 @@ def f():
g(3)
"""
)
- excinfo = pytest.raises(ValueError, mod.f)
+ with pytest.raises(ValueError) as excinfo:
+ mod.f()
tmp_path.joinpath("mod.py").unlink()
excinfo.traceback = excinfo.traceback.filter(excinfo)
repr = excinfo.getrepr()
@@ -1203,6 +1238,7 @@ def f():
def test_toterminal_long_incomplete_source(
self, importasmod, tmp_path: Path, tw_mock
) -> None:
+ __tracebackhide__ = True
mod = importasmod(
"""
def g(x):
@@ -1211,7 +1247,8 @@ def f():
g(3)
"""
)
- excinfo = pytest.raises(ValueError, mod.f)
+ with pytest.raises(ValueError) as excinfo:
+ mod.f()
tmp_path.joinpath("mod.py").write_text("asdf", encoding="utf-8")
excinfo.traceback = excinfo.traceback.filter(excinfo)
repr = excinfo.getrepr()
@@ -1235,13 +1272,15 @@ def f():
def test_toterminal_long_filenames(
self, importasmod, tw_mock, monkeypatch: MonkeyPatch
) -> None:
+ __tracebackhide__ = True
mod = importasmod(
"""
def f():
raise ValueError()
"""
)
- excinfo = pytest.raises(ValueError, mod.f)
+ with pytest.raises(ValueError) as excinfo:
+ mod.f()
path = Path(mod.__file__)
monkeypatch.chdir(path.parent)
repr = excinfo.getrepr(abspath=False)
@@ -1268,7 +1307,8 @@ def f():
g('some_value')
"""
)
- excinfo = pytest.raises(ValueError, mod.f)
+ with pytest.raises(ValueError) as excinfo:
+ mod.f()
excinfo.traceback = excinfo.traceback.filter(excinfo)
repr = excinfo.getrepr(style="value")
repr.toterminal(tw_mock)
@@ -1312,6 +1352,7 @@ def foo():
assert file.getvalue()
def test_traceback_repr_style(self, importasmod, tw_mock):
+ __tracebackhide__ = True
mod = importasmod(
"""
def f():
@@ -1324,7 +1365,8 @@ def i():
raise ValueError()
"""
)
- excinfo = pytest.raises(ValueError, mod.f)
+ with pytest.raises(ValueError) as excinfo:
+ mod.f()
excinfo.traceback = excinfo.traceback.filter(excinfo)
excinfo.traceback = _pytest._code.Traceback(
entry if i not in (1, 2) else entry.with_repr_style("short")
@@ -1359,6 +1401,7 @@ def i():
assert tw_mock.lines[20] == ":9: ValueError"
def test_exc_chain_repr(self, importasmod, tw_mock):
+ __tracebackhide__ = True
mod = importasmod(
"""
class Err(Exception):
@@ -1377,7 +1420,8 @@ def h():
if True: raise AttributeError()
"""
)
- excinfo = pytest.raises(AttributeError, mod.f)
+ with pytest.raises(AttributeError) as excinfo:
+ mod.f()
r = excinfo.getrepr(style="long")
r.toterminal(tw_mock)
for line in tw_mock.lines:
@@ -1458,6 +1502,7 @@ def test_exc_repr_chain_suppression(self, importasmod, mode, tw_mock):
- When the exception is raised with "from None"
- Explicitly suppressed with "chain=False" to ExceptionInfo.getrepr().
"""
+ __tracebackhide__ = True
raise_suffix = " from None" if mode == "from_none" else ""
mod = importasmod(
f"""
@@ -1470,7 +1515,8 @@ def g():
raise ValueError()
"""
)
- excinfo = pytest.raises(AttributeError, mod.f)
+ with pytest.raises(AttributeError) as excinfo:
+ mod.f()
r = excinfo.getrepr(style="long", chain=mode != "explicit_suppress")
r.toterminal(tw_mock)
for line in tw_mock.lines:
@@ -1547,6 +1593,7 @@ def g():
)
def test_exc_chain_repr_cycle(self, importasmod, tw_mock):
+ __tracebackhide__ = True
mod = importasmod(
"""
class Err(Exception):
@@ -1565,7 +1612,8 @@ def unreraise():
raise e.__cause__
"""
)
- excinfo = pytest.raises(ZeroDivisionError, mod.unreraise)
+ with pytest.raises(ZeroDivisionError) as excinfo:
+ mod.unreraise()
r = excinfo.getrepr(style="short")
r.toterminal(tw_mock)
out = "\n".join(line for line in tw_mock.lines if isinstance(line, str))
diff --git a/testing/code/test_source.py b/testing/code/test_source.py
index 843233fe21e..d5d5f5e1612 100644
--- a/testing/code/test_source.py
+++ b/testing/code/test_source.py
@@ -212,7 +212,8 @@ def test_getstatementrange_out_of_bounds_py3(self) -> None:
def test_getstatementrange_with_syntaxerror_issue7(self) -> None:
source = Source(":")
- pytest.raises(SyntaxError, lambda: source.getstatementrange(0))
+ with pytest.raises(SyntaxError):
+ source.getstatementrange(0)
def test_getstartingblock_singleline() -> None:
@@ -381,7 +382,8 @@ def test_code_of_object_instance_with_call() -> None:
class A:
pass
- pytest.raises(TypeError, lambda: Source(A()))
+ with pytest.raises(TypeError):
+ Source(A())
class WithCall:
def __call__(self) -> None:
@@ -394,7 +396,8 @@ class Hello:
def __call__(self) -> None:
pass
- pytest.raises(TypeError, lambda: Code.from_function(Hello))
+ with pytest.raises(TypeError):
+ Code.from_function(Hello)
def getstatement(lineno: int, source) -> Source:
diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py b/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py
index 112d1e05f27..7119764273b 100644
--- a/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py
+++ b/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py
@@ -6,4 +6,5 @@
@pytest.fixture
def arg2(request):
- pytest.raises(Exception, request.getfixturevalue, "arg1")
+ with pytest.raises(Exception): # noqa: B017
+ request.getfixturevalue("arg1")
diff --git a/testing/python/collect.py b/testing/python/collect.py
index 530f1c340ff..0834204be84 100644
--- a/testing/python/collect.py
+++ b/testing/python/collect.py
@@ -20,7 +20,8 @@
class TestModule:
def test_failing_import(self, pytester: Pytester) -> None:
modcol = pytester.getmodulecol("import alksdjalskdjalkjals")
- pytest.raises(Collector.CollectError, modcol.collect)
+ with pytest.raises(Collector.CollectError):
+ modcol.collect()
def test_import_duplicate(self, pytester: Pytester) -> None:
a = pytester.mkdir("a")
@@ -72,12 +73,15 @@ def test():
def test_syntax_error_in_module(self, pytester: Pytester) -> None:
modcol = pytester.getmodulecol("this is a syntax error")
- pytest.raises(modcol.CollectError, modcol.collect)
- pytest.raises(modcol.CollectError, modcol.collect)
+ with pytest.raises(modcol.CollectError):
+ modcol.collect()
+ with pytest.raises(modcol.CollectError):
+ modcol.collect()
def test_module_considers_pluginmanager_at_import(self, pytester: Pytester) -> None:
modcol = pytester.getmodulecol("pytest_plugins='xasdlkj',")
- pytest.raises(ImportError, lambda: modcol.obj)
+ with pytest.raises(ImportError):
+ modcol.obj()
def test_invalid_test_module_name(self, pytester: Pytester) -> None:
a = pytester.mkdir("a")
diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py
index 4e7e441768c..a0bc05a184e 100644
--- a/testing/python/metafunc.py
+++ b/testing/python/metafunc.py
@@ -81,11 +81,15 @@ def func(x, y):
metafunc = self.Metafunc(func)
metafunc.parametrize("x", [1, 2])
- pytest.raises(ValueError, lambda: metafunc.parametrize("x", [5, 6]))
- pytest.raises(ValueError, lambda: metafunc.parametrize("x", [5, 6]))
+ with pytest.raises(ValueError):
+ metafunc.parametrize("x", [5, 6])
+ with pytest.raises(ValueError):
+ metafunc.parametrize("x", [5, 6])
metafunc.parametrize("y", [1, 2])
- pytest.raises(ValueError, lambda: metafunc.parametrize("y", [5, 6]))
- pytest.raises(ValueError, lambda: metafunc.parametrize("y", [5, 6]))
+ with pytest.raises(ValueError):
+ metafunc.parametrize("y", [5, 6])
+ with pytest.raises(ValueError):
+ metafunc.parametrize("y", [5, 6])
with pytest.raises(TypeError, match="^ids must be a callable or an iterable$"):
metafunc.parametrize("y", [5, 6], ids=42) # type: ignore[arg-type]
diff --git a/testing/python/raises.py b/testing/python/raises.py
index e75115ade67..78c540d292b 100644
--- a/testing/python/raises.py
+++ b/testing/python/raises.py
@@ -15,11 +15,13 @@ def test_check_callable(self) -> None:
pytest.raises(RuntimeError, "int('qwe')") # type: ignore[call-overload]
def test_raises(self):
- excinfo = pytest.raises(ValueError, int, "qwe")
+ with pytest.raises(ValueError) as excinfo:
+ int("qwe")
assert "invalid literal" in str(excinfo.value)
def test_raises_function(self):
- excinfo = pytest.raises(ValueError, int, "hello")
+ with pytest.raises(ValueError) as excinfo:
+ int("hello")
assert "invalid literal" in str(excinfo.value)
def test_raises_does_not_allow_none(self):
@@ -38,7 +40,8 @@ def __call__(self):
pass
try:
- pytest.raises(ValueError, A())
+ with pytest.warns(pytest.PytestDeprecationWarning):
+ pytest.raises(ValueError, A())
except pytest.fail.Exception:
pass
@@ -154,7 +157,8 @@ def test_invalid_regex():
def test_noclass(self) -> None:
with pytest.raises(TypeError):
- pytest.raises("wrong", lambda: None) # type: ignore[call-overload]
+ with pytest.raises("wrong"): # type: ignore[call-overload]
+ ... # pragma: no cover
def test_invalid_arguments_to_raises(self) -> None:
with pytest.raises(TypeError, match="unknown"):
@@ -167,7 +171,8 @@ def test_tuple(self):
def test_no_raise_message(self) -> None:
try:
- pytest.raises(ValueError, int, "0")
+ with pytest.raises(ValueError):
+ int("0")
except pytest.fail.Exception as e:
assert e.msg == f"DID NOT RAISE {ValueError!r}"
else:
@@ -194,9 +199,11 @@ def __call__(self):
refcount = len(gc.get_referrers(t))
if method == "function":
- pytest.raises(ValueError, t)
+ with pytest.warns(pytest.PytestDeprecationWarning):
+ pytest.raises(ValueError, t)
elif method == "function_match":
- pytest.raises(ValueError, t).match("^$")
+ with pytest.warns(pytest.PytestDeprecationWarning):
+ pytest.raises(ValueError, t).match("^$")
else:
with pytest.raises(ValueError):
t()
@@ -226,18 +233,23 @@ def test_raises_match(self) -> None:
int("asdf", base=10)
# "match" without context manager.
- pytest.raises(ValueError, int, "asdf").match("invalid literal")
+ with pytest.warns(pytest.PytestDeprecationWarning):
+ pytest.raises(ValueError, int, "asdf").match("invalid literal")
with pytest.raises(AssertionError) as excinfo:
- pytest.raises(ValueError, int, "asdf").match(msg)
+ with pytest.warns(pytest.PytestDeprecationWarning):
+ pytest.raises(ValueError, int, "asdf").match(msg)
assert str(excinfo.value) == expr
- pytest.raises(TypeError, int, match="invalid") # type: ignore[call-overload]
+ with pytest.warns(pytest.PytestDeprecationWarning):
+ pytest.raises(TypeError, int, match="invalid") # type: ignore[call-overload]
def tfunc(match):
raise ValueError(f"match={match}")
- pytest.raises(ValueError, tfunc, match="asdf").match("match=asdf")
- pytest.raises(ValueError, tfunc, match="").match("match=")
+ with pytest.warns(pytest.PytestDeprecationWarning):
+ pytest.raises(ValueError, tfunc, match="asdf").match("match=asdf")
+ with pytest.warns(pytest.PytestDeprecationWarning):
+ pytest.raises(ValueError, tfunc, match="").match("match=")
def test_match_failure_string_quoting(self):
with pytest.raises(AssertionError) as excinfo:
@@ -284,7 +296,8 @@ class ClassLooksIterableException(Exception, metaclass=Meta):
Failed,
match=r"DID NOT RAISE ",
):
- pytest.raises(ClassLooksIterableException, lambda: None)
+ with pytest.raises(ClassLooksIterableException):
+ ... # pragma: no cover
def test_raises_with_raising_dunder_class(self) -> None:
"""Test current behavior with regard to exceptions via __class__ (#4284)."""
@@ -344,4 +357,5 @@ def test_callable_func_kwarg(self) -> None:
def my_raise() -> None:
raise ValueError
- pytest.raises(expected_exception=ValueError, func=my_raise)
+ with pytest.warns(pytest.PytestDeprecationWarning):
+ pytest.raises(expected_exception=ValueError, func=my_raise)
diff --git a/testing/test_capture.py b/testing/test_capture.py
index f2da428e303..af258041289 100644
--- a/testing/test_capture.py
+++ b/testing/test_capture.py
@@ -96,7 +96,8 @@ def test_init_capturing(self):
try:
capman = CaptureManager("fd")
capman.start_global_capturing()
- pytest.raises(AssertionError, capman.start_global_capturing)
+ with pytest.raises(AssertionError):
+ capman.start_global_capturing()
capman.stop_global_capturing()
finally:
capouter.stop_capturing()
@@ -853,7 +854,8 @@ def test_text(self) -> None:
def test_unicode_and_str_mixture(self) -> None:
f = capture.CaptureIO()
f.write("\u00f6")
- pytest.raises(TypeError, f.write, b"hello") # type: ignore[call-overload]
+ with pytest.raises(TypeError):
+ f.write(b"hello") # type: ignore[arg-type]
def test_write_bytes_to_buffer(self) -> None:
"""In python3, stdout / stderr are text io wrappers (exposing a buffer
@@ -880,7 +882,8 @@ def test_unicode_and_str_mixture(self) -> None:
sio = io.StringIO()
f = capture.TeeCaptureIO(sio)
f.write("\u00f6")
- pytest.raises(TypeError, f.write, b"hello") # type: ignore[call-overload]
+ with pytest.raises(TypeError):
+ f.write(b"hello") # type: ignore[arg-type]
def test_dontreadfrominput() -> None:
@@ -889,19 +892,29 @@ def test_dontreadfrominput() -> None:
f = DontReadFromInput()
assert f.buffer is f # type: ignore[comparison-overlap]
assert not f.isatty()
- pytest.raises(OSError, f.read)
- pytest.raises(OSError, f.readlines)
+ with pytest.raises(OSError):
+ f.read()
+ with pytest.raises(OSError):
+ f.readlines()
iter_f = iter(f)
- pytest.raises(OSError, next, iter_f)
- pytest.raises(UnsupportedOperation, f.fileno)
- pytest.raises(UnsupportedOperation, f.flush)
+ with pytest.raises(OSError):
+ next(iter_f)
+ with pytest.raises(UnsupportedOperation):
+ f.fileno()
+ with pytest.raises(UnsupportedOperation):
+ f.flush()
assert not f.readable()
- pytest.raises(UnsupportedOperation, f.seek, 0)
+ with pytest.raises(UnsupportedOperation):
+ f.seek(0)
assert not f.seekable()
- pytest.raises(UnsupportedOperation, f.tell)
- pytest.raises(UnsupportedOperation, f.truncate, 0)
- pytest.raises(UnsupportedOperation, f.write, b"") # type: ignore[call-overload]
- pytest.raises(UnsupportedOperation, f.writelines, [])
+ with pytest.raises(UnsupportedOperation):
+ f.tell()
+ with pytest.raises(UnsupportedOperation):
+ f.truncate(0)
+ with pytest.raises(UnsupportedOperation):
+ f.write(b"") # type: ignore[arg-type]
+ with pytest.raises(UnsupportedOperation):
+ f.writelines([])
assert not f.writable()
assert isinstance(f.encoding, str)
f.close() # just for completeness
@@ -968,7 +981,8 @@ def test_simple(self, tmpfile: BinaryIO) -> None:
cap = capture.FDCapture(fd)
data = b"hello"
os.write(fd, data)
- pytest.raises(AssertionError, cap.snap)
+ with pytest.raises(AssertionError):
+ cap.snap()
cap.done()
cap = capture.FDCapture(fd)
cap.start()
@@ -990,7 +1004,8 @@ def test_simple_fail_second_start(self, tmpfile: BinaryIO) -> None:
fd = tmpfile.fileno()
cap = capture.FDCapture(fd)
cap.done()
- pytest.raises(AssertionError, cap.start)
+ with pytest.raises(AssertionError):
+ cap.start()
def test_stderr(self) -> None:
cap = capture.FDCapture(2)
@@ -1041,7 +1056,8 @@ def test_simple_resume_suspend(self) -> None:
assert s == "but now yes\n"
cap.suspend()
cap.done()
- pytest.raises(AssertionError, cap.suspend)
+ with pytest.raises(AssertionError):
+ cap.suspend()
assert repr(cap) == (
f""
@@ -1123,7 +1139,8 @@ def test_reset_twice_error(self) -> None:
with self.getcapture() as cap:
print("hello")
out, err = cap.readouterr()
- pytest.raises(ValueError, cap.stop_capturing)
+ with pytest.raises(ValueError):
+ cap.stop_capturing()
assert out == "hello\n"
assert not err
@@ -1181,7 +1198,8 @@ def test_stdin_nulled_by_default(self) -> None:
print("XXX which indicates an error in the underlying capturing")
print("XXX mechanisms")
with self.getcapture():
- pytest.raises(OSError, sys.stdin.read)
+ with pytest.raises(OSError):
+ sys.stdin.read()
class TestTeeStdCapture(TestStdCapture):
@@ -1635,9 +1653,8 @@ def test_encodedfile_writelines(tmpfile: BinaryIO) -> None:
def test__get_multicapture() -> None:
assert isinstance(_get_multicapture("no"), MultiCapture)
- pytest.raises(ValueError, _get_multicapture, "unknown").match( # type: ignore[call-overload]
- r"^unknown capturing method: 'unknown'"
- )
+ with pytest.raises(ValueError, match=r"^unknown capturing method: 'unknown'$"):
+ _get_multicapture("unknown") # type: ignore[arg-type]
def test_logging_while_collecting(pytester: Pytester) -> None:
diff --git a/testing/test_config.py b/testing/test_config.py
index de07141238c..612fb432848 100644
--- a/testing/test_config.py
+++ b/testing/test_config.py
@@ -555,7 +555,8 @@ def test_args_source_testpaths(self, pytester: Pytester):
class TestConfigCmdlineParsing:
def test_parsing_again_fails(self, pytester: Pytester) -> None:
config = pytester.parseconfig()
- pytest.raises(AssertionError, lambda: config.parse([]))
+ with pytest.raises(AssertionError):
+ config.parse([])
def test_explicitly_specified_config_file_is_loaded(
self, pytester: Pytester
@@ -646,7 +647,8 @@ def pytest_addoption(parser):
config = pytester.parseconfig("--hello=this")
for x in ("hello", "--hello", "-X"):
assert config.getoption(x) == "this"
- pytest.raises(ValueError, config.getoption, "qweqwe")
+ with pytest.raises(ValueError):
+ config.getoption("qweqwe")
config_novalue = pytester.parseconfig()
assert config_novalue.getoption("hello") is None
@@ -672,7 +674,8 @@ def pytest_addoption(parser):
def test_config_getvalueorskip(self, pytester: Pytester) -> None:
config = pytester.parseconfig()
- pytest.raises(pytest.skip.Exception, config.getvalueorskip, "hello")
+ with pytest.raises(pytest.skip.Exception):
+ config.getvalueorskip("hello")
verbose = config.getvalueorskip("verbose")
assert verbose == config.option.verbose
@@ -720,7 +723,8 @@ def pytest_addoption(parser):
config = pytester.parseconfig()
val = config.getini("myname")
assert val == "hello"
- pytest.raises(ValueError, config.getini, "other")
+ with pytest.raises(ValueError):
+ config.getini("other")
@pytest.mark.parametrize("config_type", ["ini", "pyproject"])
def test_addini_paths(self, pytester: Pytester, config_type: str) -> None:
@@ -750,7 +754,8 @@ def pytest_addoption(parser):
assert len(values) == 2
assert values[0] == inipath.parent.joinpath("hello")
assert values[1] == inipath.parent.joinpath("world/sub.py")
- pytest.raises(ValueError, config.getini, "other")
+ with pytest.raises(ValueError):
+ config.getini("other")
def make_conftest_for_args(self, pytester: Pytester) -> None:
pytester.makeconftest(
diff --git a/testing/test_debugging.py b/testing/test_debugging.py
index 9588da8936f..03610147410 100644
--- a/testing/test_debugging.py
+++ b/testing/test_debugging.py
@@ -304,7 +304,8 @@ def test_pdb_interaction_exception(self, pytester: Pytester) -> None:
def globalfunc():
pass
def test_1():
- pytest.raises(ValueError, globalfunc)
+ with pytest.raises(ValueError):
+ globalfunc()
"""
)
child = pytester.spawn_pytest(f"--pdb {p1}")
diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py
index 72854e4e5c0..ba7f93b1016 100644
--- a/testing/test_legacypath.py
+++ b/testing/test_legacypath.py
@@ -141,7 +141,8 @@ def pytest_addoption(parser):
assert len(values) == 2
assert values[0] == inipath.parent.joinpath("hello")
assert values[1] == inipath.parent.joinpath("world/sub.py")
- pytest.raises(ValueError, config.getini, "other")
+ with pytest.raises(ValueError):
+ config.getini("other")
def test_override_ini_paths(pytester: pytest.Pytester) -> None:
diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py
index ad75273d703..4d45f6a55c1 100644
--- a/testing/test_monkeypatch.py
+++ b/testing/test_monkeypatch.py
@@ -27,7 +27,8 @@ class A:
x = 1
monkeypatch = MonkeyPatch()
- pytest.raises(AttributeError, monkeypatch.setattr, A, "notexists", 2)
+ with pytest.raises(AttributeError):
+ monkeypatch.setattr(A, "notexists", 2)
monkeypatch.setattr(A, "y", 2, raising=False)
assert A.y == 2 # type: ignore
monkeypatch.undo()
@@ -108,7 +109,8 @@ class A:
monkeypatch = MonkeyPatch()
monkeypatch.delattr(A, "x")
- pytest.raises(AttributeError, monkeypatch.delattr, A, "y")
+ with pytest.raises(AttributeError):
+ monkeypatch.delattr(A, "y")
monkeypatch.delattr(A, "y", raising=False)
monkeypatch.setattr(A, "x", 5, raising=False)
assert A.x == 5
@@ -165,7 +167,8 @@ def test_delitem() -> None:
monkeypatch.delitem(d, "x")
assert "x" not in d
monkeypatch.delitem(d, "y", raising=False)
- pytest.raises(KeyError, monkeypatch.delitem, d, "y")
+ with pytest.raises(KeyError):
+ monkeypatch.delitem(d, "y")
assert not d
monkeypatch.setitem(d, "y", 1700)
assert d["y"] == 1700
@@ -191,7 +194,8 @@ def test_delenv() -> None:
name = "xyz1234"
assert name not in os.environ
monkeypatch = MonkeyPatch()
- pytest.raises(KeyError, monkeypatch.delenv, name, raising=True)
+ with pytest.raises(KeyError):
+ monkeypatch.delenv(name, raising=True)
monkeypatch.delenv(name, raising=False)
monkeypatch.undo()
os.environ[name] = "1"
diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py
index 14e2b5f69fb..dfd29494362 100644
--- a/testing/test_parseopt.py
+++ b/testing/test_parseopt.py
@@ -24,7 +24,8 @@ def parser() -> parseopt.Parser:
class TestParser:
def test_no_help_by_default(self) -> None:
parser = parseopt.Parser(usage="xyz", _ispytest=True)
- pytest.raises(UsageError, lambda: parser.parse(["-h"]))
+ with pytest.raises(UsageError):
+ parser.parse(["-h"])
def test_custom_prog(self, parser: parseopt.Parser) -> None:
"""Custom prog can be set for `argparse.ArgumentParser`."""
diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py
index db85124bf0d..d6f21167b7e 100644
--- a/testing/test_pluginmanager.py
+++ b/testing/test_pluginmanager.py
@@ -268,8 +268,10 @@ def test_register_imported_modules(self) -> None:
assert pm.is_registered(mod)
values = pm.get_plugins()
assert mod in values
- pytest.raises(ValueError, pm.register, mod)
- pytest.raises(ValueError, lambda: pm.register(mod))
+ with pytest.raises(ValueError):
+ pm.register(mod)
+ with pytest.raises(ValueError):
+ pm.register(mod)
# assert not pm.is_registered(mod2)
assert pm.get_plugins() == values
@@ -376,8 +378,10 @@ def test_hello(pytestconfig):
def test_import_plugin_importname(
self, pytester: Pytester, pytestpm: PytestPluginManager
) -> None:
- pytest.raises(ImportError, pytestpm.import_plugin, "qweqwex.y")
- pytest.raises(ImportError, pytestpm.import_plugin, "pytest_qweqwx.y")
+ with pytest.raises(ImportError):
+ pytestpm.import_plugin("qweqwex.y")
+ with pytest.raises(ImportError):
+ pytestpm.import_plugin("pytest_qweqwx.y")
pytester.syspathinsert()
pluginname = "pytest_hello"
@@ -396,8 +400,10 @@ def test_import_plugin_importname(
def test_import_plugin_dotted_name(
self, pytester: Pytester, pytestpm: PytestPluginManager
) -> None:
- pytest.raises(ImportError, pytestpm.import_plugin, "qweqwex.y")
- pytest.raises(ImportError, pytestpm.import_plugin, "pytest_qweqwex.y")
+ with pytest.raises(ImportError):
+ pytestpm.import_plugin("qweqwex.y")
+ with pytest.raises(ImportError):
+ pytestpm.import_plugin("pytest_qweqwex.y")
pytester.syspathinsert()
pytester.mkpydir("pkg").joinpath("plug.py").write_text("x=3", encoding="utf-8")
@@ -423,9 +429,8 @@ def test_consider_conftest_deps(
class TestPytestPluginManagerBootstrapping:
def test_preparse_args(self, pytestpm: PytestPluginManager) -> None:
- pytest.raises(
- ImportError, lambda: pytestpm.consider_preparse(["xyz", "-p", "hello123"])
- )
+ with pytest.raises(ImportError):
+ pytestpm.consider_preparse(["xyz", "-p", "hello123"])
# Handles -p without space (#3532).
with pytest.raises(ImportError) as excinfo:
diff --git a/testing/test_pytester.py b/testing/test_pytester.py
index 87714b4708f..ac6ab7141a3 100644
--- a/testing/test_pytester.py
+++ b/testing/test_pytester.py
@@ -71,7 +71,8 @@ class rep2:
recorder.unregister() # type: ignore[attr-defined]
recorder.clear()
recorder.hook.pytest_runtest_logreport(report=rep3) # type: ignore[attr-defined]
- pytest.raises(ValueError, recorder.getfailures)
+ with pytest.raises(ValueError):
+ recorder.getfailures()
def test_parseconfig(pytester: Pytester) -> None:
@@ -196,7 +197,8 @@ def test_hookrecorder_basic(holder) -> None:
call = rec.popcall("pytest_xyz")
assert call.arg == 123
assert call._name == "pytest_xyz"
- pytest.raises(pytest.fail.Exception, rec.popcall, "abc")
+ with pytest.raises(pytest.fail.Exception):
+ rec.popcall("abc")
pm.hook.pytest_xyz_noarg()
call = rec.popcall("pytest_xyz_noarg")
assert call._name == "pytest_xyz_noarg"
diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py
index 384f2b66a15..ffb9ab80b85 100644
--- a/testing/test_recwarn.py
+++ b/testing/test_recwarn.py
@@ -1,15 +1,21 @@
# mypy: allow-untyped-defs
from __future__ import annotations
+import re
import sys
import warnings
+from _pytest.warning_types import PytestDeprecationWarning
import pytest
from pytest import ExitCode
from pytest import Pytester
from pytest import WarningsRecorder
+def wrap_escape(s: str) -> str:
+ return "^" + re.escape(s) + "$"
+
+
def test_recwarn_stacklevel(recwarn: WarningsRecorder) -> None:
warnings.warn("hello")
warn = recwarn.pop()
@@ -97,7 +103,8 @@ def test_recording(self) -> None:
rec.clear()
assert len(rec.list) == 0
assert values is rec.list
- pytest.raises(AssertionError, rec.pop)
+ with pytest.raises(AssertionError):
+ rec.pop()
def test_warn_stacklevel(self) -> None:
"""#4243"""
@@ -145,13 +152,27 @@ def dep_explicit(self, i: int) -> None:
def test_deprecated_call_raises(self) -> None:
with pytest.raises(pytest.fail.Exception, match="No warnings of type"):
- pytest.deprecated_call(self.dep, 3, 5)
+ with pytest.deprecated_call():
+ self.dep(3, 5)
def test_deprecated_call(self) -> None:
- pytest.deprecated_call(self.dep, 0, 5)
+ with pytest.deprecated_call():
+ self.dep(0, 5)
def test_deprecated_call_ret(self) -> None:
- ret = pytest.deprecated_call(self.dep, 0)
+ with pytest.warns(
+ PytestDeprecationWarning,
+ match=(
+ wrap_escape(
+ "The callable form of pytest.deprecated_call is deprecated.\n"
+ "Use `with pytest.deprecated_call():` instead."
+ )
+ ),
+ ):
+ ret = pytest.deprecated_call(self.dep, 0)
+ assert ret == 42
+ with pytest.deprecated_call():
+ ret = self.dep(0)
assert ret == 42
def test_deprecated_call_preserves(self) -> None:
@@ -170,11 +191,14 @@ def test_deprecated_call_preserves(self) -> None:
def test_deprecated_explicit_call_raises(self) -> None:
with pytest.raises(pytest.fail.Exception):
- pytest.deprecated_call(self.dep_explicit, 3)
+ with pytest.deprecated_call():
+ self.dep_explicit(3)
def test_deprecated_explicit_call(self) -> None:
- pytest.deprecated_call(self.dep_explicit, 0)
- pytest.deprecated_call(self.dep_explicit, 0)
+ with pytest.deprecated_call():
+ self.dep_explicit(0)
+ with pytest.deprecated_call():
+ self.dep_explicit(0)
@pytest.mark.parametrize("mode", ["context_manager", "call"])
def test_deprecated_call_no_warning(self, mode) -> None:
@@ -188,7 +212,8 @@ def f():
msg = "No warnings of type (.*DeprecationWarning.*, .*PendingDeprecationWarning.*)"
with pytest.raises(pytest.fail.Exception, match=msg):
if mode == "call":
- pytest.deprecated_call(f)
+ with pytest.warns(PytestDeprecationWarning):
+ pytest.deprecated_call(f)
else:
with pytest.deprecated_call():
f()
@@ -233,7 +258,8 @@ def f():
with pytest.warns(warning):
with pytest.raises(pytest.fail.Exception):
- pytest.deprecated_call(f)
+ with pytest.warns(PytestDeprecationWarning):
+ pytest.deprecated_call(f)
with pytest.raises(pytest.fail.Exception):
with pytest.deprecated_call():
f()
@@ -256,32 +282,38 @@ def test_check_callable(self) -> None:
def test_several_messages(self) -> None:
# different messages, b/c Python suppresses multiple identical warnings
- pytest.warns(RuntimeWarning, lambda: warnings.warn("w1", RuntimeWarning))
+ with pytest.warns(RuntimeWarning):
+ warnings.warn("w1", RuntimeWarning)
with pytest.warns(RuntimeWarning):
with pytest.raises(pytest.fail.Exception):
- pytest.warns(UserWarning, lambda: warnings.warn("w2", RuntimeWarning))
- pytest.warns(RuntimeWarning, lambda: warnings.warn("w3", RuntimeWarning))
+ with pytest.warns(UserWarning):
+ warnings.warn("w2", RuntimeWarning)
+ with pytest.warns(RuntimeWarning):
+ warnings.warn("w3", RuntimeWarning)
def test_function(self) -> None:
- pytest.warns(
- SyntaxWarning, lambda msg: warnings.warn(msg, SyntaxWarning), "syntax"
- )
+ with pytest.warns(
+ PytestDeprecationWarning,
+ match=(
+ wrap_escape(
+ "The callable form of pytest.warns is deprecated.\n"
+ "Use `with pytest.warns(...):` instead."
+ )
+ ),
+ ):
+ pytest.warns(
+ SyntaxWarning, lambda msg: warnings.warn(msg, SyntaxWarning), "syntax"
+ )
def test_warning_tuple(self) -> None:
- pytest.warns(
- (RuntimeWarning, SyntaxWarning), lambda: warnings.warn("w1", RuntimeWarning)
- )
- pytest.warns(
- (RuntimeWarning, SyntaxWarning), lambda: warnings.warn("w2", SyntaxWarning)
- )
- with pytest.warns():
- pytest.raises(
- pytest.fail.Exception,
- lambda: pytest.warns(
- (RuntimeWarning, SyntaxWarning),
- lambda: warnings.warn("w3", UserWarning),
- ),
- )
+ with pytest.warns((RuntimeWarning, SyntaxWarning)):
+ warnings.warn("w1", RuntimeWarning)
+ with pytest.warns((RuntimeWarning, SyntaxWarning)):
+ warnings.warn("w2", SyntaxWarning)
+ with pytest.warns(UserWarning, match="^w3$"):
+ with pytest.raises(pytest.fail.Exception):
+ with pytest.warns((RuntimeWarning, SyntaxWarning)):
+ warnings.warn("w3", UserWarning)
def test_as_contextmanager(self) -> None:
with pytest.warns(RuntimeWarning):
diff --git a/testing/test_runner.py b/testing/test_runner.py
index 0245438a47d..a6021ba06a8 100644
--- a/testing/test_runner.py
+++ b/testing/test_runner.py
@@ -771,7 +771,8 @@ def f():
assert sysmod is sys
# path = pytest.importorskip("os.path")
# assert path == os.path
- excinfo = pytest.raises(pytest.skip.Exception, f)
+ with pytest.raises(pytest.skip.Exception) as excinfo:
+ f()
assert excinfo is not None
excrepr = excinfo.getrepr()
assert excrepr is not None
@@ -780,8 +781,10 @@ def f():
# check that importorskip reports the actual call
# in this test the test_runner.py file
assert path.stem == "test_runner"
- pytest.raises(SyntaxError, pytest.importorskip, "x y z")
- pytest.raises(SyntaxError, pytest.importorskip, "x=y")
+ with pytest.raises(SyntaxError):
+ pytest.importorskip("x y z")
+ with pytest.raises(SyntaxError):
+ pytest.importorskip("x=y")
mod = types.ModuleType("hello123")
mod.__version__ = "1.3" # type: ignore
monkeypatch.setitem(sys.modules, "hello123", mod)
diff --git a/testing/test_session.py b/testing/test_session.py
index ba904916033..c6b5717ca83 100644
--- a/testing/test_session.py
+++ b/testing/test_session.py
@@ -63,7 +63,8 @@ def test_raises_output(self, pytester: Pytester) -> None:
"""
import pytest
def test_raises_doesnt():
- pytest.raises(ValueError, int, "3")
+ with pytest.raises(ValueError):
+ int("3")
"""
)
passed, skipped, failed = reprec.listoutcomes()
From d097b367ed5915fa2dd7188c79400c3c790afd64 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Fri, 21 Feb 2025 18:08:04 +0100
Subject: [PATCH 04/15] fix decorator
---
src/_pytest/deprecated.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py
index f10da3ad2ad..9a4b1850c46 100644
--- a/src/_pytest/deprecated.py
+++ b/src/_pytest/deprecated.py
@@ -28,8 +28,11 @@
from typing_extensions import deprecated as deprecated
else:
- def deprecated(func: object) -> object:
- return func
+ def deprecated(reason: str = "") -> object:
+ def decorator(func: object) -> object:
+ return func
+
+ return decorator
CALLABLE_RAISES = PytestDeprecationWarning(
From c8566ca52a7e62e2e71ed491e7b1b2e7dc0545e1 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Fri, 21 Feb 2025 18:13:26 +0100
Subject: [PATCH 05/15] ignore typevars in conftest
---
doc/en/conf.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/doc/en/conf.py b/doc/en/conf.py
index f43e3b3b951..9bb5dc29d2f 100644
--- a/doc/en/conf.py
+++ b/doc/en/conf.py
@@ -97,7 +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
From fee7cf75debbab3e8fa42658b8b3d8ec42500d94 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Mon, 24 Feb 2025 11:47:04 +0100
Subject: [PATCH 06/15] update docs, add changelog, fix tests
---
changelog/12141.improvement.rst | 1 -
changelog/13241.deprecation.rst | 1 +
changelog/13241.improvement.rst | 2 ++
doc/en/deprecations.rst | 31 +++++++++++++++++++
doc/en/how-to/assert.rst | 24 --------------
doc/en/how-to/capture-warnings.rst | 7 -----
src/_pytest/python_api.py | 21 +------------
src/_pytest/recwarn.py | 22 ++++++-------
.../sub2/conftest.py | 2 +-
testing/test_debugging.py | 2 +-
10 files changed, 47 insertions(+), 66 deletions(-)
delete mode 100644 changelog/12141.improvement.rst
create mode 100644 changelog/13241.deprecation.rst
create mode 100644 changelog/13241.improvement.rst
diff --git a/changelog/12141.improvement.rst b/changelog/12141.improvement.rst
deleted file mode 100644
index da8f6bbbfda..00000000000
--- a/changelog/12141.improvement.rst
+++ /dev/null
@@ -1 +0,0 @@
-: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.
diff --git a/changelog/13241.deprecation.rst b/changelog/13241.deprecation.rst
new file mode 100644
index 00000000000..2a57bc075af
--- /dev/null
+++ b/changelog/13241.deprecation.rst
@@ -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.
diff --git a/changelog/13241.improvement.rst b/changelog/13241.improvement.rst
new file mode 100644
index 00000000000..1ac82f051cf
--- /dev/null
+++ b/changelog/13241.improvement.rst
@@ -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.
diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst
index 18df64c9204..58e1f623851 100644
--- a/doc/en/deprecations.rst
+++ b/doc/en/deprecations.rst
@@ -14,6 +14,37 @@ 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 `.
+legacy callable form of :func:`raises`, :func:`warns` and :func:`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 callable form
+
+ excinfo = pytest.raises(ValueError, int, "hello")
+ ret1 = pytest.warns(DeprecationWarning, my_warns, "a", "b", "c")
+ ret2 = pytest.deprecated_call(my_warns, "d", "e", "f")
+
+ # 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")
+
.. _sync-test-async-fixture:
diff --git a/doc/en/how-to/assert.rst b/doc/en/how-to/assert.rst
index 7b027744695..844213723b6 100644
--- a/doc/en/how-to/assert.rst
+++ b/doc/en/how-to/assert.rst
@@ -194,30 +194,6 @@ exception at a specific level; exceptions contained directly in the top
assert not excinfo.group_contains(RuntimeError, depth=2)
assert not excinfo.group_contains(TypeError, depth=1)
-Alternate form (legacy)
-~~~~~~~~~~~~~~~~~~~~~~~
-
-There is an alternate form where you pass
-a function that will be executed, along ``*args`` and ``**kwargs``, and :func:`pytest.raises`
-will execute the function with the arguments and assert that the given exception is raised:
-
-.. code-block:: python
-
- def func(x):
- if x <= 0:
- raise ValueError("x needs to be larger than zero")
-
-
- 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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/doc/en/how-to/capture-warnings.rst b/doc/en/how-to/capture-warnings.rst
index 4b1de6f3704..33ddd6ddfdd 100644
--- a/doc/en/how-to/capture-warnings.rst
+++ b/doc/en/how-to/capture-warnings.rst
@@ -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:
diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py
index 4756d3ccb1f..20cd9c32b0b 100644
--- a/src/_pytest/python_api.py
+++ b/src/_pytest/python_api.py
@@ -824,7 +824,7 @@ def raises(
*args: Any,
**kwargs: Any,
) -> RaisesContext[E] | _pytest._code.ExceptionInfo[E]:
- r"""Assert that a code block/function call raises an exception type, or one of its subclasses.
+ r"""Assert that a code block raises an exception type, or one of its subclasses.
:param expected_exception:
The expected exception type, or a tuple if one of multiple possible
@@ -930,25 +930,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)
-
-
- or you can specify an arbitrary callable with arguments::
-
- >>> def f(x): return 1/x
- ...
- >>> raises(ZeroDivisionError, f, 0)
-
- >>> raises(ZeroDivisionError, f, x=0)
-
-
- 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
diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py
index 881e492da25..e03c6424eee 100644
--- a/src/_pytest/recwarn.py
+++ b/src/_pytest/recwarn.py
@@ -62,9 +62,9 @@ def deprecated_call(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) ->
def deprecated_call(
func: Callable[..., Any] | None = None, *args: Any, **kwargs: Any
) -> WarningsRecorder | Any:
- """Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning`` or ``FutureWarning``.
+ """Assert that a code block produces a ``DeprecationWarning`` or ``PendingDeprecationWarning`` or ``FutureWarning``.
- This function can be used as a context manager::
+ This function is used as a context manager::
>>> import warnings
>>> def api_call_v2():
@@ -74,16 +74,14 @@ 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.
+ This helper produces a list of :class:`warnings.WarningMessage` objects, one for
+ each warning emitted (regardless of whether it is an ``expected_warning`` or not).
"""
__tracebackhide__ = True
# potential QoL: allow `with deprecated_call:` - i.e. no parens
@@ -119,7 +117,7 @@ def warns(
*args: Any,
**kwargs: Any,
) -> WarningsChecker | Any:
- r"""Assert that code raises a particular class of warning.
+ r"""Assert that a code block raises a particular class of warning.
Specifically, the parameter ``expected_warning`` can be a warning class or tuple
of warning classes, and the code inside the ``with`` block must issue at least one
@@ -129,13 +127,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::
+ Use this function 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
+ You can use the keyword argument ``match`` to assert
that the warning matches a text or regex::
>>> with pytest.warns(UserWarning, match='must be 0 or None'):
diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py b/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py
index 7119764273b..678dd06d907 100644
--- a/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py
+++ b/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py
@@ -6,5 +6,5 @@
@pytest.fixture
def arg2(request):
- with pytest.raises(Exception): # noqa: B017
+ with pytest.raises(Exception): # noqa: B017 # too general exception
request.getfixturevalue("arg1")
diff --git a/testing/test_debugging.py b/testing/test_debugging.py
index 03610147410..e1ced12209e 100644
--- a/testing/test_debugging.py
+++ b/testing/test_debugging.py
@@ -310,7 +310,7 @@ def test_1():
)
child = pytester.spawn_pytest(f"--pdb {p1}")
child.expect(".*def test_1")
- child.expect(".*pytest.raises.*globalfunc")
+ child.expect(r"with pytest.raises\(ValueError\)")
child.expect("Pdb")
child.sendline("globalfunc")
child.expect(".*function")
From 62370d0f6ad05d913295bbe82cc77ca0a000b60b Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Mon, 24 Feb 2025 12:49:52 +0100
Subject: [PATCH 07/15] make coverage ignore 'elif TYPE_CHECKING'
---
.coveragerc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.coveragerc b/.coveragerc
index b810471417f..eb22da57dfd 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -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
From 7692e300c38765740fcd6280245fd26b4b663cad Mon Sep 17 00:00:00 2001
From: John Litborn <11260241+jakkdl@users.noreply.github.com>
Date: Tue, 25 Feb 2025 15:33:03 +0100
Subject: [PATCH 08/15] Update doc/en/deprecations.rst
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко)
---
doc/en/deprecations.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst
index 58e1f623851..8035ba9c5d5 100644
--- a/doc/en/deprecations.rst
+++ b/doc/en/deprecations.rst
@@ -14,8 +14,8 @@ 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 `.
-legacy callable form of :func:`raises`, :func:`warns` and :func:`deprecated_call`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+legacy callable form of :func:`raises `, :func:`warns ` and :func:`deprecated_call `
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 8.4
From 8ebf3a4885a81039ee4d07b90d465f708569e031 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Tue, 25 Feb 2025 15:33:44 +0100
Subject: [PATCH 09/15] flip the conditional to reduce nesting level
---
src/_pytest/recwarn.py | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py
index e03c6424eee..b0adbb2df51 100644
--- a/src/_pytest/recwarn.py
+++ b/src/_pytest/recwarn.py
@@ -86,11 +86,12 @@ def deprecated_call(
__tracebackhide__ = True
# potential QoL: allow `with deprecated_call:` - i.e. no parens
dep_warnings = (DeprecationWarning, PendingDeprecationWarning, FutureWarning)
- if func is not None:
- warnings.warn(CALLABLE_DEPRECATED_CALL, stacklevel=2)
- with warns(dep_warnings):
- return func(*args, **kwargs)
- return warns(dep_warnings, *args, **kwargs)
+ 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
From 62cc03781d4a641dce0aba8f88785b88447441f1 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Tue, 11 Mar 2025 16:38:44 +0100
Subject: [PATCH 10/15] use pendingdeprecationwarning instead. Narrow some
filterwarnings. Fix more tests
---
src/_pytest/deprecated.py | 21 ++++++++++++++-------
src/_pytest/python_api.py | 2 +-
src/_pytest/warning_types.py | 6 ++++++
src/pytest/__init__.py | 2 ++
testing/python/raises.py | 16 ++++++++--------
testing/test_cacheprovider.py | 6 ++++--
testing/test_recwarn.py | 24 +++++++++++++-----------
7 files changed, 48 insertions(+), 29 deletions(-)
diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py
index 9a4b1850c46..df8c5ab22c6 100644
--- a/src/_pytest/deprecated.py
+++ b/src/_pytest/deprecated.py
@@ -16,6 +16,7 @@
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
@@ -35,18 +36,24 @@ def decorator(func: object) -> object:
return decorator
-CALLABLE_RAISES = PytestDeprecationWarning(
- "The callable form of pytest.raises is deprecated.\n"
- "Use `with pytest.raises(...):` instead."
+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"
)
-CALLABLE_WARNS = PytestDeprecationWarning(
- "The callable form of pytest.warns is deprecated.\n"
+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"
)
-CALLABLE_DEPRECATED_CALL = PytestDeprecationWarning(
- "The callable form of pytest.deprecated_call is deprecated.\n"
+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"
)
diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py
index 36defce04ea..6f8235799c6 100644
--- a/src/_pytest/python_api.py
+++ b/src/_pytest/python_api.py
@@ -957,7 +957,7 @@ def raises(
>>> raises(ZeroDivisionError, f, x=0)
- 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::
diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py
index 8c9ff2d9a36..8a902a00860 100644
--- a/src/_pytest/warning_types.py
+++ b/src/_pytest/warning_types.py
@@ -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."""
diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py
index e5098fe6e61..98c284fe7b9 100644
--- a/src/pytest/__init__.py
+++ b/src/pytest/__init__.py
@@ -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",
diff --git a/testing/python/raises.py b/testing/python/raises.py
index 28a2aded434..971560d02f3 100644
--- a/testing/python/raises.py
+++ b/testing/python/raises.py
@@ -225,10 +225,10 @@ def __call__(self):
refcount = len(gc.get_referrers(t))
if method == "function":
- with pytest.warns(pytest.PytestDeprecationWarning):
+ with pytest.warns(pytest.PytestPendingDeprecationWarning):
pytest.raises(ValueError, t)
elif method == "function_match":
- with pytest.warns(pytest.PytestDeprecationWarning):
+ with pytest.warns(pytest.PytestPendingDeprecationWarning):
pytest.raises(ValueError, t).match("^$")
elif method == "with":
with pytest.raises(ValueError):
@@ -267,22 +267,22 @@ def test_raises_match(self) -> None:
int("asdf", base=10)
# "match" without context manager.
- with pytest.warns(pytest.PytestDeprecationWarning):
+ with pytest.warns(pytest.PytestPendingDeprecationWarning):
pytest.raises(ValueError, int, "asdf").match("invalid literal")
with pytest.raises(AssertionError) as excinfo:
- with pytest.warns(pytest.PytestDeprecationWarning):
+ with pytest.warns(pytest.PytestPendingDeprecationWarning):
pytest.raises(ValueError, int, "asdf").match(msg)
assert str(excinfo.value) == expr
- with pytest.warns(pytest.PytestDeprecationWarning):
+ with pytest.warns(pytest.PytestPendingDeprecationWarning):
pytest.raises(TypeError, int, match="invalid") # type: ignore[call-overload]
def tfunc(match):
raise ValueError(f"match={match}")
- with pytest.warns(pytest.PytestDeprecationWarning):
+ with pytest.warns(pytest.PytestPendingDeprecationWarning):
pytest.raises(ValueError, tfunc, match="asdf").match("match=asdf")
- with pytest.warns(pytest.PytestDeprecationWarning):
+ with pytest.warns(pytest.PytestPendingDeprecationWarning):
pytest.raises(ValueError, tfunc, match="").match("match=")
# empty string matches everything, which is probably not what the user wants
@@ -411,5 +411,5 @@ def test_callable_func_kwarg(self) -> None:
def my_raise() -> None:
raise ValueError
- with pytest.warns(pytest.PytestDeprecationWarning):
+ with pytest.warns(pytest.PytestPendingDeprecationWarning):
pytest.raises(expected_exception=ValueError, func=my_raise)
diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py
index ca417e86ee5..32065308550 100644
--- a/testing/test_cacheprovider.py
+++ b/testing/test_cacheprovider.py
@@ -51,7 +51,8 @@ def test_config_cache_dataerror(self, pytester: Pytester) -> None:
config = pytester.parseconfigure()
assert config.cache is not None
cache = config.cache
- pytest.raises(TypeError, lambda: cache.set("key/name", cache))
+ with pytest.raises(TypeError):
+ cache.set("key/name", cache)
config.cache.set("key/name", 0)
config.cache._getvaluepath("key/name").write_bytes(b"123invalid")
val = config.cache.get("key/name", -2)
@@ -143,7 +144,8 @@ def test_cachefuncarg(cache):
val = cache.get("some/thing", None)
assert val is None
cache.set("some/thing", [1])
- pytest.raises(TypeError, lambda: cache.get("some/thing"))
+ with pytest.raises(TypeError):
+ cache.get("some/thing")
val = cache.get("some/thing", [])
assert val == [1]
"""
diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py
index ffb9ab80b85..76dd5e9d49b 100644
--- a/testing/test_recwarn.py
+++ b/testing/test_recwarn.py
@@ -5,7 +5,7 @@
import sys
import warnings
-from _pytest.warning_types import PytestDeprecationWarning
+from _pytest.warning_types import PytestPendingDeprecationWarning
import pytest
from pytest import ExitCode
from pytest import Pytester
@@ -161,7 +161,7 @@ def test_deprecated_call(self) -> None:
def test_deprecated_call_ret(self) -> None:
with pytest.warns(
- PytestDeprecationWarning,
+ PytestPendingDeprecationWarning,
match=(
wrap_escape(
"The callable form of pytest.deprecated_call is deprecated.\n"
@@ -212,7 +212,7 @@ def f():
msg = "No warnings of type (.*DeprecationWarning.*, .*PendingDeprecationWarning.*)"
with pytest.raises(pytest.fail.Exception, match=msg):
if mode == "call":
- with pytest.warns(PytestDeprecationWarning):
+ with pytest.warns(PytestPendingDeprecationWarning):
pytest.deprecated_call(f)
else:
with pytest.deprecated_call():
@@ -223,7 +223,7 @@ def f():
)
@pytest.mark.parametrize("mode", ["context_manager", "call"])
@pytest.mark.parametrize("call_f_first", [True, False])
- @pytest.mark.filterwarnings("ignore")
+ @pytest.mark.filterwarnings("ignore:hi")
def test_deprecated_call_modes(self, warning_type, mode, call_f_first) -> None:
"""Ensure deprecated_call() captures a deprecation warning as expected inside its
block/function.
@@ -237,7 +237,8 @@ def f():
if call_f_first:
assert f() == 10
if mode == "call":
- assert pytest.deprecated_call(f) == 10
+ with pytest.warns(PytestPendingDeprecationWarning):
+ assert pytest.deprecated_call(f) == 10
else:
with pytest.deprecated_call():
assert f() == 10
@@ -258,7 +259,7 @@ def f():
with pytest.warns(warning):
with pytest.raises(pytest.fail.Exception):
- with pytest.warns(PytestDeprecationWarning):
+ with pytest.warns(PytestPendingDeprecationWarning):
pytest.deprecated_call(f)
with pytest.raises(pytest.fail.Exception):
with pytest.deprecated_call():
@@ -293,7 +294,7 @@ def test_several_messages(self) -> None:
def test_function(self) -> None:
with pytest.warns(
- PytestDeprecationWarning,
+ PytestPendingDeprecationWarning,
match=(
wrap_escape(
"The callable form of pytest.warns is deprecated.\n"
@@ -452,16 +453,17 @@ def test_none_of_multiple_warns(self) -> None:
warnings.warn("bbbbbbbbbb", UserWarning)
warnings.warn("cccccccccc", UserWarning)
- @pytest.mark.filterwarnings("ignore")
+ @pytest.mark.filterwarnings("ignore:ohai")
def test_can_capture_previously_warned(self) -> None:
def f() -> int:
warnings.warn(UserWarning("ohai"))
return 10
assert f() == 10
- assert pytest.warns(UserWarning, f) == 10
- assert pytest.warns(UserWarning, f) == 10
- assert pytest.warns(UserWarning, f) != "10" # type: ignore[comparison-overlap]
+ with pytest.warns(PytestPendingDeprecationWarning):
+ assert pytest.warns(UserWarning, f) == 10
+ assert pytest.warns(UserWarning, f) == 10
+ assert pytest.warns(UserWarning, f) != "10" # type: ignore[comparison-overlap]
def test_warns_context_manager_with_kwargs(self) -> None:
with pytest.raises(TypeError) as excinfo:
From 358e367475c2cd3846989d959f5bba5947299b87 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Tue, 11 Mar 2025 16:55:17 +0100
Subject: [PATCH 11/15] fixes after review by nicoddemus
---
doc/en/deprecations.rst | 10 +++++++---
src/_pytest/deprecated.py | 24 +++++++++++++-----------
src/_pytest/recwarn.py | 23 ++++++++++++++---------
3 files changed, 34 insertions(+), 23 deletions(-)
diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst
index 8035ba9c5d5..626da2ad697 100644
--- a/doc/en/deprecations.rst
+++ b/doc/en/deprecations.rst
@@ -14,7 +14,7 @@ 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 `.
-legacy callable form of :func:`raises `, :func:`warns ` and :func:`deprecated_call `
+Legacy callable form of :func:`raises `, :func:`warns ` and :func:`deprecated_call `
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 8.4
@@ -30,13 +30,13 @@ to read and doesn't allow passing `match` or other parameters.
return 6.28
- # deprecated callable form
+ # 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")
- # context-manager form
+ # The calls above can be upgraded to the context-manager form
with pytest.raises(ValueError) as excinfo:
int("hello")
@@ -46,6 +46,10 @@ to read and doesn't allow passing `match` or other parameters.
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:
sync test depending on async fixture
diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py
index df8c5ab22c6..7b73bc40616 100644
--- a/src/_pytest/deprecated.py
+++ b/src/_pytest/deprecated.py
@@ -11,7 +11,6 @@
from __future__ import annotations
-import sys
from typing import TYPE_CHECKING
from warnings import warn
@@ -23,37 +22,40 @@
# the `as` indicates explicit re-export to type checkers
# mypy currently does not support overload+deprecated
-if sys.version_info >= (3, 13):
- from warnings import deprecated as deprecated
-elif TYPE_CHECKING:
+if TYPE_CHECKING:
from typing_extensions import deprecated as deprecated
else:
def deprecated(reason: str = "") -> object:
- def decorator(func: object) -> object:
- return func
-
- return decorator
+ raise AssertionError(
+ "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 decorators
+ # to deprecated, we can consider adapting this function to raise warnings
+ # at runtime
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"
+ " 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"
+ " 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"
+ " 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"
)
diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py
index b0adbb2df51..28f45290915 100644
--- a/src/_pytest/recwarn.py
+++ b/src/_pytest/recwarn.py
@@ -62,9 +62,9 @@ def deprecated_call(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) ->
def deprecated_call(
func: Callable[..., Any] | None = None, *args: Any, **kwargs: Any
) -> WarningsRecorder | Any:
- """Assert that a code block produces a ``DeprecationWarning`` or ``PendingDeprecationWarning`` or ``FutureWarning``.
+ """Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning`` or ``FutureWarning``.
- This function is used as a context manager::
+ This function can be used as a context manager::
>>> import warnings
>>> def api_call_v2():
@@ -77,14 +77,19 @@ def deprecated_call(
>>> with pytest.deprecated_call(match="^use v3 of this api$") as warning_messages:
... assert api_call_v2() == 200
- You may use the keyword argument ``match`` to assert
+ 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
that the warning matches a text or regex.
- This helper produces a list of :class:`warnings.WarningMessage` objects, one for
- each warning emitted (regardless of whether it is an ``expected_warning`` or not).
+ The context manager produces a list of :class:`warnings.WarningMessage` objects,
+ one for each warning emitted
+ (regardless of whether it is an ``expected_warning`` or not).
"""
__tracebackhide__ = True
- # potential QoL: allow `with deprecated_call:` - i.e. no parens
+ # 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)
@@ -118,7 +123,7 @@ def warns(
*args: Any,
**kwargs: Any,
) -> WarningsChecker | Any:
- r"""Assert that a code block raises a particular class of warning.
+ r"""Assert that code raises a particular class of warning.
Specifically, the parameter ``expected_warning`` can be a warning class or tuple
of warning classes, and the code inside the ``with`` block must issue at least one
@@ -128,13 +133,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.
- Use this function as a context manager::
+ This function can be used as a context manager::
>>> import pytest
>>> with pytest.warns(RuntimeWarning):
... warnings.warn("my warning", RuntimeWarning)
- You can use the keyword argument ``match`` to assert
+ In the context manager form you may use the keyword argument ``match`` to assert
that the warning matches a text or regex::
>>> with pytest.warns(UserWarning, match='must be 0 or None'):
From b6c3cd3d35277aaca35ad4f7942723abc688d4c1 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Tue, 11 Mar 2025 17:02:36 +0100
Subject: [PATCH 12/15] fix tests. And stop the decorator from erroring cause
idk why it did
---
src/_pytest/deprecated.py | 13 +++++++------
testing/test_recwarn.py | 10 ++++++++--
2 files changed, 15 insertions(+), 8 deletions(-)
diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py
index 7b73bc40616..34eb8c743c7 100644
--- a/src/_pytest/deprecated.py
+++ b/src/_pytest/deprecated.py
@@ -27,12 +27,13 @@
else:
def deprecated(reason: str = "") -> object:
- raise AssertionError(
- "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 decorators
- # to deprecated, we can consider adapting this function to raise warnings
- # at runtime
+ # 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(
diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py
index 76dd5e9d49b..d0d1edefe4f 100644
--- a/testing/test_recwarn.py
+++ b/testing/test_recwarn.py
@@ -164,8 +164,11 @@ def test_deprecated_call_ret(self) -> None:
PytestPendingDeprecationWarning,
match=(
wrap_escape(
- "The callable form of pytest.deprecated_call is deprecated.\n"
+ "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"
)
),
):
@@ -297,8 +300,11 @@ def test_function(self) -> None:
PytestPendingDeprecationWarning,
match=(
wrap_escape(
- "The callable form of pytest.warns is deprecated.\n"
+ "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"
)
),
):
From 9f7255e732939246fb1528bd4d66e41fcb257f93 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Tue, 11 Mar 2025 17:15:29 +0100
Subject: [PATCH 13/15] ignore the warning from demonstrating the
pending-deprecated callable form in docstrings
---
tox.ini | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tox.ini b/tox.ini
index 97a74dde937..fff573ec699 100644
--- a/tox.ini
+++ b/tox.ini
@@ -41,7 +41,7 @@ description =
doctesting: including doctests
commands =
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest {posargs:{env:_PYTEST_TOX_DEFAULT_POSARGS:}}
- doctesting: {env:_PYTEST_TOX_COVERAGE_RUN:} pytest --doctest-modules --pyargs _pytest
+ doctesting: {env:_PYTEST_TOX_COVERAGE_RUN:} pytest --doctest-modules --pyargs _pytest -Wignore:"The callable form of pytest.raises":PendingDeprecationWarning
coverage: coverage combine
coverage: coverage report -m
passenv =
From 3d96dc657c910e0a919b8761fc79f17f851642c7 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Mon, 31 Mar 2025 12:04:08 +0200
Subject: [PATCH 14/15] add no-cover
---
testing/python/raises.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/testing/python/raises.py b/testing/python/raises.py
index 971560d02f3..73742630d73 100644
--- a/testing/python/raises.py
+++ b/testing/python/raises.py
@@ -331,10 +331,10 @@ def test_raises_match_wrong_type(self):
def test_raises_exception_looks_iterable(self):
class Meta(type):
def __getitem__(self, item):
- return 1 / 0
+ return 1 / 0 # pragma: no cover
def __len__(self):
- return 1
+ return 1 # pragma: no cover
class ClassLooksIterableException(Exception, metaclass=Meta):
pass
From e2eb4eec547c9e0b240370ce3a0fb9c1248289cb Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Mon, 31 Mar 2025 12:32:33 +0200
Subject: [PATCH 15/15] fix tests
---
src/_pytest/raises.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/_pytest/raises.py b/src/_pytest/raises.py
index c4dde32663a..596f451b63b 100644
--- a/src/_pytest/raises.py
+++ b/src/_pytest/raises.py
@@ -276,7 +276,7 @@ def raises(
"""
__tracebackhide__ = True
- if func is not None and 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))