Skip to content

Commit

Permalink
adds keep_ignores kwarg to pytest.warns
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronzo committed Oct 16, 2024
1 parent 9cc6b50 commit f239b06
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 4 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ merlinux GmbH, Germany, office at merlinux eu
Contributors include::

Aaron Coleman
Aaron Zolnai-Lucas
Abdeali JK
Abdelrahman Elbehery
Abhijeet Kasurde
Expand Down
1 change: 1 addition & 0 deletions changelog/11933.improvement.rst
Original file line number Diff line number Diff line change
@@ -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.
46 changes: 42 additions & 4 deletions src/_pytest/recwarn.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def warns(
expected_warning: type[Warning] | tuple[type[Warning], ...] = ...,
*,
match: str | Pattern[str] | None = ...,
keep_ignores: bool = ...,
) -> WarningsChecker: ...


Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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]:
Expand Down Expand Up @@ -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__(
Expand All @@ -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):
Expand Down
12 changes: 12 additions & 0 deletions testing/test_recwarn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down

0 comments on commit f239b06

Please sign in to comment.