recwarn.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. """ recording warnings during test function execution. """
  2. from __future__ import absolute_import, division, print_function
  3. import inspect
  4. import _pytest._code
  5. import re
  6. import sys
  7. import warnings
  8. import six
  9. from _pytest.fixtures import yield_fixture
  10. from _pytest.outcomes import fail
  11. @yield_fixture
  12. def recwarn():
  13. """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
  14. See http://docs.python.org/library/warnings.html for information
  15. on warning categories.
  16. """
  17. wrec = WarningsRecorder()
  18. with wrec:
  19. warnings.simplefilter("default")
  20. yield wrec
  21. def deprecated_call(func=None, *args, **kwargs):
  22. """context manager that can be used to ensure a block of code triggers a
  23. ``DeprecationWarning`` or ``PendingDeprecationWarning``::
  24. >>> import warnings
  25. >>> def api_call_v2():
  26. ... warnings.warn('use v3 of this api', DeprecationWarning)
  27. ... return 200
  28. >>> with deprecated_call():
  29. ... assert api_call_v2() == 200
  30. ``deprecated_call`` can also be used by passing a function and ``*args`` and ``*kwargs``,
  31. in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings
  32. types above.
  33. """
  34. if not func:
  35. return _DeprecatedCallContext()
  36. else:
  37. __tracebackhide__ = True
  38. with _DeprecatedCallContext():
  39. return func(*args, **kwargs)
  40. class _DeprecatedCallContext(object):
  41. """Implements the logic to capture deprecation warnings as a context manager."""
  42. def __enter__(self):
  43. self._captured_categories = []
  44. self._old_warn = warnings.warn
  45. self._old_warn_explicit = warnings.warn_explicit
  46. warnings.warn_explicit = self._warn_explicit
  47. warnings.warn = self._warn
  48. def _warn_explicit(self, message, category, *args, **kwargs):
  49. self._captured_categories.append(category)
  50. def _warn(self, message, category=None, *args, **kwargs):
  51. if isinstance(message, Warning):
  52. self._captured_categories.append(message.__class__)
  53. else:
  54. self._captured_categories.append(category)
  55. def __exit__(self, exc_type, exc_val, exc_tb):
  56. warnings.warn_explicit = self._old_warn_explicit
  57. warnings.warn = self._old_warn
  58. if exc_type is None:
  59. deprecation_categories = (DeprecationWarning, PendingDeprecationWarning)
  60. if not any(
  61. issubclass(c, deprecation_categories) for c in self._captured_categories
  62. ):
  63. __tracebackhide__ = True
  64. msg = "Did not produce DeprecationWarning or PendingDeprecationWarning"
  65. raise AssertionError(msg)
  66. def warns(expected_warning, *args, **kwargs):
  67. r"""Assert that code raises a particular class of warning.
  68. Specifically, the parameter ``expected_warning`` can be a warning class or
  69. sequence of warning classes, and the inside the ``with`` block must issue a warning of that class or
  70. classes.
  71. This helper produces a list of :class:`warnings.WarningMessage` objects,
  72. one for each warning raised.
  73. This function can be used as a context manager, or any of the other ways
  74. ``pytest.raises`` can be used::
  75. >>> with warns(RuntimeWarning):
  76. ... warnings.warn("my warning", RuntimeWarning)
  77. In the context manager form you may use the keyword argument ``match`` to assert
  78. that the exception matches a text or regex::
  79. >>> with warns(UserWarning, match='must be 0 or None'):
  80. ... warnings.warn("value must be 0 or None", UserWarning)
  81. >>> with warns(UserWarning, match=r'must be \d+$'):
  82. ... warnings.warn("value must be 42", UserWarning)
  83. >>> with warns(UserWarning, match=r'must be \d+$'):
  84. ... warnings.warn("this is not here", UserWarning)
  85. Traceback (most recent call last):
  86. ...
  87. Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted...
  88. """
  89. match_expr = None
  90. if not args:
  91. if "match" in kwargs:
  92. match_expr = kwargs.pop("match")
  93. return WarningsChecker(expected_warning, match_expr=match_expr)
  94. elif isinstance(args[0], str):
  95. code, = args
  96. assert isinstance(code, str)
  97. frame = sys._getframe(1)
  98. loc = frame.f_locals.copy()
  99. loc.update(kwargs)
  100. with WarningsChecker(expected_warning, match_expr=match_expr):
  101. code = _pytest._code.Source(code).compile()
  102. six.exec_(code, frame.f_globals, loc)
  103. else:
  104. func = args[0]
  105. with WarningsChecker(expected_warning, match_expr=match_expr):
  106. return func(*args[1:], **kwargs)
  107. class WarningsRecorder(warnings.catch_warnings):
  108. """A context manager to record raised warnings.
  109. Adapted from `warnings.catch_warnings`.
  110. """
  111. def __init__(self):
  112. super(WarningsRecorder, self).__init__(record=True)
  113. self._entered = False
  114. self._list = []
  115. @property
  116. def list(self):
  117. """The list of recorded warnings."""
  118. return self._list
  119. def __getitem__(self, i):
  120. """Get a recorded warning by index."""
  121. return self._list[i]
  122. def __iter__(self):
  123. """Iterate through the recorded warnings."""
  124. return iter(self._list)
  125. def __len__(self):
  126. """The number of recorded warnings."""
  127. return len(self._list)
  128. def pop(self, cls=Warning):
  129. """Pop the first recorded warning, raise exception if not exists."""
  130. for i, w in enumerate(self._list):
  131. if issubclass(w.category, cls):
  132. return self._list.pop(i)
  133. __tracebackhide__ = True
  134. raise AssertionError("%r not found in warning list" % cls)
  135. def clear(self):
  136. """Clear the list of recorded warnings."""
  137. self._list[:] = []
  138. def __enter__(self):
  139. if self._entered:
  140. __tracebackhide__ = True
  141. raise RuntimeError("Cannot enter %r twice" % self)
  142. self._list = super(WarningsRecorder, self).__enter__()
  143. warnings.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. super(WarningsRecorder, self).__exit__(*exc_info)
  150. class WarningsChecker(WarningsRecorder):
  151. def __init__(self, expected_warning=None, match_expr=None):
  152. super(WarningsChecker, self).__init__()
  153. msg = "exceptions must be old-style classes or " "derived from Warning, not %s"
  154. if isinstance(expected_warning, tuple):
  155. for exc in expected_warning:
  156. if not inspect.isclass(exc):
  157. raise TypeError(msg % type(exc))
  158. elif inspect.isclass(expected_warning):
  159. expected_warning = (expected_warning,)
  160. elif expected_warning is not None:
  161. raise TypeError(msg % type(expected_warning))
  162. self.expected_warning = expected_warning
  163. self.match_expr = match_expr
  164. def __exit__(self, *exc_info):
  165. super(WarningsChecker, self).__exit__(*exc_info)
  166. # only check if we're not currently handling an exception
  167. if all(a is None for a in exc_info):
  168. if self.expected_warning is not None:
  169. if not any(issubclass(r.category, self.expected_warning) for r in self):
  170. __tracebackhide__ = True
  171. fail(
  172. "DID NOT WARN. No warnings of type {} was emitted. "
  173. "The list of emitted warnings is: {}.".format(
  174. self.expected_warning, [each.message for each in self]
  175. )
  176. )
  177. elif self.match_expr is not None:
  178. for r in self:
  179. if issubclass(r.category, self.expected_warning):
  180. if re.compile(self.match_expr).search(str(r.message)):
  181. break
  182. else:
  183. fail(
  184. "DID NOT WARN. No warnings of type {} matching"
  185. " ('{}') was emitted. The list of emitted warnings"
  186. " is: {}.".format(
  187. self.expected_warning,
  188. self.match_expr,
  189. [each.message for each in self],
  190. )
  191. )