Skip to content

Commit 6967f30

Browse files
Merge pull request #2711 from massich/mimic_raises_signature_in_warns
[MRG][feature] Change warns signature to mimic the raises call
2 parents b55a4f8 + a0c6758 commit 6967f30

File tree

4 files changed

+77
-6
lines changed

4 files changed

+77
-6
lines changed

_pytest/recwarn.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import sys
99
import warnings
1010

11+
import re
12+
1113
from _pytest.fixtures import yield_fixture
1214
from _pytest.outcomes import fail
1315

@@ -98,23 +100,41 @@ def warns(expected_warning, *args, **kwargs):
98100
99101
>>> with warns(RuntimeWarning):
100102
... warnings.warn("my warning", RuntimeWarning)
103+
104+
In the context manager form you may use the keyword argument ``match`` to assert
105+
that the exception matches a text or regex::
106+
107+
>>> with warns(UserWarning, match='must be 0 or None'):
108+
... warnings.warn("value must be 0 or None", UserWarning)
109+
110+
>>> with warns(UserWarning, match=r'must be \d+$'):
111+
... warnings.warn("value must be 42", UserWarning)
112+
113+
>>> with warns(UserWarning, match=r'must be \d+$'):
114+
... warnings.warn("this is not here", UserWarning)
115+
Traceback (most recent call last):
116+
...
117+
Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted...
118+
101119
"""
102-
wcheck = WarningsChecker(expected_warning)
120+
match_expr = None
103121
if not args:
104-
return wcheck
122+
if "match" in kwargs:
123+
match_expr = kwargs.pop("match")
124+
return WarningsChecker(expected_warning, match_expr=match_expr)
105125
elif isinstance(args[0], str):
106126
code, = args
107127
assert isinstance(code, str)
108128
frame = sys._getframe(1)
109129
loc = frame.f_locals.copy()
110130
loc.update(kwargs)
111131

112-
with wcheck:
132+
with WarningsChecker(expected_warning, match_expr=match_expr):
113133
code = _pytest._code.Source(code).compile()
114134
py.builtin.exec_(code, frame.f_globals, loc)
115135
else:
116136
func = args[0]
117-
with wcheck:
137+
with WarningsChecker(expected_warning, match_expr=match_expr):
118138
return func(*args[1:], **kwargs)
119139

120140

@@ -174,7 +194,7 @@ def __exit__(self, *exc_info):
174194

175195

176196
class WarningsChecker(WarningsRecorder):
177-
def __init__(self, expected_warning=None):
197+
def __init__(self, expected_warning=None, match_expr=None):
178198
super(WarningsChecker, self).__init__()
179199

180200
msg = ("exceptions must be old-style classes or "
@@ -189,6 +209,7 @@ def __init__(self, expected_warning=None):
189209
raise TypeError(msg % type(expected_warning))
190210

191211
self.expected_warning = expected_warning
212+
self.match_expr = match_expr
192213

193214
def __exit__(self, *exc_info):
194215
super(WarningsChecker, self).__exit__(*exc_info)
@@ -203,3 +224,15 @@ def __exit__(self, *exc_info):
203224
"The list of emitted warnings is: {1}.".format(
204225
self.expected_warning,
205226
[each.message for each in self]))
227+
elif self.match_expr is not None:
228+
for r in self:
229+
if issubclass(r.category, self.expected_warning):
230+
if re.compile(self.match_expr).search(str(r.message)):
231+
break
232+
else:
233+
fail("DID NOT WARN. No warnings of type {0} matching"
234+
" ('{1}') was emitted. The list of emitted warnings"
235+
" is: {2}.".format(
236+
self.expected_warning,
237+
self.match_expr,
238+
[each.message for each in self]))

changelog/2708.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Match ``warns`` signature to ``raises`` by adding ``match`` keyworkd.

doc/en/warnings.rst

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,20 @@ which works in a similar manner to :ref:`raises <assertraises>`::
168168
with pytest.warns(UserWarning):
169169
warnings.warn("my warning", UserWarning)
170170

171-
The test will fail if the warning in question is not raised.
171+
The test will fail if the warning in question is not raised. The keyword
172+
argument ``match`` to assert that the exception matches a text or regex::
173+
174+
>>> with warns(UserWarning, match='must be 0 or None'):
175+
... warnings.warn("value must be 0 or None", UserWarning)
176+
177+
>>> with warns(UserWarning, match=r'must be \d+$'):
178+
... warnings.warn("value must be 42", UserWarning)
179+
180+
>>> with warns(UserWarning, match=r'must be \d+$'):
181+
... warnings.warn("this is not here", UserWarning)
182+
Traceback (most recent call last):
183+
...
184+
Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted...
172185

173186
You can also call ``pytest.warns`` on a function or code string::
174187

testing/test_recwarn.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,27 @@ def test(run):
284284
''')
285285
result = testdir.runpytest()
286286
result.stdout.fnmatch_lines(['*2 passed in*'])
287+
288+
def test_match_regex(self):
289+
with pytest.warns(UserWarning, match=r'must be \d+$'):
290+
warnings.warn("value must be 42", UserWarning)
291+
292+
with pytest.raises(pytest.fail.Exception):
293+
with pytest.warns(UserWarning, match=r'must be \d+$'):
294+
warnings.warn("this is not here", UserWarning)
295+
296+
with pytest.raises(pytest.fail.Exception):
297+
with pytest.warns(FutureWarning, match=r'must be \d+$'):
298+
warnings.warn("value must be 42", UserWarning)
299+
300+
def test_one_from_multiple_warns(self):
301+
with pytest.warns(UserWarning, match=r'aaa'):
302+
warnings.warn("cccccccccc", UserWarning)
303+
warnings.warn("bbbbbbbbbb", UserWarning)
304+
warnings.warn("aaaaaaaaaa", UserWarning)
305+
306+
def test_none_of_multiple_warns(self):
307+
with pytest.raises(pytest.fail.Exception):
308+
with pytest.warns(UserWarning, match=r'aaa'):
309+
warnings.warn("bbbbbbbbbb", UserWarning)
310+
warnings.warn("cccccccccc", UserWarning)

0 commit comments

Comments
 (0)