recwarn.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. """Record warnings during test function execution."""
  2. import re
  3. import warnings
  4. from pprint import pformat
  5. from types import TracebackType
  6. from typing import Any
  7. from typing import Callable
  8. from typing import Generator
  9. from typing import Iterator
  10. from typing import List
  11. from typing import Optional
  12. from typing import Pattern
  13. from typing import Tuple
  14. from typing import Type
  15. from typing import TypeVar
  16. from typing import Union
  17. from _pytest.compat import final
  18. from _pytest.compat import overload
  19. from _pytest.deprecated import check_ispytest
  20. from _pytest.deprecated import WARNS_NONE_ARG
  21. from _pytest.fixtures import fixture
  22. from _pytest.outcomes import fail
  23. T = TypeVar("T")
  24. @fixture
  25. def recwarn() -> Generator["WarningsRecorder", None, None]:
  26. """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
  27. See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
  28. on warning categories.
  29. """
  30. wrec = WarningsRecorder(_ispytest=True)
  31. with wrec:
  32. warnings.simplefilter("default")
  33. yield wrec
  34. @overload
  35. def deprecated_call(
  36. *, match: Optional[Union[str, Pattern[str]]] = ...
  37. ) -> "WarningsRecorder":
  38. ...
  39. @overload
  40. def deprecated_call( # noqa: F811
  41. func: Callable[..., T], *args: Any, **kwargs: Any
  42. ) -> T:
  43. ...
  44. def deprecated_call( # noqa: F811
  45. func: Optional[Callable[..., Any]] = None, *args: Any, **kwargs: Any
  46. ) -> Union["WarningsRecorder", Any]:
  47. """Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning``.
  48. This function can be used as a context manager::
  49. >>> import warnings
  50. >>> def api_call_v2():
  51. ... warnings.warn('use v3 of this api', DeprecationWarning)
  52. ... return 200
  53. >>> import pytest
  54. >>> with pytest.deprecated_call():
  55. ... assert api_call_v2() == 200
  56. It can also be used by passing a function and ``*args`` and ``**kwargs``,
  57. in which case it will ensure calling ``func(*args, **kwargs)`` produces one of
  58. the warnings types above. The return value is the return value of the function.
  59. In the context manager form you may use the keyword argument ``match`` to assert
  60. that the warning matches a text or regex.
  61. The context manager produces a list of :class:`warnings.WarningMessage` objects,
  62. one for each warning raised.
  63. """
  64. __tracebackhide__ = True
  65. if func is not None:
  66. args = (func,) + args
  67. return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs)
  68. @overload
  69. def warns(
  70. expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = ...,
  71. *,
  72. match: Optional[Union[str, Pattern[str]]] = ...,
  73. ) -> "WarningsChecker":
  74. ...
  75. @overload
  76. def warns( # noqa: F811
  77. expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]],
  78. func: Callable[..., T],
  79. *args: Any,
  80. **kwargs: Any,
  81. ) -> T:
  82. ...
  83. def warns( # noqa: F811
  84. expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = Warning,
  85. *args: Any,
  86. match: Optional[Union[str, Pattern[str]]] = None,
  87. **kwargs: Any,
  88. ) -> Union["WarningsChecker", Any]:
  89. r"""Assert that code raises a particular class of warning.
  90. Specifically, the parameter ``expected_warning`` can be a warning class or sequence
  91. of warning classes, and the code inside the ``with`` block must issue at least one
  92. warning of that class or classes.
  93. This helper produces a list of :class:`warnings.WarningMessage` objects, one for
  94. each warning raised (regardless of whether it is an ``expected_warning`` or not).
  95. This function can be used as a context manager, which will capture all the raised
  96. warnings inside it::
  97. >>> import pytest
  98. >>> with pytest.warns(RuntimeWarning):
  99. ... warnings.warn("my warning", RuntimeWarning)
  100. In the context manager form you may use the keyword argument ``match`` to assert
  101. that the warning matches a text or regex::
  102. >>> with pytest.warns(UserWarning, match='must be 0 or None'):
  103. ... warnings.warn("value must be 0 or None", UserWarning)
  104. >>> with pytest.warns(UserWarning, match=r'must be \d+$'):
  105. ... warnings.warn("value must be 42", UserWarning)
  106. >>> with pytest.warns(UserWarning, match=r'must be \d+$'):
  107. ... warnings.warn("this is not here", UserWarning)
  108. Traceback (most recent call last):
  109. ...
  110. Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
  111. **Using with** ``pytest.mark.parametrize``
  112. When using :ref:`pytest.mark.parametrize ref` it is possible to parametrize tests
  113. such that some runs raise a warning and others do not.
  114. This could be achieved in the same way as with exceptions, see
  115. :ref:`parametrizing_conditional_raising` for an example.
  116. """
  117. __tracebackhide__ = True
  118. if not args:
  119. if kwargs:
  120. argnames = ", ".join(sorted(kwargs))
  121. raise TypeError(
  122. f"Unexpected keyword arguments passed to pytest.warns: {argnames}"
  123. "\nUse context-manager form instead?"
  124. )
  125. return WarningsChecker(expected_warning, match_expr=match, _ispytest=True)
  126. else:
  127. func = args[0]
  128. if not callable(func):
  129. raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
  130. with WarningsChecker(expected_warning, _ispytest=True):
  131. return func(*args[1:], **kwargs)
  132. class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg]
  133. """A context manager to record raised warnings.
  134. Each recorded warning is an instance of :class:`warnings.WarningMessage`.
  135. Adapted from `warnings.catch_warnings`.
  136. .. note::
  137. ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated
  138. differently; see :ref:`ensuring_function_triggers`.
  139. """
  140. def __init__(self, *, _ispytest: bool = False) -> None:
  141. check_ispytest(_ispytest)
  142. # Type ignored due to the way typeshed handles warnings.catch_warnings.
  143. super().__init__(record=True) # type: ignore[call-arg]
  144. self._entered = False
  145. self._list: List[warnings.WarningMessage] = []
  146. @property
  147. def list(self) -> List["warnings.WarningMessage"]:
  148. """The list of recorded warnings."""
  149. return self._list
  150. def __getitem__(self, i: int) -> "warnings.WarningMessage":
  151. """Get a recorded warning by index."""
  152. return self._list[i]
  153. def __iter__(self) -> Iterator["warnings.WarningMessage"]:
  154. """Iterate through the recorded warnings."""
  155. return iter(self._list)
  156. def __len__(self) -> int:
  157. """The number of recorded warnings."""
  158. return len(self._list)
  159. def pop(self, cls: Type[Warning] = Warning) -> "warnings.WarningMessage":
  160. """Pop the first recorded warning, raise exception if not exists."""
  161. for i, w in enumerate(self._list):
  162. if issubclass(w.category, cls):
  163. return self._list.pop(i)
  164. __tracebackhide__ = True
  165. raise AssertionError(f"{cls!r} not found in warning list")
  166. def clear(self) -> None:
  167. """Clear the list of recorded warnings."""
  168. self._list[:] = []
  169. # Type ignored because it doesn't exactly warnings.catch_warnings.__enter__
  170. # -- it returns a List but we only emulate one.
  171. def __enter__(self) -> "WarningsRecorder": # type: ignore
  172. if self._entered:
  173. __tracebackhide__ = True
  174. raise RuntimeError(f"Cannot enter {self!r} twice")
  175. _list = super().__enter__()
  176. # record=True means it's None.
  177. assert _list is not None
  178. self._list = _list
  179. warnings.simplefilter("always")
  180. return self
  181. def __exit__(
  182. self,
  183. exc_type: Optional[Type[BaseException]],
  184. exc_val: Optional[BaseException],
  185. exc_tb: Optional[TracebackType],
  186. ) -> None:
  187. if not self._entered:
  188. __tracebackhide__ = True
  189. raise RuntimeError(f"Cannot exit {self!r} without entering first")
  190. super().__exit__(exc_type, exc_val, exc_tb)
  191. # Built-in catch_warnings does not reset entered state so we do it
  192. # manually here for this context manager to become reusable.
  193. self._entered = False
  194. @final
  195. class WarningsChecker(WarningsRecorder):
  196. def __init__(
  197. self,
  198. expected_warning: Optional[
  199. Union[Type[Warning], Tuple[Type[Warning], ...]]
  200. ] = Warning,
  201. match_expr: Optional[Union[str, Pattern[str]]] = None,
  202. *,
  203. _ispytest: bool = False,
  204. ) -> None:
  205. check_ispytest(_ispytest)
  206. super().__init__(_ispytest=True)
  207. msg = "exceptions must be derived from Warning, not %s"
  208. if expected_warning is None:
  209. warnings.warn(WARNS_NONE_ARG, stacklevel=4)
  210. expected_warning_tup = None
  211. elif isinstance(expected_warning, tuple):
  212. for exc in expected_warning:
  213. if not issubclass(exc, Warning):
  214. raise TypeError(msg % type(exc))
  215. expected_warning_tup = expected_warning
  216. elif issubclass(expected_warning, Warning):
  217. expected_warning_tup = (expected_warning,)
  218. else:
  219. raise TypeError(msg % type(expected_warning))
  220. self.expected_warning = expected_warning_tup
  221. self.match_expr = match_expr
  222. def __exit__(
  223. self,
  224. exc_type: Optional[Type[BaseException]],
  225. exc_val: Optional[BaseException],
  226. exc_tb: Optional[TracebackType],
  227. ) -> None:
  228. super().__exit__(exc_type, exc_val, exc_tb)
  229. __tracebackhide__ = True
  230. def found_str():
  231. return pformat([record.message for record in self], indent=2)
  232. # only check if we're not currently handling an exception
  233. if exc_type is None and exc_val is None and exc_tb is None:
  234. if self.expected_warning is not None:
  235. if not any(issubclass(r.category, self.expected_warning) for r in self):
  236. __tracebackhide__ = True
  237. fail(
  238. f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n"
  239. f"The list of emitted warnings is: {found_str()}."
  240. )
  241. elif self.match_expr is not None:
  242. for r in self:
  243. if issubclass(r.category, self.expected_warning):
  244. if re.compile(self.match_expr).search(str(r.message)):
  245. break
  246. else:
  247. fail(
  248. f"""\
  249. DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted.
  250. Regex: {self.match_expr}
  251. Emitted warnings: {found_str()}"""
  252. )