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 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/conf.py b/doc/en/conf.py index c89e14d07fa..9deee5230bd 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -97,6 +97,10 @@ # TypeVars ("py:class", "_pytest._code.code.E"), ("py:class", "E"), # due to delayed annotation + ("py:class", "T"), + ("py:class", "P"), + ("py:class", "P.args"), + ("py:class", "P.kwargs"), ("py:class", "_pytest.fixtures.FixtureFunction"), ("py:class", "_pytest.nodes._NodeType"), ("py:class", "_NodeType"), # due to delayed annotation diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 18df64c9204..626da2ad697 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -14,6 +14,41 @@ 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 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") + + # The calls above can be upgraded to the 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") + + +.. 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: diff --git a/doc/en/how-to/assert.rst b/doc/en/how-to/assert.rst index 6bc8f6fed33..8f8208b77ee 100644 --- a/doc/en/how-to/assert.rst +++ b/doc/en/how-to/assert.rst @@ -282,6 +282,10 @@ exception at a specific level; exceptions contained directly in the top Alternate `pytest.raises` form (legacy) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. warning:: + This will be deprecated and removed in a future release. + + There is an alternate form of :func:`pytest.raises` where you pass a function that will be executed, along with ``*args`` and ``**kwargs``. :func:`pytest.raises` will then execute the function with those arguments and assert that the given exception is raised: @@ -301,7 +305,6 @@ 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/deprecated.py b/src/_pytest/deprecated.py index a605c24e58f..34eb8c743c7 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -11,13 +11,55 @@ from __future__ import annotations +from typing import TYPE_CHECKING 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 +# the `as` indicates explicit re-export to type checkers +# mypy currently does not support overload+deprecated +if TYPE_CHECKING: + from typing_extensions import deprecated as deprecated +else: + + def deprecated(reason: str = "") -> object: + # 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( + "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.\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.\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.\n" + "See https://docs.pytest.org/en/stable/reference/deprecations.html#legacy-callable-form-of-raises-warns-and-deprecated-call" +) + + # 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 af078e25256..4a7d9572037 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -16,6 +16,9 @@ if TYPE_CHECKING: from numpy import ndarray + from typing_extensions import ParamSpec + + P = ParamSpec("P") def _compare_approx( diff --git a/src/_pytest/raises.py b/src/_pytest/raises.py index 2eba53bf10b..596f451b63b 100644 --- a/src/_pytest/raises.py +++ b/src/_pytest/raises.py @@ -19,6 +19,8 @@ from _pytest._code import ExceptionInfo from _pytest._code.code import stringify_exception +from _pytest.deprecated import CALLABLE_RAISES +from _pytest.deprecated import deprecated from _pytest.outcomes import fail from _pytest.warning_types import PytestWarning @@ -93,16 +95,18 @@ def raises(*, check: Callable[[BaseException], bool]) -> RaisesExc[BaseException @overload +@deprecated("Use context-manager form instead") def raises( expected_exception: type[E] | tuple[type[E], ...], - func: Callable[..., Any], - *args: Any, - **kwargs: Any, + func: Callable[P, object], + *args: P.args, + **kwargs: P.kwargs, ) -> ExceptionInfo[E]: ... def raises( expected_exception: type[E] | tuple[type[E], ...] | None = None, + func: Callable[P, object] | None = None, *args: Any, **kwargs: Any, ) -> RaisesExc[BaseException] | ExceptionInfo[E]: @@ -253,7 +257,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:: @@ -272,7 +276,7 @@ def raises( """ __tracebackhide__ = True - if not args: + if func is None and not args: if set(kwargs) - {"match", "check", "expected_exception"}: msg = "Unexpected keyword arguments passed to pytest.raises: " msg += ", ".join(sorted(kwargs)) @@ -289,11 +293,11 @@ def raises( f"Raising exceptions is already understood as failing the test, so you don't need " f"any special code to say 'this should never raise an exception'." ) - func = args[0] if not callable(func): raise TypeError(f"{func!r} object (type: {type(func)}) must be callable") + warnings.warn(CALLABLE_RAISES, stacklevel=2) with RaisesExc(expected_exception) as excinfo: - func(*args[1:], **kwargs) + func(*args, **kwargs) try: return excinfo finally: diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 440e3efac8a..28f45290915 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( @@ -67,6 +74,8 @@ 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 @@ -76,14 +85,18 @@ def deprecated_call( that the warning matches a text or regex. The context manager produces a list of :class:`warnings.WarningMessage` objects, - one for each warning raised. + one for each warning emitted + (regardless of whether it is an ``expected_warning`` or not). """ __tracebackhide__ = True - if func is not None: - args = (func, *args) - return warns( - (DeprecationWarning, PendingDeprecationWarning, FutureWarning), *args, **kwargs - ) + # 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) + + warnings.warn(CALLABLE_DEPRECATED_CALL, stacklevel=2) + with warns(dep_warnings): + return func(*args, **kwargs) @overload @@ -95,18 +108,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 +165,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 +175,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/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 e36d3e704c1..bf98049f585 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/_py/test_local.py b/testing/_py/test_local.py index 03a828c64f0..2246fe5ce1e 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 321372d4b59..bf7ed38cefe 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -210,7 +210,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: @@ -379,7 +380,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: @@ -392,7 +394,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..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,4 +6,5 @@ @pytest.fixture def arg2(request): - pytest.raises(Exception, request.getfixturevalue, "arg1") + with pytest.raises(Exception): # noqa: B017 # too general exception + 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 e8b345aecc6..f73c60716eb 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -82,11 +82,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 3da260d1837..16832d67055 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -20,11 +20,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): @@ -62,7 +64,8 @@ def __call__(self): pass try: - pytest.raises(ValueError, A()) + with pytest.warns(pytest.PytestDeprecationWarning): + pytest.raises(ValueError, A()) except pytest.fail.Exception: pass @@ -178,7 +181,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"): @@ -191,7 +195,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: @@ -220,9 +225,11 @@ def __call__(self): refcount = len(gc.get_referrers(t)) if method == "function": - pytest.raises(ValueError, t) + with pytest.warns(pytest.PytestPendingDeprecationWarning): + pytest.raises(ValueError, t) elif method == "function_match": - pytest.raises(ValueError, t).match("^$") + with pytest.warns(pytest.PytestPendingDeprecationWarning): + pytest.raises(ValueError, t).match("^$") elif method == "with": with pytest.raises(ValueError): t() @@ -260,18 +267,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.PytestPendingDeprecationWarning): + 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.PytestPendingDeprecationWarning): + pytest.raises(ValueError, int, "asdf").match(msg) assert str(excinfo.value) == expr - pytest.raises(TypeError, int, match="invalid") + with pytest.warns(pytest.PytestPendingDeprecationWarning): + 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.PytestPendingDeprecationWarning): + pytest.raises(ValueError, tfunc, match="asdf").match("match=asdf") + with pytest.warns(pytest.PytestPendingDeprecationWarning): + pytest.raises(ValueError, tfunc, match="").match("match=") # empty string matches everything, which is probably not what the user wants with pytest.warns( @@ -319,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 @@ -331,7 +343,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).""" @@ -403,3 +416,12 @@ 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 + + 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_capture.py b/testing/test_capture.py index d9dacebd938..330050589f1 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() @@ -885,7 +886,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") + 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 @@ -912,7 +914,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") + with pytest.raises(TypeError): + f.write(b"hello") # type: ignore[arg-type] def test_dontreadfrominput() -> None: @@ -921,19 +924,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"") - 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 @@ -1000,7 +1013,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() @@ -1022,7 +1036,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) @@ -1073,7 +1088,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"" @@ -1155,7 +1171,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 @@ -1213,7 +1230,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): @@ -1667,9 +1685,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( - 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 bb08c40fef4..a40ef6e36d7 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -556,7 +556,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 @@ -647,7 +648,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 @@ -673,7 +675,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 @@ -721,7 +724,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: @@ -751,7 +755,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 45883568b11..8dff87030f7 100644 --- a/testing/test_debugging.py +++ b/testing/test_debugging.py @@ -324,12 +324,13 @@ 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}") 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") 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..d0d1edefe4f 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 PytestPendingDeprecationWarning 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,30 @@ 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( + PytestPendingDeprecationWarning, + match=( + wrap_escape( + "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" + ) + ), + ): + 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 +194,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 +215,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(PytestPendingDeprecationWarning): + pytest.deprecated_call(f) else: with pytest.deprecated_call(): f() @@ -198,7 +226,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. @@ -212,7 +240,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 @@ -233,7 +262,8 @@ def f(): with pytest.warns(warning): with pytest.raises(pytest.fail.Exception): - pytest.deprecated_call(f) + with pytest.warns(PytestPendingDeprecationWarning): + pytest.deprecated_call(f) with pytest.raises(pytest.fail.Exception): with pytest.deprecated_call(): f() @@ -256,32 +286,41 @@ 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( + PytestPendingDeprecationWarning, + match=( + wrap_escape( + "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" + ) + ), + ): + 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): @@ -420,16 +459,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: 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() diff --git a/tox.ini b/tox.ini index 850def411cb..da1b721c326 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 =