From 8c43eb9e04a35dc0cb19e4ed362da22bec6e3f0d Mon Sep 17 00:00:00 2001 From: Andrew Laski Date: Tue, 22 Mar 2016 15:29:46 -0400 Subject: [PATCH] MonkeyPatch staticmethod In Python setattr(Class, name, func) automatically converts a function into an instancemethod. To keep type(Class.func) as function, staticmethod(func) must be applied explicitly. This was previously fixed for Python 2 when cleaning up the patched function but Python 3 needs the same handling. When patching a function it was being converted to an instancemethod for both Python 2 and 3 and this has now been fixed. This is a breaking change as it was previously acceptable to patch a staticmethod with an instancemethod. The test for this case was updated to correctly check both cases. The patched function is called as both Class.function() and Class().function(), and then called again after the cleanup has occurred resetting the function to its original state. The Class().function() check is important because the method does not become bound until the class it is defined on is instantiated. Sem-Ver: api-break --- fixtures/_fixtures/monkeypatch.py | 26 ++++++++++++++------ fixtures/tests/_fixtures/test_monkeypatch.py | 4 ++- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/fixtures/_fixtures/monkeypatch.py b/fixtures/_fixtures/monkeypatch.py index 7db3671..4198b4f 100644 --- a/fixtures/_fixtures/monkeypatch.py +++ b/fixtures/_fixtures/monkeypatch.py @@ -23,6 +23,23 @@ from fixtures import Fixture +def _setattr(obj, name, value): + """Handle some corner cases when calling setattr. + + setattr transforms a function into instancemethod, so where appropriate + value needs to be wrapped with staticmethod(). + """ + if sys.version_info[0] == 2: + class_types = (type, types.ClassType) + else: + # All classes are in Python 3 + class_types = type + if (isinstance(obj, class_types) and + isinstance(value, types.FunctionType)): + value = staticmethod(value) + setattr(obj, name, value) + + class MonkeyPatch(Fixture): """Replace or delete an attribute.""" @@ -60,16 +77,11 @@ def _setUp(self): if old_value is not sentinel: delattr(current, attribute) else: - setattr(current, attribute, self.new_value) + _setattr(current, attribute, self.new_value) if old_value is sentinel: self.addCleanup(self._safe_delete, current, attribute) else: - # Python 2's setattr transforms function into instancemethod - if (sys.version_info[0] == 2 and - isinstance(current, (type, types.ClassType)) and - isinstance(old_value, types.FunctionType)): - old_value = staticmethod(old_value) - self.addCleanup(setattr, current, attribute, old_value) + self.addCleanup(_setattr, current, attribute, old_value) def _safe_delete(self, obj, attribute): """Delete obj.attribute handling the case where its missing.""" diff --git a/fixtures/tests/_fixtures/test_monkeypatch.py b/fixtures/tests/_fixtures/test_monkeypatch.py index 1a84d7f..81927c8 100644 --- a/fixtures/tests/_fixtures/test_monkeypatch.py +++ b/fixtures/tests/_fixtures/test_monkeypatch.py @@ -78,6 +78,8 @@ def test_patch_staticmethod(self): 'fixtures.tests._fixtures.test_monkeypatch.C.foo', bar) with fixture: - pass + C.foo() + C().foo() self.assertEqual(oldfoo, C.foo) + self.assertEqual(oldfoo, C().foo)