diff options
author | Andrew Laski <andrew@lascii.com> | 2016-04-07 14:26:45 -0400 |
---|---|---|
committer | Robert Collins <robertc@robertcollins.net> | 2016-05-19 11:51:19 +1200 |
commit | 78527ff5ab5452b5d91c361d040f3f31469b37ec (patch) | |
tree | 9a5700e5822705c113cea62af0d3f226d86e7efd /fixtures/tests | |
parent | 8d7bde077d26c8baeceb634dce409f854fb26d87 (diff) | |
download | fixtures-git-78527ff5ab5452b5d91c361d040f3f31469b37ec.tar.gz |
Update the semantics on _fixtures.MonkeyPatch
A previous change added some logic so that when monkeypatching a
staticmethod the old_value was restored as a staticmethod. There was an
issue where the determination of whether or not a method should be
static was incorrectly checking the new function not the one to be
replaced. That caused an unintended side-effect that in order to patch
an instance method of a class the new function needed to be an instance
method of a class.
This change now reworks the semantics of MonkeyPatch to address that
issue and at the same time be explicit about how it should work in a
large number of different cases. The rule is simple and provides great
flexibility. Given a callable bar to be patched on to foo bar will be
called with any bound arguments first and then arguments of foo
appended. This is easier to visualize. Given:
class C(object):
@classmethod
def foo(cls, arg):
pass
class D(object):
@classmethod
def bar(cls, tgtcls, arg):
pass
def baz(cls, arg):
pass
MonkeyPatch('...C.foo', D.bar) will result in C.foo(1) calling bar like
bar(D, C, 1) because cls on bar was already bound to D when patching.
And MonkeyPatch('...C.foo', baz) will result in baz being called with
baz(C, 1).
Diffstat (limited to 'fixtures/tests')
-rw-r--r-- | fixtures/tests/_fixtures/test_monkeypatch.py | 373 |
1 files changed, 365 insertions, 8 deletions
diff --git a/fixtures/tests/_fixtures/test_monkeypatch.py b/fixtures/tests/_fixtures/test_monkeypatch.py index 81927c8..d1e0af7 100644 --- a/fixtures/tests/_fixtures/test_monkeypatch.py +++ b/fixtures/tests/_fixtures/test_monkeypatch.py @@ -13,6 +13,8 @@ # license you chose for the specific language governing permissions and # limitations under that license. +import functools + import testtools from fixtures import MonkeyPatch, TestWithFixtures @@ -20,9 +22,42 @@ from fixtures import MonkeyPatch, TestWithFixtures reference = 23 class C(object): + def foo(self, arg): + return arg + @staticmethod + def foo_static(): pass + @classmethod + def foo_cls(cls): pass + +class D(object): + def bar(self): pass + def bar_two_args(self, arg): + return (self, arg) + @classmethod + def bar_cls(cls): + return cls + @classmethod + def bar_cls_args(cls, *args): + return tuple([cls] + [arg for arg in args]) + @staticmethod + def bar_static(): + pass @staticmethod - def foo(): pass -def bar(): pass + def bar_static_args(*args): + return args + def bar_self_referential(self, *args, **kwargs): + self.bar() + +INST_C = C() + +def fake(*args): + return args +def fake2(arg): pass +def fake_no_args(): pass +def fake_no_args2(): pass +@staticmethod +def fake_static(): pass + class TestMonkeyPatch(testtools.TestCase, TestWithFixtures): @@ -72,14 +107,336 @@ class TestMonkeyPatch(testtools.TestCase, TestWithFixtures): fixture.cleanUp() self.assertFalse('new_attr' in globals()) - def test_patch_staticmethod(self): - oldfoo = C.foo + def _check_restored_static_or_class_method(self, oldmethod, oldmethod_inst, + klass, methodname): + restored_method = getattr(klass, methodname) + restored_method_inst = getattr(klass(), methodname) + self.assertEqual(oldmethod, restored_method) + self.assertEqual(oldmethod, restored_method_inst) + self.assertEqual(oldmethod_inst, restored_method) + self.assertEqual(oldmethod_inst, restored_method_inst) + restored_method() + restored_method_inst() + + def test_patch_staticmethod_with_staticmethod(self): + oldmethod = C.foo_static + oldmethod_inst = C().foo_static + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo_static', + D.bar_static) + with fixture: + C.foo_static() + C().foo_static() + + self._check_restored_static_or_class_method(oldmethod, oldmethod_inst, + C, 'foo_static') + + def test_patch_staticmethod_with_classmethod(self): + oldmethod = C.foo_static + oldmethod_inst = C().foo_static + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo_static', + D.bar_cls) + with fixture: + C.foo_static() + C().foo_static() + + self._check_restored_static_or_class_method(oldmethod, oldmethod_inst, + C, 'foo_static') + + def test_patch_staticmethod_with_function(self): + oldmethod = C.foo_static + oldmethod_inst = C().foo_static + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo_static', + fake_no_args) + with fixture: + C.foo_static() + C().foo_static() + + self._check_restored_static_or_class_method(oldmethod, oldmethod_inst, + C, 'foo_static') + + def test_patch_staticmethod_with_boundmethod(self): + oldmethod = C.foo_static + oldmethod_inst = C().foo_static + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo_static', + D().bar) + with fixture: + C.foo_static() + C().foo_static() + + self._check_restored_static_or_class_method(oldmethod, oldmethod_inst, + C, 'foo_static') + + def test_patch_classmethod_with_staticmethod(self): + oldmethod = C.foo_cls + oldmethod_inst = C().foo_cls + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo_cls', + D.bar_static_args) + with fixture: + (cls,) = C.foo_cls() + self.assertTrue(issubclass(cls, C)) + (cls,) = C().foo_cls() + self.assertTrue(issubclass(cls, C)) + + self._check_restored_static_or_class_method(oldmethod, oldmethod_inst, + C, 'foo_cls') + + def test_patch_classmethod_with_classmethod(self): + oldmethod = C.foo_cls + oldmethod_inst = C().foo_cls + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo_cls', + D.bar_cls_args) + with fixture: + cls, tgtcls = C.foo_cls() + self.assertTrue(issubclass(cls, D)) + self.assertTrue(issubclass(tgtcls, C)) + cls, tgtcls = C().foo_cls() + self.assertTrue(issubclass(cls, D)) + self.assertTrue(issubclass(tgtcls, C)) + + self._check_restored_static_or_class_method(oldmethod, oldmethod_inst, + C, 'foo_cls') + + def test_patch_classmethod_with_function(self): + oldmethod = C.foo_cls + oldmethod_inst = C().foo_cls + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo_cls', + fake) + with fixture: + (cls,) = C.foo_cls() + self.assertTrue(issubclass(cls, C)) + (cls, arg) = C().foo_cls(1) + self.assertTrue(issubclass(cls, C)) + self.assertEqual(1, arg) + + self._check_restored_static_or_class_method(oldmethod, oldmethod_inst, + C, 'foo_cls') + + def test_patch_classmethod_with_boundmethod(self): + oldmethod = C.foo_cls + oldmethod_inst = C().foo_cls + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo_cls', + D().bar_two_args) + with fixture: + slf, cls = C.foo_cls() + self.assertTrue(isinstance(slf, D)) + self.assertTrue(issubclass(cls, C)) + slf, cls = C().foo_cls() + self.assertTrue(isinstance(slf, D)) + self.assertTrue(issubclass(cls, C)) + + self._check_restored_static_or_class_method(oldmethod, oldmethod_inst, + C, 'foo_cls') + + def test_patch_function_with_staticmethod(self): + oldmethod = fake_no_args + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.fake_no_args', + D.bar_static) + with fixture: + fake_no_args() + + self.assertEqual(oldmethod, fake_no_args) + + def test_patch_function_with_classmethod(self): + oldmethod = fake_no_args + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.fake_no_args', + D.bar_cls) + with fixture: + fake_no_args() + + self.assertEqual(oldmethod, fake_no_args) + + def test_patch_function_with_function(self): + oldmethod = fake_no_args + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.fake_no_args', + fake_no_args2) + with fixture: + fake_no_args() + + self.assertEqual(oldmethod, fake_no_args) + + def test_patch_function_with_partial(self): + oldmethod = fake_no_args + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.fake_no_args', + functools.partial(fake, 1)) + with fixture: + (ret,) = fake_no_args() + self.assertEqual(1, ret) + + self.assertEqual(oldmethod, fake_no_args) + + def test_patch_function_with_boundmethod(self): + oldmethod = fake_no_args + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.fake_no_args', + D().bar) + with fixture: + fake_no_args() + + self.assertEqual(oldmethod, fake_no_args) + + def test_patch_boundmethod_with_staticmethod(self): + oldmethod = INST_C.foo + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.INST_C.foo', + D.bar_static) + with fixture: + INST_C.foo() + + self.assertEqual(oldmethod, INST_C.foo) + + def test_patch_boundmethod_with_classmethod(self): + oldmethod = INST_C.foo + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.INST_C.foo', + D.bar_cls) + with fixture: + INST_C.foo() + + self.assertEqual(oldmethod, INST_C.foo) + + def test_patch_boundmethod_with_function(self): + oldmethod = INST_C.foo + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.INST_C.foo', + fake_no_args) + with fixture: + INST_C.foo() + + self.assertEqual(oldmethod, INST_C.foo) + + def test_patch_boundmethod_with_boundmethod(self): + oldmethod = INST_C.foo + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.INST_C.foo', + D().bar) + with fixture: + INST_C.foo() + + self.assertEqual(oldmethod, INST_C.foo) + + def test_patch_unboundmethod_with_staticmethod(self): + oldmethod = C.foo + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo', + D.bar_static_args) + with fixture: + tgtslf, arg = C().foo(1) + self.assertTrue(isinstance(tgtslf, C)) + self.assertEqual(1, arg) + + self.assertEqual(oldmethod, C.foo) + + def test_patch_unboundmethod_with_classmethod(self): + oldmethod = C.foo + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo', + D.bar_cls_args) + with fixture: + cls, tgtslf, arg = C().foo(1) + self.assertTrue(issubclass(cls, D)) + self.assertTrue(isinstance(tgtslf, C)) + self.assertEqual(1, arg) + + self.assertEqual(oldmethod, C.foo) + + def test_patch_unboundmethod_with_function(self): + oldmethod = C.foo + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo', + fake) + with fixture: + tgtslf, arg = C().foo(1) + self.assertTrue(isinstance(tgtslf, C)) + self.assertTrue(1, arg) + + self.assertEqual(oldmethod, C.foo) + + def test_patch_unboundmethod_with_boundmethod(self): + oldmethod = C.foo + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo', + D().bar_two_args) + with fixture: + slf, tgtslf = C().foo() + self.assertTrue(isinstance(slf, D)) + self.assertTrue(isinstance(tgtslf, C)) + + self.assertEqual(oldmethod, C.foo) + + def test_double_patch_instancemethod(self): + oldmethod = C.foo + oldmethod_inst = C().foo + fixture1 = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo', + fake) + fixture2 = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo', + fake2) + with fixture1: + with fixture2: + C().foo() + + self.assertEqual(oldmethod, C.foo) + # The method address changes with each instantiation of C, and method + # equivalence just tests that. Compare the code objects instead. + self.assertEqual(oldmethod_inst.__code__, C().foo.__code__) + + def test_double_patch_staticmethod(self): + oldmethod = C.foo_static + oldmethod_inst = C().foo_static + fixture1 = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo_static', + fake_no_args) + fixture2 = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo_static', + fake_static) + with fixture1: + with fixture2: + C.foo_static() + C().foo_static() + + self._check_restored_static_or_class_method(oldmethod, oldmethod_inst, + C, 'foo_static') + + def test_double_patch_classmethod(self): + oldmethod = C.foo_cls + oldmethod_inst = C().foo_cls + fixture1 = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo_cls', + fake) + fixture2 = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo_cls', + fake2) + with fixture1: + with fixture2: + C.foo_cls() + C().foo_cls() + + self._check_restored_static_or_class_method(oldmethod, oldmethod_inst, + C, 'foo_cls') + + def test_patch_c_foo_with_instance_d_bar_self_referential(self): + oldmethod = C.foo + oldmethod_inst = C().foo fixture = MonkeyPatch( 'fixtures.tests._fixtures.test_monkeypatch.C.foo', - bar) + D().bar_self_referential) with fixture: - C.foo() C().foo() - self.assertEqual(oldfoo, C.foo) - self.assertEqual(oldfoo, C().foo) + self.assertEqual(oldmethod, C.foo) + # The method address changes with each instantiation of C, and method + # equivalence just tests that. Compare the code objects instead. + self.assertEqual(oldmethod_inst.__code__, C().foo.__code__) |