monkeypatch.replace() now only accepts a string. Improved error handling and
docs thanks to suggestions from flub, pelme, schmir, ronny.
This commit is contained in:
parent
407283ef81
commit
4b88d6d2d7
11
CHANGELOG
11
CHANGELOG
|
@ -1,15 +1,12 @@
|
||||||
Changes between 2.3.5 and 2.4.DEV
|
Changes between 2.3.5 and 2.4.DEV
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
- new monkeypatch.replace() to allow for more direct patching::
|
- new monkeypatch.replace() to avoid imports and provide a shorter
|
||||||
|
invocation for patching out classes/functions from modules:
|
||||||
|
|
||||||
monkeypatch.replace(os.path.abspath, lambda x: "mocked")
|
monkeypatch.replace("requests.get", myfunc
|
||||||
|
|
||||||
instead of: monkeypatch.setattr(os.path, "abspath", lambda x: "mocked")
|
will replace the "get" function of the "requests" module with ``myfunc``.
|
||||||
|
|
||||||
You can also avoid imports by specifying a python path string::
|
|
||||||
|
|
||||||
monkeypatch.replace("requests.get", ...)
|
|
||||||
|
|
||||||
- fix issue322: tearDownClass is not run if setUpClass failed. Thanks
|
- fix issue322: tearDownClass is not run if setUpClass failed. Thanks
|
||||||
Mathieu Agopian for the initial fix. Also make all of pytest/nose finalizer
|
Mathieu Agopian for the initial fix. Also make all of pytest/nose finalizer
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
""" monkeypatching and mocking functionality. """
|
""" monkeypatching and mocking functionality. """
|
||||||
|
|
||||||
import os, sys, inspect
|
import os, sys, inspect
|
||||||
|
import pytest
|
||||||
|
|
||||||
def pytest_funcarg__monkeypatch(request):
|
def pytest_funcarg__monkeypatch(request):
|
||||||
"""The returned ``monkeypatch`` funcarg provides these
|
"""The returned ``monkeypatch`` funcarg provides these
|
||||||
|
@ -26,47 +27,6 @@ def pytest_funcarg__monkeypatch(request):
|
||||||
|
|
||||||
notset = object()
|
notset = object()
|
||||||
|
|
||||||
if sys.version_info < (3,0):
|
|
||||||
def derive_obj_and_name(obj):
|
|
||||||
name = obj.__name__
|
|
||||||
real_obj = getattr(obj, "im_self", None)
|
|
||||||
if real_obj is None:
|
|
||||||
real_obj = getattr(obj, "im_class", None)
|
|
||||||
if real_obj is None:
|
|
||||||
real_obj = sys.modules[obj.__module__]
|
|
||||||
assert getattr(real_obj, name) == obj, \
|
|
||||||
"could not derive object/name pair"
|
|
||||||
return name, real_obj
|
|
||||||
|
|
||||||
else:
|
|
||||||
def derive_obj_and_name(obj):
|
|
||||||
name = obj.__name__
|
|
||||||
real_obj = getattr(obj, "__self__", None)
|
|
||||||
if real_obj is None:
|
|
||||||
current = sys.modules[obj.__module__]
|
|
||||||
for name in obj.__qualname__.split("."):
|
|
||||||
real_obj = current
|
|
||||||
current = getattr(current, name)
|
|
||||||
assert getattr(real_obj, name) == obj, \
|
|
||||||
"could not derive object/name pair"
|
|
||||||
return name, real_obj
|
|
||||||
|
|
||||||
def derive_from_string(target):
|
|
||||||
rest = []
|
|
||||||
while target:
|
|
||||||
try:
|
|
||||||
obj = __import__(target, None, None, "__doc__")
|
|
||||||
except ImportError:
|
|
||||||
if "." not in target:
|
|
||||||
raise
|
|
||||||
target, name = target.rsplit(".", 1)
|
|
||||||
rest.append(name)
|
|
||||||
else:
|
|
||||||
assert len(rest) >= 1
|
|
||||||
while len(rest) != 1:
|
|
||||||
obj = getattr(obj, rest.pop())
|
|
||||||
return rest[0], obj
|
|
||||||
|
|
||||||
class monkeypatch:
|
class monkeypatch:
|
||||||
""" object keeping a record of setattr/item/env/syspath changes. """
|
""" object keeping a record of setattr/item/env/syspath changes. """
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -74,22 +34,45 @@ class monkeypatch:
|
||||||
self._setitem = []
|
self._setitem = []
|
||||||
self._cwd = None
|
self._cwd = None
|
||||||
|
|
||||||
def replace(self, target, value):
|
def replace(self, import_path, value):
|
||||||
""" derive monkeypatching location from ``target`` and call
|
""" replace the object specified by a dotted ``import_path``
|
||||||
setattr(derived_obj, derived_name, value).
|
with the given ``value``.
|
||||||
|
|
||||||
This function can usually derive monkeypatch locations
|
For example ``replace("os.path.abspath", value)`` will
|
||||||
for function, method or class targets. It also accepts
|
trigger an ``import os.path`` and a subsequent
|
||||||
a string which is taken as a python path which is then
|
setattr(os.path, "abspath", value). Or to prevent
|
||||||
tried to be imported. For example the target "os.path.abspath"
|
the requests library from performing requests you can call
|
||||||
will lead to a call to setattr(os.path, "abspath", value)
|
``replace("requests.sessions.Session.request", None)``
|
||||||
without the need to import "os.path" yourself.
|
which will lead to an import of ``requests.sessions`` and a call
|
||||||
|
to ``setattr(requests.sessions.Session, "request", None)``.
|
||||||
"""
|
"""
|
||||||
if isinstance(target, str):
|
if not isinstance(import_path, str) or "." not in import_path:
|
||||||
name, obj = derive_from_string(target)
|
raise TypeError("must be absolute import path string, not %r" %
|
||||||
else:
|
(import_path,))
|
||||||
name, obj = derive_obj_and_name(target)
|
rest = []
|
||||||
return self.setattr(obj, name, value)
|
target = import_path
|
||||||
|
while target:
|
||||||
|
try:
|
||||||
|
obj = __import__(target, None, None, "__doc__")
|
||||||
|
except ImportError:
|
||||||
|
if "." not in target:
|
||||||
|
__tracebackhide__ = True
|
||||||
|
pytest.fail("could not import any sub part: %s" %
|
||||||
|
import_path)
|
||||||
|
target, name = target.rsplit(".", 1)
|
||||||
|
rest.append(name)
|
||||||
|
else:
|
||||||
|
assert rest
|
||||||
|
try:
|
||||||
|
while len(rest) > 1:
|
||||||
|
attr = rest.pop()
|
||||||
|
obj = getattr(obj, attr)
|
||||||
|
attr = rest[0]
|
||||||
|
getattr(obj, attr)
|
||||||
|
except AttributeError:
|
||||||
|
__tracebackhide__ = True
|
||||||
|
pytest.fail("object %r has no attribute %r" % (obj, attr))
|
||||||
|
return self.setattr(obj, attr, value)
|
||||||
|
|
||||||
def setattr(self, obj, name, value, raising=True):
|
def setattr(self, obj, name, value, raising=True):
|
||||||
""" set attribute ``name`` on ``obj`` to ``value``, by default
|
""" set attribute ``name`` on ``obj`` to ``value``, by default
|
||||||
|
|
|
@ -29,7 +29,7 @@ patch this function before calling into a function which uses it::
|
||||||
def test_mytest(monkeypatch):
|
def test_mytest(monkeypatch):
|
||||||
def mockreturn(path):
|
def mockreturn(path):
|
||||||
return '/abc'
|
return '/abc'
|
||||||
monkeypatch.setattr(os.path., 'expanduser', mockreturn)
|
monkeypatch.setattr(os.path, 'expanduser', mockreturn)
|
||||||
x = getssh()
|
x = getssh()
|
||||||
assert x == '/abc/.ssh'
|
assert x == '/abc/.ssh'
|
||||||
|
|
||||||
|
@ -37,6 +37,23 @@ Here our test function monkeypatches ``os.path.expanduser`` and
|
||||||
then calls into an function that calls it. After the test function
|
then calls into an function that calls it. After the test function
|
||||||
finishes the ``os.path.expanduser`` modification will be undone.
|
finishes the ``os.path.expanduser`` modification will be undone.
|
||||||
|
|
||||||
|
example: preventing "requests" from remote operations
|
||||||
|
------------------------------------------------------
|
||||||
|
|
||||||
|
If you want to prevent the "requests" library from performing http
|
||||||
|
requests in all your tests, you can do::
|
||||||
|
|
||||||
|
# content of conftest.py
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def no_requests(monkeypatch):
|
||||||
|
monkeypatch.replace("requests.session.Session.request", None)
|
||||||
|
|
||||||
|
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,29 +35,7 @@ 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 TestDerived:
|
class TestReplace:
|
||||||
def f(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_class_function(self, monkeypatch):
|
|
||||||
monkeypatch.replace(TestDerived.f, lambda x: 42)
|
|
||||||
assert TestDerived().f() == 42
|
|
||||||
|
|
||||||
def test_instance_function(self, monkeypatch):
|
|
||||||
t = TestDerived()
|
|
||||||
monkeypatch.replace(t.f, lambda: 42)
|
|
||||||
assert t.f() == 42
|
|
||||||
|
|
||||||
def test_module_class(self, monkeypatch):
|
|
||||||
class New:
|
|
||||||
pass
|
|
||||||
monkeypatch.replace(TestDerived, New)
|
|
||||||
assert TestDerived == New
|
|
||||||
|
|
||||||
def test_nested_module(self, monkeypatch):
|
|
||||||
monkeypatch.replace(os.path.abspath, lambda x: "hello")
|
|
||||||
assert os.path.abspath("123") == "hello"
|
|
||||||
|
|
||||||
def test_string_expression(self, monkeypatch):
|
def test_string_expression(self, monkeypatch):
|
||||||
monkeypatch.replace("os.path.abspath", lambda x: "hello2")
|
monkeypatch.replace("os.path.abspath", lambda x: "hello2")
|
||||||
assert os.path.abspath("123") == "hello2"
|
assert os.path.abspath("123") == "hello2"
|
||||||
|
@ -67,6 +45,17 @@ class TestDerived:
|
||||||
import _pytest
|
import _pytest
|
||||||
assert _pytest.config.Config == 42
|
assert _pytest.config.Config == 42
|
||||||
|
|
||||||
|
def test_wrong_target(self, monkeypatch):
|
||||||
|
pytest.raises(TypeError, lambda: monkeypatch.replace(None, None))
|
||||||
|
|
||||||
|
def test_unknown_import(self, monkeypatch):
|
||||||
|
pytest.raises(pytest.fail.Exception,
|
||||||
|
lambda: monkeypatch.replace("unkn123.classx", None))
|
||||||
|
|
||||||
|
def test_unknown_attr(self, monkeypatch):
|
||||||
|
pytest.raises(pytest.fail.Exception,
|
||||||
|
lambda: monkeypatch.replace("os.path.qweqwe", None))
|
||||||
|
|
||||||
def test_delattr():
|
def test_delattr():
|
||||||
class A:
|
class A:
|
||||||
x = 1
|
x = 1
|
||||||
|
|
Loading…
Reference in New Issue