Source

protopype / protopype.py

Full commit
import inspect
import types
from pprint import pformat

class ProtoIter(object):
    """A simple wrapping/delegating iterator.  Uses the `iterget`
    function passed to the initializer in order to fetch an iterator
    from the `i` object upon ever call to __iter__().  Meanwhile,
    next() simply delegates to underlying iterator's next method,
    which is stored in the `i_iter` attribute."""
    def __init__(self, i, iterget):
        self.i = i
        self.iterget = iterget
        self.__iter__()

    def __iter__(self):
        self.i_iter = self.iterget(self.i)
        return self

    def next(self):
        return self.i_iter.next()
        
class undefined(object):
    """Class that defines a value that represents undefined
    attributes, similar to the JavaScript keyword and value, 
    'undefined'.  Can be used as a stand-in for missing keys
    and/or attributes, and can be returned in lieu of raising 
    KeyError and AttributeErrors."""
    def __getattribute__(self, *args):
        """The value undefined is also "toxic", in that 
        attempting to access its attributes returns itself."""
        return self
    __getitem__ = __getattribute__

    def __eq__(self, other):
        """The value undefined returns False when compared to any other
        value, except for None and undefined itself."""
        if other is None or other is self:
            return True
        return False

    def __unicode__(self):
        return "undefined"
    __repr__ = __unicode__
    __str__ = __unicode__

# declare the singleton instance of undefined
undefined = undefined()

def rebind(name, fn, obj):
    """This method rebinds the function or method `fn` to `obj` 
    using the provided `name`.  If it is a method, unbinds from prior
    bound method instance, and then rebinds; if a function, simply binds."""
    if inspect.ismethod(fn):
        fn = fn.im_func # unbind a bound method
    bound = types.MethodType(fn, obj, obj.__class__)
    #print "bound function " + repr(fn) + " to bound method " + repr(bound)
    obj.__dict__[name] = bound

class attr(object):
    # TODO: attr should probably be a descriptor to really work well
    # XXX: experimental idea right now
    def __init__(self, default=None):
        self.default = default

def make_proto_binder(proto):
    """A function that returns a metaclass that assists with protopype's 
    dynamic attribute/method definition syntax.  Each metaclass is tied,
    via closure, to the passed in `proto` instance.  Classes who have 
    `ProtoBinderMeta` as their metaclass will imbue declared methods
    and attributes into `proto`."""
    class ProtoBinderMeta(type):
        def __new__(mcs, name, bases, dict):
            for k, v in dict.iteritems():
                if inspect.isfunction(v):
                    rebind(k, v, proto)
                elif isinstance(v, attr):
                    # this is just an experiment in higher-level attribute
                    # declaration
                    d = proto.__dict__
                    d[k] = v.default
                else:
                    proto.__dict__[k] = v
            return type.__new__(mcs, name, bases, dict)
    return ProtoBinderMeta

class InvalidPrototypeChain(Exception): pass

class protopype(object):
    pass
protopype = protopype()

def nslookup(ns, key):
    if key in ns:
        return ns[key]
    return undefined

def datalookup(ns, key):
    if not key.startswith("__") and key in ns:
        val = ns[key]
        if not inspect.ismethod(val):
            return ns[key]
    return undefined

def protowalk(prototype, fn=nslookup, single_key=None):
    seen = set()

    while prototype is not protopype:
        ns = prototype.__dict__
        if prototype is None:
            raise InvalidPrototypeChain("prototype chain ended before " +
                                        "protopype object was reached.")
        keys = ns.iterkeys() \
                if single_key is None \
                else [single_key]
        for key in keys:
            resolved = fn(ns, key)
            if resolved is not undefined:
                if key in seen:
                    continue
                seen.add(key)
                yield (key, resolved, prototype)
        prototype = prototype.__dict__['__prototype__']

def protoget(proto, name):
    g = protowalk(proto, single_key=name)
    return g.next()[1]

