Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/_pytest/monkeypatch.py : 8%

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
7from typing import Any
8from typing import Generator
9from typing import List
10from typing import MutableMapping
11from typing import Optional
12from typing import Tuple
13from typing import TypeVar
14from typing import Union
16import pytest
17from _pytest.compat import overload
18from _pytest.fixtures import fixture
19from _pytest.pathlib import Path
21RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$")
24K = TypeVar("K")
25V = TypeVar("V")
28@fixture
29def monkeypatch() -> Generator["MonkeyPatch", None, None]:
30 """The returned ``monkeypatch`` fixture provides these
31 helper methods to modify objects, dictionaries or os.environ::
33 monkeypatch.setattr(obj, name, value, raising=True)
34 monkeypatch.delattr(obj, name, raising=True)
35 monkeypatch.setitem(mapping, name, value)
36 monkeypatch.delitem(obj, name, raising=True)
37 monkeypatch.setenv(name, value, prepend=False)
38 monkeypatch.delenv(name, raising=True)
39 monkeypatch.syspath_prepend(path)
40 monkeypatch.chdir(path)
42 All modifications will be undone after the requesting
43 test function or fixture has finished. The ``raising``
44 parameter determines if a KeyError or AttributeError
45 will be raised if the set/deletion operation has no target.
46 """
47 mpatch = MonkeyPatch()
48 yield mpatch
49 mpatch.undo()
52def resolve(name: str) -> object:
53 # simplified from zope.dottedname
54 parts = name.split(".")
56 used = parts.pop(0)
57 found = __import__(used)
58 for part in parts:
59 used += "." + part
60 try:
61 found = getattr(found, part)
62 except AttributeError:
63 pass
64 else:
65 continue
66 # we use explicit un-nesting of the handling block in order
67 # to avoid nested exceptions on python 3
68 try:
69 __import__(used)
70 except ImportError as ex:
71 # str is used for py2 vs py3
72 expected = str(ex).split()[-1]
73 if expected == used:
74 raise
75 else:
76 raise ImportError("import error in {}: {}".format(used, ex)) from ex
77 found = annotated_getattr(found, part, used)
78 return found
81def annotated_getattr(obj: object, name: str, ann: str) -> object:
82 try:
83 obj = getattr(obj, name)
84 except AttributeError as e:
85 raise AttributeError(
86 "{!r} object at {} has no attribute {!r}".format(
87 type(obj).__name__, ann, name
88 )
89 ) from e
90 return obj
93def derive_importpath(import_path: str, raising: bool) -> Tuple[str, object]:
94 if not isinstance(import_path, str) or "." not in import_path:
95 raise TypeError(
96 "must be absolute import path string, not {!r}".format(import_path)
97 )
98 module, attr = import_path.rsplit(".", 1)
99 target = resolve(module)
100 if raising:
101 annotated_getattr(target, attr, ann=module)
102 return attr, target
105class Notset:
106 def __repr__(self) -> str:
107 return "<notset>"
110notset = Notset()
113class MonkeyPatch:
114 """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes.
115 """
117 def __init__(self) -> None:
118 self._setattr = [] # type: List[Tuple[object, str, object]]
119 self._setitem = (
120 []
121 ) # type: List[Tuple[MutableMapping[Any, Any], object, object]]
122 self._cwd = None # type: Optional[str]
123 self._savesyspath = None # type: Optional[List[str]]
125 @contextmanager
126 def context(self) -> Generator["MonkeyPatch", None, None]:
127 """
128 Context manager that returns a new :class:`MonkeyPatch` object which
129 undoes any patching done inside the ``with`` block upon exit:
131 .. code-block:: python
133 import functools
136 def test_partial(monkeypatch):
137 with monkeypatch.context() as m:
138 m.setattr(functools, "partial", 3)
140 Useful in situations where it is desired to undo some patches before the test ends,
141 such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples
142 of this see `#3290 <https://github.com/pytest-dev/pytest/issues/3290>`_.
143 """
144 m = MonkeyPatch()
145 try:
146 yield m
147 finally:
148 m.undo()
150 @overload
151 def setattr(
152 self, target: str, name: object, value: Notset = ..., raising: bool = ...,
153 ) -> None:
154 raise NotImplementedError()
156 @overload # noqa: F811
157 def setattr( # noqa: F811
158 self, target: object, name: str, value: object, raising: bool = ...,
159 ) -> None:
160 raise NotImplementedError()
162 def setattr( # noqa: F811
163 self,
164 target: Union[str, object],
165 name: Union[object, str],
166 value: object = notset,
167 raising: bool = True,
168 ) -> None:
169 """ Set attribute value on target, memorizing the old value.
170 By default raise AttributeError if the attribute did not exist.
172 For convenience you can specify a string as ``target`` which
173 will be interpreted as a dotted import path, with the last part
174 being the attribute name. Example:
175 ``monkeypatch.setattr("os.getcwd", lambda: "/")``
176 would set the ``getcwd`` function of the ``os`` module.
178 The ``raising`` value determines if the setattr should fail
179 if the attribute is not already present (defaults to True
180 which means it will raise).
181 """
182 __tracebackhide__ = True
183 import inspect
185 if isinstance(value, Notset):
186 if not isinstance(target, str):
187 raise TypeError(
188 "use setattr(target, name, value) or "
189 "setattr(target, value) with target being a dotted "
190 "import string"
191 )
192 value = name
193 name, target = derive_importpath(target, raising)
194 else:
195 if not isinstance(name, str):
196 raise TypeError(
197 "use setattr(target, name, value) with name being a string or "
198 "setattr(target, value) with target being a dotted "
199 "import string"
200 )
202 oldval = getattr(target, name, notset)
203 if raising and oldval is notset:
204 raise AttributeError("{!r} has no attribute {!r}".format(target, name))
206 # avoid class descriptors like staticmethod/classmethod
207 if inspect.isclass(target):
208 oldval = target.__dict__.get(name, notset)
209 self._setattr.append((target, name, oldval))
210 setattr(target, name, value)
212 def delattr(
213 self,
214 target: Union[object, str],
215 name: Union[str, Notset] = notset,
216 raising: bool = True,
217 ) -> None:
218 """ Delete attribute ``name`` from ``target``, by default raise
219 AttributeError it the attribute did not previously exist.
221 If no ``name`` is specified and ``target`` is a string
222 it will be interpreted as a dotted import path with the
223 last part being the attribute name.
225 If ``raising`` is set to False, no exception will be raised if the
226 attribute is missing.
227 """
228 __tracebackhide__ = True
229 import inspect
231 if isinstance(name, Notset):
232 if not isinstance(target, str):
233 raise TypeError(
234 "use delattr(target, name) or "
235 "delattr(target) with target being a dotted "
236 "import string"
237 )
238 name, target = derive_importpath(target, raising)
240 if not hasattr(target, name):
241 if raising:
242 raise AttributeError(name)
243 else:
244 oldval = getattr(target, name, notset)
245 # Avoid class descriptors like staticmethod/classmethod.
246 if inspect.isclass(target):
247 oldval = target.__dict__.get(name, notset)
248 self._setattr.append((target, name, oldval))
249 delattr(target, name)
251 def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None:
252 """ Set dictionary entry ``name`` to value. """
253 self._setitem.append((dic, name, dic.get(name, notset)))
254 dic[name] = value
256 def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
257 """ Delete ``name`` from dict. Raise KeyError if it doesn't exist.
259 If ``raising`` is set to False, no exception will be raised if the
260 key is missing.
261 """
262 if name not in dic:
263 if raising:
264 raise KeyError(name)
265 else:
266 self._setitem.append((dic, name, dic.get(name, notset)))
267 del dic[name]
269 def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
270 """ Set environment variable ``name`` to ``value``. If ``prepend``
271 is a character, read the current environment variable value
272 and prepend the ``value`` adjoined with the ``prepend`` character."""
273 if not isinstance(value, str):
274 warnings.warn(
275 pytest.PytestWarning(
276 "Value of environment variable {name} type should be str, but got "
277 "{value!r} (type: {type}); converted to str implicitly".format(
278 name=name, value=value, type=type(value).__name__
279 )
280 ),
281 stacklevel=2,
282 )
283 value = str(value)
284 if prepend and name in os.environ:
285 value = value + prepend + os.environ[name]
286 self.setitem(os.environ, name, value)
288 def delenv(self, name: str, raising: bool = True) -> None:
289 """ Delete ``name`` from the environment. Raise KeyError if it does
290 not exist.
292 If ``raising`` is set to False, no exception will be raised if the
293 environment variable is missing.
294 """
295 environ = os.environ # type: MutableMapping[str, str]
296 self.delitem(environ, name, raising=raising)
298 def syspath_prepend(self, path) -> None:
299 """ Prepend ``path`` to ``sys.path`` list of import locations. """
300 from pkg_resources import fixup_namespace_packages
302 if self._savesyspath is None:
303 self._savesyspath = sys.path[:]
304 sys.path.insert(0, str(path))
306 # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171
307 fixup_namespace_packages(str(path))
309 # A call to syspathinsert() usually means that the caller wants to
310 # import some dynamically created files, thus with python3 we
311 # invalidate its import caches.
312 # This is especially important when any namespace package is in use,
313 # since then the mtime based FileFinder cache (that gets created in
314 # this case already) gets not invalidated when writing the new files
315 # quickly afterwards.
316 from importlib import invalidate_caches
318 invalidate_caches()
320 def chdir(self, path) -> None:
321 """ Change the current working directory to the specified path.
322 Path can be a string or a py.path.local object.
323 """
324 if self._cwd is None:
325 self._cwd = os.getcwd()
326 if hasattr(path, "chdir"):
327 path.chdir()
328 elif isinstance(path, Path):
329 # modern python uses the fspath protocol here LEGACY
330 os.chdir(str(path))
331 else:
332 os.chdir(path)
334 def undo(self) -> None:
335 """ Undo previous changes. This call consumes the
336 undo stack. Calling it a second time has no effect unless
337 you do more monkeypatching after the undo call.
339 There is generally no need to call `undo()`, since it is
340 called automatically during tear-down.
342 Note that the same `monkeypatch` fixture is used across a
343 single test function invocation. If `monkeypatch` is used both by
344 the test function itself and one of the test fixtures,
345 calling `undo()` will undo all of the changes made in
346 both functions.
347 """
348 for obj, name, value in reversed(self._setattr):
349 if value is not notset:
350 setattr(obj, name, value)
351 else:
352 delattr(obj, name)
353 self._setattr[:] = []
354 for dictionary, key, value in reversed(self._setitem):
355 if value is notset:
356 try:
357 del dictionary[key]
358 except KeyError:
359 pass # was already deleted, so we have the desired state
360 else:
361 dictionary[key] = value
362 self._setitem[:] = []
363 if self._savesyspath is not None:
364 sys.path[:] = self._savesyspath
365 self._savesyspath = None
367 if self._cwd is not None:
368 os.chdir(self._cwd)
369 self._cwd = None