monkeypatch.py 13 KB

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