1 new commit in pytest: https://bitbucket.org/hpk42/pytest/commits/7f5dba9095f4/ Changeset: 7f5dba9095f4 User: hpk42 Date: 2013-08-07 16:49:29 Summary: monkeypatch.replace() now only accepts a string. Improved error handling and docs thanks to suggestions from flub, pelme, schmir, ronny. Affected #: 4 files
diff -r b4cd6235587f2259678b618c1c5a429babe2a2c5 -r 7f5dba9095f4d77f06c46f19b4c7f4026e6182bd CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,15 +1,12 @@ 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") - - You can also avoid imports by specifying a python path string:: - - monkeypatch.replace("requests.get", ...) + will replace the "get" function of the "requests" module with ``myfunc``. - fix issue322: tearDownClass is not run if setUpClass failed. Thanks Mathieu Agopian for the initial fix. Also make all of pytest/nose finalizer diff -r b4cd6235587f2259678b618c1c5a429babe2a2c5 -r 7f5dba9095f4d77f06c46f19b4c7f4026e6182bd _pytest/monkeypatch.py --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -1,6 +1,7 @@ """ monkeypatching and mocking functionality. """ import os, sys, inspect +import pytest def pytest_funcarg__monkeypatch(request): """The returned ``monkeypatch`` funcarg provides these @@ -26,47 +27,6 @@ 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: """ object keeping a record of setattr/item/env/syspath changes. """ def __init__(self): @@ -74,22 +34,45 @@ self._setitem = [] self._cwd = None - def replace(self, target, value): - """ derive monkeypatching location from ``target`` and call - setattr(derived_obj, derived_name, value). + def replace(self, import_path, value): + """ replace the object specified by a dotted ``import_path`` + with the given ``value``. - This function can usually derive monkeypatch locations - for function, method or class targets. It also accepts - a string which is taken as a python path which is then - tried to be imported. For example the target "os.path.abspath" - will lead to a call to setattr(os.path, "abspath", value) - without the need to import "os.path" yourself. + 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 isinstance(target, str): - name, obj = derive_from_string(target) - else: - name, obj = derive_obj_and_name(target) - return self.setattr(obj, name, value) + if not isinstance(import_path, str) or "." not in import_path: + raise TypeError("must be absolute import path string, not %r" % + (import_path,)) + rest = [] + 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): """ set attribute ``name`` on ``obj`` to ``value``, by default diff -r b4cd6235587f2259678b618c1c5a429babe2a2c5 -r 7f5dba9095f4d77f06c46f19b4c7f4026e6182bd doc/en/monkeypatch.txt --- a/doc/en/monkeypatch.txt +++ b/doc/en/monkeypatch.txt @@ -29,7 +29,7 @@ def test_mytest(monkeypatch): def mockreturn(path): return '/abc' - monkeypatch.setattr(os.path., 'expanduser', mockreturn) + monkeypatch.setattr(os.path, 'expanduser', mockreturn) x = getssh() assert x == '/abc/.ssh' @@ -37,6 +37,23 @@ then calls into an function that calls it. After the test function 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 ----------------------------------------------------- diff -r b4cd6235587f2259678b618c1c5a429babe2a2c5 -r 7f5dba9095f4d77f06c46f19b4c7f4026e6182bd testing/test_monkeypatch.py --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -35,29 +35,7 @@ monkeypatch.undo() # double-undo makes no modification assert A.x == 5 -class TestDerived: - 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" - +class TestReplace: def test_string_expression(self, monkeypatch): monkeypatch.replace("os.path.abspath", lambda x: "hello2") assert os.path.abspath("123") == "hello2" @@ -67,6 +45,17 @@ import _pytest 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(): class A: x = 1 Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. _______________________________________________ pytest-commit mailing list pytest-commit@python.org http://mail.python.org/mailman/listinfo/pytest-commit