# -*- coding: utf-8 -*- """ recording warnings during test function execution. """ from __future__ import absolute_import from __future__ import division from __future__ import print_function import inspect import re import sys import warnings import six import _pytest._code from _pytest.deprecated import PYTEST_WARNS_UNKNOWN_KWARGS from _pytest.deprecated import WARNS_EXEC from _pytest.fixtures import yield_fixture from _pytest.outcomes import fail @yield_fixture def recwarn(): """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. See http://docs.python.org/library/warnings.html for information on warning categories. """ wrec = WarningsRecorder() with wrec: warnings.simplefilter("default") yield wrec def deprecated_call(func=None, *args, **kwargs): """context manager that can be used to ensure a block of code triggers a ``DeprecationWarning`` or ``PendingDeprecationWarning``:: >>> import warnings >>> def api_call_v2(): ... warnings.warn('use v3 of this api', DeprecationWarning) ... return 200 >>> with deprecated_call(): ... assert api_call_v2() == 200 ``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. """ __tracebackhide__ = True if func is not None: args = (func,) + args return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs) def warns(expected_warning, *args, **kwargs): r"""Assert that code raises a particular class of warning. Specifically, the parameter ``expected_warning`` can be a warning class or sequence of warning classes, and the inside the ``with`` block must issue a warning of that class or classes. This helper produces a list of :class:`warnings.WarningMessage` objects, one for each warning raised. This function can be used as a context manager, or any of the other ways ``pytest.raises`` can be used:: >>> with warns(RuntimeWarning): ... warnings.warn("my warning", RuntimeWarning) In the context manager form you may use the keyword argument ``match`` to assert that the exception matches a text or regex:: >>> with warns(UserWarning, match='must be 0 or None'): ... warnings.warn("value must be 0 or None", UserWarning) >>> with warns(UserWarning, match=r'must be \d+$'): ... warnings.warn("value must be 42", UserWarning) >>> with warns(UserWarning, match=r'must be \d+$'): ... warnings.warn("this is not here", UserWarning) Traceback (most recent call last): ... Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted... """ __tracebackhide__ = True if not args: match_expr = kwargs.pop("match", None) if kwargs: warnings.warn( PYTEST_WARNS_UNKNOWN_KWARGS.format(args=sorted(kwargs)), stacklevel=2 ) return WarningsChecker(expected_warning, match_expr=match_expr) elif isinstance(args[0], str): warnings.warn(WARNS_EXEC, stacklevel=2) (code,) = args assert isinstance(code, str) frame = sys._getframe(1) loc = frame.f_locals.copy() loc.update(kwargs) with WarningsChecker(expected_warning): code = _pytest._code.Source(code).compile() exec(code, frame.f_globals, loc) else: func = args[0] with WarningsChecker(expected_warning): return func(*args[1:], **kwargs) class WarningsRecorder(warnings.catch_warnings): """A context manager to record raised warnings. Adapted from `warnings.catch_warnings`. """ def __init__(self): super(WarningsRecorder, self).__init__(record=True) self._entered = False self._list = [] @property def list(self): """The list of recorded warnings.""" return self._list def __getitem__(self, i): """Get a recorded warning by index.""" return self._list[i] def __iter__(self): """Iterate through the recorded warnings.""" return iter(self._list) def __len__(self): """The number of recorded warnings.""" return len(self._list) def pop(self, cls=Warning): """Pop the first recorded warning, raise exception if not exists.""" for i, w in enumerate(self._list): if issubclass(w.category, cls): return self._list.pop(i) __tracebackhide__ = True raise AssertionError("%r not found in warning list" % cls) def clear(self): """Clear the list of recorded warnings.""" self._list[:] = [] def __enter__(self): if self._entered: __tracebackhide__ = True 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(message, category=None, stacklevel=1): # duplicate the stdlib logic due to # bad handing in the c version of warnings if isinstance(message, Warning): category = message.__class__ # Check category argument if category is None: category = UserWarning assert issubclass(category, Warning) # emulate resetting the warn registry f_globals = sys._getframe(stacklevel).f_globals if "__warningregistry__" in f_globals: orig = f_globals["__warningregistry__"] f_globals["__warningregistry__"] = None try: return self._saved_warn(message, category, stacklevel + 1) finally: f_globals["__warningregistry__"] = orig else: return self._saved_warn(message, category, stacklevel + 1) 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) # Built-in catch_warnings does not reset entered state so we do it # manually here for this context manager to become reusable. self._entered = False class WarningsChecker(WarningsRecorder): def __init__(self, expected_warning=None, match_expr=None): super(WarningsChecker, self).__init__() msg = "exceptions must be old-style classes or derived from Warning, not %s" if isinstance(expected_warning, tuple): for exc in expected_warning: if not inspect.isclass(exc): raise TypeError(msg % type(exc)) elif inspect.isclass(expected_warning): expected_warning = (expected_warning,) elif expected_warning is not None: raise TypeError(msg % type(expected_warning)) self.expected_warning = expected_warning self.match_expr = match_expr def __exit__(self, *exc_info): super(WarningsChecker, self).__exit__(*exc_info) __tracebackhide__ = True # only check if we're not currently handling an exception if all(a is None for a in exc_info): if self.expected_warning is not None: if not any(issubclass(r.category, self.expected_warning) for r in self): __tracebackhide__ = True fail( "DID NOT WARN. No warnings of type {} was emitted. " "The list of emitted warnings is: {}.".format( self.expected_warning, [each.message for each in self] ) ) elif self.match_expr is not None: for r in self: if issubclass(r.category, self.expected_warning): if re.compile(self.match_expr).search(str(r.message)): break else: fail( "DID NOT WARN. No warnings of type {} matching" " ('{}') was emitted. The list of emitted warnings" " is: {}.".format( self.expected_warning, self.match_expr, [each.message for each in self], ) )