monkeypatch.py 14 KB

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