Skip to content

Commit 679bd6f

Browse files
authored
Merge pull request #10937 from reaganjlee/re-emit
2 parents a50ea1b + 2d48171 commit 679bd6f

File tree

4 files changed

+140
-70
lines changed

4 files changed

+140
-70
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ Raphael Pierzina
311311
Rafal Semik
312312
Raquel Alegre
313313
Ravi Chandra
314+
Reagan Lee
314315
Robert Holt
315316
Roberto Aldera
316317
Roberto Polli

changelog/9288.breaking.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
:func:`pytest.warns <warns>` now re-emits unmatched warnings when the context
2+
closes -- previously it would consume all warnings, hiding those that were not
3+
matched by the function.
4+
5+
While this is a new feature, we decided to announce this as a breaking change
6+
because many test suites are configured to error-out on warnings, and will
7+
therefore fail on the newly-re-emitted warnings.

src/_pytest/recwarn.py

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,10 @@ def warns( # noqa: F811
117117
warning of that class or classes.
118118
119119
This helper produces a list of :class:`warnings.WarningMessage` objects, one for
120-
each warning raised (regardless of whether it is an ``expected_warning`` or not).
120+
each warning emitted (regardless of whether it is an ``expected_warning`` or not).
121+
Since pytest 8.0, unmatched warnings are also re-emitted when the context closes.
121122
122-
This function can be used as a context manager, which will capture all the raised
123-
warnings inside it::
123+
This function can be used as a context manager::
124124
125125
>>> import pytest
126126
>>> with pytest.warns(RuntimeWarning):
@@ -135,8 +135,9 @@ def warns( # noqa: F811
135135
>>> with pytest.warns(UserWarning, match=r'must be \d+$'):
136136
... warnings.warn("value must be 42", UserWarning)
137137
138-
>>> with pytest.warns(UserWarning, match=r'must be \d+$'):
139-
... warnings.warn("this is not here", UserWarning)
138+
>>> with pytest.warns(UserWarning): # catch re-emitted warning
139+
... with pytest.warns(UserWarning, match=r'must be \d+$'):
140+
... warnings.warn("this is not here", UserWarning)
140141
Traceback (most recent call last):
141142
...
142143
Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
@@ -277,6 +278,12 @@ def __init__(
277278
self.expected_warning = expected_warning_tup
278279
self.match_expr = match_expr
279280

281+
def matches(self, warning: warnings.WarningMessage) -> bool:
282+
assert self.expected_warning is not None
283+
return issubclass(warning.category, self.expected_warning) and bool(
284+
self.match_expr is None or re.search(self.match_expr, str(warning.message))
285+
)
286+
280287
def __exit__(
281288
self,
282289
exc_type: Optional[Type[BaseException]],
@@ -287,27 +294,39 @@ def __exit__(
287294

288295
__tracebackhide__ = True
289296

297+
if self.expected_warning is None:
298+
# nothing to do in this deprecated case, see WARNS_NONE_ARG above
299+
return
300+
301+
if not (exc_type is None and exc_val is None and exc_tb is None):
302+
# We currently ignore missing warnings if an exception is active.
303+
# TODO: fix this, because it means things are surprisingly order-sensitive.
304+
return
305+
290306
def found_str():
291307
return pformat([record.message for record in self], indent=2)
292308

293-
# only check if we're not currently handling an exception
294-
if exc_type is None and exc_val is None and exc_tb is None:
295-
if self.expected_warning is not None:
296-
if not any(issubclass(r.category, self.expected_warning) for r in self):
297-
__tracebackhide__ = True
298-
fail(
299-
f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n"
300-
f"The list of emitted warnings is: {found_str()}."
309+
try:
310+
if not any(issubclass(w.category, self.expected_warning) for w in self):
311+
fail(
312+
f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n"
313+
f" Emitted warnings: {found_str()}."
314+
)
315+
elif not any(self.matches(w) for w in self):
316+
fail(
317+
f"DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted.\n"
318+
f" Regex: {self.match_expr}\n"
319+
f" Emitted warnings: {found_str()}."
320+
)
321+
finally:
322+
# Whether or not any warnings matched, we want to re-emit all unmatched warnings.
323+
for w in self:
324+
if not self.matches(w):
325+
warnings.warn_explicit(
326+
str(w.message),
327+
w.message.__class__, # type: ignore[arg-type]
328+
w.filename,
329+
w.lineno,
330+
module=w.__module__,
331+
source=w.source,
301332
)
302-
elif self.match_expr is not None:
303-
for r in self:
304-
if issubclass(r.category, self.expected_warning):
305-
if re.compile(self.match_expr).search(str(r.message)):
306-
break
307-
else:
308-
fail(
309-
f"""\
310-
DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted.
311-
Regex: {self.match_expr}
312-
Emitted warnings: {found_str()}"""
313-
)

testing/test_recwarn.py

Lines changed: 88 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -203,19 +203,21 @@ def test_deprecated_call_specificity(self) -> None:
203203
def f():
204204
warnings.warn(warning("hi"))
205205

206-
with pytest.raises(pytest.fail.Exception):
207-
pytest.deprecated_call(f)
208-
with pytest.raises(pytest.fail.Exception):
209-
with pytest.deprecated_call():
210-
f()
206+
with pytest.warns(warning):
207+
with pytest.raises(pytest.fail.Exception):
208+
pytest.deprecated_call(f)
209+
with pytest.raises(pytest.fail.Exception):
210+
with pytest.deprecated_call():
211+
f()
211212

212213
def test_deprecated_call_supports_match(self) -> None:
213214
with pytest.deprecated_call(match=r"must be \d+$"):
214215
warnings.warn("value must be 42", DeprecationWarning)
215216

216-
with pytest.raises(pytest.fail.Exception):
217-
with pytest.deprecated_call(match=r"must be \d+$"):
218-
warnings.warn("this is not here", DeprecationWarning)
217+
with pytest.deprecated_call():
218+
with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"):
219+
with pytest.deprecated_call(match=r"must be \d+$"):
220+
warnings.warn("this is not here", DeprecationWarning)
219221

220222

221223
class TestWarns:
@@ -227,8 +229,9 @@ def test_check_callable(self) -> None:
227229
def test_several_messages(self) -> None:
228230
# different messages, b/c Python suppresses multiple identical warnings
229231
pytest.warns(RuntimeWarning, lambda: warnings.warn("w1", RuntimeWarning))
230-
with pytest.raises(pytest.fail.Exception):
231-
pytest.warns(UserWarning, lambda: warnings.warn("w2", RuntimeWarning))
232+
with pytest.warns(RuntimeWarning):
233+
with pytest.raises(pytest.fail.Exception):
234+
pytest.warns(UserWarning, lambda: warnings.warn("w2", RuntimeWarning))
232235
pytest.warns(RuntimeWarning, lambda: warnings.warn("w3", RuntimeWarning))
233236

234237
def test_function(self) -> None:
@@ -243,13 +246,14 @@ def test_warning_tuple(self) -> None:
243246
pytest.warns(
244247
(RuntimeWarning, SyntaxWarning), lambda: warnings.warn("w2", SyntaxWarning)
245248
)
246-
pytest.raises(
247-
pytest.fail.Exception,
248-
lambda: pytest.warns(
249-
(RuntimeWarning, SyntaxWarning),
250-
lambda: warnings.warn("w3", UserWarning),
251-
),
252-
)
249+
with pytest.warns():
250+
pytest.raises(
251+
pytest.fail.Exception,
252+
lambda: pytest.warns(
253+
(RuntimeWarning, SyntaxWarning),
254+
lambda: warnings.warn("w3", UserWarning),
255+
),
256+
)
253257

254258
def test_as_contextmanager(self) -> None:
255259
with pytest.warns(RuntimeWarning):
@@ -258,40 +262,43 @@ def test_as_contextmanager(self) -> None:
258262
with pytest.warns(UserWarning):
259263
warnings.warn("user", UserWarning)
260264

261-
with pytest.raises(pytest.fail.Exception) as excinfo:
262-
with pytest.warns(RuntimeWarning):
263-
warnings.warn("user", UserWarning)
265+
with pytest.warns():
266+
with pytest.raises(pytest.fail.Exception) as excinfo:
267+
with pytest.warns(RuntimeWarning):
268+
warnings.warn("user", UserWarning)
264269
excinfo.match(
265270
r"DID NOT WARN. No warnings of type \(.+RuntimeWarning.+,\) were emitted.\n"
266-
r"The list of emitted warnings is: \[UserWarning\('user',?\)\]."
271+
r" Emitted warnings: \[UserWarning\('user',?\)\]."
267272
)
268273

269-
with pytest.raises(pytest.fail.Exception) as excinfo:
270-
with pytest.warns(UserWarning):
271-
warnings.warn("runtime", RuntimeWarning)
274+
with pytest.warns():
275+
with pytest.raises(pytest.fail.Exception) as excinfo:
276+
with pytest.warns(UserWarning):
277+
warnings.warn("runtime", RuntimeWarning)
272278
excinfo.match(
273279
r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted.\n"
274-
r"The list of emitted warnings is: \[RuntimeWarning\('runtime',?\)]."
280+
r" Emitted warnings: \[RuntimeWarning\('runtime',?\)]."
275281
)
276282

277283
with pytest.raises(pytest.fail.Exception) as excinfo:
278284
with pytest.warns(UserWarning):
279285
pass
280286
excinfo.match(
281287
r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted.\n"
282-
r"The list of emitted warnings is: \[\]."
288+
r" Emitted warnings: \[\]."
283289
)
284290

285291
warning_classes = (UserWarning, FutureWarning)
286-
with pytest.raises(pytest.fail.Exception) as excinfo:
287-
with pytest.warns(warning_classes) as warninfo:
288-
warnings.warn("runtime", RuntimeWarning)
289-
warnings.warn("import", ImportWarning)
292+
with pytest.warns():
293+
with pytest.raises(pytest.fail.Exception) as excinfo:
294+
with pytest.warns(warning_classes) as warninfo:
295+
warnings.warn("runtime", RuntimeWarning)
296+
warnings.warn("import", ImportWarning)
290297

291298
messages = [each.message for each in warninfo]
292299
expected_str = (
293300
f"DID NOT WARN. No warnings of type {warning_classes} were emitted.\n"
294-
f"The list of emitted warnings is: {messages}."
301+
f" Emitted warnings: {messages}."
295302
)
296303

297304
assert str(excinfo.value) == expected_str
@@ -367,25 +374,31 @@ def test_match_regex(self) -> None:
367374
with pytest.warns(UserWarning, match=r"must be \d+$"):
368375
warnings.warn("value must be 42", UserWarning)
369376

370-
with pytest.raises(pytest.fail.Exception):
371-
with pytest.warns(UserWarning, match=r"must be \d+$"):
372-
warnings.warn("this is not here", UserWarning)
377+
with pytest.warns():
378+
with pytest.raises(pytest.fail.Exception):
379+
with pytest.warns(UserWarning, match=r"must be \d+$"):
380+
warnings.warn("this is not here", UserWarning)
373381

374-
with pytest.raises(pytest.fail.Exception):
375-
with pytest.warns(FutureWarning, match=r"must be \d+$"):
376-
warnings.warn("value must be 42", UserWarning)
382+
with pytest.warns():
383+
with pytest.raises(pytest.fail.Exception):
384+
with pytest.warns(FutureWarning, match=r"must be \d+$"):
385+
warnings.warn("value must be 42", UserWarning)
377386

378387
def test_one_from_multiple_warns(self) -> None:
379-
with pytest.warns(UserWarning, match=r"aaa"):
380-
warnings.warn("cccccccccc", UserWarning)
381-
warnings.warn("bbbbbbbbbb", UserWarning)
382-
warnings.warn("aaaaaaaaaa", UserWarning)
388+
with pytest.warns():
389+
with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"):
390+
with pytest.warns(UserWarning, match=r"aaa"):
391+
with pytest.warns(UserWarning, match=r"aaa"):
392+
warnings.warn("cccccccccc", UserWarning)
393+
warnings.warn("bbbbbbbbbb", UserWarning)
394+
warnings.warn("aaaaaaaaaa", UserWarning)
383395

384396
def test_none_of_multiple_warns(self) -> None:
385-
with pytest.raises(pytest.fail.Exception):
386-
with pytest.warns(UserWarning, match=r"aaa"):
387-
warnings.warn("bbbbbbbbbb", UserWarning)
388-
warnings.warn("cccccccccc", UserWarning)
397+
with pytest.warns():
398+
with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"):
399+
with pytest.warns(UserWarning, match=r"aaa"):
400+
warnings.warn("bbbbbbbbbb", UserWarning)
401+
warnings.warn("cccccccccc", UserWarning)
389402

390403
@pytest.mark.filterwarnings("ignore")
391404
def test_can_capture_previously_warned(self) -> None:
@@ -403,3 +416,33 @@ def test_warns_context_manager_with_kwargs(self) -> None:
403416
with pytest.warns(UserWarning, foo="bar"): # type: ignore
404417
pass
405418
assert "Unexpected keyword arguments" in str(excinfo.value)
419+
420+
def test_re_emit_single(self) -> None:
421+
with pytest.warns(DeprecationWarning):
422+
with pytest.warns(UserWarning):
423+
warnings.warn("user warning", UserWarning)
424+
warnings.warn("some deprecation warning", DeprecationWarning)
425+
426+
def test_re_emit_multiple(self) -> None:
427+
with pytest.warns(UserWarning):
428+
warnings.warn("first warning", UserWarning)
429+
warnings.warn("second warning", UserWarning)
430+
431+
def test_re_emit_match_single(self) -> None:
432+
with pytest.warns(DeprecationWarning):
433+
with pytest.warns(UserWarning, match="user warning"):
434+
warnings.warn("user warning", UserWarning)
435+
warnings.warn("some deprecation warning", DeprecationWarning)
436+
437+
def test_re_emit_match_multiple(self) -> None:
438+
with warnings.catch_warnings():
439+
warnings.simplefilter("error") # if anything is re-emitted
440+
with pytest.warns(UserWarning, match="user warning"):
441+
warnings.warn("first user warning", UserWarning)
442+
warnings.warn("second user warning", UserWarning)
443+
444+
def test_re_emit_non_match_single(self) -> None:
445+
with pytest.warns(UserWarning, match="v2 warning"):
446+
with pytest.warns(UserWarning, match="v1 warning"):
447+
warnings.warn("v1 warning", UserWarning)
448+
warnings.warn("non-matching v2 warning", UserWarning)

0 commit comments

Comments
 (0)