class proto(object):
    def __init__(self, *args, **kwargs):
        if len(args) > 0:
            for k, v in args[0].iteritems():
                if inspect.ismethod(v):
                    rebind(k, v, self)
                else:
                    self.__dict__[k] = v
        if len(kwargs) > 0:
            self.__dict__.update(kwargs)
        self.__init_binder()
        self.__prototype__ = protopype

    def __init_binder(self):
        class ProtoBinder(object):
            __metaclass__ = make_proto_binder(self)
        self.__bind__ = ProtoBinder

    def resolve(self, name):
        return protoget(self, name)

    def inherit(self, proto):
        self.__prototype__ = proto

    def clone(self, src):
        newp = proto()
        newp.inherit(src)
        return newp

    def keys(self):
        return list(self.iterkeys())

    def iterkeys(self):
        for key, val in self.iteritems():
            yield key

    def itervalues(self):
        return list(self.itervalues())

    def values(self):
        for key, val in self.iteritems():
            yield val

    def items(self):
        return list(self.__public_members())

    def iteritems(self):
        return self.__public_members()

    def allitems(self):
        return list(self.iterallitems())

    def iterallitems(self):
        return protowalk(self)

    def dataitems(self):
        return list(self.iterdataitems())

    def iterdataitems(self):
        return self.__data_members()

    def __getattr__(self, attr):
        return self.resolve(attr)

    def __getitem__(self, key):
        return self.resolve(key)

    def __setitem__(self, key, val):
        self.__dict__[key] = val

    def __enter__(self):
        return self.__bind__

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def __iter__(self):
        return ProtoIter(self.__dict__, lambda d: iter(d.items()))

    def __public_members(self):
        for key, val in self.__dict__.iteritems():
            if not key.startswith("__"):
                yield (key, val)

    def __pretty_repr(self):
        for key, val in self.__public_members():
            if inspect.ismethod(val):
                val = "<method>"
            yield (key, val)

    def __data_members(self):
        return protowalk(self, fn=datalookup)

    def __unicode__(self):
        return unicode(self.__pprint__())

    __str__ = __unicode__

    def __pprint__(self):
        return pformat(dict(self.__pretty_repr()))

class SoftNone(object):
    """SoftNone is a None-like singleton that is useful in situations 
    where chained method calls can be expected to return None, but where
    a raised AttributeError is not actually desirable.  For example,
    jQuery-like APIs where you might run a series of method calls, but
    if any one of them returns None, "they all should".

    Accessing an attribute, calling, or getting an item off a SoftNone
    instance will simply return itself.  When evaluated for truthiness,
    SoftNone returns False, like None does.  When compared via equality,
    SoftNone compares True only to itself, None, and False."""
    def _self(self, *args):
        return self
    __getattribute__ = _self
    __getitem__ = _self
    __call__ = _self
    __ilshift__ = _self

    def __nonzero__(self):
        return False

    def __len__(self):
        return 0

    def __eq__(self, other):
        if other is self or other is None or other == False:
            return True
        return False

    def __ne__(self, other):
        return not self.__eq__(other)

SoftNone = SoftNone()
"""It shall be a singleton; there shall only be one!"""

class SoftNoneProxy(object):
    """SoftNoneProxy is SoftNone's cousin, which makes it easy to
    wrap an object with a Chained Method / Fluent Interface pattern
    such that attribute lookups are forwarded (proxied) to that object,
    but any results that are None are converted to SoftNone instead.

    The SoftNoneProxy can thus handled chained attribute lookup, like:

        x.a.b.c.d.e

    without an issue.  But, it can also handle chained method calls, or
    even the mixing of these two styles, such as:
        
        x.a().b.c().d

    This is what makes it truly useful.
    
    One issue with SoftNoneProxy is how to recover the underlying value
    at the end of a chain.  In the above cases, the end result of all 
    of those lookups will be either SoftNone, or a SoftNoneProxy with 
    the desired value.  This is where the "dereference" operator comes in.
        
        x.a().b.c().d
        x <<= x
        if x:
            # do something

    This one line will "dereference" the proxy and recover the value.  If
    x was SoftNone, it will dereference to SoftNone.  Otherwise, it
    will simply return the wrapped value from the proxy.  The choice to 
    use augmented lshift was debated heartily.  The main issue with using
    an OO API is that any chosen name would conflict with a name that could
    be used in the underlying object.  The augmented lshift operator is esoteric
    enough that it is probably OK to use -- plus, the "<<=" visually looks 
    like a kind of dereferenced assignment."""
    def __init__(self, wrapped, silence_exceptions=False):
        self.wrapped = wrapped
        self.silence_exceptions = silence_exceptions

    def __getattr__(self, name):
        try:
            if self.wrapped is None:
                return SoftNone
            val = getattr(self.wrapped, name)
            if inspect.isfunction(val) or inspect.ismethod(val):
                def proxy(*args, **kwargs):
                    return SoftNoneProxy(val(*args, **kwargs))
                return proxy
        except AttributeError, e:
            raise e
        except Exception, e:
            if self.silence_exceptions:
                val = None
            else:
                raise e
        if val is None:
            return SoftNone
        return SoftNoneProxy(val)

    def __ilshift__(self, other):
        return self.wrapped