diff --git a/AUTHORS b/AUTHORS index c38f74d9980..1d1f2a8e22c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -4,6 +4,7 @@ merlinux GmbH, Germany, office at merlinux eu Contributors include:: Aaron Coleman +Aaron Zolnai-Lucas Abdeali JK Abdelrahman Elbehery Abhijeet Kasurde diff --git a/changelog/11933.improvement.rst b/changelog/11933.improvement.rst new file mode 100644 index 00000000000..bab400b1b7f --- /dev/null +++ b/changelog/11933.improvement.rst @@ -0,0 +1 @@ +Now :func:`pytest.warns` can take an optional boolean keyword argument ``keep_ignores`` to keep existing ignore filters active when used as a context manager. diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 0dc002edd94..965f4cffd02 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -90,6 +90,7 @@ def warns( expected_warning: type[Warning] | tuple[type[Warning], ...] = ..., *, match: str | Pattern[str] | None = ..., + keep_ignores: bool = ..., ) -> WarningsChecker: ... @@ -106,6 +107,7 @@ def warns( expected_warning: type[Warning] | tuple[type[Warning], ...] = Warning, *args: Any, match: str | Pattern[str] | None = None, + keep_ignores: bool = False, **kwargs: Any, ) -> WarningsChecker | Any: r"""Assert that code raises a particular class of warning. @@ -140,6 +142,22 @@ def warns( ... Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted... + You may also set the keyword argument ``keep_ignores`` to avoid catching warnings + which were filtered out, in pytest configuration or otherwise:: + + >>> warnings.simplefilter("ignore", category=FutureWarning) + >>> with pytest.warns(UserWarning, keep_ignores=True): + ... warnings.warn("ignore this warning", UserWarning) + Traceback (most recent call last): + ... + Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted... + + >>> with pytest.warns(RuntimeWarning): + >>> warnings.simplefilter("ignore", category=FutureWarning) + >>> with pytest.warns(UserWarning, keep_ignores=True): + ... warnings.warn("ignore this warning", UserWarning) + warnings.warn("keep this warning", RuntimeWarning) + **Using with** ``pytest.mark.parametrize`` When using :ref:`pytest.mark.parametrize ref` it is possible to parametrize tests @@ -157,7 +175,12 @@ def warns( f"Unexpected keyword arguments passed to pytest.warns: {argnames}" "\nUse context-manager form instead?" ) - return WarningsChecker(expected_warning, match_expr=match, _ispytest=True) + return WarningsChecker( + expected_warning, + match_expr=match, + keep_ignores=keep_ignores, + _ispytest=True, + ) else: func = args[0] if not callable(func): @@ -179,11 +202,12 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg] """ - def __init__(self, *, _ispytest: bool = False) -> None: + def __init__(self, *, keep_ignores: bool = False, _ispytest: bool = False) -> None: check_ispytest(_ispytest) super().__init__(record=True) self._entered = False self._list: list[warnings.WarningMessage] = [] + self._keep_ignores = keep_ignores @property def list(self) -> list[warnings.WarningMessage]: @@ -233,7 +257,20 @@ def __enter__(self) -> Self: # record=True means it's None. assert _list is not None self._list = _list - warnings.simplefilter("always") + + if self._keep_ignores: + for action, message, category, module, lineno in reversed(warnings.filters): + if isinstance(module, re.Pattern): + module = getattr(module, "pattern", None) # type: ignore[unreachable] + warnings.filterwarnings( + action="always" if action != "ignore" else "ignore", + message=message if isinstance(message, str) else "", + category=category, + module=module if isinstance(module, str) else "", + lineno=lineno, + ) + else: + warnings.simplefilter("always") return self def __exit__( @@ -259,11 +296,12 @@ def __init__( self, expected_warning: type[Warning] | tuple[type[Warning], ...] = Warning, match_expr: str | Pattern[str] | None = None, + keep_ignores: bool = False, *, _ispytest: bool = False, ) -> None: check_ispytest(_ispytest) - super().__init__(_ispytest=True) + super().__init__(keep_ignores=keep_ignores, _ispytest=True) msg = "exceptions must be derived from Warning, not %s" if isinstance(expected_warning, tuple): diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 384f2b66a15..de417d38f9d 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -404,6 +404,18 @@ def test_match_regex(self) -> None: with pytest.warns(FutureWarning, match=r"must be \d+$"): warnings.warn("value must be 42", UserWarning) + def test_keep_ignores(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("error", category=UserWarning) + with pytest.warns(UserWarning, keep_ignores=True): + warnings.warn("keep this warning", UserWarning) + + with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="ignore this") + with pytest.warns(UserWarning, keep_ignores=True): + warnings.warn("ignore this warning", FutureWarning) + def test_one_from_multiple_warns(self) -> None: with pytest.warns(): with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"):