recwarn.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. """ recording warnings during test function execution. """
  2. import inspect
  3. import _pytest._code
  4. import py
  5. import sys
  6. import warnings
  7. import pytest
  8. @pytest.yield_fixture
  9. def recwarn(request):
  10. """Return a WarningsRecorder instance that provides these methods:
  11. * ``pop(category=None)``: return last warning matching the category.
  12. * ``clear()``: clear list of warnings
  13. See http://docs.python.org/library/warnings.html for information
  14. on warning categories.
  15. """
  16. wrec = WarningsRecorder()
  17. with wrec:
  18. warnings.simplefilter('default')
  19. yield wrec
  20. def pytest_namespace():
  21. return {'deprecated_call': deprecated_call,
  22. 'warns': warns}
  23. def deprecated_call(func=None, *args, **kwargs):
  24. """ assert that calling ``func(*args, **kwargs)`` triggers a
  25. ``DeprecationWarning`` or ``PendingDeprecationWarning``.
  26. This function can be used as a context manager::
  27. >>> with deprecated_call():
  28. ... myobject.deprecated_method()
  29. Note: we cannot use WarningsRecorder here because it is still subject
  30. to the mechanism that prevents warnings of the same type from being
  31. triggered twice for the same module. See #1190.
  32. """
  33. if not func:
  34. return WarningsChecker(expected_warning=DeprecationWarning)
  35. categories = []
  36. def warn_explicit(message, category, *args, **kwargs):
  37. categories.append(category)
  38. old_warn_explicit(message, category, *args, **kwargs)
  39. def warn(message, category=None, *args, **kwargs):
  40. if isinstance(message, Warning):
  41. categories.append(message.__class__)
  42. else:
  43. categories.append(category)
  44. old_warn(message, category, *args, **kwargs)
  45. old_warn = warnings.warn
  46. old_warn_explicit = warnings.warn_explicit
  47. warnings.warn_explicit = warn_explicit
  48. warnings.warn = warn
  49. try:
  50. ret = func(*args, **kwargs)
  51. finally:
  52. warnings.warn_explicit = old_warn_explicit
  53. warnings.warn = old_warn
  54. deprecation_categories = (DeprecationWarning, PendingDeprecationWarning)
  55. if not any(issubclass(c, deprecation_categories) for c in categories):
  56. __tracebackhide__ = True
  57. raise AssertionError("%r did not produce DeprecationWarning" % (func,))
  58. return ret
  59. def warns(expected_warning, *args, **kwargs):
  60. """Assert that code raises a particular class of warning.
  61. Specifically, the input @expected_warning can be a warning class or
  62. tuple of warning classes, and the code must return that warning
  63. (if a single class) or one of those warnings (if a tuple).
  64. This helper produces a list of ``warnings.WarningMessage`` objects,
  65. one for each warning raised.
  66. This function can be used as a context manager, or any of the other ways
  67. ``pytest.raises`` can be used::
  68. >>> with warns(RuntimeWarning):
  69. ... warnings.warn("my warning", RuntimeWarning)
  70. """
  71. wcheck = WarningsChecker(expected_warning)
  72. if not args:
  73. return wcheck
  74. elif isinstance(args[0], str):
  75. code, = args
  76. assert isinstance(code, str)
  77. frame = sys._getframe(1)
  78. loc = frame.f_locals.copy()
  79. loc.update(kwargs)
  80. with wcheck:
  81. code = _pytest._code.Source(code).compile()
  82. py.builtin.exec_(code, frame.f_globals, loc)
  83. else:
  84. func = args[0]
  85. with wcheck:
  86. return func(*args[1:], **kwargs)
  87. class RecordedWarning(object):
  88. def __init__(self, message, category, filename, lineno, file, line):
  89. self.message = message
  90. self.category = category
  91. self.filename = filename
  92. self.lineno = lineno
  93. self.file = file
  94. self.line = line
  95. class WarningsRecorder(object):
  96. """A context manager to record raised warnings.
  97. Adapted from `warnings.catch_warnings`.
  98. """
  99. def __init__(self, module=None):
  100. self._module = sys.modules['warnings'] if module is None else module
  101. self._entered = False
  102. self._list = []
  103. @property
  104. def list(self):
  105. """The list of recorded warnings."""
  106. return self._list
  107. def __getitem__(self, i):
  108. """Get a recorded warning by index."""
  109. return self._list[i]
  110. def __iter__(self):
  111. """Iterate through the recorded warnings."""
  112. return iter(self._list)
  113. def __len__(self):
  114. """The number of recorded warnings."""
  115. return len(self._list)
  116. def pop(self, cls=Warning):
  117. """Pop the first recorded warning, raise exception if not exists."""
  118. for i, w in enumerate(self._list):
  119. if issubclass(w.category, cls):
  120. return self._list.pop(i)
  121. __tracebackhide__ = True
  122. raise AssertionError("%r not found in warning list" % cls)
  123. def clear(self):
  124. """Clear the list of recorded warnings."""
  125. self._list[:] = []
  126. def __enter__(self):
  127. if self._entered:
  128. __tracebackhide__ = True
  129. raise RuntimeError("Cannot enter %r twice" % self)
  130. self._entered = True
  131. self._filters = self._module.filters
  132. self._module.filters = self._filters[:]
  133. self._showwarning = self._module.showwarning
  134. def showwarning(message, category, filename, lineno,
  135. file=None, line=None):
  136. self._list.append(RecordedWarning(
  137. message, category, filename, lineno, file, line))
  138. # still perform old showwarning functionality
  139. self._showwarning(
  140. message, category, filename, lineno, file=file, line=line)
  141. self._module.showwarning = showwarning
  142. # allow the same warning to be raised more than once
  143. self._module.simplefilter('always')
  144. return self
  145. def __exit__(self, *exc_info):
  146. if not self._entered:
  147. __tracebackhide__ = True
  148. raise RuntimeError("Cannot exit %r without entering first" % self)
  149. self._module.filters = self._filters
  150. self._module.showwarning = self._showwarning
  151. class WarningsChecker(WarningsRecorder):
  152. def __init__(self, expected_warning=None, module=None):
  153. super(WarningsChecker, self).__init__(module=module)
  154. msg = ("exceptions must be old-style classes or "
  155. "derived from Warning, not %s")
  156. if isinstance(expected_warning, tuple):
  157. for exc in expected_warning:
  158. if not inspect.isclass(exc):
  159. raise TypeError(msg % type(exc))
  160. elif inspect.isclass(expected_warning):
  161. expected_warning = (expected_warning,)
  162. elif expected_warning is not None:
  163. raise TypeError(msg % type(expected_warning))
  164. self.expected_warning = expected_warning
  165. def __exit__(self, *exc_info):
  166. super(WarningsChecker, self).__exit__(*exc_info)
  167. # only check if we're not currently handling an exception
  168. if all(a is None for a in exc_info):
  169. if self.expected_warning is not None:
  170. if not any(r.category in self.expected_warning for r in self):
  171. __tracebackhide__ = True
  172. pytest.fail("DID NOT WARN")