Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/outcomes.py: 44%

110 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-05-04 13:14 +0700

1"""Exception classes and constants handling test outcomes as well as 

2functions creating them.""" 

3import sys 

4import warnings 

5from typing import Any 

6from typing import Callable 

7from typing import cast 

8from typing import NoReturn 

9from typing import Optional 

10from typing import Type 

11from typing import TypeVar 

12 

13from _pytest.deprecated import KEYWORD_MSG_ARG 

14 

15TYPE_CHECKING = False # Avoid circular import through compat. 

16 

17if TYPE_CHECKING: 

18 from typing_extensions import Protocol 

19else: 

20 # typing.Protocol is only available starting from Python 3.8. It is also 

21 # available from typing_extensions, but we don't want a runtime dependency 

22 # on that. So use a dummy runtime implementation. 

23 from typing import Generic 

24 

25 Protocol = Generic 

26 

27 

28class OutcomeException(BaseException): 

29 """OutcomeException and its subclass instances indicate and contain info 

30 about test and collection outcomes.""" 

31 

32 def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None: 

33 if msg is not None and not isinstance(msg, str): 

34 error_msg = ( # type: ignore[unreachable] 

35 "{} expected string as 'msg' parameter, got '{}' instead.\n" 

36 "Perhaps you meant to use a mark?" 

37 ) 

38 raise TypeError(error_msg.format(type(self).__name__, type(msg).__name__)) 

39 super().__init__(msg) 

40 self.msg = msg 

41 self.pytrace = pytrace 

42 

43 def __repr__(self) -> str: 

44 if self.msg is not None: 

45 return self.msg 

46 return f"<{self.__class__.__name__} instance>" 

47 

48 __str__ = __repr__ 

49 

50 

51TEST_OUTCOME = (OutcomeException, Exception) 

52 

53 

54class Skipped(OutcomeException): 

55 # XXX hackish: on 3k we fake to live in the builtins 

56 # in order to have Skipped exception printing shorter/nicer 

57 __module__ = "builtins" 

58 

59 def __init__( 

60 self, 

61 msg: Optional[str] = None, 

62 pytrace: bool = True, 

63 allow_module_level: bool = False, 

64 *, 

65 _use_item_location: bool = False, 

66 ) -> None: 

67 super().__init__(msg=msg, pytrace=pytrace) 

68 self.allow_module_level = allow_module_level 

69 # If true, the skip location is reported as the item's location, 

70 # instead of the place that raises the exception/calls skip(). 

71 self._use_item_location = _use_item_location 

72 

73 

74class Failed(OutcomeException): 

75 """Raised from an explicit call to pytest.fail().""" 

76 

77 __module__ = "builtins" 

78 

79 

80class Exit(Exception): 

81 """Raised for immediate program exits (no tracebacks/summaries).""" 

82 

83 def __init__( 

84 self, msg: str = "unknown reason", returncode: Optional[int] = None 

85 ) -> None: 

86 self.msg = msg 

87 self.returncode = returncode 

88 super().__init__(msg) 

89 

90 

91# Elaborate hack to work around https://github.com/python/mypy/issues/2087. 

92# Ideally would just be `exit.Exception = Exit` etc. 

93 

94_F = TypeVar("_F", bound=Callable[..., object]) 

95_ET = TypeVar("_ET", bound=Type[BaseException]) 

96 

97 

98class _WithException(Protocol[_F, _ET]): 

99 Exception: _ET 

100 __call__: _F 

101 

102 

103def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _ET]]: 

104 def decorate(func: _F) -> _WithException[_F, _ET]: 

105 func_with_exception = cast(_WithException[_F, _ET], func) 

106 func_with_exception.Exception = exception_type 

107 return func_with_exception 

108 

109 return decorate 

110 

111 

112# Exposed helper methods. 

113 

114 

115@_with_exception(Exit) 

116def exit( 

117 reason: str = "", returncode: Optional[int] = None, *, msg: Optional[str] = None 

118) -> NoReturn: 

119 """Exit testing process. 

120 

121 :param reason: 

122 The message to show as the reason for exiting pytest. reason has a default value 

123 only because `msg` is deprecated. 

124 

125 :param returncode: 

126 Return code to be used when exiting pytest. 

127 

128 :param msg: 

129 Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead. 

130 """ 

131 __tracebackhide__ = True 

132 from _pytest.config import UsageError 

133 

134 if reason and msg: 

135 raise UsageError( 

136 "cannot pass reason and msg to exit(), `msg` is deprecated, use `reason`." 

137 ) 

138 if not reason: 

139 if msg is None: 

140 raise UsageError("exit() requires a reason argument") 

141 warnings.warn(KEYWORD_MSG_ARG.format(func="exit"), stacklevel=2) 

142 reason = msg 

143 raise Exit(reason, returncode) 

144 

145 

146@_with_exception(Skipped) 

147def skip( 

148 reason: str = "", *, allow_module_level: bool = False, msg: Optional[str] = None 

149) -> NoReturn: 

