| """ recording warnings during test function execution. """ |
| from __future__ import absolute_import, division, print_function |
| |
| import inspect |
| |
| import _pytest._code |
| import py |
| import sys |
| import warnings |
| |
| import re |
| |
| 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. |
| """ |
| if not func: |
| return _DeprecatedCallContext() |
| else: |
| __tracebackhide__ = True |
| 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): |
| 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... |
| |
| """ |
| match_expr = None |
| if not args: |
| if "match" in kwargs: |
| match_expr = kwargs.pop("match") |
| return WarningsChecker(expected_warning, match_expr=match_expr) |
| elif isinstance(args[0], str): |
| code, = args |
| assert isinstance(code, str) |
| frame = sys._getframe(1) |
| loc = frame.f_locals.copy() |
| loc.update(kwargs) |
| |
| with WarningsChecker(expected_warning, match_expr=match_expr): |
| code = _pytest._code.Source(code).compile() |
| py.builtin.exec_(code, frame.f_globals, loc) |
| else: |
| func = args[0] |
| with WarningsChecker(expected_warning, match_expr=match_expr): |
| 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") |
| return self |
| |
| def __exit__(self, *exc_info): |
| if not self._entered: |
| __tracebackhide__ = True |
| raise RuntimeError("Cannot exit %r without entering first" % self) |
| super(WarningsRecorder, self).__exit__(*exc_info) |
| |
| |
| 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) |
| |
| # 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], |
| ) |
| ) |