Merge pull request #10937 from reaganjlee/re-emit
This commit is contained in:
commit
679bd6f2ed
1
AUTHORS
1
AUTHORS
|
@ -311,6 +311,7 @@ Raphael Pierzina
|
||||||
Rafal Semik
|
Rafal Semik
|
||||||
Raquel Alegre
|
Raquel Alegre
|
||||||
Ravi Chandra
|
Ravi Chandra
|
||||||
|
Reagan Lee
|
||||||
Robert Holt
|
Robert Holt
|
||||||
Roberto Aldera
|
Roberto Aldera
|
||||||
Roberto Polli
|
Roberto Polli
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
:func:`pytest.warns <warns>` now re-emits unmatched warnings when the context
|
||||||
|
closes -- previously it would consume all warnings, hiding those that were not
|
||||||
|
matched by the function.
|
||||||
|
|
||||||
|
While this is a new feature, we decided to announce this as a breaking change
|
||||||
|
because many test suites are configured to error-out on warnings, and will
|
||||||
|
therefore fail on the newly-re-emitted warnings.
|
|
@ -117,10 +117,10 @@ def warns( # noqa: F811
|
||||||
warning of that class or classes.
|
warning of that class or classes.
|
||||||
|
|
||||||
This helper produces a list of :class:`warnings.WarningMessage` objects, one for
|
This helper produces a list of :class:`warnings.WarningMessage` objects, one for
|
||||||
each warning raised (regardless of whether it is an ``expected_warning`` or not).
|
each warning emitted (regardless of whether it is an ``expected_warning`` or not).
|
||||||
|
Since pytest 8.0, unmatched warnings are also re-emitted when the context closes.
|
||||||
|
|
||||||
This function can be used as a context manager, which will capture all the raised
|
This function can be used as a context manager::
|
||||||
warnings inside it::
|
|
||||||
|
|
||||||
>>> import pytest
|
>>> import pytest
|
||||||
>>> with pytest.warns(RuntimeWarning):
|
>>> with pytest.warns(RuntimeWarning):
|
||||||
|
@ -135,7 +135,8 @@ def warns( # noqa: F811
|
||||||
>>> with pytest.warns(UserWarning, match=r'must be \d+$'):
|
>>> with pytest.warns(UserWarning, match=r'must be \d+$'):
|
||||||
... warnings.warn("value must be 42", UserWarning)
|
... warnings.warn("value must be 42", UserWarning)
|
||||||
|
|
||||||
>>> with pytest.warns(UserWarning, match=r'must be \d+$'):
|
>>> with pytest.warns(UserWarning): # catch re-emitted warning
|
||||||
|
... with pytest.warns(UserWarning, match=r'must be \d+$'):
|
||||||
... warnings.warn("this is not here", UserWarning)
|
... warnings.warn("this is not here", UserWarning)
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
|
@ -277,6 +278,12 @@ class WarningsChecker(WarningsRecorder):
|
||||||
self.expected_warning = expected_warning_tup
|
self.expected_warning = expected_warning_tup
|
||||||
self.match_expr = match_expr
|
self.match_expr = match_expr
|
||||||
|
|
||||||
|
def matches(self, warning: warnings.WarningMessage) -> bool:
|
||||||
|
assert self.expected_warning is not None
|
||||||
|
return issubclass(warning.category, self.expected_warning) and bool(
|
||||||
|
self.match_expr is None or re.search(self.match_expr, str(warning.message))
|
||||||
|
)
|
||||||
|
|
||||||
def __exit__(
|
def __exit__(
|
||||||
self,
|
self,
|
||||||
exc_type: Optional[Type[BaseException]],
|
exc_type: Optional[Type[BaseException]],
|
||||||
|
@ -287,27 +294,39 @@ class WarningsChecker(WarningsRecorder):
|
||||||
|
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
|
|
||||||
|
if self.expected_warning is None:
|
||||||
|
# nothing to do in this deprecated case, see WARNS_NONE_ARG above
|
||||||
|
return
|
||||||
|
|
||||||
|
if not (exc_type is None and exc_val is None and exc_tb is None):
|
||||||
|
# We currently ignore missing warnings if an exception is active.
|
||||||
|
# TODO: fix this, because it means things are surprisingly order-sensitive.
|
||||||
|
return
|
||||||
|
|
||||||
def found_str():
|
def found_str():
|
||||||
return pformat([record.message for record in self], indent=2)
|
return pformat([record.message for record in self], indent=2)
|
||||||
|
|
||||||
# only check if we're not currently handling an exception
|
try:
|
||||||
if exc_type is None and exc_val is None and exc_tb is None:
|
if not any(issubclass(w.category, self.expected_warning) for w in self):
|
||||||
if self.expected_warning is not None:
|
|
||||||
if not any(issubclass(r.category, self.expected_warning) for r in self):
|
|
||||||
__tracebackhide__ = True
|
|
||||||
fail(
|
fail(
|
||||||
f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n"
|
f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n"
|
||||||
f"The list of emitted warnings is: {found_str()}."
|
f" Emitted warnings: {found_str()}."
|
||||||
)
|
)
|
||||||
elif self.match_expr is not None:
|
elif not any(self.matches(w) for w in self):
|
||||||
for r in self:
|
|
||||||
if issubclass(r.category, self.expected_warning):
|
|
||||||
if re.compile(self.match_expr).search(str(r.message)):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
fail(
|
fail(
|
||||||
f"""\
|
f"DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted.\n"
|
||||||
DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted.
|
f" Regex: {self.match_expr}\n"
|
||||||
Regex: {self.match_expr}
|
f" Emitted warnings: {found_str()}."
|
||||||
Emitted warnings: {found_str()}"""
|
)
|
||||||
|
finally:
|
||||||
|
# Whether or not any warnings matched, we want to re-emit all unmatched warnings.
|
||||||
|
for w in self:
|
||||||
|
if not self.matches(w):
|
||||||
|
warnings.warn_explicit(
|
||||||
|
str(w.message),
|
||||||
|
w.message.__class__, # type: ignore[arg-type]
|
||||||
|
w.filename,
|
||||||
|
w.lineno,
|
||||||
|
module=w.__module__,
|
||||||
|
source=w.source,
|
||||||
)
|
)
|
||||||
|
|
|
@ -203,6 +203,7 @@ class TestDeprecatedCall:
|
||||||
def f():
|
def f():
|
||||||
warnings.warn(warning("hi"))
|
warnings.warn(warning("hi"))
|
||||||
|
|
||||||
|
with pytest.warns(warning):
|
||||||
with pytest.raises(pytest.fail.Exception):
|
with pytest.raises(pytest.fail.Exception):
|
||||||
pytest.deprecated_call(f)
|
pytest.deprecated_call(f)
|
||||||
with pytest.raises(pytest.fail.Exception):
|
with pytest.raises(pytest.fail.Exception):
|
||||||
|
@ -213,7 +214,8 @@ class TestDeprecatedCall:
|
||||||
with pytest.deprecated_call(match=r"must be \d+$"):
|
with pytest.deprecated_call(match=r"must be \d+$"):
|
||||||
warnings.warn("value must be 42", DeprecationWarning)
|
warnings.warn("value must be 42", DeprecationWarning)
|
||||||
|
|
||||||
with pytest.raises(pytest.fail.Exception):
|
with pytest.deprecated_call():
|
||||||
|
with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"):
|
||||||
with pytest.deprecated_call(match=r"must be \d+$"):
|
with pytest.deprecated_call(match=r"must be \d+$"):
|
||||||
warnings.warn("this is not here", DeprecationWarning)
|
warnings.warn("this is not here", DeprecationWarning)
|
||||||
|
|
||||||
|
@ -227,6 +229,7 @@ class TestWarns:
|
||||||
def test_several_messages(self) -> None:
|
def test_several_messages(self) -> None:
|
||||||
# different messages, b/c Python suppresses multiple identical warnings
|
# different messages, b/c Python suppresses multiple identical warnings
|
||||||
pytest.warns(RuntimeWarning, lambda: warnings.warn("w1", RuntimeWarning))
|
pytest.warns(RuntimeWarning, lambda: warnings.warn("w1", RuntimeWarning))
|
||||||
|
with pytest.warns(RuntimeWarning):
|
||||||
with pytest.raises(pytest.fail.Exception):
|
with pytest.raises(pytest.fail.Exception):
|
||||||
pytest.warns(UserWarning, lambda: warnings.warn("w2", RuntimeWarning))
|
pytest.warns(UserWarning, lambda: warnings.warn("w2", RuntimeWarning))
|
||||||
pytest.warns(RuntimeWarning, lambda: warnings.warn("w3", RuntimeWarning))
|
pytest.warns(RuntimeWarning, lambda: warnings.warn("w3", RuntimeWarning))
|
||||||
|
@ -243,6 +246,7 @@ class TestWarns:
|
||||||
pytest.warns(
|
pytest.warns(
|
||||||
(RuntimeWarning, SyntaxWarning), lambda: warnings.warn("w2", SyntaxWarning)
|
(RuntimeWarning, SyntaxWarning), lambda: warnings.warn("w2", SyntaxWarning)
|
||||||
)
|
)
|
||||||
|
with pytest.warns():
|
||||||
pytest.raises(
|
pytest.raises(
|
||||||
pytest.fail.Exception,
|
pytest.fail.Exception,
|
||||||
lambda: pytest.warns(
|
lambda: pytest.warns(
|
||||||
|
@ -258,20 +262,22 @@ class TestWarns:
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
warnings.warn("user", UserWarning)
|
warnings.warn("user", UserWarning)
|
||||||
|
|
||||||
|
with pytest.warns():
|
||||||
with pytest.raises(pytest.fail.Exception) as excinfo:
|
with pytest.raises(pytest.fail.Exception) as excinfo:
|
||||||
with pytest.warns(RuntimeWarning):
|
with pytest.warns(RuntimeWarning):
|
||||||
warnings.warn("user", UserWarning)
|
warnings.warn("user", UserWarning)
|
||||||
excinfo.match(
|
excinfo.match(
|
||||||
r"DID NOT WARN. No warnings of type \(.+RuntimeWarning.+,\) were emitted.\n"
|
r"DID NOT WARN. No warnings of type \(.+RuntimeWarning.+,\) were emitted.\n"
|
||||||
r"The list of emitted warnings is: \[UserWarning\('user',?\)\]."
|
r" Emitted warnings: \[UserWarning\('user',?\)\]."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with pytest.warns():
|
||||||
with pytest.raises(pytest.fail.Exception) as excinfo:
|
with pytest.raises(pytest.fail.Exception) as excinfo:
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
warnings.warn("runtime", RuntimeWarning)
|
warnings.warn("runtime", RuntimeWarning)
|
||||||
excinfo.match(
|
excinfo.match(
|
||||||
r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted.\n"
|
r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted.\n"
|
||||||
r"The list of emitted warnings is: \[RuntimeWarning\('runtime',?\)]."
|
r" Emitted warnings: \[RuntimeWarning\('runtime',?\)]."
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(pytest.fail.Exception) as excinfo:
|
with pytest.raises(pytest.fail.Exception) as excinfo:
|
||||||
|
@ -279,10 +285,11 @@ class TestWarns:
|
||||||
pass
|
pass
|
||||||
excinfo.match(
|
excinfo.match(
|
||||||
r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted.\n"
|
r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted.\n"
|
||||||
r"The list of emitted warnings is: \[\]."
|
r" Emitted warnings: \[\]."
|
||||||
)
|
)
|
||||||
|
|
||||||
warning_classes = (UserWarning, FutureWarning)
|
warning_classes = (UserWarning, FutureWarning)
|
||||||
|
with pytest.warns():
|
||||||
with pytest.raises(pytest.fail.Exception) as excinfo:
|
with pytest.raises(pytest.fail.Exception) as excinfo:
|
||||||
with pytest.warns(warning_classes) as warninfo:
|
with pytest.warns(warning_classes) as warninfo:
|
||||||
warnings.warn("runtime", RuntimeWarning)
|
warnings.warn("runtime", RuntimeWarning)
|
||||||
|
@ -291,7 +298,7 @@ class TestWarns:
|
||||||
messages = [each.message for each in warninfo]
|
messages = [each.message for each in warninfo]
|
||||||
expected_str = (
|
expected_str = (
|
||||||
f"DID NOT WARN. No warnings of type {warning_classes} were emitted.\n"
|
f"DID NOT WARN. No warnings of type {warning_classes} were emitted.\n"
|
||||||
f"The list of emitted warnings is: {messages}."
|
f" Emitted warnings: {messages}."
|
||||||
)
|
)
|
||||||
|
|
||||||
assert str(excinfo.value) == expected_str
|
assert str(excinfo.value) == expected_str
|
||||||
|
@ -367,22 +374,28 @@ class TestWarns:
|
||||||
with pytest.warns(UserWarning, match=r"must be \d+$"):
|
with pytest.warns(UserWarning, match=r"must be \d+$"):
|
||||||
warnings.warn("value must be 42", UserWarning)
|
warnings.warn("value must be 42", UserWarning)
|
||||||
|
|
||||||
|
with pytest.warns():
|
||||||
with pytest.raises(pytest.fail.Exception):
|
with pytest.raises(pytest.fail.Exception):
|
||||||
with pytest.warns(UserWarning, match=r"must be \d+$"):
|
with pytest.warns(UserWarning, match=r"must be \d+$"):
|
||||||
warnings.warn("this is not here", UserWarning)
|
warnings.warn("this is not here", UserWarning)
|
||||||
|
|
||||||
|
with pytest.warns():
|
||||||
with pytest.raises(pytest.fail.Exception):
|
with pytest.raises(pytest.fail.Exception):
|
||||||
with pytest.warns(FutureWarning, match=r"must be \d+$"):
|
with pytest.warns(FutureWarning, match=r"must be \d+$"):
|
||||||
warnings.warn("value must be 42", UserWarning)
|
warnings.warn("value must be 42", UserWarning)
|
||||||
|
|
||||||
def test_one_from_multiple_warns(self) -> None:
|
def test_one_from_multiple_warns(self) -> None:
|
||||||
|
with pytest.warns():
|
||||||
|
with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"):
|
||||||
|
with pytest.warns(UserWarning, match=r"aaa"):
|
||||||
with pytest.warns(UserWarning, match=r"aaa"):
|
with pytest.warns(UserWarning, match=r"aaa"):
|
||||||
warnings.warn("cccccccccc", UserWarning)
|
warnings.warn("cccccccccc", UserWarning)
|
||||||
warnings.warn("bbbbbbbbbb", UserWarning)
|
warnings.warn("bbbbbbbbbb", UserWarning)
|
||||||
warnings.warn("aaaaaaaaaa", UserWarning)
|
warnings.warn("aaaaaaaaaa", UserWarning)
|
||||||
|
|
||||||
def test_none_of_multiple_warns(self) -> None:
|
def test_none_of_multiple_warns(self) -> None:
|
||||||
with pytest.raises(pytest.fail.Exception):
|
with pytest.warns():
|
||||||
|
with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"):
|
||||||
with pytest.warns(UserWarning, match=r"aaa"):
|
with pytest.warns(UserWarning, match=r"aaa"):
|
||||||
warnings.warn("bbbbbbbbbb", UserWarning)
|
warnings.warn("bbbbbbbbbb", UserWarning)
|
||||||
warnings.warn("cccccccccc", UserWarning)
|
warnings.warn("cccccccccc", UserWarning)
|
||||||
|
@ -403,3 +416,33 @@ class TestWarns:
|
||||||
with pytest.warns(UserWarning, foo="bar"): # type: ignore
|
with pytest.warns(UserWarning, foo="bar"): # type: ignore
|
||||||
pass
|
pass
|
||||||
assert "Unexpected keyword arguments" in str(excinfo.value)
|
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 warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("error") # if anything is re-emitted
|
||||||
|
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, match="v2 warning"):
|
||||||
|
with pytest.warns(UserWarning, match="v1 warning"):
|
||||||
|
warnings.warn("v1 warning", UserWarning)
|
||||||
|
warnings.warn("non-matching v2 warning", UserWarning)
|
||||||
|
|
Loading…
Reference in New Issue