compat.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. # mypy: allow-untyped-defs
  2. """Python version compatibility code and random general utilities."""
  3. from __future__ import annotations
  4. from collections.abc import Callable
  5. import enum
  6. import functools
  7. import inspect
  8. from inspect import Parameter
  9. from inspect import Signature
  10. import os
  11. from pathlib import Path
  12. import sys
  13. from typing import Any
  14. from typing import Final
  15. from typing import NoReturn
  16. import py
  17. if sys.version_info >= (3, 14):
  18. from annotationlib import Format
  19. #: constant to prepare valuing pylib path replacements/lazy proxies later on
  20. # intended for removal in pytest 8.0 or 9.0
  21. # fmt: off
  22. # intentional space to create a fake difference for the verification
  23. LEGACY_PATH = py.path. local
  24. # fmt: on
  25. def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
  26. """Internal wrapper to prepare lazy proxies for legacy_path instances"""
  27. return LEGACY_PATH(path)
  28. # fmt: off
  29. # Singleton type for NOTSET, as described in:
  30. # https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
  31. class NotSetType(enum.Enum):
  32. token = 0
  33. NOTSET: Final = NotSetType.token
  34. # fmt: on
  35. def iscoroutinefunction(func: object) -> bool:
  36. """Return True if func is a coroutine function (a function defined with async
  37. def syntax, and doesn't contain yield), or a function decorated with
  38. @asyncio.coroutine.
  39. Note: copied and modified from Python 3.5's builtin coroutines.py to avoid
  40. importing asyncio directly, which in turns also initializes the "logging"
  41. module as a side-effect (see issue #8).
  42. """
  43. return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False)
  44. def is_async_function(func: object) -> bool:
  45. """Return True if the given function seems to be an async function or
  46. an async generator."""
  47. return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
  48. def signature(obj: Callable[..., Any]) -> Signature:
  49. """Return signature without evaluating annotations."""
  50. if sys.version_info >= (3, 14):
  51. return inspect.signature(obj, annotation_format=Format.STRING)
  52. return inspect.signature(obj)
  53. def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str:
  54. function = get_real_func(function)
  55. fn = Path(inspect.getfile(function))
  56. lineno = function.__code__.co_firstlineno
  57. if curdir is not None:
  58. try:
  59. relfn = fn.relative_to(curdir)
  60. except ValueError:
  61. pass
  62. else:
  63. return f"{relfn}:{lineno + 1}"
  64. return f"{fn}:{lineno + 1}"
  65. def num_mock_patch_args(function) -> int:
  66. """Return number of arguments used up by mock arguments (if any)."""
  67. patchings = getattr(function, "patchings", None)
  68. if not patchings:
  69. return 0
  70. mock_sentinel = getattr(sys.modules.get("mock"), "DEFAULT", object())
  71. ut_mock_sentinel = getattr(sys.modules.get("unittest.mock"), "DEFAULT", object())
  72. return len(
  73. [
  74. p
  75. for p in patchings
  76. if not p.attribute_name
  77. and (p.new is mock_sentinel or p.new is ut_mock_sentinel)
  78. ]
  79. )
  80. def getfuncargnames(
  81. function: Callable[..., object],
  82. *,
  83. name: str = "",
  84. cls: type | None = None,
  85. ) -> tuple[str, ...]:
  86. """Return the names of a function's mandatory arguments.
  87. Should return the names of all function arguments that:
  88. * Aren't bound to an instance or type as in instance or class methods.
  89. * Don't have default values.
  90. * Aren't bound with functools.partial.
  91. * Aren't replaced with mocks.
  92. The cls arguments indicate that the function should be treated as a bound
  93. method even though it's not unless the function is a static method.
  94. The name parameter should be the original name in which the function was collected.
  95. """
  96. # TODO(RonnyPfannschmidt): This function should be refactored when we
  97. # revisit fixtures. The fixture mechanism should ask the node for
  98. # the fixture names, and not try to obtain directly from the
  99. # function object well after collection has occurred.
  100. # The parameters attribute of a Signature object contains an
  101. # ordered mapping of parameter names to Parameter instances. This
  102. # creates a tuple of the names of the parameters that don't have
  103. # defaults.
  104. try:
  105. parameters = signature(function).parameters.values()
  106. except (ValueError, TypeError) as e:
  107. from _pytest.outcomes import fail
  108. fail(
  109. f"Could not determine arguments of {function!r}: {e}",
  110. pytrace=False,
  111. )
  112. arg_names = tuple(
  113. p.name
  114. for p in parameters
  115. if (
  116. p.kind is Parameter.POSITIONAL_OR_KEYWORD
  117. or p.kind is Parameter.KEYWORD_ONLY
  118. )
  119. and p.default is Parameter.empty
  120. )
  121. if not name:
  122. name = function.__name__
  123. # If this function should be treated as a bound method even though
  124. # it's passed as an unbound method or function, and its first parameter
  125. # wasn't defined as positional only, remove the first parameter name.
  126. if not any(p.kind is Parameter.POSITIONAL_ONLY for p in parameters) and (
  127. # Not using `getattr` because we don't want to resolve the staticmethod.
  128. # Not using `cls.__dict__` because we want to check the entire MRO.
  129. cls
  130. and not isinstance(
  131. inspect.getattr_static(cls, name, default=None), staticmethod
  132. )
  133. ):
  134. arg_names = arg_names[1:]
  135. # Remove any names that will be replaced with mocks.
  136. if hasattr(function, "__wrapped__"):
  137. arg_names = arg_names[num_mock_patch_args(function) :]
  138. return arg_names
  139. def get_default_arg_names(function: Callable[..., Any]) -> tuple[str, ...]:
  140. # Note: this code intentionally mirrors the code at the beginning of
  141. # getfuncargnames, to get the arguments which were excluded from its result
  142. # because they had default values.
  143. return tuple(
  144. p.name
  145. for p in signature(function).parameters.values()
  146. if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
  147. and p.default is not Parameter.empty
  148. )
  149. _non_printable_ascii_translate_table = {
  150. i: f"\\x{i:02x}" for i in range(128) if i not in range(32, 127)
  151. }
  152. _non_printable_ascii_translate_table.update(
  153. {ord("\t"): "\\t", ord("\r"): "\\r", ord("\n"): "\\n"}
  154. )
  155. def ascii_escaped(val: bytes | str) -> str:
  156. r"""If val is pure ASCII, return it as an str, otherwise, escape
  157. bytes objects into a sequence of escaped bytes:
  158. b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
  159. and escapes strings into a sequence of escaped unicode ids, e.g.:
  160. r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
  161. Note:
  162. The obvious "v.decode('unicode-escape')" will return
  163. valid UTF-8 unicode if it finds them in bytes, but we
  164. want to return escaped bytes for any byte, even if they match
  165. a UTF-8 string.
  166. """
  167. if isinstance(val, bytes):
  168. ret = val.decode("ascii", "backslashreplace")
  169. else:
  170. ret = val.encode("unicode_escape").decode("ascii")
  171. return ret.translate(_non_printable_ascii_translate_table)
  172. def get_real_func(obj):
  173. """Get the real function object of the (possibly) wrapped object by
  174. :func:`functools.wraps`, or :func:`functools.partial`."""
  175. obj = inspect.unwrap(obj)
  176. if isinstance(obj, functools.partial):
  177. obj = obj.func
  178. return obj
  179. def getimfunc(func):
  180. try:
  181. return func.__func__
  182. except AttributeError:
  183. return func
  184. def safe_getattr(object: Any, name: str, default: Any) -> Any:
  185. """Like getattr but return default upon any Exception or any OutcomeException.
  186. Attribute access can potentially fail for 'evil' Python objects.
  187. See issue #214.
  188. It catches OutcomeException because of #2490 (issue #580), new outcomes
  189. are derived from BaseException instead of Exception (for more details
  190. check #2707).
  191. """
  192. from _pytest.outcomes import TEST_OUTCOME
  193. try:
  194. return getattr(object, name, default)
  195. except TEST_OUTCOME:
  196. return default
  197. def safe_isclass(obj: object) -> bool:
  198. """Ignore any exception via isinstance on Python 3."""
  199. try:
  200. return inspect.isclass(obj)
  201. except Exception:
  202. return False
  203. def get_user_id() -> int | None:
  204. """Return the current process's real user id or None if it could not be
  205. determined.
  206. :return: The user id or None if it could not be determined.
  207. """
  208. # mypy follows the version and platform checking expectation of PEP 484:
  209. # https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
  210. # Containment checks are too complex for mypy v1.5.0 and cause failure.
  211. if sys.platform == "win32" or sys.platform == "emscripten":
  212. # win32 does not have a getuid() function.
  213. # Emscripten has a return 0 stub.
  214. return None
  215. else:
  216. # On other platforms, a return value of -1 is assumed to indicate that
  217. # the current process's real user id could not be determined.
  218. ERROR = -1
  219. uid = os.getuid()
  220. return uid if uid != ERROR else None
  221. if sys.version_info >= (3, 11):
  222. from typing import assert_never
  223. else:
  224. def assert_never(value: NoReturn) -> NoReturn:
  225. assert False, f"Unhandled value: {value} ({type(value).__name__})"
  226. class CallableBool:
  227. """
  228. A bool-like object that can also be called, returning its true/false value.
  229. Used for backwards compatibility in cases where something was supposed to be a method
  230. but was implemented as a simple attribute by mistake (see `TerminalReporter.isatty`).
  231. Do not use in new code.
  232. """
  233. def __init__(self, value: bool) -> None:
  234. self._value = value
  235. def __bool__(self) -> bool:
  236. return self._value
  237. def __call__(self) -> bool:
  238. return self._value
  239. def running_on_ci() -> bool:
  240. """Check if we're currently running on a CI system."""
  241. # Only enable CI mode if one of these env variables is defined and non-empty.
  242. # Note: review `regendoc` tox env in case this list is changed.
  243. env_vars = ["CI", "BUILD_NUMBER"]
  244. return any(os.environ.get(var) for var in env_vars)