#! /usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright © 2008 Neil Moore <neil@s-z.org>.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#  
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

class FirstArg(type):
    """Metaclass to cache object creation by a key, which must be the
    first constructor parameter.
    
        >>> import cacher
        >>> class MyClass (object):
        ...     __metaclass__ = cacher.FirstArg
        ...     def __init__(self, name, extra):
        ...         self.name = name
        ...         self.extra = extra
        >>> obj = MyClass('foo', 42)

    The constructor must be called with the correct number of arguments:
        >>> bad = MyClass('bad')
        Traceback (most recent call last):
            ....
        TypeError: __init__() takes exactly 3 arguments (2 given)

    Subsequent calls to the constructor with the same first argument result
    in the same object:
        >>> MyClass('foo', 42) is obj
        True

    
    This is true even if the other parameters differ:
        >>> MyClass('foo', None) is obj
        True
        >>> MyClass('foo', 'twelve').extra
        42

    In fact, if an instance has already been constructed, the remaining
    parameters are disregarded entirely, and may be omitted:
        >>> MyClass('foo') is obj
        True
        >>> MyClass('foo', 1, 2, 3, 4, 5, 6, 7) is obj
        True

    If the first parameter differs, the constructed object will differ:
        >>> MyClass('bar', 18) is obj
        False
        >>> MyClass('bar').extra
        18

    Any immutable key may be used:
        >>> pairobj = MyClass((1,2), {})
        >>> MyClass((1,2)).name
        (1, 2)
        >>> MyClass((1,2)).extra
        {}
        >>> badobj = MyClass([1,2], 26)
        Traceback (most recent call last):
            ....
        TypeError: list objects are unhashable

    However, the key None will not be cached:
        >>> noneobj = ( MyClass(None, 'first'), MyClass(None, 'second') )
        >>> noneobj[0].extra
        'first'
        >>> noneobj[1].extra
        'second'

    The objects themselves can be mutable:
        >>> MyClass('foo').extra = 9
        >>> obj.extra
        9
        >>> MyClass('bar').name = 'something else'
        >>> MyClass('bar').name
        'something else'

    
    The cache is not shared with subclasses:
        >>> class SubClass (MyClass):
        ...     def __init__(self, name, extra):
        ...         super(SubClass,self).__init__(name, extra)
        >>> sub = SubClass('foo', 76)
        >>> sub is obj
        False
        >>> sub.extra
        76
    """
    def __init__(cls, name, bases, cdict):
        super(FirstArg, cls).__init__(name, bases, cdict)
        cls.__instcache = {}
    def __call__(cls, name, *args, **kw):
        
        if name is None or name not in cls.__instcache:
            cls.__instcache[name] = super(FirstArg, cls).__call__(
                    name, *args, **kw )
        return cls.__instcache[name]

class Singleton (type):
    """Metaclass for singleton objects.  After the first constructor call
    initializes an object, subsequent calls will return the same object.
        >>> import cacher
        >>> class MyClass (object):
        ...     __metaclass__ = cacher.Singleton
        ...     def __init__(self, data):
        ...         self.data = data

    The constructor must be called with the correct number of arguments:
        >>> bad = MyClass()
        Traceback (most recent call last):
            ....
        TypeError: __init__() takes exactly 2 arguments (1 given)
        >>> obj = MyClass('only')

    After an object is successfully constructed, subsequent calls to the
    constructor yield the same object:
        >>> MyClass('only') is obj
        True

    
    This is true even if the other parameters differ:
        >>> MyClass('another') is obj
        True
        >>> MyClass('ignored').data
        'only'

    In fact, once an instance has been constructed, subsequent calls to
    the constructor ignore the parameters entirely:
        >>> MyClass() is obj
        True
        >>> MyClass(1, 2, 3, 4, 5, 6, 7) is obj
        True
    
    The objects can be mutable:
        >>> MyClass().data = 'changed'
        >>> obj.data
        'changed'
    
    The cache is not shared with subclasses:
        >>> class SubClass (MyClass):
        ...     def __init__(self, data):
        ...         super(SubClass,self).__init__(data)
        >>> sub = SubClass('foo')
        >>> sub is obj
        False
        >>> sub.data
        'foo'
        >>> MyClass('foo').data
        'changed'
    """
    def __init__(cls, name, bases, cdict):
        super(Singleton, cls).__init__(name, bases, cdict)
        cls.__instance = None

    def __call__(cls, *args, **kw):
        if cls.__instance is None:
            cls.__instance = super(Singleton, cls).__call__(*args, **kw)
        return cls.__instance

if __name__ == "__main__":
    import doctest
    doctest.testmod()