150 """Skip an executing test with the given message. 

151 

152 This function should be called only during testing (setup, call or teardown) or 

153 during collection by using the ``allow_module_level`` flag. This function can 

154 be called in doctests as well. 

155 

156 :param reason: 

157 The message to show the user as reason for the skip. 

158 

159 :param allow_module_level: 

160 Allows this function to be called at module level. 

161 Raising the skip exception at module level will stop 

162 the execution of the module and prevent the collection of all tests in the module, 

163 even those defined before the `skip` call. 

164 

165 Defaults to False. 

166 

167 :param msg: 

168 Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead. 

169 

170 .. note:: 

171 It is better to use the :ref:`pytest.mark.skipif ref` marker when 

172 possible to declare a test to be skipped under certain conditions 

173 like mismatching platforms or dependencies. 

174 Similarly, use the ``# doctest: +SKIP`` directive (see :py:data:`doctest.SKIP`) 

175 to skip a doctest statically. 

176 """ 

177 __tracebackhide__ = True 

178 reason = _resolve_msg_to_reason("skip", reason, msg) 

179 raise Skipped(msg=reason, allow_module_level=allow_module_level) 

180 

181 

182@_with_exception(Failed) 

183def fail(reason: str = "", pytrace: bool = True, msg: Optional[str] = None) -> NoReturn: 

184 """Explicitly fail an executing test with the given message. 

185 

186 :param reason: 

187 The message to show the user as reason for the failure. 

188 

189 :param pytrace: 

190 If False, msg represents the full failure information and no 

191 python traceback will be reported. 

192 

193 :param msg: 

194 Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead. 

195 """ 

196 __tracebackhide__ = True 

197 reason = _resolve_msg_to_reason("fail", reason, msg) 

198 raise Failed(msg=reason, pytrace=pytrace) 

199 

200 

201def _resolve_msg_to_reason( 

202 func_name: str, reason: str, msg: Optional[str] = None 

203) -> str: 

204 """ 

205 Handles converting the deprecated msg parameter if provided into 

206 reason, raising a deprecation warning. This function will be removed 

207 when the optional msg argument is removed from here in future. 

208 

209 :param str func_name: 

210 The name of the offending function, this is formatted into the deprecation message. 

211 

212 :param str reason: 

213 The reason= passed into either pytest.fail() or pytest.skip() 

214 

215 :param str msg: 

216 The msg= passed into either pytest.fail() or pytest.skip(). This will 

217 be converted into reason if it is provided to allow pytest.skip(msg=) or 

218 pytest.fail(msg=) to continue working in the interim period. 

219 

220 :returns: 

221 The value to use as reason. 

222 

223 """ 

224 __tracebackhide__ = True 

225 if msg is not None: 

226 

227 if reason: 

228 from pytest import UsageError 

229 

230 raise UsageError( 

231 f"Passing both ``reason`` and ``msg`` to pytest.{func_name}(...) is not permitted." 

232 ) 

233 warnings.warn(KEYWORD_MSG_ARG.format(func=func_name), stacklevel=3) 

234 reason = msg 

235 return reason 

236 

237 

238class XFailed(Failed): 

239 """Raised from an explicit call to pytest.xfail().""" 

240 

241 

242@_with_exception(XFailed) 

243def xfail(reason: str = "") -> NoReturn: 

244 """Imperatively xfail an executing test or setup function with the given reason. 

245 

246 This function should be called only during testing (setup, call or teardown). 

247 

248 :param reason: 

249 The message to show the user as reason for the xfail. 

250 

251 .. note:: 

252 It is better to use the :ref:`pytest.mark.xfail ref` marker when 

253 possible to declare a test to be xfailed under certain conditions 

254 like known bugs or missing features. 

255 """ 

256 __tracebackhide__ = True 

257 raise XFailed(reason) 

258 

259 

260def importorskip( 

261 modname: str, minversion: Optional[str] = None, reason: Optional[str] = None 

262) -> Any: 

263 """Import and return the requested module ``modname``, or skip the 

264 current test if the module cannot be imported. 

265 

266 :param modname: 

267 The name of the module to import. 

268 :param minversion: 

269 If given, the imported module's ``__version__`` attribute must be at 

270 least this minimal version, otherwise the test is still skipped. 

271 :param reason: 

272 If given, this reason is shown as the message when the module cannot 

273 be imported. 

274 

275 :returns: 

276 The imported module. This should be assigned to its canonical name. 

277 

278 Example:: 

279 

280 docutils = pytest.importorskip("docutils") 

281 """ 

282 import warnings 

283 

284 __tracebackhide__ = True 

285 compile(modname, "", "eval") # to catch syntaxerrors 

286 

287 with warnings.catch_warnings(): 

288 # Make sure to ignore ImportWarnings that might happen because 

289 # of existing directories with the same name we're trying to 

290 # import but without a __init__.py file. 

291 warnings.simplefilter("ignore") 

292 try: 

293 __import__(modname) 

294 except ImportError as exc: 

295 if reason is None: 

296 reason = f"could not import {modname!r}: {exc}" 

297 raise Skipped(reason, allow_module_level=True) from None 

298 mod = sys.modules[modname] 

299 if minversion is None: 

300 return mod 

301 verattr = getattr(mod, "__version__", None) 

302 if minversion is not None: 

303 # Imported lazily to improve start-up time. 

304 from packaging.version import Version 

305 

306 if verattr is None or Version(verattr) < Version(minversion): 

307 raise Skipped( 

308 "module %r has __version__ %r, required is: %r" 

309 % (modname, verattr, minversion), 

310 allow_module_level=True, 

311 ) 

312 return mod