diff --git a/CHANGELOG b/CHANGELOG index 9bff7bcbf..a2e00617e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,16 @@ Changes between 2.3.5 and 2.4.DEV ----------------------------------- +- new monkeypatch.replace() to allow for more direct patching:: + + monkeypatch.replace(os.path.abspath, lambda x: "mocked") + + 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", ...) + - fix issue322: tearDownClass is not run if setUpClass failed. Thanks Mathieu Agopian for the initial fix. Also make all of pytest/nose finalizer mimick the same generic behaviour: if a setupX exists and fails, @@ -98,6 +108,7 @@ Changes between 2.3.5 and 2.4.DEV - better parametrize error messages, thanks Brianna Laugher + known incompatibilities: - if calling --genscript from python2.7 or above, you only get a diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index 292e4f76e..3db03fe29 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -26,6 +26,47 @@ def pytest_funcarg__monkeypatch(request): 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): @@ -33,9 +74,28 @@ class monkeypatch: self._setitem = [] self._cwd = None + def replace(self, target, value): + """ derive monkeypatching location from ``target`` and call + setattr(derived_obj, derived_name, 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. + """ + if isinstance(target, str): + name, obj = derive_from_string(target) + else: + name, obj = derive_obj_and_name(target) + return self.setattr(obj, name, value) + 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. """ + raise AttributeEror if the attribute did not exist. + + """ oldval = getattr(obj, name, notset) if raising and oldval is notset: raise AttributeError("%r has no attribute %r" %(obj, name)) diff --git a/doc/en/monkeypatch.txt b/doc/en/monkeypatch.txt index 74c63b850..78fec4155 100644 --- a/doc/en/monkeypatch.txt +++ b/doc/en/monkeypatch.txt @@ -29,7 +29,7 @@ patch this function before calling into a function which uses it:: 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' @@ -41,7 +41,7 @@ Method reference of the monkeypatch function argument ----------------------------------------------------- .. autoclass:: monkeypatch - :members: setattr, delattr, setitem, delitem, setenv, delenv, syspath_prepend, chdir, undo + :members: setattr, replace, delattr, setitem, delitem, setenv, delenv, syspath_prepend, chdir, undo ``monkeypatch.setattr/delattr/delitem/delenv()`` all by default raise an Exception if the target does not exist. diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 7a6150d0e..b032f6f7a 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -35,6 +35,38 @@ def test_setattr(): 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" + + def test_string_expression(self, monkeypatch): + monkeypatch.replace("os.path.abspath", lambda x: "hello2") + assert os.path.abspath("123") == "hello2" + + def test_string_expression_class(self, monkeypatch): + monkeypatch.replace("_pytest.config.Config", 42) + import _pytest + assert _pytest.config.Config == 42 + def test_delattr(): class A: x = 1