Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds keep_ignores kwarg to pytest.warns #12897

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
48 changes: 44 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=UserWarning)
>>> 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=UserWarning)
>>> 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,22 @@ 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(message, re.Pattern):
module = getattr(module, "pattern", None) # type: ignore[unreachable]
if isinstance(module, re.Pattern):
module = getattr(module, "pattern", None) # type: ignore[unreachable]
warnings.filterwarnings(
action="always" if action != "ignore" else "ignore",
message=message or "",
category=category,
module=module or "",
lineno=lineno,
)
else:
warnings.simplefilter("always")
return self

def __exit__(
Expand All @@ -259,11 +298,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
18 changes: 18 additions & 0 deletions testing/test_recwarn.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,24 @@ 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)

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", UserWarning)

def test_one_from_multiple_warns(self) -> None:
with pytest.warns():
with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"):
Expand Down
Loading