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))