recwarn.py 9.1 KB

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