Fixup the MonkeyPatch patch.
- docs. - cleanup patches of boundmethods to not leave cruft behind. - NEWS entry. Sem-Ver: api-break
This commit is contained in:
parent
76a127d8b3
commit
7e779dab35
4
NEWS
4
NEWS
|
@ -6,6 +6,10 @@ fixtures release notes
|
|||
NEXT
|
||||
~~~~
|
||||
|
||||
* Monkeypatching of callables has been thoroughly overhauled: there were deep
|
||||
flaws in usability because staticmethod and classmethods are actually
|
||||
descriptors. (Andrew Laski, Robert Collins)
|
||||
|
||||
2.0
|
||||
~~~
|
||||
|
||||
|
|
3
README
3
README
|
@ -404,6 +404,9 @@ Control the value of a named python attribute.
|
|||
... pass
|
||||
>>> fixture = fixtures.MonkeyPatch('__builtin__.open', fake_open)
|
||||
|
||||
Note that there are some complexities when patching methods - please see the
|
||||
API documentation for details.
|
||||
|
||||
NestedTempfile
|
||||
++++++++++++++
|
||||
|
||||
|
|
|
@ -17,31 +17,48 @@ __all__ = [
|
|||
'MonkeyPatch'
|
||||
]
|
||||
|
||||
import functools
|
||||
import sys
|
||||
import types
|
||||
|
||||
from fixtures import Fixture
|
||||
|
||||
|
||||
def _coerce_values(obj, name, new_value, sentinel):
|
||||
"""Handle value coercion for static and classmethods.
|
||||
_class_types = (type, )
|
||||
if getattr(types, 'ClassType', None):
|
||||
# Python 2 has multiple types of classes.
|
||||
_class_types = _class_types + (types.ClassType,)
|
||||
|
||||
|
||||
def _coerce_values(obj, name, new_value, sentinel):
|
||||
"""Return an adapted (new_value, old_value) for patching obj.name.
|
||||
|
||||
setattr transforms a function into an instancemethod when set on a class.
|
||||
This checks if the attribute to be replaced is either and wraps new_value
|
||||
if necessary. This also checks getattr(obj, name) and wraps it if necessary
|
||||
This checks if the attribute to be replaced is a callable descriptor -
|
||||
staticmethod, classmethod, or types.FunctionType - and wraps new_value if
|
||||
necessary.
|
||||
|
||||
This also checks getattr(obj, name) and wraps it if necessary
|
||||
since the staticmethod wrapper isn't preserved.
|
||||
|
||||
:param obj: The object with an attribute being patched.
|
||||
:param name: The name of the attribute being patched.
|
||||
:param new_value: The new value to be assigned.
|
||||
:param sentinel: If no old_value existed, the sentinel is returned to
|
||||
indicate that.
|
||||
"""
|
||||
old_value = getattr(obj, name, sentinel)
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
class_types = (type, types.ClassType)
|
||||
else:
|
||||
# All classes are <class 'type'> in Python 3
|
||||
class_types = type
|
||||
|
||||
if not isinstance(obj, class_types):
|
||||
# Nothing special to do here
|
||||
return (new_value, old_value)
|
||||
if not isinstance(obj, _class_types):
|
||||
# We may be dealing with an instance of a class. In that case the
|
||||
# attribute may be the result of a descriptor lookup (or a __getattr__
|
||||
# override etc). Its incomplete, but generally good enough to just
|
||||
# look and see if name is in the instance dict.
|
||||
try:
|
||||
obj.__dict__[name]
|
||||
except (AttributeError, KeyError):
|
||||
return (new_value, sentinel)
|
||||
else:
|
||||
return (new_value, old_value)
|
||||
|
||||
# getattr() returns a function, this access pattern will return a
|
||||
# staticmethod/classmethod if the name method is defined that way
|
||||
|
@ -49,24 +66,28 @@ def _coerce_values(obj, name, new_value, sentinel):
|
|||
if old_attribute is not None:
|
||||
old_value = old_attribute
|
||||
|
||||
# If new_value is not callable it is potentially wrapped with staticmethod
|
||||
# or classmethod so grab the underlying function. If it has no underlying
|
||||
# callable thing the following coercion can be skipped, just return.
|
||||
# If new_value is not callable, no special handling is needed.
|
||||
# (well, technically the same descriptor issue can happen with
|
||||
# user supplied descriptors, but that is arguably a feature - someone can
|
||||
# deliberately install a different descriptor.
|
||||
if not callable(new_value):
|
||||
if hasattr(new_value, '__func__'):
|
||||
new_value = new_value.__func__
|
||||
else:
|
||||
return (new_value, old_value)
|
||||
return (new_value, old_value)
|
||||
|
||||
if isinstance(old_value, staticmethod):
|
||||
new_value = staticmethod(new_value)
|
||||
if isinstance(old_value, classmethod):
|
||||
elif isinstance(old_value, classmethod):
|
||||
new_value = classmethod(new_value)
|
||||
if isinstance(old_value, types.FunctionType):
|
||||
captured_method = new_value
|
||||
def func_wrapper(*args, **kwargs):
|
||||
return captured_method(*args, **kwargs)
|
||||
new_value = func_wrapper
|
||||
elif isinstance(old_value, types.FunctionType):
|
||||
if hasattr(new_value, '__get__'):
|
||||
# new_value is a descriptor, and that would result in it being
|
||||
# rebound if we assign it to a class - we want to preserve the
|
||||
# bound state rather than having it bound to the new object
|
||||
# it has been patched onto.
|
||||
captured_method = new_value
|
||||
@functools.wraps(old_value)
|
||||
def avoid_get(*args, **kwargs):
|
||||
return captured_method(*args, **kwargs)
|
||||
new_value = avoid_get
|
||||
|
||||
return (new_value, old_value)
|
||||
|
||||
|
@ -85,6 +106,26 @@ class MonkeyPatch(Fixture):
|
|||
|
||||
During setup the name will be deleted or assigned the requested value,
|
||||
and this will be restored in cleanUp.
|
||||
|
||||
When patching methods, the call signature of name should be a subset
|
||||
of the parameters which can be used to call new_value.
|
||||
|
||||
For instance.
|
||||
|
||||
>>> class T:
|
||||
... def method(self, arg1):
|
||||
... pass
|
||||
>>> class N:
|
||||
... @staticmethod
|
||||
... def newmethod(arg1):
|
||||
... pass
|
||||
|
||||
Patching N.newmethod on top of T.method and then calling T().method(1)
|
||||
will not work because they do not have compatible call signatures -
|
||||
self will be passed to newmethod because the callable (N.newmethod)
|
||||
is placed onto T as a regular function. This allows capturing all the
|
||||
supplied parameters while still consulting local state in your
|
||||
new_value.
|
||||
"""
|
||||
Fixture.__init__(self)
|
||||
self.name = name
|
||||
|
|
|
@ -188,12 +188,12 @@ class TestMonkeyPatch(testtools.TestCase, TestWithFixtures):
|
|||
'fixtures.tests._fixtures.test_monkeypatch.C.foo_cls',
|
||||
D.bar_cls_args)
|
||||
with fixture:
|
||||
cls, tgtcls = C.foo_cls()
|
||||
cls, target_class = C.foo_cls()
|
||||
self.expectThat(cls, Is(D))
|
||||
self.expectThat(tgtcls, Is(C))
|
||||
cls, tgtcls = C().foo_cls()
|
||||
self.expectThat(target_class, Is(C))
|
||||
cls, target_class = C().foo_cls()
|
||||
self.expectThat(cls, Is(D))
|
||||
self.expectThat(tgtcls, Is(C))
|
||||
self.expectThat(target_class, Is(C))
|
||||
self._check_restored_static_or_class_method(oldmethod, oldmethod_inst,
|
||||
C, 'foo_cls')
|
||||
|
||||
|
@ -310,6 +310,8 @@ class TestMonkeyPatch(testtools.TestCase, TestWithFixtures):
|
|||
with fixture:
|
||||
INST_C.foo()
|
||||
self.assertEqual(oldmethod, INST_C.foo)
|
||||
sentinel = object()
|
||||
self.assertEqual(sentinel, INST_C.__dict__.get('foo', sentinel))
|
||||
|
||||
def test_patch_unboundmethod_with_staticmethod(self):
|
||||
oldmethod = C.foo
|
||||
|
@ -317,9 +319,8 @@ class TestMonkeyPatch(testtools.TestCase, TestWithFixtures):
|
|||
'fixtures.tests._fixtures.test_monkeypatch.C.foo',
|
||||
D.bar_static_args)
|
||||
with fixture:
|
||||
c = C()
|
||||
tgtslf, arg = c.foo(1)
|
||||
self.expectThat(tgtslf, Is(c))
|
||||
target_self, arg = INST_C.foo(1)
|
||||
self.expectThat(target_self, Is(INST_C))
|
||||
self.assertEqual(1, arg)
|
||||
self.assertEqual(oldmethod, C.foo)
|
||||
|
||||
|
@ -330,9 +331,9 @@ class TestMonkeyPatch(testtools.TestCase, TestWithFixtures):
|
|||
D.bar_cls_args)
|
||||
with fixture:
|
||||
c = C()
|
||||
cls, tgtslf, arg = c.foo(1)
|
||||
cls, target_self, arg = c.foo(1)
|
||||
self.expectThat(cls, Is(D))
|
||||
self.expectThat(tgtslf, Is(c))
|
||||
self.expectThat(target_self, Is(c))
|
||||
self.assertEqual(1, arg)
|
||||
self.assertEqual(oldmethod, C.foo)
|
||||
|
||||
|
@ -343,8 +344,8 @@ class TestMonkeyPatch(testtools.TestCase, TestWithFixtures):
|
|||
fake)
|
||||
with fixture:
|
||||
c = C()
|
||||
tgtslf, arg = c.foo(1)
|
||||
self.expectThat(tgtslf, Is(c))
|
||||
target_self, arg = c.foo(1)
|
||||
self.expectThat(target_self, Is(c))
|
||||
self.assertTrue(1, arg)
|
||||
self.assertEqual(oldmethod, C.foo)
|
||||
|
||||
|
@ -356,9 +357,9 @@ class TestMonkeyPatch(testtools.TestCase, TestWithFixtures):
|
|||
d.bar_two_args)
|
||||
with fixture:
|
||||
c = C()
|
||||
slf, tgtslf = c.foo()
|
||||
slf, target_self = c.foo()
|
||||
self.expectThat(slf, Is(d))
|
||||
self.expectThat(tgtslf, Is(c))
|
||||
self.expectThat(target_self, Is(c))
|
||||
self.assertEqual(oldmethod, C.foo)
|
||||
|
||||
def test_double_patch_instancemethod(self):
|
||||
|
|
Loading…
Reference in New Issue