monkeypatch.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. """ monkeypatching and mocking functionality. """
  2. from __future__ import absolute_import, division, print_function
  3. import os
  4. import sys
  5. import re
  6. from contextlib import contextmanager
  7. import six
  8. from _pytest.fixtures import fixture
  9. RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$")
  10. @fixture
  11. def monkeypatch():
  12. """The returned ``monkeypatch`` fixture provides these
  13. helper methods to modify objects, dictionaries or os.environ::
  14. monkeypatch.setattr(obj, name, value, raising=True)
  15. monkeypatch.delattr(obj, name, raising=True)
  16. monkeypatch.setitem(mapping, name, value)
  17. monkeypatch.delitem(obj, name, raising=True)
  18. monkeypatch.setenv(name, value, prepend=False)
  19. monkeypatch.delenv(name, raising=True)
  20. monkeypatch.syspath_prepend(path)
  21. monkeypatch.chdir(path)
  22. All modifications will be undone after the requesting
  23. test function or fixture has finished. The ``raising``
  24. parameter determines if a KeyError or AttributeError
  25. will be raised if the set/deletion operation has no target.
  26. """
  27. mpatch = MonkeyPatch()
  28. yield mpatch
  29. mpatch.undo()
  30. def resolve(name):
  31. # simplified from zope.dottedname
  32. parts = name.split(".")
  33. used = parts.pop(0)
  34. found = __import__(used)
  35. for part in parts:
  36. used += "." + part
  37. try:
  38. found = getattr(found, part)
  39. except AttributeError:
  40. pass
  41. else:
  42. continue
  43. # we use explicit un-nesting of the handling block in order
  44. # to avoid nested exceptions on python 3
  45. try:
  46. __import__(used)
  47. except ImportError as ex:
  48. # str is used for py2 vs py3
  49. expected = str(ex).split()[-1]
  50. if expected == used:
  51. raise
  52. else:
  53. raise ImportError("import error in %s: %s" % (used, ex))
  54. found = annotated_getattr(found, part, used)
  55. return found
  56. def annotated_getattr(obj, name, ann):
  57. try:
  58. obj = getattr(obj, name)
  59. except AttributeError:
  60. raise AttributeError(
  61. "%r object at %s has no attribute %r" % (type(obj).__name__, ann, name)
  62. )
  63. return obj
  64. def derive_importpath(import_path, raising):
  65. if not isinstance(import_path, six.string_types) or "." not in import_path:
  66. raise TypeError("must be absolute import path string, not %r" % (import_path,))
  67. module, attr = import_path.rsplit(".", 1)
  68. target = resolve(module)
  69. if raising:
  70. annotated_getattr(target, attr, ann=module)
  71. return attr, target
  72. class Notset(object):
  73. def __repr__(self):
  74. return "<notset>"
  75. notset = Notset()
  76. class MonkeyPatch(object):
  77. """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes.
  78. """
  79. def __init__(self):
  80. self._setattr = []
  81. self._setitem = []
  82. self._cwd = None
  83. self._savesyspath = None
  84. @contextmanager
  85. def context(self):
  86. """
  87. Context manager that returns a new :class:`MonkeyPatch` object which
  88. undoes any patching done inside the ``with`` block upon exit:
  89. .. code-block:: python
  90. import functools
  91. def test_partial(monkeypatch):
  92. with monkeypatch.context() as m:
  93. m.setattr(functools, "partial", 3)
  94. Useful in situations where it is desired to undo some patches before the test ends,
  95. such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples
  96. of this see `#3290 <https://github.com/pytest-dev/pytest/issues/3290>`_.
  97. """
  98. m = MonkeyPatch()
  99. try:
  100. yield m
  101. finally:
  102. m.undo()
  103. def setattr(self, target, name, value=notset, raising=True):
  104. """ Set attribute value on target, memorizing the old value.
  105. By default raise AttributeError if the attribute did not exist.
  106. For convenience you can specify a string as ``target`` which
  107. will be interpreted as a dotted import path, with the last part
  108. being the attribute name. Example:
  109. ``monkeypatch.setattr("os.getcwd", lambda: "/")``
  110. would set the ``getcwd`` function of the ``os`` module.
  111. The ``raising`` value determines if the setattr should fail
  112. if the attribute is not already present (defaults to True
  113. which means it will raise).
  114. """
  115. __tracebackhide__ = True
  116. import inspect
  117. if value is notset:
  118. if not isinstance(target, six.string_types):
  119. raise TypeError(
  120. "use setattr(target, name, value) or "
  121. "setattr(target, value) with target being a dotted "
  122. "import string"
  123. )
  124. value = name
  125. name, target = derive_importpath(target, raising)
  126. oldval = getattr(target, name, notset)
  127. if raising and oldval is notset:
  128. raise AttributeError("%r has no attribute %r" % (target, name))
  129. # avoid class descriptors like staticmethod/classmethod
  130. if inspect.isclass(target):
  131. oldval = target.__dict__.get(name, notset)
  132. self._setattr.append((target, name, oldval))
  133. setattr(target, name, value)
  134. def delattr(self, target, name=notset, raising=True):
  135. """ Delete attribute ``name`` from ``target``, by default raise
  136. AttributeError it the attribute did not previously exist.
  137. If no ``name`` is specified and ``target`` is a string
  138. it will be interpreted as a dotted import path with the
  139. last part being the attribute name.
  140. If ``raising`` is set to False, no exception will be raised if the
  141. attribute is missing.
  142. """
  143. __tracebackhide__ = True
  144. if name is notset:
  145. if not isinstance(target, six.string_types):
  146. raise TypeError(
  147. "use delattr(target, name) or "
  148. "delattr(target) with target being a dotted "
  149. "import string"
  150. )
  151. name, target = derive_importpath(target, raising)
  152. if not hasattr(target, name):
  153. if raising:
  154. raise AttributeError(name)
  155. else:
  156. self._setattr.append((target, name, getattr(target, name, notset)))
  157. delattr(target, name)
  158. def setitem(self, dic, name, value):
  159. """ Set dictionary entry ``name`` to value. """
  160. self._setitem.append((dic, name, dic.get(name, notset)))
  161. dic[name] = value
  162. def delitem(self, dic, name, raising=True):
  163. """ Delete ``name`` from dict. Raise KeyError if it doesn't exist.
  164. If ``raising`` is set to False, no exception will be raised if the
  165. key is missing.
  166. """
  167. if name not in dic:
  168. if raising:
  169. raise KeyError(name)
  170. else:
  171. self._setitem.append((dic, name, dic.get(name, notset)))
  172. del dic[name]
  173. def setenv(self, name, value, prepend=None):
  174. """ Set environment variable ``name`` to ``value``. If ``prepend``
  175. is a character, read the current environment variable value
  176. and prepend the ``value`` adjoined with the ``prepend`` character."""
  177. value = str(value)
  178. if prepend and name in os.environ:
  179. value = value + prepend + os.environ[name]
  180. self.setitem(os.environ, name, value)
  181. def delenv(self, name, raising=True):
  182. """ Delete ``name`` from the environment. Raise KeyError it does not
  183. exist.
  184. If ``raising`` is set to False, no exception will be raised if the
  185. environment variable is missing.
  186. """
  187. self.delitem(os.environ, name, raising=raising)
  188. def syspath_prepend(self, path):
  189. """ Prepend ``path`` to ``sys.path`` list of import locations. """
  190. if self._savesyspath is None:
  191. self._savesyspath = sys.path[:]
  192. sys.path.insert(0, str(path))
  193. def chdir(self, path):
  194. """ Change the current working directory to the specified path.
  195. Path can be a string or a py.path.local object.
  196. """
  197. if self._cwd is None:
  198. self._cwd = os.getcwd()
  199. if hasattr(path, "chdir"):
  200. path.chdir()
  201. else:
  202. os.chdir(path)
  203. def undo(self):
  204. """ Undo previous changes. This call consumes the
  205. undo stack. Calling it a second time has no effect unless
  206. you do more monkeypatching after the undo call.
  207. There is generally no need to call `undo()`, since it is
  208. called automatically during tear-down.
  209. Note that the same `monkeypatch` fixture is used across a
  210. single test function invocation. If `monkeypatch` is used both by
  211. the test function itself and one of the test fixtures,
  212. calling `undo()` will undo all of the changes made in
  213. both functions.
  214. """
  215. for obj, name, value in reversed(self._setattr):
  216. if value is not notset:
  217. setattr(obj, name, value)
  218. else:
  219. delattr(obj, name)
  220. self._setattr[:] = []
  221. for dictionary, name, value in reversed(self._setitem):
  222. if value is notset:
  223. try:
  224. del dictionary[name]
  225. except KeyError:
  226. pass # was already deleted, so we have the desired state
  227. else:
  228. dictionary[name] = value
  229. self._setitem[:] = []
  230. if self._savesyspath is not None:
  231. sys.path[:] = self._savesyspath
  232. self._savesyspath = None
  233. if self._cwd is not None:
  234. os.chdir(self._cwd)
  235. self._cwd = None