Coverage for /usr/local/lib/python3.7/site-packages/_pytest/monkeypatch.py : 14%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1""" monkeypatching and mocking functionality. """
2import os
3import re
4import sys
5import warnings
6from contextlib import contextmanager
8import pytest
9from _pytest.fixtures import fixture
10from _pytest.pathlib import Path
12RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$")
15@fixture
16def monkeypatch():
17 """The returned ``monkeypatch`` fixture provides these
18 helper methods to modify objects, dictionaries or os.environ::
20 monkeypatch.setattr(obj, name, value, raising=True)
21 monkeypatch.delattr(obj, name, raising=True)
22 monkeypatch.setitem(mapping, name, value)
23 monkeypatch.delitem(obj, name, raising=True)
24 monkeypatch.setenv(name, value, prepend=False)
25 monkeypatch.delenv(name, raising=True)
26 monkeypatch.syspath_prepend(path)
27 monkeypatch.chdir(path)
29 All modifications will be undone after the requesting
30 test function or fixture has finished. The ``raising``
31 parameter determines if a KeyError or AttributeError
32 will be raised if the set/deletion operation has no target.
33 """
34 mpatch = MonkeyPatch()
35 yield mpatch
36 mpatch.undo()
39def resolve(name):
40 # simplified from zope.dottedname
41 parts = name.split(".")
43 used = parts.pop(0)
44 found = __import__(used)
45 for part in parts:
46 used += "." + part
47 try:
48 found = getattr(found, part)
49 except AttributeError:
50 pass
51 else:
52 continue
53 # we use explicit un-nesting of the handling block in order
54 # to avoid nested exceptions on python 3
55 try:
56 __import__(used)
57 except ImportError as ex:
58 # str is used for py2 vs py3
59 expected = str(ex).split()[-1]
60 if expected == used:
61 raise
62 else:
63 raise ImportError("import error in {}: {}".format(used, ex))
64 found = annotated_getattr(found, part, used)
65 return found
68def annotated_getattr(obj, name, ann):
69 try:
70 obj = getattr(obj, name)
71 except AttributeError:
72 raise AttributeError(
73 "{!r} object at {} has no attribute {!r}".format(
74 type(obj).__name__, ann, name
75 )
76 )
77 return obj
80def derive_importpath(import_path, raising):
81 if not isinstance(import_path, str) or "." not in import_path:
82 raise TypeError(
83 "must be absolute import path string, not {!r}".format(import_path)
84 )
85 module, attr = import_path.rsplit(".", 1)
86 target = resolve(module)
87 if raising:
88 annotated_getattr(target, attr, ann=module)
89 return attr, target
92class Notset:
93 def __repr__(self):
94 return "<notset>"
97notset = Notset()
100class MonkeyPatch:
101 """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes.
102 """
104 def __init__(self):
105 self._setattr = []
106 self._setitem = []
107 self._cwd = None
108 self._savesyspath = None
110 @contextmanager
111 def context(self):
112 """
113 Context manager that returns a new :class:`MonkeyPatch` object which
114 undoes any patching done inside the ``with`` block upon exit:
116 .. code-block:: python
118 import functools
119 def test_partial(monkeypatch):
120 with monkeypatch.context() as m:
121 m.setattr(functools, "partial", 3)
123 Useful in situations where it is desired to undo some patches before the test ends,
124 such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples
125 of this see `#3290 <https://github.com/pytest-dev/pytest/issues/3290>`_.
126 """
127 m = MonkeyPatch()
128 try:
129 yield m
130 finally:
131 m.undo()
133 def setattr(self, target, name, value=notset, raising=True):
134 """ Set attribute value on target, memorizing the old value.
135 By default raise AttributeError if the attribute did not exist.
137 For convenience you can specify a string as ``target`` which
138 will be interpreted as a dotted import path, with the last part
139 being the attribute name. Example:
140 ``monkeypatch.setattr("os.getcwd", lambda: "/")``
141 would set the ``getcwd`` function of the ``os`` module.
143 The ``raising`` value determines if the setattr should fail
144 if the attribute is not already present (defaults to True
145 which means it will raise).
146 """
147 __tracebackhide__ = True
148 import inspect
150 if value is notset:
151 if not isinstance(target, str):
152 raise TypeError(
153 "use setattr(target, name, value) or "
154 "setattr(target, value) with target being a dotted "
155 "import string"
156 )
157 value = name
158 name, target = derive_importpath(target, raising)
160 oldval = getattr(target, name, notset)
161 if raising and oldval is notset:
162 raise AttributeError("{!r} has no attribute {!r}".format(target, name))
164 # avoid class descriptors like staticmethod/classmethod
165 if inspect.isclass(target):
166 oldval = target.__dict__.get(name, notset)
167 self._setattr.append((target, name, oldval))
168 setattr(target, name, value)
170 def delattr(self, target, name=notset, raising=True):
171 """ Delete attribute ``name`` from ``target``, by default raise
172 AttributeError it the attribute did not previously exist.
174 If no ``name`` is specified and ``target`` is a string
175 it will be interpreted as a dotted import path with the
176 last part being the attribute name.
178 If ``raising`` is set to False, no exception will be raised if the
179 attribute is missing.
180 """
181 __tracebackhide__ = True
182 import inspect
184 if name is notset:
185 if not isinstance(target, str):
186 raise TypeError(
187 "use delattr(target, name) or "
188 "delattr(target) with target being a dotted "
189 "import string"
190 )
191 name, target = derive_importpath(target, raising)
193 if not hasattr(target, name):
194 if raising:
195 raise AttributeError(name)
196 else:
197 oldval = getattr(target, name, notset)
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 delattr(target, name)
204 def setitem(self, dic, name, value):
205 """ Set dictionary entry ``name`` to value. """
206 self._setitem.append((dic, name, dic.get(name, notset)))
207 dic[name] = value
209 def delitem(self, dic, name, raising=True):
210 """ Delete ``name`` from dict. Raise KeyError if it doesn't exist.
212 If ``raising`` is set to False, no exception will be raised if the
213 key is missing.
214 """
215 if name not in dic:
216 if raising:
217 raise KeyError(name)
218 else:
219 self._setitem.append((dic, name, dic.get(name, notset)))
220 del dic[name]
222 def setenv(self, name, value, prepend=None):
223 """ Set environment variable ``name`` to ``value``. If ``prepend``
224 is a character, read the current environment variable value
225 and prepend the ``value`` adjoined with the ``prepend`` character."""
226 if not isinstance(value, str):
227 warnings.warn(
228 pytest.PytestWarning(
229 "Value of environment variable {name} type should be str, but got "
230 "{value!r} (type: {type}); converted to str implicitly".format(
231 name=name, value=value, type=type(value).__name__
232 )
233 ),
234 stacklevel=2,
235 )
236 value = str(value)
237 if prepend and name in os.environ:
238 value = value + prepend + os.environ[name]
239 self.setitem(os.environ, name, value)
241 def delenv(self, name, raising=True):
242 """ Delete ``name`` from the environment. Raise KeyError if it does
243 not exist.
245 If ``raising`` is set to False, no exception will be raised if the
246 environment variable is missing.
247 """
248 self.delitem(os.environ, name, raising=raising)
250 def syspath_prepend(self, path):
251 """ Prepend ``path`` to ``sys.path`` list of import locations. """
252 from pkg_resources import fixup_namespace_packages
254 if self._savesyspath is None:
255 self._savesyspath = sys.path[:]
256 sys.path.insert(0, str(path))
258 # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171
259 fixup_namespace_packages(str(path))
261 # A call to syspathinsert() usually means that the caller wants to
262 # import some dynamically created files, thus with python3 we
263 # invalidate its import caches.
264 # This is especially important when any namespace package is in used,
265 # since then the mtime based FileFinder cache (that gets created in
266 # this case already) gets not invalidated when writing the new files
267 # quickly afterwards.
268 from importlib import invalidate_caches
270 invalidate_caches()
272 def chdir(self, path):
273 """ Change the current working directory to the specified path.
274 Path can be a string or a py.path.local object.
275 """
276 if self._cwd is None:
277 self._cwd = os.getcwd()
278 if hasattr(path, "chdir"):
279 path.chdir()
280 elif isinstance(path, Path):
281 # modern python uses the fspath protocol here LEGACY
282 os.chdir(str(path))
283 else:
284 os.chdir(path)
286 def undo(self):
287 """ Undo previous changes. This call consumes the
288 undo stack. Calling it a second time has no effect unless
289 you do more monkeypatching after the undo call.
291 There is generally no need to call `undo()`, since it is
292 called automatically during tear-down.
294 Note that the same `monkeypatch` fixture is used across a
295 single test function invocation. If `monkeypatch` is used both by
296 the test function itself and one of the test fixtures,
297 calling `undo()` will undo all of the changes made in
298 both functions.
299 """
300 for obj, name, value in reversed(self._setattr):
301 if value is not notset:
302 setattr(obj, name, value)
303 else:
304 delattr(obj, name)
305 self._setattr[:] = []
306 for dictionary, name, value in reversed(self._setitem):
307 if value is notset:
308 try:
309 del dictionary[name]
310 except KeyError:
311 pass # was already deleted, so we have the desired state
312 else:
313 dictionary[name] = value
314 self._setitem[:] = []
315 if self._savesyspath is not None:
316 sys.path[:] = self._savesyspath
317 self._savesyspath = None
319 if self._cwd is not None:
320 os.chdir(self._cwd)
321 self._cwd = None