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 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    ## because it's not re-bound when copied to another class obj.
0129    ## really this doesn't do anything anyway, and can be overidden
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            # not sure if we need to go this far
0149            continue
0150
0151        attr = getattr(cls,name)
0152        atype = type(attr)
0153        #print name, attr, atype
0154
0155        # should we make copies instead?
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            # ok to reference ?
0164            setattr(newcls, name, attr)
0165        elif atype in (TypeType, ObjectType):
0166            setattr(newcls, name, template._object)
0167        else:
0168            # ok to forget about it?
0169            setattr(newcls, name, None)
0170
0171    newcls.__name__ = cls.__name__
0172    return newcls