0001# Copyright (C) 2006 Kumar McMillan
0002
0003# This library is free software; you can redistribute it and/or
0004# modify it under the terms of the GNU Lesser General Public
0005# License as published by the Free Software Foundation; either
0006# version 2.1 of the License, or (at your option) any later version.
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    ## because it's not re-bound when copied to another class obj.
0133    ## really this doesn't do anything anyway, and can be overidden
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            # not sure if we need to go this far
0153            continue
0154
0155        attr = getattr(cls,name)
0156        atype = type(attr)
0157        #print name, attr, atype
0158
0159        # should we make copies instead?
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            # ok to reference ?
0168            setattr(newcls, name, attr)
0169        elif atype in (TypeType, ObjectType):
0170            setattr(newcls, name, template._object)
0171        else:
0172            # ok to forget about it?
0173            setattr(newcls, name, None)
0174
0175    newcls.__name__ = cls.__name__
0176    return newcls