From 10498906d622b8468bf6bc9522b4bab2ce33d1c4 Mon Sep 17 00:00:00 2001 From: FumeDev Date: Sun, 24 Mar 2024 14:51:55 -0700 Subject: [PATCH] Refactor error handling for `raises`, `warns`, and `xfail` in pytest. --- src/_pytest/mark/structures.py | 17 ++++++++++++++++- src/_pytest/python_api.py | 13 +++++++++++++ src/_pytest/recwarn.py | 14 ++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index b8cbf0d1875..d3da227790f 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -426,9 +426,24 @@ def __call__( *conditions: Union[str, bool], reason: str = ..., run: bool = ..., - raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = ..., + raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = (), strict: bool = ..., ) -> MarkDecorator: + __tracebackhide__ = True + if raises is None: + raise UsageError( + "Passing `raises=None` to xfail is an error, because it's " + "impossible to raise an exception which is not an instance of any type. " + "Raising exceptions is already understood as failing the test, so you " + "don't need any special code to say 'this should never raise an exception'." + ) + elif isinstance(raises, tuple) and not raises: + raise UsageError( + "Passing `raises=()` to xfail is an error, because it's impossible " + "to raise an exception which is not an instance of any type. Raising " + "exceptions is already understood as failing the test, so you don't need " + "any special code to say 'this should never raise an exception'." + ) ... class _ParametrizeMarkDecorator(MarkDecorator): diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 5fa21961924..82990150a62 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -708,6 +708,19 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: __tracebackhide__ = True + if expected_exception is None: + raise UsageError( + "Passing `expected_exception=None` is invalid, as it is ambiguous for " + "what exceptions pytest should catch. To assert that no exception is " + "expected, simply execute the block without using `pytest.raises()`." + ) + if isinstance(expected_exception, tuple) and not expected_exception: + raise UsageError( + "Passing an empty tuple `expected_exception=()` is invalid, as it would " + "not specify any exceptions to catch. To assert that no exception is " + "expected, simply execute the block without using `pytest.raises()`." + ) + if isinstance(expected, Decimal): cls: Type[ApproxBase] = ApproxDecimal elif isinstance(expected, Mapping): diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 49e1de2827f..bf49bbd39fa 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -78,6 +78,20 @@ def deprecated_call( one for each warning raised. """ __tracebackhide__ = True + if expected_warning is None: + raise TypeError( + "Passing `expected_warning=None` is an error, because it's " + "impossible to emit a warning which is not an instance of any type. " + "To assert that no warnings are emitted, use `pytest.warns(None)` to explicitly " + "declare that no warnings are expected." + ) + elif isinstance(expected_warning, tuple) and not expected_warning: + raise TypeError( + "Passing `expected_warning=()` is an error, because it's " + "impossible to emit a warning which is not an instance of any type. " + "To assert that no warnings are emitted, use `pytest.warns(None)` to explicitly " + "declare that no warnings are expected." + ) if func is not None: args = (func,) + args return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs)