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
0011stubbing naturally. 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
0040 coding either:
0041
0042 >>> def foo():
0043 ... return 'foo'
0044 >>> def newfoo():
0045 ... return 'bar'
0046 >>> try:
0047 ... foo = Stub(foo, replace=newfoo)
0048 ... foo()
0049 ... finally:
0050 ... foo = foo.restore()
0051 'bar'
0052
0053 However, you can do more complex things too, like mimic sequential calls:
0054
0055 >>> def next_ingredient():
0056 ... for i in ('eggs', 'flour'):
0057 ... yield i
0058 >>> orig_id = id(next_ingredient)
0059 >>> next_ingredient = Stub(next_ingredient)
0060 >>> def yeast():
0061 ... return 'yeast'
0062 >>> def butter():
0063 ... return 'butter'
0064 >>> try:
0065 ... next_ingredient.replace([yeast, butter])
0066 ... assert next_ingredient() == 'yeast'
0067 ... assert next_ingredient() == 'butter'
0068 ... next_ingredient()
0069 ... finally:
0070 ... next_ingredient = next_ingredient.restore()
0071 Traceback (most recent call last):
0072 ...
0073 StopIteration
0074 >>> assert id(next_ingredient) == orig_id
0075
0076 To accomplish this without `Stub` you would have to proxy a generator,
0077 which would be just a bit more coding.
0078
0079 In closing ... you probably don't need Stub for simple stubbing but it
0080 might come in handy. The only reason I added it to testtools is because I
0081 had some tests that needed urlopen() to send a sequence of responses back
0082 in a test.
0083
0084 """
0085 class IterableCallProxy:
0086 def __init__(self, calls, getcall=None):
0087 self.calls = calls
0088 if getcall is None:
0089 getcall = iter(calls)
0090 self.getcall = getcall
0091
0092 def __call__(self, *args, **kw):
0093 call = self.getcall.next()
0094 return call(*args, **kw)
0095
0096 def __init__(self, fn, replace=NoValue):
0097 self.original = fn
0098 def nocall(*args, **kw):
0099 raise ValueError, (
0100 "no stub call has been set for fn %s" % self.original)
0101 self.call_proxy = nocall
0102
0103 if replace is not NoValue:
0104 self.replace(replace)
0105
0106 def __call__(self, *args, **kw):
0107 return self.call_proxy(*args, **kw)
0108
0109 def replace(self, new):
0110 """Replace stub with an iterable of calls or a single callable"""
0111 try:
0112 getcall = iter(new)
0113 except TypeError:
0114 if not callable(new):
0115 raise TypeError, (
0116 "call replacement is not iterable and is not callable")
0117 self.call_proxy = new
0118 else:
0119 self.call_proxy = Stub.IterableCallProxy(new, getcall=getcall)
0120
0121 def restore(self):
0122 """returns original object for id"""
0123 return self.original
0124
0125
0126class stub_template:
0127 class _class:
0128 def __init__(*a,**kw): pass
0129 class _object(object):
0130 def __init__(*a,**kw): pass
0131 def _def_unbound(self, *a,**kw): pass
0132
0133
0134 _def_unbound = staticmethod(_def_unbound)
0135
0136def _def(*a,**kw): pass
0137stub_template._def = _def
0138
0139def mkinterface(cls, template=stub_template):
0140 """make a class having the same interface as cls.
0141
0142 every method accepts any input but does nothing.
0143 returns a class object.
0144
0145 """
0146 if issubclass(cls, object):
0147 newcls = template._object
0148 else:
0149 newcls = template._class
0150 for name in dir(cls):
0151 if name.startswith('__'):
0152
0153 continue
0154
0155 attr = getattr(cls,name)
0156 atype = type(attr)
0157
0158
0159
0160 if atype in (FunctionType, LambdaType, GeneratorType):
0161 setattr(newcls, name, template._def)
0162 elif atype == UnboundMethodType:
0163 setattr(newcls, name, template._def_unbound)
0164 elif atype == ClassType:
0165 setattr(newcls, name, template._class)
0166 elif atype in (StringType, IntType, LongType, FloatType, BooleanType):
0167
0168 setattr(newcls, name, attr)
0169 elif atype in (TypeType, ObjectType):
0170 setattr(newcls, name, template._object)
0171 else:
0172
0173 setattr(newcls, name, None)
0174
0175 newcls.__name__ = cls.__name__
0176 return newcls