recwarn.py 7.03 KB
Newer Older
1
""" recording warnings during test function execution. """
2

3 4 5
import inspect

import _pytest._code
6
import py
7
import sys
8 9 10
import warnings
import pytest

11

12 13
@pytest.yield_fixture
def recwarn(request):
14 15 16
    """Return a WarningsRecorder instance that provides these methods:

    * ``pop(category=None)``: return last warning matching the category.
17
    * ``clear()``: clear list of warnings
18

19 20
    See http://docs.python.org/library/warnings.html for information
    on warning categories.
21
    """
22
    wrec = WarningsRecorder()
23 24 25 26
    with wrec:
        warnings.simplefilter('default')
        yield wrec

27 28

def pytest_namespace():
29 30 31
    return {'deprecated_call': deprecated_call,
            'warns': warns}

32

33 34 35 36 37 38 39 40 41 42 43 44
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::

        >>> with deprecated_call():
        ...    myobject.deprecated_method()

    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.
45
    """
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
    if not func:
        return WarningsChecker(expected_warning=DeprecationWarning)

    categories = []

    def warn_explicit(message, category, *args, **kwargs):
        categories.append(category)
        old_warn_explicit(message, category, *args, **kwargs)

    def warn(message, category=None, *args, **kwargs):
        if isinstance(message, Warning):
            categories.append(message.__class__)
        else:
            categories.append(category)
        old_warn(message, category, *args, **kwargs)

    old_warn = warnings.warn
    old_warn_explicit = warnings.warn_explicit
    warnings.warn_explicit = warn_explicit
    warnings.warn = warn
66 67 68
    try:
        ret = func(*args, **kwargs)
    finally:
69 70 71 72
        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):
73
        __tracebackhide__ = True
74
        raise AssertionError("%r did not produce DeprecationWarning" % (func,))
75 76 77
    return ret


78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
def warns(expected_warning, *args, **kwargs):
    """Assert that code raises a particular class of warning.

    Specifically, the input @expected_warning can be a warning class or
    tuple of warning classes, and the code must return that warning
    (if a single class) or one of those warnings (if a tuple).

    This helper produces a list of ``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)
    """
    wcheck = WarningsChecker(expected_warning)
    if not args:
        return wcheck
    elif isinstance(args[0], str):
        code, = args
        assert isinstance(code, str)
        frame = sys._getframe(1)
        loc = frame.f_locals.copy()
        loc.update(kwargs)

        with wcheck:
            code = _pytest._code.Source(code).compile()
            py.builtin.exec_(code, frame.f_globals, loc)
    else:
        func = args[0]
        with wcheck:
            return func(*args[1:], **kwargs)


class RecordedWarning(object):
    def __init__(self, message, category, filename, lineno, file, line):
115 116 117 118
        self.message = message
        self.category = category
        self.filename = filename
        self.lineno = lineno
119
        self.file = file
120 121
        self.line = line

122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149

class WarningsRecorder(object):
    """A context manager to record raised warnings.

    Adapted from `warnings.catch_warnings`.
    """

    def __init__(self, module=None):
        self._module = sys.modules['warnings'] if module is None else module
        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)
150 151

    def pop(self, cls=Warning):
152 153
        """Pop the first recorded warning, raise exception if not exists."""
        for i, w in enumerate(self._list):
154
            if issubclass(w.category, cls):
155
                return self._list.pop(i)
156
        __tracebackhide__ = True
157
        raise AssertionError("%r not found in warning list" % cls)
158

159
    def clear(self):
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
        """Clear the list of recorded warnings."""
        self._list[:] = []

    def __enter__(self):
        if self._entered:
            __tracebackhide__ = True
            raise RuntimeError("Cannot enter %r twice" % self)
        self._entered = True
        self._filters = self._module.filters
        self._module.filters = self._filters[:]
        self._showwarning = self._module.showwarning

        def showwarning(message, category, filename, lineno,
                        file=None, line=None):
            self._list.append(RecordedWarning(
                message, category, filename, lineno, file, line))

            # still perform old showwarning functionality
            self._showwarning(
                message, category, filename, lineno, file=file, line=line)

        self._module.showwarning = showwarning

        # allow the same warning to be raised more than once

        self._module.simplefilter('always')
        return self

    def __exit__(self, *exc_info):
        if not self._entered:
            __tracebackhide__ = True
            raise RuntimeError("Cannot exit %r without entering first" % self)
        self._module.filters = self._filters
        self._module.showwarning = self._showwarning


class WarningsChecker(WarningsRecorder):
    def __init__(self, expected_warning=None, module=None):
        super(WarningsChecker, self).__init__(module=module)

        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

    def __exit__(self, *exc_info):
        super(WarningsChecker, self).__exit__(*exc_info)
215

216 217 218 219 220 221
        # 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(r.category in self.expected_warning for r in self):
                    __tracebackhide__ = True
                    pytest.fail("DID NOT WARN")