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:
holger krekel 2013-08-07 16:49:29 +02:00
parent 407283ef81
commit 4b88d6d2d7
4 changed files with 72 additions and 86 deletions

View File

@ -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

View File

@ -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

View File

@ -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
----------------------------------------------------- -----------------------------------------------------

View File

@ -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