From 9279ea2882b96be6f7de3f8fbaa38062b1f2353e Mon Sep 17 00:00:00 2001 From: Reagan Lee <96998476+reaganjlee@users.noreply.github.com> Date: Fri, 30 Jun 2023 15:29:02 -0700 Subject: [PATCH] Emit unmatched warnings from pytest.warns() --- AUTHORS | 1 + changelog/9288.breaking.rst | 1 + src/_pytest/recwarn.py | 33 ++++++++++++++++++++++++++++++ testing/test_recwarn.py | 40 +++++++++++++++++++++++++++++++++---- 4 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 changelog/9288.breaking.rst diff --git a/AUTHORS b/AUTHORS index e0ed531b0..28116c3c8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -311,6 +311,7 @@ Raphael Pierzina Rafal Semik Raquel Alegre Ravi Chandra +Reagan Lee Robert Holt Roberto Aldera Roberto Polli diff --git a/changelog/9288.breaking.rst b/changelog/9288.breaking.rst new file mode 100644 index 000000000..7f101123d --- /dev/null +++ b/changelog/9288.breaking.rst @@ -0,0 +1 @@ +Set :func:`warns` to re-emit unmatched warnings when the context closes diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index b422f3627..334b3b850 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -149,6 +149,10 @@ def warns( # noqa: F811 This could be achieved in the same way as with exceptions, see :ref:`parametrizing_conditional_raising` for an example. + .. note:: + Unlike the stdlib :func:`warnings.catch_warnings` context manager, + unmatched warnings will be re-emitted when the context closes. + """ __tracebackhide__ = True if not args: @@ -290,6 +294,32 @@ class WarningsChecker(WarningsRecorder): def found_str(): return pformat([record.message for record in self], indent=2) + def re_emit() -> None: + for r in self: + if matches(r): + continue + + assert issubclass(r.message.__class__, Warning) + + warnings.warn_explicit( + str(r.message), + r.message.__class__, + r.filename, + r.lineno, + module=r.__module__, + source=r.source, + ) + + def matches(warning) -> bool: + if self.expected_warning is not None: + if issubclass(warning.category, self.expected_warning): + if self.match_expr is not None: + if re.compile(self.match_expr).search(str(warning.message)): + return True + return False + return True + return False + # only check if we're not currently handling an exception if exc_type is None and exc_val is None and exc_tb is None: if self.expected_warning is not None: @@ -303,6 +333,7 @@ class WarningsChecker(WarningsRecorder): for r in self: if issubclass(r.category, self.expected_warning): if re.compile(self.match_expr).search(str(r.message)): + re_emit() break else: fail( @@ -311,3 +342,5 @@ DID NOT WARN. No warnings of type {self.expected_warning} matching the regex wer Regex: {self.match_expr} Emitted warnings: {found_str()}""" ) + else: + re_emit() diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 7e0f836a6..d9cd1d778 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -376,10 +376,12 @@ class TestWarns: warnings.warn("value must be 42", UserWarning) def test_one_from_multiple_warns(self) -> None: - with pytest.warns(UserWarning, match=r"aaa"): - warnings.warn("cccccccccc", UserWarning) - warnings.warn("bbbbbbbbbb", UserWarning) - warnings.warn("aaaaaaaaaa", UserWarning) + with pytest.raises(pytest.fail.Exception): + with pytest.warns(UserWarning, match=r"aaa"): + with pytest.warns(UserWarning, match=r"aaa"): + warnings.warn("cccccccccc", UserWarning) + warnings.warn("bbbbbbbbbb", UserWarning) + warnings.warn("aaaaaaaaaa", UserWarning) def test_none_of_multiple_warns(self) -> None: with pytest.raises(pytest.fail.Exception): @@ -403,3 +405,33 @@ class TestWarns: with pytest.warns(UserWarning, foo="bar"): # type: ignore pass assert "Unexpected keyword arguments" in str(excinfo.value) + + def test_re_emit_single(self) -> None: + with pytest.warns(DeprecationWarning): + with pytest.warns(UserWarning): + warnings.warn("user warning", UserWarning) + warnings.warn("some deprecation warning", DeprecationWarning) + + def test_re_emit_multiple(self) -> None: + with pytest.warns(UserWarning): + warnings.warn("first warning", UserWarning) + warnings.warn("second warning", UserWarning) + + def test_re_emit_match_single(self) -> None: + with pytest.warns(DeprecationWarning): + with pytest.warns(UserWarning, match="user warning"): + warnings.warn("user warning", UserWarning) + warnings.warn("some deprecation warning", DeprecationWarning) + + def test_re_emit_match_multiple(self) -> None: + # with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="user warning"): + warnings.warn("first user warning", UserWarning) + warnings.warn("second user warning", UserWarning) + + def test_re_emit_non_match_single(self) -> None: + # with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="v2 warning"): + with pytest.warns(UserWarning, match="v1 warning"): + warnings.warn("v1 warning", UserWarning) + warnings.warn("non-matching v2 warning", UserWarning)