Skip to content

Commit 8ecdd4e

Browse files
authored
Merge pull request #4104 from asottile/deprecated_call_match
Implement pytest.deprecated_call with pytest.warns
2 parents b394066 + bf265a4 commit 8ecdd4e

File tree

4 files changed

+47
-46
lines changed

4 files changed

+47
-46
lines changed

changelog/4102.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``pytest.warn`` will capture previously-warned warnings in Python 2. Previously they were never raised.

changelog/4102.feature.rst

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Reimplement ``pytest.deprecated_call`` using ``pytest.warns`` so it supports the ``match='...'`` keyword argument.
2+
3+
This has the side effect that ``pytest.deprecated_call`` now raises ``pytest.fail.Exception`` instead
4+
of ``AssertionError``.

src/_pytest/recwarn.py

+18-39
Original file line numberDiff line numberDiff line change
@@ -43,45 +43,10 @@ def deprecated_call(func=None, *args, **kwargs):
4343
in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings
4444
types above.
4545
"""
46-
if not func:
47-
return _DeprecatedCallContext()
48-
else:
49-
__tracebackhide__ = True
50-
with _DeprecatedCallContext():
51-
return func(*args, **kwargs)
52-
53-
54-
class _DeprecatedCallContext(object):
55-
"""Implements the logic to capture deprecation warnings as a context manager."""
56-
57-
def __enter__(self):
58-
self._captured_categories = []
59-
self._old_warn = warnings.warn
60-
self._old_warn_explicit = warnings.warn_explicit
61-
warnings.warn_explicit = self._warn_explicit
62-
warnings.warn = self._warn
63-
64-
def _warn_explicit(self, message, category, *args, **kwargs):
65-
self._captured_categories.append(category)
66-
67-
def _warn(self, message, category=None, *args, **kwargs):
68-
if isinstance(message, Warning):
69-
self._captured_categories.append(message.__class__)
70-
else:
71-
self._captured_categories.append(category)
72-
73-
def __exit__(self, exc_type, exc_val, exc_tb):
74-
warnings.warn_explicit = self._old_warn_explicit
75-
warnings.warn = self._old_warn
76-
77-
if exc_type is None:
78-
deprecation_categories = (DeprecationWarning, PendingDeprecationWarning)
79-
if not any(
80-
issubclass(c, deprecation_categories) for c in self._captured_categories
81-
):
82-
__tracebackhide__ = True
83-
msg = "Did not produce DeprecationWarning or PendingDeprecationWarning"
84-
raise AssertionError(msg)
46+
__tracebackhide__ = True
47+
if func is not None:
48+
args = (func,) + args
49+
return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs)
8550

8651

8752
def warns(expected_warning, *args, **kwargs):
@@ -116,6 +81,7 @@ def warns(expected_warning, *args, **kwargs):
11681
Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted...
11782
11883
"""
84+
__tracebackhide__ = True
11985
match_expr = None
12086
if not args:
12187
if "match" in kwargs:
@@ -183,12 +149,25 @@ def __enter__(self):
183149
raise RuntimeError("Cannot enter %r twice" % self)
184150
self._list = super(WarningsRecorder, self).__enter__()
185151
warnings.simplefilter("always")
152+
# python3 keeps track of a "filter version", when the filters are
153+
# updated previously seen warnings can be re-warned. python2 has no
154+
# concept of this so we must reset the warnings registry manually.
155+
# trivial patching of `warnings.warn` seems to be enough somehow?
156+
if six.PY2:
157+
158+
def warn(*args, **kwargs):
159+
return self._saved_warn(*args, **kwargs)
160+
161+
warnings.warn, self._saved_warn = warn, warnings.warn
186162
return self
187163

188164
def __exit__(self, *exc_info):
189165
if not self._entered:
190166
__tracebackhide__ = True
191167
raise RuntimeError("Cannot exit %r without entering first" % self)
168+
# see above where `self._saved_warn` is assigned
169+
if six.PY2:
170+
warnings.warn = self._saved_warn
192171
super(WarningsRecorder, self).__exit__(*exc_info)
193172

194173

testing/test_recwarn.py

+24-7
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,8 @@ def dep_explicit(self, i):
7676
)
7777

7878
def test_deprecated_call_raises(self):
79-
with pytest.raises(AssertionError) as excinfo:
79+
with pytest.raises(pytest.fail.Exception, match="No warnings of type"):
8080
pytest.deprecated_call(self.dep, 3, 5)
81-
assert "Did not produce" in str(excinfo)
8281

8382
def test_deprecated_call(self):
8483
pytest.deprecated_call(self.dep, 0, 5)
@@ -100,7 +99,7 @@ def test_deprecated_call_preserves(self):
10099
assert warn_explicit is warnings.warn_explicit
101100

102101
def test_deprecated_explicit_call_raises(self):
103-
with pytest.raises(AssertionError):
102+
with pytest.raises(pytest.fail.Exception):
104103
pytest.deprecated_call(self.dep_explicit, 3)
105104

106105
def test_deprecated_explicit_call(self):
@@ -116,8 +115,8 @@ def test_deprecated_call_no_warning(self, mode):
116115
def f():
117116
pass
118117

119-
msg = "Did not produce DeprecationWarning or PendingDeprecationWarning"
120-
with pytest.raises(AssertionError, match=msg):
118+
msg = "No warnings of type (.*DeprecationWarning.*, .*PendingDeprecationWarning.*)"
119+
with pytest.raises(pytest.fail.Exception, match=msg):
121120
if mode == "call":
122121
pytest.deprecated_call(f)
123122
else:
@@ -179,12 +178,20 @@ def test_deprecated_call_specificity(self):
179178
def f():
180179
warnings.warn(warning("hi"))
181180

182-
with pytest.raises(AssertionError):
181+
with pytest.raises(pytest.fail.Exception):
183182
pytest.deprecated_call(f)
184-
with pytest.raises(AssertionError):
183+
with pytest.raises(pytest.fail.Exception):
185184
with pytest.deprecated_call():
186185
f()
187186

187+
def test_deprecated_call_supports_match(self):
188+
with pytest.deprecated_call(match=r"must be \d+$"):
189+
warnings.warn("value must be 42", DeprecationWarning)
190+
191+
with pytest.raises(pytest.fail.Exception):
192+
with pytest.deprecated_call(match=r"must be \d+$"):
193+
warnings.warn("this is not here", DeprecationWarning)
194+
188195

189196
class TestWarns(object):
190197
def test_strings(self):
@@ -343,3 +350,13 @@ def test_none_of_multiple_warns(self):
343350
with pytest.warns(UserWarning, match=r"aaa"):
344351
warnings.warn("bbbbbbbbbb", UserWarning)
345352
warnings.warn("cccccccccc", UserWarning)
353+
354+
@pytest.mark.filterwarnings("ignore")
355+
def test_can_capture_previously_warned(self):
356+
def f():
357+
warnings.warn(UserWarning("ohai"))
358+
return 10
359+
360+
assert f() == 10
361+
assert pytest.warns(UserWarning, f) == 10
362+
assert pytest.warns(UserWarning, f) == 10

0 commit comments

Comments
 (0)