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