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

2 functions creating them."""

3 import sys

4 import warnings

5 from typing import Any

6 from typing import Callable

7 from typing import cast

8 from typing import NoReturn

9 from typing import Optional

10 from typing import Type

11 from typing import TypeVar

12

13 from _pytest.deprecated import KEYWORD_MSG_ARG

14

15 TYPE_CHECKING = False # Avoid circular import through compat.

16

17 if TYPE_CHECKING:

18 from typing_extensions import Protocol

19 else:

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

28 class 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

51 TEST_OUTCOME = (OutcomeException, Exception)

52

53

54 class 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

74 class Failed(OutcomeException):

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

76

77 __module__ = "builtins"

78

79

80 class 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

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

99 Exception: _ET

100 __call__: _F

101

102

103 def _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)

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

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

183 def 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

201 def _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

238 class XFailed(Failed):

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

240

241

242@_with_exception(XFailed)

243 def 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

260 def 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