0001
0002
0003
0004
0005
0006
0007
0008"""Tools for replacing objects with stubs.
0009
0010There is not very much here yet because Python allows you to do a lot of stubbing
0011naturally. The `Stub` class provides a few shortcuts though.
0012
0013"""
0014
0015from copy import copy
0016import weakref
0017from types import *
0018
0019class NoValue: pass
0020
0021class Stub(object):
0022 """Replace an object with a stub object
0023
0024 Let's face it, creating stub objects is dead simple in Python.
0025 For example, a simple way to create a stub of foo is :
0026
0027 >>> def foo():
0028 ... return 'foo'
0029 >>> def newfoo():
0030 ... return 'bar'
0031 >>> oldfoo = foo
0032 >>> try:
0033 ... foo = newfoo
0034 ... foo()
0035 ... finally:
0036 ... foo = oldfoo
0037 'bar'
0038
0039 With `Stub`, this is still possible, like so, and it doesn't save you much coding either:
0040
0041 >>> def foo():
0042 ... return 'foo'
0043 >>> def newfoo():
0044 ... return 'bar'
0045 >>> try:
0046 ... foo = Stub(foo, replace=newfoo)
0047 ... foo()
0048 ... finally:
0049 ... foo = foo.restore()
0050 'bar'
0051
0052 However, you can do more complex things too, like mimic sequential calls:
0053
0054 >>> def next_ingredient():
0055 ... for i in ('eggs', 'flour'):
0056 ... yield i
0057 >>> orig_id = id(next_ingredient)
0058 >>> next_ingredient = Stub(next_ingredient)
0059 >>> def yeast():
0060 ... return 'yeast'
0061 >>> def butter():
0062 ... return 'butter'
0063 >>> try:
0064 ... next_ingredient.replace([yeast, butter])
0065 ... assert next_ingredient() == 'yeast'
0066 ... assert next_ingredient() == 'butter'
0067 ... next_ingredient()
0068 ... finally:
0069 ... next_ingredient = next_ingredient.restore()
0070 Traceback (most recent call last):
0071 ...
0072 StopIteration
0073 >>> assert id(next_ingredient) == orig_id
0074
0075 To accomplish this without `Stub` you would have to proxy a generator,
0076 which would be just a bit more coding.
0077
0078 In closing ... you probably don't need stub for simple stubbing but it
0079 might come in handy. The only reason I added it to testtools is because I had
0080 some tests that needed urlopen() to send a sequence of responses back in a test.
0081
0082 """
0083 class IterableCallProxy:
0084 def __init__(self, calls, getcall=None):
0085 self.calls = calls
0086 if getcall is None:
0087 getcall = iter(calls)
0088 self.getcall = getcall
0089
0090 def __call__(self, *args, **kw):
0091 call = self.getcall.next()
0092 return call(*args, **kw)
0093
0094 def __init__(self, fn, replace=NoValue):
0095 self.original = fn
0096 def nocall(*args, **kw):
0097 raise ValueError, "no stub call has been set for fn %s" % self.original
0098 self.call_proxy = nocall
0099
0100 if replace is not NoValue:
0101 self.replace(replace)
0102
0103 def __call__(self, *args, **kw):
0104 return self.call_proxy(*args, **kw)
0105
0106 def replace(self, new):
0107 """Replace stub with an iterable of calls or a single callable"""
0108 try:
0109 getcall = iter(new)
0110 except TypeError:
0111 if not callable(new):
0112 raise TypeError, "call replacement is not iterable and is not callable"
0113 self.call_proxy = new
0114 else:
0115 self.call_proxy = Stub.IterableCallProxy(new, getcall=getcall)
0116
0117 def restore(self):
0118 """returns original object for id"""
0119 return self.original
0120
0121
0122class stub_template:
0123 class _class:
0124 def __init__(*a,**kw): pass
0125 class _object(object):
0126 def __init__(*a,**kw): pass
0127 def _def_unbound(self, *a,**kw): pass
0128
0129
0130 _def_unbound = staticmethod(_def_unbound)
0131
0132def _def(*a,**kw): pass
0133stub_template._def = _def
0134
0135def mkinterface(cls, template=stub_template):
0136 """make a class having the same interface as cls.
0137
0138 every method accepts any input but does nothing.
0139 returns a class object.
0140
0141 """
0142 if issubclass(cls, object):
0143 newcls = template._object
0144 else:
0145 newcls = template._class
0146 for name in dir(cls):
0147 if name.startswith('__'):
0148
0149 continue
0150
0151 attr = getattr(cls,name)
0152 atype = type(attr)
0153
0154
0155
0156 if atype in (FunctionType, LambdaType, GeneratorType):
0157 setattr(newcls, name, template._def)
0158 elif atype == UnboundMethodType:
0159 setattr(newcls, name, template._def_unbound)
0160 elif atype == ClassType:
0161 setattr(newcls, name, template._class)
0162 elif atype in (StringType, IntType, LongType, FloatType, BooleanType):
0163
0164 setattr(newcls, name, attr)
0165 elif atype in (TypeType, ObjectType):
0166 setattr(newcls, name, template._object)
0167 else:
0168
0169 setattr(newcls, name, None)
0170
0171 return newcls