Skip to content

Commit

Permalink
deprecated_call context manager captures warnings already raised
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoddemus committed Jun 7, 2017
1 parent 57e2ced commit a409ee3
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 39 deletions.
64 changes: 35 additions & 29 deletions _pytest/recwarn.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@ def recwarn():


def deprecated_call(func=None, *args, **kwargs):
""" assert that calling ``func(*args, **kwargs)`` triggers a
``DeprecationWarning`` or ``PendingDeprecationWarning``.
This function can be used as a context manager::
"""context manager that can be used to ensure a block of code triggers a
``DeprecationWarning`` or ``PendingDeprecationWarning``::
>>> import warnings
>>> def api_call_v2():
Expand All @@ -40,38 +38,46 @@ def deprecated_call(func=None, *args, **kwargs):
>>> with deprecated_call():
... assert api_call_v2() == 200
Note: we cannot use WarningsRecorder here because it is still subject
to the mechanism that prevents warnings of the same type from being
triggered twice for the same module. See #1190.
``deprecated_call`` can also be used by passing a function and ``*args`` and ``*kwargs``,
in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings
types above.
"""
if not func:
return WarningsChecker(expected_warning=(DeprecationWarning, PendingDeprecationWarning))
return _DeprecatedCallContext()
else:
with _DeprecatedCallContext():
return func(*args, **kwargs)


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

def warn_explicit(message, category, *args, **kwargs):
categories.append(category)
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(message, category=None, *args, **kwargs):
def _warn(self, message, category=None, *args, **kwargs):
if isinstance(message, Warning):
categories.append(message.__class__)
self._captured_categories.append(message.__class__)
else:
categories.append(category)

old_warn = warnings.warn
old_warn_explicit = warnings.warn_explicit
warnings.warn_explicit = warn_explicit
warnings.warn = warn
try:
ret = func(*args, **kwargs)
finally:
warnings.warn_explicit = old_warn_explicit
warnings.warn = old_warn
deprecation_categories = (DeprecationWarning, PendingDeprecationWarning)
if not any(issubclass(c, deprecation_categories) for c in categories):
__tracebackhide__ = True
raise AssertionError("%r did not produce DeprecationWarning" % (func,))
return ret
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)


def warns(expected_warning, *args, **kwargs):
Expand Down
4 changes: 4 additions & 0 deletions changelog/2469.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
``deprecated_call`` in context-manager form now captures deprecation warnings even if
the same warning has already been raised. Also, ``deprecated_call`` will always produce
the same error message (previously it would produce different messages in context-manager vs.
function-call mode).
43 changes: 33 additions & 10 deletions testing/test_recwarn.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def dep_explicit(self, i):
def test_deprecated_call_raises(self):
with pytest.raises(AssertionError) as excinfo:
pytest.deprecated_call(self.dep, 3, 5)
assert str(excinfo).find("did not produce") != -1
assert 'Did not produce' in str(excinfo)

def test_deprecated_call(self):
pytest.deprecated_call(self.dep, 0, 5)
Expand Down Expand Up @@ -106,31 +106,54 @@ def test_deprecated_explicit_call(self):
pytest.deprecated_call(self.dep_explicit, 0)
pytest.deprecated_call(self.dep_explicit, 0)

def test_deprecated_call_as_context_manager_no_warning(self):
with pytest.raises(pytest.fail.Exception, matches='^DID NOT WARN'):
with pytest.deprecated_call():
self.dep(1)
@pytest.mark.parametrize('mode', ['context_manager', 'call'])
def test_deprecated_call_no_warning(self, mode):
"""Ensure deprecated_call() raises the expected failure when its block/function does
not raise a deprecation warning.
"""
def f():
pass

msg = 'Did not produce DeprecationWarning or PendingDeprecationWarning'
with pytest.raises(AssertionError, matches=msg):
if mode == 'call':
pytest.deprecated_call(f)
else:
with pytest.deprecated_call():
f()

@pytest.mark.parametrize('warning_type', [PendingDeprecationWarning, DeprecationWarning])
@pytest.mark.parametrize('mode', ['context_manager', 'call'])
def test_deprecated_call_modes(self, warning_type, mode):
@pytest.mark.parametrize('call_f_first', [True, False])
def test_deprecated_call_modes(self, warning_type, mode, call_f_first):
"""Ensure deprecated_call() captures a deprecation warning as expected inside its
block/function.
"""
def f():
warnings.warn(warning_type("hi"))

return 10

# ensure deprecated_call() can capture the warning even if it has already been triggered
if call_f_first:
assert f() == 10
if mode == 'call':
pytest.deprecated_call(f)
assert pytest.deprecated_call(f) == 10
else:
with pytest.deprecated_call():
f()
assert f() == 10

def test_deprecated_call_specificity(self):
other_warnings = [Warning, UserWarning, SyntaxWarning, RuntimeWarning,
FutureWarning, ImportWarning, UnicodeWarning]
for warning in other_warnings:
def f():
py.std.warnings.warn(warning("hi"))
warnings.warn(warning("hi"))

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

def test_deprecated_function_already_called(self, testdir):
"""deprecated_call should be able to catch a call to a deprecated
Expand Down

0 comments on commit a409ee3

Please sign in to comment.