monkeypatch.py 8.7 KB

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