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

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 

17 

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 

24 

25 

26T = TypeVar("T") 

27 

28 

29@fixture 

30def recwarn() -> Generator["WarningsRecorder", None, None]: 

31 """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. 

32 

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 

40 

41 

42@overload 

43def deprecated_call( 

44 *, match: Optional[Union[str, Pattern[str]]] = ... 

45) -> "WarningsRecorder": 

46 ... 

47 

48 

49@overload 

50def deprecated_call( # noqa: F811 

51 func: Callable[..., T], *args: Any, **kwargs: Any 

52) -> T: 

53 ... 

54 

55 

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``. 

60 

61 This function can be used as a context manager:: 

62 

63 >>> import warnings 

64 >>> def api_call_v2(): 

65 ... warnings.warn('use v3 of this api', DeprecationWarning) 

66 ... return 200 

67 

68 >>> import pytest 

69 >>> with pytest.deprecated_call(): 

70 ... assert api_call_v2() == 200 

71 

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. 

75 

76 In the context manager form you may use the keyword argument ``match`` to assert 

77 that the warning matches a text or regex. 

78 

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) 

86 

87 

88@overload 

89def warns( 

90 expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = ..., 

91 *, 

92 match: Optional[Union[str, Pattern[str]]] = ..., 

93) -> "WarningsChecker": 

94 ... 

95 

96 

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 ... 

105 

106 

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. 

114 

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. 

118 

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). 

121 

122 This function can be used as a context manager, which will capture all the raised 

123 warnings inside it:: 

124 

125 >>> import pytest 

126 >>> with pytest.warns(RuntimeWarning): 

127 ... warnings.warn("my warning", RuntimeWarning) 

128 

129 In the context manager form you may use the keyword argument ``match`` to assert 

130 that the warning matches a text or regex:: 

131 

132 >>> with pytest.warns(UserWarning, match='must be 0 or None'): 

133 ... warnings.warn("value must be 0 or None", UserWarning) 

134 

135 >>> with pytest.warns(UserWarning, match=r'must be \d+$'): 

136 ... warnings.warn("value must be 42", UserWarning) 

137 

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... 

143 

144 **Using with** ``pytest.mark.parametrize`` 

145 

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. 

148 

149 This could be achieved in the same way as with exceptions, see 

150 :ref:`parametrizing_conditional_raising` for an example. 

151 

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) 

168 

169 

170class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg] 

171 """A context manager to record raised warnings. 

172 

173 Each recorded warning is an instance of :class:`warnings.WarningMessage`. 

174 

175 Adapted from `warnings.catch_warnings`. 

176 

177 .. note:: 

178 ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated 

179 differently; see :ref:`ensuring_function_triggers`. 

180 

181 """ 

182 

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] = [] 

189 

190 @property 

191 def list(self) -> List["warnings.WarningMessage"]: 

192 """The list of recorded warnings.""" 

193 return self._list 

194 

195 def __getitem__(self, i: int) -> "warnings.WarningMessage": 

196 """Get a recorded warning by index.""" 

197 return self._list[i] 

198 

199 def __iter__(self) -> Iterator["warnings.WarningMessage"]: 

200 """Iterate through the recorded warnings.""" 

201 return iter(self._list) 

202 

203 def __len__(self) -> int: 

204 """The number of recorded warnings.""" 

205 return len(self._list) 

206 

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") 

214 

215 def clear(self) -> None: 

216 """Clear the list of recorded warnings.""" 

217 self._list[:] = [] 

218 

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 

231 

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") 

241 

242 super().__exit__(exc_type, exc_val, exc_tb) 

243 

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 

247 

248 

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) 

262 

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)) 

276 

277 self.expected_warning = expected_warning_tup 

278 self.match_expr = match_expr 

279 

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) 

287 

288 __tracebackhide__ = True 

289 

290 def found_str(): 

291 return pformat([record.message for record in self], indent=2) 

292 

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 )