outcomes.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. """Exception classes and constants handling test outcomes as well as
  2. functions creating them."""
  3. from __future__ import annotations
  4. from collections.abc import Callable
  5. import sys
  6. from typing import Any
  7. from typing import cast
  8. from typing import NoReturn
  9. from typing import Protocol
  10. from typing import TypeVar
  11. from .warning_types import PytestDeprecationWarning
  12. class OutcomeException(BaseException):
  13. """OutcomeException and its subclass instances indicate and contain info
  14. about test and collection outcomes."""
  15. def __init__(self, msg: str | None = None, pytrace: bool = True) -> None:
  16. if msg is not None and not isinstance(msg, str):
  17. error_msg = ( # type: ignore[unreachable]
  18. "{} expected string as 'msg' parameter, got '{}' instead.\n"
  19. "Perhaps you meant to use a mark?"
  20. )
  21. raise TypeError(error_msg.format(type(self).__name__, type(msg).__name__))
  22. super().__init__(msg)
  23. self.msg = msg
  24. self.pytrace = pytrace
  25. def __repr__(self) -> str:
  26. if self.msg is not None:
  27. return self.msg
  28. return f"<{self.__class__.__name__} instance>"
  29. __str__ = __repr__
  30. TEST_OUTCOME = (OutcomeException, Exception)
  31. class Skipped(OutcomeException):
  32. # XXX hackish: on 3k we fake to live in the builtins
  33. # in order to have Skipped exception printing shorter/nicer
  34. __module__ = "builtins"
  35. def __init__(
  36. self,
  37. msg: str | None = None,
  38. pytrace: bool = True,
  39. allow_module_level: bool = False,
  40. *,
  41. _use_item_location: bool = False,
  42. ) -> None:
  43. super().__init__(msg=msg, pytrace=pytrace)
  44. self.allow_module_level = allow_module_level
  45. # If true, the skip location is reported as the item's location,
  46. # instead of the place that raises the exception/calls skip().
  47. self._use_item_location = _use_item_location
  48. class Failed(OutcomeException):
  49. """Raised from an explicit call to pytest.fail()."""
  50. __module__ = "builtins"
  51. class Exit(Exception):
  52. """Raised for immediate program exits (no tracebacks/summaries)."""
  53. def __init__(
  54. self, msg: str = "unknown reason", returncode: int | None = None
  55. ) -> None:
  56. self.msg = msg
  57. self.returncode = returncode
  58. super().__init__(msg)
  59. # We need a callable protocol to add attributes, for discussion see
  60. # https://github.com/python/mypy/issues/2087.
  61. _F = TypeVar("_F", bound=Callable[..., object])
  62. _ET = TypeVar("_ET", bound=type[BaseException])
  63. class _WithException(Protocol[_F, _ET]):
  64. Exception: _ET
  65. __call__: _F
  66. def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _ET]]:
  67. def decorate(func: _F) -> _WithException[_F, _ET]:
  68. func_with_exception = cast(_WithException[_F, _ET], func)
  69. func_with_exception.Exception = exception_type
  70. return func_with_exception
  71. return decorate
  72. # Exposed helper methods.
  73. @_with_exception(Exit)
  74. def exit(
  75. reason: str = "",
  76. returncode: int | None = None,
  77. ) -> NoReturn:
  78. """Exit testing process.
  79. :param reason:
  80. The message to show as the reason for exiting pytest. reason has a default value
  81. only because `msg` is deprecated.
  82. :param returncode:
  83. Return code to be used when exiting pytest. None means the same as ``0`` (no error), same as :func:`sys.exit`.
  84. :raises pytest.exit.Exception:
  85. The exception that is raised.
  86. """
  87. __tracebackhide__ = True
  88. raise Exit(reason, returncode)
  89. @_with_exception(Skipped)
  90. def skip(
  91. reason: str = "",
  92. *,
  93. allow_module_level: bool = False,
  94. ) -> NoReturn:
  95. """Skip an executing test with the given message.
  96. This function should be called only during testing (setup, call or teardown) or
  97. during collection by using the ``allow_module_level`` flag. This function can
  98. be called in doctests as well.
  99. :param reason:
  100. The message to show the user as reason for the skip.
  101. :param allow_module_level:
  102. Allows this function to be called at module level.
  103. Raising the skip exception at module level will stop
  104. the execution of the module and prevent the collection of all tests in the module,
  105. even those defined before the `skip` call.
  106. Defaults to False.
  107. :raises pytest.skip.Exception:
  108. The exception that is raised.
  109. .. note::
  110. It is better to use the :ref:`pytest.mark.skipif ref` marker when
  111. possible to declare a test to be skipped under certain conditions
  112. like mismatching platforms or dependencies.
  113. Similarly, use the ``# doctest: +SKIP`` directive (see :py:data:`doctest.SKIP`)
  114. to skip a doctest statically.
  115. """
  116. __tracebackhide__ = True
  117. raise Skipped(msg=reason, allow_module_level=allow_module_level)
  118. @_with_exception(Failed)
  119. def fail(reason: str = "", pytrace: bool = True) -> NoReturn:
  120. """Explicitly fail an executing test with the given message.
  121. :param reason:
  122. The message to show the user as reason for the failure.
  123. :param pytrace:
  124. If False, msg represents the full failure information and no
  125. python traceback will be reported.
  126. :raises pytest.fail.Exception:
  127. The exception that is raised.
  128. """
  129. __tracebackhide__ = True
  130. raise Failed(msg=reason, pytrace=pytrace)
  131. class XFailed(Failed):
  132. """Raised from an explicit call to pytest.xfail()."""
  133. @_with_exception(XFailed)
  134. def xfail(reason: str = "") -> NoReturn:
  135. """Imperatively xfail an executing test or setup function with the given reason.
  136. This function should be called only during testing (setup, call or teardown).
  137. No other code is executed after using ``xfail()`` (it is implemented
  138. internally by raising an exception).
  139. :param reason:
  140. The message to show the user as reason for the xfail.
  141. .. note::
  142. It is better to use the :ref:`pytest.mark.xfail ref` marker when
  143. possible to declare a test to be xfailed under certain conditions
  144. like known bugs or missing features.
  145. :raises pytest.xfail.Exception:
  146. The exception that is raised.
  147. """
  148. __tracebackhide__ = True
  149. raise XFailed(reason)
  150. def importorskip(
  151. modname: str,
  152. minversion: str | None = None,
  153. reason: str | None = None,
  154. *,
  155. exc_type: type[ImportError] | None = None,
  156. ) -> Any:
  157. """Import and return the requested module ``modname``, or skip the
  158. current test if the module cannot be imported.
  159. :param modname:
  160. The name of the module to import.
  161. :param minversion:
  162. If given, the imported module's ``__version__`` attribute must be at
  163. least this minimal version, otherwise the test is still skipped.
  164. :param reason:
  165. If given, this reason is shown as the message when the module cannot
  166. be imported.
  167. :param exc_type:
  168. The exception that should be captured in order to skip modules.
  169. Must be :py:class:`ImportError` or a subclass.
  170. If the module can be imported but raises :class:`ImportError`, pytest will
  171. issue a warning to the user, as often users expect the module not to be
  172. found (which would raise :class:`ModuleNotFoundError` instead).
  173. This warning can be suppressed by passing ``exc_type=ImportError`` explicitly.
  174. See :ref:`import-or-skip-import-error` for details.
  175. :returns:
  176. The imported module. This should be assigned to its canonical name.
  177. :raises pytest.skip.Exception:
  178. If the module cannot be imported.
  179. Example::
  180. docutils = pytest.importorskip("docutils")
  181. .. versionadded:: 8.2
  182. The ``exc_type`` parameter.
  183. """
  184. import warnings
  185. __tracebackhide__ = True
  186. compile(modname, "", "eval") # to catch syntaxerrors
  187. # Until pytest 9.1, we will warn the user if we catch ImportError (instead of ModuleNotFoundError),
  188. # as this might be hiding an installation/environment problem, which is not usually what is intended
  189. # when using importorskip() (#11523).
  190. # In 9.1, to keep the function signature compatible, we just change the code below to:
  191. # 1. Use `exc_type = ModuleNotFoundError` if `exc_type` is not given.
  192. # 2. Remove `warn_on_import` and the warning handling.
  193. if exc_type is None:
  194. exc_type = ImportError
  195. warn_on_import_error = True
  196. else:
  197. warn_on_import_error = False
  198. skipped: Skipped | None = None
  199. warning: Warning | None = None
  200. with warnings.catch_warnings():
  201. # Make sure to ignore ImportWarnings that might happen because
  202. # of existing directories with the same name we're trying to
  203. # import but without a __init__.py file.
  204. warnings.simplefilter("ignore")
  205. try:
  206. __import__(modname)
  207. except exc_type as exc:
  208. # Do not raise or issue warnings inside the catch_warnings() block.
  209. if reason is None:
  210. reason = f"could not import {modname!r}: {exc}"
  211. skipped = Skipped(reason, allow_module_level=True)
  212. if warn_on_import_error and not isinstance(exc, ModuleNotFoundError):
  213. lines = [
  214. "",
  215. f"Module '{modname}' was found, but when imported by pytest it raised:",
  216. f" {exc!r}",
  217. "In pytest 9.1 this warning will become an error by default.",
  218. "You can fix the underlying problem, or alternatively overwrite this behavior and silence this "
  219. "warning by passing exc_type=ImportError explicitly.",
  220. "See https://docs.pytest.org/en/stable/deprecations.html#pytest-importorskip-default-behavior-regarding-importerror",
  221. ]
  222. warning = PytestDeprecationWarning("\n".join(lines))
  223. if warning:
  224. warnings.warn(warning, stacklevel=2)
  225. if skipped:
  226. raise skipped
  227. mod = sys.modules[modname]
  228. if minversion is None:
  229. return mod
  230. verattr = getattr(mod, "__version__", None)
  231. if minversion is not None:
  232. # Imported lazily to improve start-up time.
  233. from packaging.version import Version
  234. if verattr is None or Version(verattr) < Version(minversion):
  235. raise Skipped(
  236. f"module {modname!r} has __version__ {verattr!r}, required is: {minversion!r}",
  237. allow_module_level=True,
  238. )
  239. return mod