merge monkeypatch.replace into monkeypatch.setattr, also support monkeypatch.delattr.
This commit is contained in:
parent
da7133d201
commit
39503932a4
|
@ -17,10 +17,10 @@ Changes between 2.3.5 and 2.4.DEV
|
||||||
- PR27: correctly handle nose.SkipTest during collection. Thanks
|
- PR27: correctly handle nose.SkipTest during collection. Thanks
|
||||||
Antonio Cuni, Ronny Pfannschmidt.
|
Antonio Cuni, Ronny Pfannschmidt.
|
||||||
|
|
||||||
- new monkeypatch.replace() to avoid imports and provide a shorter
|
- new monkeypatch.setattr() variant to provide a shorter
|
||||||
invocation for patching out classes/functions from modules:
|
invocation for patching out classes/functions from modules:
|
||||||
|
|
||||||
monkeypatch.replace("requests.get", myfunc
|
monkeypatch.setattr("requests.get", myfunc)
|
||||||
|
|
||||||
will replace the "get" function of the "requests" module with ``myfunc``.
|
will replace the "get" function of the "requests" module with ``myfunc``.
|
||||||
|
|
||||||
|
|
|
@ -25,27 +25,9 @@ def pytest_funcarg__monkeypatch(request):
|
||||||
request.addfinalizer(mpatch.undo)
|
request.addfinalizer(mpatch.undo)
|
||||||
return mpatch
|
return mpatch
|
||||||
|
|
||||||
notset = object()
|
|
||||||
|
|
||||||
class monkeypatch:
|
|
||||||
""" object keeping a record of setattr/item/env/syspath changes. """
|
|
||||||
def __init__(self):
|
|
||||||
self._setattr = []
|
|
||||||
self._setitem = []
|
|
||||||
self._cwd = None
|
|
||||||
|
|
||||||
def replace(self, import_path, value):
|
def derive_importpath(import_path):
|
||||||
""" replace the object specified by a dotted ``import_path``
|
|
||||||
with the given ``value``.
|
|
||||||
|
|
||||||
For example ``replace("os.path.abspath", value)`` will
|
|
||||||
trigger an ``import os.path`` and a subsequent
|
|
||||||
setattr(os.path, "abspath", value). Or to prevent
|
|
||||||
the requests library from performing requests you can call
|
|
||||||
``replace("requests.sessions.Session.request", None)``
|
|
||||||
which will lead to an import of ``requests.sessions`` and a call
|
|
||||||
to ``setattr(requests.sessions.Session, "request", None)``.
|
|
||||||
"""
|
|
||||||
if not isinstance(import_path, str) or "." not in import_path:
|
if not isinstance(import_path, str) or "." not in import_path:
|
||||||
raise TypeError("must be absolute import path string, not %r" %
|
raise TypeError("must be absolute import path string, not %r" %
|
||||||
(import_path,))
|
(import_path,))
|
||||||
|
@ -72,32 +54,79 @@ class monkeypatch:
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
pytest.fail("object %r has no attribute %r" % (obj, attr))
|
pytest.fail("object %r has no attribute %r" % (obj, attr))
|
||||||
return self.setattr(obj, attr, value)
|
return attr, obj
|
||||||
|
|
||||||
def setattr(self, obj, name, value, raising=True):
|
|
||||||
""" set attribute ``name`` on ``obj`` to ``value``, by default
|
|
||||||
raise AttributeEror if the attribute did not exist.
|
|
||||||
|
|
||||||
|
|
||||||
|
notset = object()
|
||||||
|
|
||||||
|
class monkeypatch:
|
||||||
|
""" object keeping a record of setattr/item/env/syspath changes. """
|
||||||
|
def __init__(self):
|
||||||
|
self._setattr = []
|
||||||
|
self._setitem = []
|
||||||
|
self._cwd = None
|
||||||
|
|
||||||
|
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 x: "/")``
|
||||||
|
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).
|
||||||
"""
|
"""
|
||||||
oldval = getattr(obj, name, notset)
|
__tracebackhide__ = True
|
||||||
|
|
||||||
|
if value is notset:
|
||||||
|
if not isinstance(target, str):
|
||||||
|
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)
|
||||||
|
|
||||||
|
oldval = getattr(target, name, notset)
|
||||||
if raising and oldval is notset:
|
if raising and oldval is notset:
|
||||||
raise AttributeError("%r has no attribute %r" %(obj, name))
|
raise AttributeError("%r has no attribute %r" %(target, name))
|
||||||
|
|
||||||
# avoid class descriptors like staticmethod/classmethod
|
# avoid class descriptors like staticmethod/classmethod
|
||||||
if inspect.isclass(obj):
|
if inspect.isclass(target):
|
||||||
oldval = obj.__dict__.get(name, notset)
|
oldval = target.__dict__.get(name, notset)
|
||||||
self._setattr.insert(0, (obj, name, oldval))
|
self._setattr.insert(0, (target, name, oldval))
|
||||||
setattr(obj, name, value)
|
setattr(target, name, value)
|
||||||
|
|
||||||
def delattr(self, obj, name, raising=True):
|
def delattr(self, target, name=notset, raising=True):
|
||||||
""" delete attribute ``name`` from ``obj``, by default raise
|
""" delete attribute ``name`` from ``target``, by default raise
|
||||||
AttributeError it the attribute did not previously exist. """
|
AttributeError it the attribute did not previously exist.
|
||||||
if not hasattr(obj, name):
|
|
||||||
|
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, the attribute is allowed to not
|
||||||
|
pre-exist.
|
||||||
|
"""
|
||||||
|
__tracebackhide__ = True
|
||||||
|
if name is notset:
|
||||||
|
if not isinstance(target, str):
|
||||||
|
raise TypeError("use delattr(target, name) or "
|
||||||
|
"delattr(target) with target being a dotted "
|
||||||
|
"import string")
|
||||||
|
name, target = derive_importpath(target)
|
||||||
|
|
||||||
|
if not hasattr(target, name):
|
||||||
if raising:
|
if raising:
|
||||||
raise AttributeError(name)
|
raise AttributeError(name)
|
||||||
else:
|
else:
|
||||||
self._setattr.insert(0, (obj, name, getattr(obj, name, notset)))
|
self._setattr.insert(0, (target, name,
|
||||||
delattr(obj, name)
|
getattr(target, name, notset)))
|
||||||
|
delattr(target, name)
|
||||||
|
|
||||||
def setitem(self, dic, name, value):
|
def setitem(self, dic, name, value):
|
||||||
""" set dictionary entry ``name`` to value. """
|
""" set dictionary entry ``name`` to value. """
|
||||||
|
|
|
@ -48,11 +48,28 @@ requests in all your tests, you can do::
|
||||||
import pytest
|
import pytest
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def no_requests(monkeypatch):
|
def no_requests(monkeypatch):
|
||||||
monkeypatch.replace("requests.session.Session.request", None)
|
monkeypatch.delattr("requests.session.Session.request")
|
||||||
|
|
||||||
|
This autouse fixture will be executed for each test function and it
|
||||||
|
will delete the method ``request.session.Session.request``
|
||||||
|
so that any attempts within tests to create http requests will fail.
|
||||||
|
|
||||||
|
example: setting an attribute on some class
|
||||||
|
------------------------------------------------------
|
||||||
|
|
||||||
|
If you need to patch out ``os.getcwd()`` to return an artifical
|
||||||
|
value::
|
||||||
|
|
||||||
|
def test_some_interaction(monkeypatch):
|
||||||
|
monkeypatch.setattr("os.getcwd", lambda: "/")
|
||||||
|
|
||||||
|
which is equivalent to the long form::
|
||||||
|
|
||||||
|
def test_some_interaction(monkeypatch):
|
||||||
|
import os
|
||||||
|
monkeypatch.setattr(os, "getcwd", lambda: "/")
|
||||||
|
|
||||||
|
|
||||||
This autouse fixture will be executed for all test functions and it
|
|
||||||
will replace the method ``request.session.Session.request`` with the
|
|
||||||
value None so that any attempts to create http requests will fail.
|
|
||||||
|
|
||||||
Method reference of the monkeypatch function argument
|
Method reference of the monkeypatch function argument
|
||||||
-----------------------------------------------------
|
-----------------------------------------------------
|
||||||
|
|
|
@ -35,26 +35,32 @@ def test_setattr():
|
||||||
monkeypatch.undo() # double-undo makes no modification
|
monkeypatch.undo() # double-undo makes no modification
|
||||||
assert A.x == 5
|
assert A.x == 5
|
||||||
|
|
||||||
class TestReplace:
|
class TestSetattrWithImportPath:
|
||||||
def test_string_expression(self, monkeypatch):
|
def test_string_expression(self, monkeypatch):
|
||||||
monkeypatch.replace("os.path.abspath", lambda x: "hello2")
|
monkeypatch.setattr("os.path.abspath", lambda x: "hello2")
|
||||||
assert os.path.abspath("123") == "hello2"
|
assert os.path.abspath("123") == "hello2"
|
||||||
|
|
||||||
def test_string_expression_class(self, monkeypatch):
|
def test_string_expression_class(self, monkeypatch):
|
||||||
monkeypatch.replace("_pytest.config.Config", 42)
|
monkeypatch.setattr("_pytest.config.Config", 42)
|
||||||
import _pytest
|
import _pytest
|
||||||
assert _pytest.config.Config == 42
|
assert _pytest.config.Config == 42
|
||||||
|
|
||||||
def test_wrong_target(self, monkeypatch):
|
def test_wrong_target(self, monkeypatch):
|
||||||
pytest.raises(TypeError, lambda: monkeypatch.replace(None, None))
|
pytest.raises(TypeError, lambda: monkeypatch.setattr(None, None))
|
||||||
|
|
||||||
def test_unknown_import(self, monkeypatch):
|
def test_unknown_import(self, monkeypatch):
|
||||||
pytest.raises(pytest.fail.Exception,
|
pytest.raises(pytest.fail.Exception,
|
||||||
lambda: monkeypatch.replace("unkn123.classx", None))
|
lambda: monkeypatch.setattr("unkn123.classx", None))
|
||||||
|
|
||||||
def test_unknown_attr(self, monkeypatch):
|
def test_unknown_attr(self, monkeypatch):
|
||||||
pytest.raises(pytest.fail.Exception,
|
pytest.raises(pytest.fail.Exception,
|
||||||
lambda: monkeypatch.replace("os.path.qweqwe", None))
|
lambda: monkeypatch.setattr("os.path.qweqwe", None))
|
||||||
|
|
||||||
|
def test_delattr(self, monkeypatch):
|
||||||
|
monkeypatch.delattr("os.path.abspath")
|
||||||
|
assert not hasattr(os.path, "abspath")
|
||||||
|
monkeypatch.undo()
|
||||||
|
assert os.path.abspath
|
||||||
|
|
||||||
def test_delattr():
|
def test_delattr():
|
||||||
class A:
|
class A:
|
||||||
|
@ -262,3 +268,5 @@ def test_issue156_undo_staticmethod(Sample):
|
||||||
|
|
||||||
monkeypatch.undo()
|
monkeypatch.undo()
|
||||||
assert Sample.hello()
|
assert Sample.hello()
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue