Merge pull request #2480 from nicoddemus/issue-2469-deprecated-call-ctx
deprecated_call context manager captures warnings already raised
This commit is contained in:
commit
22b7701431
|
@ -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__)
|
||||
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):
|
||||
return _DeprecatedCallContext()
|
||||
else:
|
||||
__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):
|
||||
|
|
|
@ -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).
|
|
@ -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,31 +106,69 @@ 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'):
|
||||
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
|
||||
|
||||
@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:
|
||||
with pytest.deprecated_call():
|
||||
f()
|
||||
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue