Skip to content

Commit

Permalink
MonkeyPatch staticmethod
Browse files Browse the repository at this point in the history
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
  • Loading branch information
alaski authored and rbtcollins committed Apr 7, 2016
1 parent bdf1455 commit 8c43eb9
Show file tree
Hide file tree
Showing 2 changed files with 22 additions and 8 deletions.
26 changes: 19 additions & 7 deletions fixtures/_fixtures/monkeypatch.py
Expand Up @@ -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 <class 'type'> 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."""

Expand Down Expand Up @@ -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."""
Expand Down
4 changes: 3 additions & 1 deletion fixtures/tests/_fixtures/test_monkeypatch.py
Expand Up @@ -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)

0 comments on commit 8c43eb9

Please sign in to comment.