Merge pull request #2480 from nicoddemus/issue-2469-deprecated-call-ctx

deprecated_call context manager captures warnings already raised
This commit is contained in:
Bruno Oliveira 2017-06-22 10:53:28 -03:00 committed by GitHub
commit 22b7701431
3 changed files with 92 additions and 43 deletions

View File

@ -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():
@ -40,38 +38,47 @@ 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))
categories = []
def warn_explicit(message, category, *args, **kwargs):
categories.append(category)
def warn(message, category=None, *args, **kwargs):
if isinstance(message, Warning):
categories.append(message.__class__)
return _DeprecatedCallContext()
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
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)
def warns(expected_warning, *args, **kwargs):

4
changelog/2469.bugfix Normal file
View File

@ -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).

View File

@ -77,7 +77,7 @@ class TestDeprecatedCall(object):
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)
@ -106,17 +106,51 @@ class TestDeprecatedCall(object):
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'):
@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():
self.dep(1)
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':
assert pytest.deprecated_call(f) == 10
else:
with pytest.deprecated_call():
assert f() == 10
@pytest.mark.parametrize('mode', ['context_manager', 'call'])
def test_deprecated_call_exception_is_raised(self, mode):
"""If the block of the code being tested by deprecated_call() raises an exception,
it must raise the exception undisturbed.
"""
def f():
raise ValueError('some exception')
with pytest.raises(ValueError, match='some exception'):
if mode == 'call':
pytest.deprecated_call(f)
else:
@ -128,9 +162,13 @@ class TestDeprecatedCall(object):
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