Skip to content

Implement pytest.deprecated_call with pytest.warns #4104

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

img

"""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