Skip to content

Commit

Permalink
Merge pull request #4104 from asottile/deprecated_call_match
Browse files Browse the repository at this point in the history
Implement pytest.deprecated_call with pytest.warns
  • Loading branch information
asottile authored Oct 11, 2018
2 parents b394066 + bf265a4 commit 8ecdd4e
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 46 deletions.
1 change: 1 addition & 0 deletions changelog/4102.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``pytest.warn`` will capture previously-warned warnings in Python 2. Previously they were never raised.
4 changes: 4 additions & 0 deletions changelog/4102.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Reimplement ``pytest.deprecated_call`` using ``pytest.warns`` so it supports the ``match='...'`` keyword argument.

This has the side effect that ``pytest.deprecated_call`` now raises ``pytest.fail.Exception`` instead
of ``AssertionError``.
57 changes: 18 additions & 39 deletions src/_pytest/recwarn.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,45 +43,10 @@ def deprecated_call(func=None, *args, **kwargs):
in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings
types above.
"""
if not func:
return _DeprecatedCallContext()
else:
__tracebackhide__ = True
with _DeprecatedCallContext():
return func(*args, **kwargs)


class _DeprecatedCallContext(object):
"""Implements the logic to capture deprecation warnings as a context manager."""

def __enter__(self):
self._captured_categories = []
self._old_warn = warnings.warn
self._old_warn_explicit = warnings.warn_explicit
warnings.warn_explicit = self._warn_explicit
warnings.warn = self._warn

def _warn_explicit(self, message, category, *args, **kwargs):
self._captured_categories.append(category)

def _warn(self, message, category=None, *args, **kwargs):
if isinstance(message, Warning):
self._captured_categories.append(message.__class__)
else:
self._captured_categories.append(category)

def __exit__(self, exc_type, exc_val, exc_tb):
warnings.warn_explicit = self._old_warn_explicit
warnings.warn = self._old_warn

if exc_type is None:
deprecation_categories = (DeprecationWarning, PendingDeprecationWarning)
if not any(
issubclass(c, deprecation_categories) for c in self._captured_categories
):
__tracebackhide__ = True
msg = "Did not produce DeprecationWarning or PendingDeprecationWarning"
raise AssertionError(msg)
__tracebackhide__ = True
if func is not None:
args = (func,) + args
return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs)


def warns(expected_warning, *args, **kwargs):
Expand Down Expand Up @@ -116,6 +81,7 @@ def warns(expected_warning, *args, **kwargs):
Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted...
"""
__tracebackhide__ = True
match_expr = None
if not args:
if "match" in kwargs:
Expand Down Expand Up @@ -183,12 +149,25 @@ def __enter__(self):
raise RuntimeError("Cannot enter %r twice" % self)
self._list = super(WarningsRecorder, self).__enter__()
warnings.simplefilter("always")
# python3 keeps track of a "filter version", when the filters are
# updated previously seen warnings can be re-warned. python2 has no
# concept of this so we must reset the warnings registry manually.
# trivial patching of `warnings.warn` seems to be enough somehow?
if six.PY2:

def warn(*args, **kwargs):
return self._saved_warn(*args, **kwargs)

warnings.warn, self._saved_warn = warn, warnings.warn
return self

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


Expand Down
31 changes: 24 additions & 7 deletions testing/test_recwarn.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,8 @@ def dep_explicit(self, i):
)

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

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

def test_deprecated_explicit_call_raises(self):
with pytest.raises(AssertionError):
with pytest.raises(pytest.fail.Exception):
pytest.deprecated_call(self.dep_explicit, 3)

def test_deprecated_explicit_call(self):
Expand All @@ -116,8 +115,8 @@ def test_deprecated_call_no_warning(self, mode):
def f():
pass

msg = "Did not produce DeprecationWarning or PendingDeprecationWarning"
with pytest.raises(AssertionError, match=msg):
msg = "No warnings of type (.*DeprecationWarning.*, .*PendingDeprecationWarning.*)"
with pytest.raises(pytest.fail.Exception, match=msg):
if mode == "call":
pytest.deprecated_call(f)
else:
Expand Down Expand Up @@ -179,12 +178,20 @@ def test_deprecated_call_specificity(self):
def f():
warnings.warn(warning("hi"))

with pytest.raises(AssertionError):
with pytest.raises(pytest.fail.Exception):
pytest.deprecated_call(f)
with pytest.raises(AssertionError):
with pytest.raises(pytest.fail.Exception):
with pytest.deprecated_call():
f()

def test_deprecated_call_supports_match(self):
with pytest.deprecated_call(match=r"must be \d+$"):
warnings.warn("value must be 42", DeprecationWarning)

with pytest.raises(pytest.fail.Exception):
with pytest.deprecated_call(match=r"must be \d+$"):
warnings.warn("this is not here", DeprecationWarning)


class TestWarns(object):
def test_strings(self):
Expand Down Expand Up @@ -343,3 +350,13 @@ def test_none_of_multiple_warns(self):
with pytest.warns(UserWarning, match=r"aaa"):
warnings.warn("bbbbbbbbbb", UserWarning)
warnings.warn("cccccccccc", UserWarning)

@pytest.mark.filterwarnings("ignore")
def test_can_capture_previously_warned(self):
def f():
warnings.warn(UserWarning("ohai"))
return 10

assert f() == 10
assert pytest.warns(UserWarning, f) == 10
assert pytest.warns(UserWarning, f) == 10

0 comments on commit 8ecdd4e

Please sign in to comment.