123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- # -*- coding: utf-8 -*-
- """ monkeypatching and mocking functionality. """
- from __future__ import absolute_import
- from __future__ import division
- from __future__ import print_function
- import os
- import re
- import sys
- import warnings
- from contextlib import contextmanager
- import six
- import pytest
- from _pytest.fixtures import fixture
- from _pytest.pathlib import Path
- RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$")
- @fixture
- def monkeypatch():
- """The returned ``monkeypatch`` fixture provides these
- helper methods to modify objects, dictionaries or os.environ::
- monkeypatch.setattr(obj, name, value, raising=True)
- monkeypatch.delattr(obj, name, raising=True)
- monkeypatch.setitem(mapping, name, value)
- monkeypatch.delitem(obj, name, raising=True)
- monkeypatch.setenv(name, value, prepend=False)
- monkeypatch.delenv(name, raising=True)
- monkeypatch.syspath_prepend(path)
- monkeypatch.chdir(path)
- All modifications will be undone after the requesting
- test function or fixture has finished. The ``raising``
- parameter determines if a KeyError or AttributeError
- will be raised if the set/deletion operation has no target.
- """
- mpatch = MonkeyPatch()
- yield mpatch
- mpatch.undo()
- def resolve(name):
- # simplified from zope.dottedname
- parts = name.split(".")
- used = parts.pop(0)
- found = __import__(used)
- for part in parts:
- used += "." + part
- try:
- found = getattr(found, part)
- except AttributeError:
- pass
- else:
- continue
- # we use explicit un-nesting of the handling block in order
- # to avoid nested exceptions on python 3
- try:
- __import__(used)
- except ImportError as ex:
- # str is used for py2 vs py3
- expected = str(ex).split()[-1]
- if expected == used:
- raise
- else:
- raise ImportError("import error in %s: %s" % (used, ex))
- found = annotated_getattr(found, part, used)
- return found
- def annotated_getattr(obj, name, ann):
- try:
- obj = getattr(obj, name)
- except AttributeError:
- raise AttributeError(
- "%r object at %s has no attribute %r" % (type(obj).__name__, ann, name)
- )
- return obj
- def derive_importpath(import_path, raising):
- if not isinstance(import_path, six.string_types) or "." not in import_path:
- raise TypeError("must be absolute import path string, not %r" % (import_path,))
- module, attr = import_path.rsplit(".", 1)
- target = resolve(module)
- if raising:
- annotated_getattr(target, attr, ann=module)
- return attr, target
- class Notset(object):
- def __repr__(self):
- return "<notset>"
- notset = Notset()
- class MonkeyPatch(object):
- """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes.
- """
- def __init__(self):
- self._setattr = []
- self._setitem = []
- self._cwd = None
- self._savesyspath = None
- @contextmanager
- def context(self):
- """
- Context manager that returns a new :class:`MonkeyPatch` object which
- undoes any patching done inside the ``with`` block upon exit:
- .. code-block:: python
- import functools
- def test_partial(monkeypatch):
- with monkeypatch.context() as m:
- m.setattr(functools, "partial", 3)
- Useful in situations where it is desired to undo some patches before the test ends,
- such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples
- of this see `#3290 <https://github.com/pytest-dev/pytest/issues/3290>`_.
- """
- m = MonkeyPatch()
- try:
- yield m
- finally:
- m.undo()
- def setattr(self, target, name, value=notset, raising=True):
- """ Set attribute value on target, memorizing the old value.
- By default raise AttributeError if the attribute did not exist.
- For convenience you can specify a string as ``target`` which
- will be interpreted as a dotted import path, with the last part
- being the attribute name. Example:
- ``monkeypatch.setattr("os.getcwd", lambda: "/")``
- would set the ``getcwd`` function of the ``os`` module.
- The ``raising`` value determines if the setattr should fail
- if the attribute is not already present (defaults to True
- which means it will raise).
- """
- __tracebackhide__ = True
- import inspect
- if value is notset:
- if not isinstance(target, six.string_types):
- raise TypeError(
- "use setattr(target, name, value) or "
- "setattr(target, value) with target being a dotted "
- "import string"
- )
- value = name
- name, target = derive_importpath(target, raising)
- oldval = getattr(target, name, notset)
- if raising and oldval is notset:
- raise AttributeError("%r has no attribute %r" % (target, name))
- # avoid class descriptors like staticmethod/classmethod
- if inspect.isclass(target):
- oldval = target.__dict__.get(name, notset)
- self._setattr.append((target, name, oldval))
- setattr(target, name, value)
- def delattr(self, target, name=notset, raising=True):
- """ Delete attribute ``name`` from ``target``, by default raise
- AttributeError it the attribute did not previously exist.
- If no ``name`` is specified and ``target`` is a string
- it will be interpreted as a dotted import path with the
- last part being the attribute name.
- If ``raising`` is set to False, no exception will be raised if the
- attribute is missing.
- """
- __tracebackhide__ = True
- import inspect
- if name is notset:
- if not isinstance(target, six.string_types):
- raise TypeError(
- "use delattr(target, name) or "
- "delattr(target) with target being a dotted "
- "import string"
- )
- name, target = derive_importpath(target, raising)
- if not hasattr(target, name):
- if raising:
- raise AttributeError(name)
- else:
- oldval = getattr(target, name, notset)
- # Avoid class descriptors like staticmethod/classmethod.
- if inspect.isclass(target):
- oldval = target.__dict__.get(name, notset)
- self._setattr.append((target, name, oldval))
- delattr(target, name)
- def setitem(self, dic, name, value):
- """ Set dictionary entry ``name`` to value. """
- self._setitem.append((dic, name, dic.get(name, notset)))
- dic[name] = value
- def delitem(self, dic, name, raising=True):
- """ Delete ``name`` from dict. Raise KeyError if it doesn't exist.
- If ``raising`` is set to False, no exception will be raised if the
- key is missing.
- """
- if name not in dic:
- if raising:
- raise KeyError(name)
- else:
- self._setitem.append((dic, name, dic.get(name, notset)))
- del dic[name]
- def _warn_if_env_name_is_not_str(self, name):
- """On Python 2, warn if the given environment variable name is not a native str (#4056)"""
- if six.PY2 and not isinstance(name, str):
- warnings.warn(
- pytest.PytestWarning(
- "Environment variable name {!r} should be str".format(name)
- )
- )
- def setenv(self, name, value, prepend=None):
- """ Set environment variable ``name`` to ``value``. If ``prepend``
- is a character, read the current environment variable value
- and prepend the ``value`` adjoined with the ``prepend`` character."""
- if not isinstance(value, str):
- warnings.warn(
- pytest.PytestWarning(
- "Value of environment variable {name} type should be str, but got "
- "{value!r} (type: {type}); converted to str implicitly".format(
- name=name, value=value, type=type(value).__name__
- )
- ),
- stacklevel=2,
- )
- value = str(value)
- if prepend and name in os.environ:
- value = value + prepend + os.environ[name]
- self._warn_if_env_name_is_not_str(name)
- self.setitem(os.environ, name, value)
- def delenv(self, name, raising=True):
- """ Delete ``name`` from the environment. Raise KeyError if it does
- not exist.
- If ``raising`` is set to False, no exception will be raised if the
- environment variable is missing.
- """
- self._warn_if_env_name_is_not_str(name)
- self.delitem(os.environ, name, raising=raising)
- def syspath_prepend(self, path):
- """ Prepend ``path`` to ``sys.path`` list of import locations. """
- from pkg_resources import fixup_namespace_packages
- if self._savesyspath is None:
- self._savesyspath = sys.path[:]
- sys.path.insert(0, str(path))
- # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171
- fixup_namespace_packages(str(path))
- # A call to syspathinsert() usually means that the caller wants to
- # import some dynamically created files, thus with python3 we
- # invalidate its import caches.
- # This is especially important when any namespace package is in used,
- # since then the mtime based FileFinder cache (that gets created in
- # this case already) gets not invalidated when writing the new files
- # quickly afterwards.
- if sys.version_info >= (3, 3):
- from importlib import invalidate_caches
- invalidate_caches()
- def chdir(self, path):
- """ Change the current working directory to the specified path.
- Path can be a string or a py.path.local object.
- """
- if self._cwd is None:
- self._cwd = os.getcwd()
- if hasattr(path, "chdir"):
- path.chdir()
- elif isinstance(path, Path):
- # modern python uses the fspath protocol here LEGACY
- os.chdir(str(path))
- else:
- os.chdir(path)
- def undo(self):
- """ Undo previous changes. This call consumes the
- undo stack. Calling it a second time has no effect unless
- you do more monkeypatching after the undo call.
- There is generally no need to call `undo()`, since it is
- called automatically during tear-down.
- Note that the same `monkeypatch` fixture is used across a
- single test function invocation. If `monkeypatch` is used both by
- the test function itself and one of the test fixtures,
- calling `undo()` will undo all of the changes made in
- both functions.
- """
- for obj, name, value in reversed(self._setattr):
- if value is not notset:
- setattr(obj, name, value)
- else:
- delattr(obj, name)
- self._setattr[:] = []
- for dictionary, name, value in reversed(self._setitem):
- if value is notset:
- try:
- del dictionary[name]
- except KeyError:
- pass # was already deleted, so we have the desired state
- else:
- dictionary[name] = value
- self._setitem[:] = []
- if self._savesyspath is not None:
- sys.path[:] = self._savesyspath
- self._savesyspath = None
- if self._cwd is not None:
- os.chdir(self._cwd)
- self._cwd = None
|