outcomes.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  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. from _pytest.deprecated import KEYWORD_MSG_ARG
  13. TYPE_CHECKING = False # Avoid circular import through compat.
  14. if TYPE_CHECKING:
  15. from typing_extensions import Protocol
  16. else:
  17. # typing.Protocol is only available starting from Python 3.8. It is also
  18. # available from typing_extensions, but we don't want a runtime dependency
  19. # on that. So use a dummy runtime implementation.
  20. from typing import Generic
  21. Protocol = Generic
  22. class OutcomeException(BaseException):
  23. """OutcomeException and its subclass instances indicate and contain info
  24. about test and collection outcomes."""
  25. def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None:
  26. if msg is not None and not isinstance(msg, str):
  27. error_msg = ( # type: ignore[unreachable]
  28. "{} expected string as 'msg' parameter, got '{}' instead.\n"
  29. "Perhaps you meant to use a mark?"
  30. )
  31. raise TypeError(error_msg.format(type(self).__name__, type(msg).__name__))
  32. super().__init__(msg)
  33. self.msg = msg
  34. self.pytrace = pytrace
  35. def __repr__(self) -> str:
  36. if self.msg is not None:
  37. return self.msg
  38. return f"<{self.__class__.__name__} instance>"
  39. __str__ = __repr__
  40. TEST_OUTCOME = (OutcomeException, Exception)
  41. class Skipped(OutcomeException):
  42. # XXX hackish: on 3k we fake to live in the builtins
  43. # in order to have Skipped exception printing shorter/nicer
  44. __module__ = "builtins"
  45. def __init__(
  46. self,
  47. msg: Optional[str] = None,
  48. pytrace: bool = True,
  49. allow_module_level: bool = False,
  50. *,
  51. _use_item_location: bool = False,
  52. ) -> None:
  53. super().__init__(msg=msg, pytrace=pytrace)
  54. self.allow_module_level = allow_module_level
  55. # If true, the skip location is reported as the item's location,
  56. # instead of the place that raises the exception/calls skip().
  57. self._use_item_location = _use_item_location
  58. class Failed(OutcomeException):
  59. """Raised from an explicit call to pytest.fail()."""
  60. __module__ = "builtins"
  61. class Exit(Exception):
  62. """Raised for immediate program exits (no tracebacks/summaries)."""
  63. def __init__(
  64. self, msg: str = "unknown reason", returncode: Optional[int] = None
  65. ) -> None:
  66. self.msg = msg
  67. self.returncode = returncode
  68. super().__init__(msg)
  69. # Elaborate hack to work around https://github.com/python/mypy/issues/2087.
  70. # Ideally would just be `exit.Exception = Exit` etc.
  71. _F = TypeVar("_F", bound=Callable[..., object])
  72. _ET = TypeVar("_ET", bound=Type[BaseException])
  73. class _WithException(Protocol[_F, _ET]):
  74. Exception: _ET
  75. __call__: _F
  76. def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _ET]]:
  77. def decorate(func: _F) -> _WithException[_F, _ET]:
  78. func_with_exception = cast(_WithException[_F, _ET], func)
  79. func_with_exception.Exception = exception_type
  80. return func_with_exception
  81. return decorate
  82. # Exposed helper methods.
  83. @_with_exception(Exit)
  84. def exit(
  85. reason: str = "", returncode: Optional[int] = None, *, msg: Optional[str] = None
  86. ) -> NoReturn:
  87. """Exit testing process.
  88. :param reason:
  89. The message to show as the reason for exiting pytest. reason has a default value
  90. only because `msg` is deprecated.
  91. :param returncode:
  92. Return code to be used when exiting pytest. None means the same as ``0`` (no error), same as :func:`sys.exit`.
  93. :param msg:
  94. Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
  95. """
  96. __tracebackhide__ = True
  97. from _pytest.config import UsageError
  98. if reason and msg:
  99. raise UsageError(
  100. "cannot pass reason and msg to exit(), `msg` is deprecated, use `reason`."
  101. )
  102. if not reason:
  103. if msg is None:
  104. raise UsageError("exit() requires a reason argument")
  105. warnings.warn(KEYWORD_MSG_ARG.format(func="exit"), stacklevel=2)
  106. reason = msg
  107. raise Exit(reason, returncode)
  108. @_with_exception(Skipped)
  109. def skip(
  110. reason: str = "", *, allow_module_level: bool = False, msg: Optional[str] = None
  111. ) -> NoReturn:
  112. """Skip an executing test with the given message.
  113. This function should be called only during testing (setup, call or teardown) or
  114. during collection by using the ``allow_module_level`` flag. This function can
  115. be called in doctests as well.
  116. :param reason:
  117. The message to show the user as reason for the skip.
  118. :param allow_module_level:
  119. Allows this function to be called at module level.
  120. Raising the skip exception at module level will stop
  121. the execution of the module and prevent the collection of all tests in the module,
  122. even those defined before the `skip` call.
  123. Defaults to False.
  124. :param msg:
  125. Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
  126. .. note::
  127. It is better to use the :ref:`pytest.mark.skipif ref` marker when
  128. possible to declare a test to be skipped under certain conditions
  129. like mismatching platforms or dependencies.
  130. Similarly, use the ``# doctest: +SKIP`` directive (see :py:data:`doctest.SKIP`)
  131. to skip a doctest statically.
  132. """
  133. __tracebackhide__ = True
  134. reason = _resolve_msg_to_reason("skip", reason, msg)
  135. raise Skipped(msg=reason, allow_module_level=allow_module_level)
  136. @_with_exception(Failed)
  137. def fail(reason: str = "", pytrace: bool = True, msg: Optional[str] = None) -> NoReturn:
  138. """Explicitly fail an executing test with the given message.
  139. :param reason:
  140. The message to show the user as reason for the failure.
  141. :param pytrace:
  142. If False, msg represents the full failure information and no
  143. python traceback will be reported.
  144. :param msg:
  145. Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
  146. """
  147. __tracebackhide__ = True
  148. reason = _resolve_msg_to_reason("fail", reason, msg)
  149. raise Failed(msg=reason, pytrace=pytrace)
  150. def _resolve_msg_to_reason(
  151. func_name: str, reason: str, msg: Optional[str] = None
  152. ) -> str:
  153. """
  154. Handles converting the deprecated msg parameter if provided into
  155. reason, raising a deprecation warning. This function will be removed
  156. when the optional msg argument is removed from here in future.
  157. :param str func_name:
  158. The name of the offending function, this is formatted into the deprecation message.
  159. :param str reason:
  160. The reason= passed into either pytest.fail() or pytest.skip()
  161. :param str msg:
  162. The msg= passed into either pytest.fail() or pytest.skip(). This will
  163. be converted into reason if it is provided to allow pytest.skip(msg=) or
  164. pytest.fail(msg=) to continue working in the interim period.
  165. :returns:
  166. The value to use as reason.
  167. """
  168. __tracebackhide__ = True
  169. if msg is not None:
  170. if reason:
  171. from pytest import UsageError
  172. raise UsageError(
  173. f"Passing both ``reason`` and ``msg`` to pytest.{func_name}(...) is not permitted."
  174. )
  175. warnings.warn(KEYWORD_MSG_ARG.format(func=func_name), stacklevel=3)
  176. reason = msg
  177. return reason
  178. class XFailed(Failed):
  179. """Raised from an explicit call to pytest.xfail()."""
  180. @_with_exception(XFailed)
  181. def xfail(reason: str = "") -> NoReturn:
  182. """Imperatively xfail an executing test or setup function with the given reason.
  183. This function should be called only during testing (setup, call or teardown).
  184. :param reason:
  185. The message to show the user as reason for the xfail.
  186. .. note::
  187. It is better to use the :ref:`pytest.mark.xfail ref` marker when
  188. possible to declare a test to be xfailed under certain conditions
  189. like known bugs or missing features.
  190. """
  191. __tracebackhide__ = True
  192. raise XFailed(reason)
  193. def importorskip(
  194. modname: str, minversion: Optional[str] = None, reason: Optional[str] = None
  195. ) -> Any:
  196. """Import and return the requested module ``modname``, or skip the
  197. current test if the module cannot be imported.
  198. :param modname:
  199. The name of the module to import.
  200. :param minversion:
  201. If given, the imported module's ``__version__`` attribute must be at
  202. least this minimal version, otherwise the test is still skipped.
  203. :param reason:
  204. If given, this reason is shown as the message when the module cannot
  205. be imported.
  206. :returns:
  207. The imported module. This should be assigned to its canonical name.
  208. Example::
  209. docutils = pytest.importorskip("docutils")
  210. """
  211. import warnings
  212. __tracebackhide__ = True
  213. compile(modname, "", "eval") # to catch syntaxerrors
  214. with warnings.catch_warnings():
  215. # Make sure to ignore ImportWarnings that might happen because
  216. # of existing directories with the same name we're trying to
  217. # import but without a __init__.py file.
  218. warnings.simplefilter("ignore")
  219. try:
  220. __import__(modname)
  221. except ImportError as exc:
  222. if reason is None:
  223. reason = f"could not import {modname!r}: {exc}"
  224. raise Skipped(reason, allow_module_level=True) from None
  225. mod = sys.modules[modname]
  226. if minversion is None:
  227. return mod
  228. verattr = getattr(mod, "__version__", None)
  229. if minversion is not None:
  230. # Imported lazily to improve start-up time.
  231. from packaging.version import Version
  232. if verattr is None or Version(verattr) < Version(minversion):
  233. raise Skipped(
  234. "module %r has __version__ %r, required is: %r"
  235. % (modname, verattr, minversion),
  236. allow_module_level=True,
  237. )
  238. return mod