## @brief Defines a class to create abstract methods
#
# @author Ivo Timmermans
# @date 2004/01/23
# @version 1.1
#
# Example:
# @code
#    import Abstract;
#    class Foo:
#        __metaclass__ = Abstract.Metaclass
#        foo = Abstract.AbstractMethod('foo')
# @endcode
class AbstractMethod (object):
    ## @brief Constructor
    #
    # @param func name of the function (used when raising an
    #    exception). Its type is str.
    def __init__(self, func):
        self._function = func

    ## @brief Get callable object
    #
    # @return An instance of AbstractMethodHelper.
    # This trickery is needed to get the name of the class for which
    # an abstract method was requested, otherwise it would be
    # sufficient to include a __call__ method in the AbstractMethod
    # class itself.
    def __get__(self, obj, type):
        return self.AbstractMethodHelper(self._function, type)

    ## @brief Abstract method helper class
    #
    # An AbstractMethodHelper instance is a callable object that
    # represents an abstract method.
    class AbstractMethodHelper (object):
        def __init__(self, func, cls):
            self._function = func
            self._class = cls

        ## @brief Call abstract method
	# 
	# Raises a TypeError, because abstract methods can not be
	# called.
        def __call__(self, *args, **kwargs):
            raise TypeError('Abstract method `' + self._class.__name__ \
                            + '.' + self._function + '\' called')

## @brief Configure a new class to be abstract
#
# @author Ivo Timmermans
# @date 2004/01/23
# @version 1.1
class Metaclass (type):
    ## Configure a new class
    #
    # @param cls Class object
    # @param name Name of the class
    # @param bases All base classes for cls
    def __init__(cls, name, bases, *args, **kwargs):
        super(Metaclass, cls).__init__(cls, name, bases, *args, **kwargs)

        # Detach cls.new() from class Metaclass, and make it a method
        # of cls.
        cls.__new__ = staticmethod(cls.new)

        # Find all abstract methods, and assign the resulting list to
        # cls.__abstractmethods__, so we can read that variable when a
        # request for allocation (__new__) is done.
        abstractmethods = []
        ancestors = list(cls.__mro__)
        ancestors.reverse()  # Start with __builtin__.object
        for ancestor in ancestors:
            for clsname, clst in ancestor.__dict__.items():
                if isinstance(clst, AbstractMethod):
                    abstractmethods.append(clsname)
                else:
                    if clsname in abstractmethods:
                        abstractmethods.remove(clsname)

        abstractmethods.sort()
        setattr(cls, '__abstractmethods__', abstractmethods)

    ## @brief Allocator for class cls
    #
    # @param self Class object for which an instance should be
    # created.
    # @param cls Same as self.
    def new(self, cls):
        if len(cls.__abstractmethods__):
            raise NotImplementedError('Can\'t instantiate class `' + \
                                      cls.__name__ + '\';\n' + \
                                      'Abstract methods: ' + \
                                      ", ".join(cls.__abstractmethods__))

        return object.__new__(self)
