monkeypatch.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. # mypy: allow-untyped-defs
  2. """Monkeypatching and mocking functionality."""
  3. from __future__ import annotations
  4. from collections.abc import Generator
  5. from collections.abc import Mapping
  6. from collections.abc import MutableMapping
  7. from contextlib import contextmanager
  8. import os
  9. import re
  10. import sys
  11. from typing import Any
  12. from typing import final
  13. from typing import overload
  14. from typing import TypeVar
  15. import warnings
  16. from _pytest.fixtures import fixture
  17. from _pytest.warning_types import PytestWarning
  18. RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$")
  19. K = TypeVar("K")
  20. V = TypeVar("V")
  21. @fixture
  22. def monkeypatch() -> Generator[MonkeyPatch]:
  23. """A convenient fixture for monkey-patching.
  24. The fixture provides these methods to modify objects, dictionaries, or
  25. :data:`os.environ`:
  26. * :meth:`monkeypatch.setattr(obj, name, value, raising=True) <pytest.MonkeyPatch.setattr>`
  27. * :meth:`monkeypatch.delattr(obj, name, raising=True) <pytest.MonkeyPatch.delattr>`
  28. * :meth:`monkeypatch.setitem(mapping, name, value) <pytest.MonkeyPatch.setitem>`
  29. * :meth:`monkeypatch.delitem(obj, name, raising=True) <pytest.MonkeyPatch.delitem>`
  30. * :meth:`monkeypatch.setenv(name, value, prepend=None) <pytest.MonkeyPatch.setenv>`
  31. * :meth:`monkeypatch.delenv(name, raising=True) <pytest.MonkeyPatch.delenv>`
  32. * :meth:`monkeypatch.syspath_prepend(path) <pytest.MonkeyPatch.syspath_prepend>`
  33. * :meth:`monkeypatch.chdir(path) <pytest.MonkeyPatch.chdir>`
  34. * :meth:`monkeypatch.context() <pytest.MonkeyPatch.context>`
  35. All modifications will be undone after the requesting test function or
  36. fixture has finished. The ``raising`` parameter determines if a :class:`KeyError`
  37. or :class:`AttributeError` will be raised if the set/deletion operation does not have the
  38. specified target.
  39. To undo modifications done by the fixture in a contained scope,
  40. use :meth:`context() <pytest.MonkeyPatch.context>`.
  41. """
  42. mpatch = MonkeyPatch()
  43. yield mpatch
  44. mpatch.undo()
  45. def resolve(name: str) -> object:
  46. # Simplified from zope.dottedname.
  47. parts = name.split(".")
  48. used = parts.pop(0)
  49. found: object = __import__(used)
  50. for part in parts:
  51. used += "." + part
  52. try:
  53. found = getattr(found, part)
  54. except AttributeError:
  55. pass
  56. else:
  57. continue
  58. # We use explicit un-nesting of the handling block in order
  59. # to avoid nested exceptions.
  60. try:
  61. __import__(used)
  62. except ImportError as ex:
  63. expected = str(ex).split()[-1]
  64. if expected == used:
  65. raise
  66. else:
  67. raise ImportError(f"import error in {used}: {ex}") from ex
  68. found = annotated_getattr(found, part, used)
  69. return found
  70. def annotated_getattr(obj: object, name: str, ann: str) -> object:
  71. try:
  72. obj = getattr(obj, name)
  73. except AttributeError as e:
  74. raise AttributeError(
  75. f"{type(obj).__name__!r} object at {ann} has no attribute {name!r}"
  76. ) from e
  77. return obj
  78. def derive_importpath(import_path: str, raising: bool) -> tuple[str, object]:
  79. if not isinstance(import_path, str) or "." not in import_path:
  80. raise TypeError(f"must be absolute import path string, not {import_path!r}")
  81. module, attr = import_path.rsplit(".", 1)
  82. target = resolve(module)
  83. if raising:
  84. annotated_getattr(target, attr, ann=module)
  85. return attr, target
  86. class Notset:
  87. def __repr__(self) -> str:
  88. return "<notset>"
  89. notset = Notset()
  90. @final
  91. class MonkeyPatch:
  92. """Helper to conveniently monkeypatch attributes/items/environment
  93. variables/syspath.
  94. Returned by the :fixture:`monkeypatch` fixture.
  95. .. versionchanged:: 6.2
  96. Can now also be used directly as `pytest.MonkeyPatch()`, for when
  97. the fixture is not available. In this case, use
  98. :meth:`with MonkeyPatch.context() as mp: <context>` or remember to call
  99. :meth:`undo` explicitly.
  100. """
  101. def __init__(self) -> None:
  102. self._setattr: list[tuple[object, str, object]] = []
  103. self._setitem: list[tuple[Mapping[Any, Any], object, object]] = []
  104. self._cwd: str | None = None
  105. self._savesyspath: list[str] | None = None
  106. @classmethod
  107. @contextmanager
  108. def context(cls) -> Generator[MonkeyPatch]:
  109. """Context manager that returns a new :class:`MonkeyPatch` object
  110. which undoes any patching done inside the ``with`` block upon exit.
  111. Example:
  112. .. code-block:: python
  113. import functools
  114. def test_partial(monkeypatch):
  115. with monkeypatch.context() as m:
  116. m.setattr(functools, "partial", 3)
  117. Useful in situations where it is desired to undo some patches before the test ends,
  118. such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples
  119. of this see :issue:`3290`).
  120. """
  121. m = cls()
  122. try:
  123. yield m
  124. finally:
  125. m.undo()
  126. @overload
  127. def setattr(
  128. self,
  129. target: str,
  130. name: object,
  131. value: Notset = ...,
  132. raising: bool = ...,
  133. ) -> None: ...
  134. @overload
  135. def setattr(
  136. self,
  137. target: object,
  138. name: str,
  139. value: object,
  140. raising: bool = ...,
  141. ) -> None: ...
  142. def setattr(
  143. self,
  144. target: str | object,
  145. name: object | str,
  146. value: object = notset,
  147. raising: bool = True,
  148. ) -> None:
  149. """
  150. Set attribute value on target, memorizing the old value.
  151. For example:
  152. .. code-block:: python
  153. import os
  154. monkeypatch.setattr(os, "getcwd", lambda: "/")
  155. The code above replaces the :func:`os.getcwd` function by a ``lambda`` which
  156. always returns ``"/"``.
  157. For convenience, you can specify a string as ``target`` which
  158. will be interpreted as a dotted import path, with the last part
  159. being the attribute name:
  160. .. code-block:: python
  161. monkeypatch.setattr("os.getcwd", lambda: "/")
  162. Raises :class:`AttributeError` if the attribute does not exist, unless
  163. ``raising`` is set to False.
  164. **Where to patch**
  165. ``monkeypatch.setattr`` works by (temporarily) changing the object that a name points to with another one.
  166. There can be many names pointing to any individual object, so for patching to work you must ensure
  167. that you patch the name used by the system under test.
  168. See the section :ref:`Where to patch <python:where-to-patch>` in the :mod:`unittest.mock`
  169. docs for a complete explanation, which is meant for :func:`unittest.mock.patch` but
  170. applies to ``monkeypatch.setattr`` as well.
  171. """
  172. __tracebackhide__ = True
  173. import inspect
  174. if isinstance(value, Notset):
  175. if not isinstance(target, str):
  176. raise TypeError(
  177. "use setattr(target, name, value) or "
  178. "setattr(target, value) with target being a dotted "
  179. "import string"
  180. )
  181. value = name
  182. name, target = derive_importpath(target, raising)
  183. else:
  184. if not isinstance(name, str):
  185. raise TypeError(
  186. "use setattr(target, name, value) with name being a string or "
  187. "setattr(target, value) with target being a dotted "
  188. "import string"
  189. )
  190. oldval = getattr(target, name, notset)
  191. if raising and oldval is notset:
  192. raise AttributeError(f"{target!r} has no attribute {name!r}")
  193. # avoid class descriptors like staticmethod/classmethod
  194. if inspect.isclass(target):
  195. oldval = target.__dict__.get(name, notset)
  196. self._setattr.append((target, name, oldval))
  197. setattr(target, name, value)
  198. def delattr(
  199. self,
  200. target: object | str,
  201. name: str | Notset = notset,
  202. raising: bool = True,
  203. ) -> None:
  204. """Delete attribute ``name`` from ``target``.
  205. If no ``name`` is specified and ``target`` is a string
  206. it will be interpreted as a dotted import path with the
  207. last part being the attribute name.
  208. Raises AttributeError it the attribute does not exist, unless
  209. ``raising`` is set to False.
  210. """
  211. __tracebackhide__ = True
  212. import inspect
  213. if isinstance(name, Notset):
  214. if not isinstance(target, str):
  215. raise TypeError(
  216. "use delattr(target, name) or "
  217. "delattr(target) with target being a dotted "
  218. "import string"
  219. )
  220. name, target = derive_importpath(target, raising)
  221. if not hasattr(target, name):
  222. if raising:
  223. raise AttributeError(name)
  224. else:
  225. oldval = getattr(target, name, notset)
  226. # Avoid class descriptors like staticmethod/classmethod.
  227. if inspect.isclass(target):
  228. oldval = target.__dict__.get(name, notset)
  229. self._setattr.append((target, name, oldval))
  230. delattr(target, name)
  231. def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None:
  232. """Set dictionary entry ``name`` to value."""
  233. self._setitem.append((dic, name, dic.get(name, notset)))
  234. # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
  235. dic[name] = value # type: ignore[index]
  236. def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None:
  237. """Delete ``name`` from dict.
  238. Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
  239. False.
  240. """
  241. if name not in dic:
  242. if raising:
  243. raise KeyError(name)
  244. else:
  245. self._setitem.append((dic, name, dic.get(name, notset)))
  246. # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
  247. del dic[name] # type: ignore[attr-defined]
  248. def setenv(self, name: str, value: str, prepend: str | None = None) -> None:
  249. """Set environment variable ``name`` to ``value``.
  250. If ``prepend`` is a character, read the current environment variable
  251. value and prepend the ``value`` adjoined with the ``prepend``
  252. character.
  253. """
  254. if not isinstance(value, str):
  255. warnings.warn( # type: ignore[unreachable]
  256. PytestWarning(
  257. f"Value of environment variable {name} type should be str, but got "
  258. f"{value!r} (type: {type(value).__name__}); converted to str implicitly"
  259. ),
  260. stacklevel=2,
  261. )
  262. value = str(value)
  263. if prepend and name in os.environ:
  264. value = value + prepend + os.environ[name]
  265. self.setitem(os.environ, name, value)
  266. def delenv(self, name: str, raising: bool = True) -> None:
  267. """Delete ``name`` from the environment.
  268. Raises ``KeyError`` if it does not exist, unless ``raising`` is set to
  269. False.
  270. """
  271. environ: MutableMapping[str, str] = os.environ
  272. self.delitem(environ, name, raising=raising)
  273. def syspath_prepend(self, path) -> None:
  274. """Prepend ``path`` to ``sys.path`` list of import locations."""
  275. if self._savesyspath is None:
  276. self._savesyspath = sys.path[:]
  277. sys.path.insert(0, str(path))
  278. # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171
  279. # this is only needed when pkg_resources was already loaded by the namespace package
  280. if "pkg_resources" in sys.modules:
  281. from pkg_resources import fixup_namespace_packages
  282. fixup_namespace_packages(str(path))
  283. # A call to syspathinsert() usually means that the caller wants to
  284. # import some dynamically created files, thus with python3 we
  285. # invalidate its import caches.
  286. # This is especially important when any namespace package is in use,
  287. # since then the mtime based FileFinder cache (that gets created in
  288. # this case already) gets not invalidated when writing the new files
  289. # quickly afterwards.
  290. from importlib import invalidate_caches
  291. invalidate_caches()
  292. def chdir(self, path: str | os.PathLike[str]) -> None:
  293. """Change the current working directory to the specified path.
  294. :param path:
  295. The path to change into.
  296. """
  297. if self._cwd is None:
  298. self._cwd = os.getcwd()
  299. os.chdir(path)
  300. def undo(self) -> None:
  301. """Undo previous changes.
  302. This call consumes the undo stack. Calling it a second time has no
  303. effect unless you do more monkeypatching after the undo call.
  304. There is generally no need to call `undo()`, since it is
  305. called automatically during tear-down.
  306. .. note::
  307. The same `monkeypatch` fixture is used across a
  308. single test function invocation. If `monkeypatch` is used both by
  309. the test function itself and one of the test fixtures,
  310. calling `undo()` will undo all of the changes made in
  311. both functions.
  312. Prefer to use :meth:`context() <pytest.MonkeyPatch.context>` instead.
  313. """
  314. for obj, name, value in reversed(self._setattr):
  315. if value is not notset:
  316. setattr(obj, name, value)
  317. else:
  318. delattr(obj, name)
  319. self._setattr[:] = []
  320. for dictionary, key, value in reversed(self._setitem):
  321. if value is notset:
  322. try:
  323. # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
  324. del dictionary[key] # type: ignore[attr-defined]
  325. except KeyError:
  326. pass # Was already deleted, so we have the desired state.
  327. else:
  328. # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
  329. dictionary[key] = value # type: ignore[index]
  330. self._setitem[:] = []
  331. if self._savesyspath is not None:
  332. sys.path[:] = self._savesyspath
  333. self._savesyspath = None
  334. if self._cwd is not None:
  335. os.chdir(self._cwd)
  336. self._cwd = None