0001"""
0002Declarative objects.
0003
0004Declarative objects have a simple protocol: you can use classes in
0005lieu of instances and they are equivalent, and any keyword arguments
0006you give to the constructor will override those instance variables.
0007(So if a class is received, we'll simply instantiate an instance with
0008no arguments).
0009
0010You can provide a variable __unpackargs__ (a list of strings), and if
0011the constructor is called with non-keyword arguments they will be
0012interpreted as the given keyword arguments.
0013
0014If __unpackargs__ is ('*', name), then all the arguments will be put
0015in a variable by that name.
0016
0017You can define a __classinit__(cls, new_attrs) method, which will be
0018called when the class is created (including subclasses).  Note: you
0019can't use super() in __classinit__ because the class isn't bound to a
0020name.  As an analog to __classinit__, Declarative adds
0021__instanceinit__ which is called with the same argument (new_attrs).
0022This is like __init__, but after __unpackargs__ and other factors have
0023been taken into account.
0024
0025If __mutableattributes__ is defined as a sequence of strings, these
0026attributes will not be shared between superclasses and their
0027subclasses.  E.g., if you have a class variable that contains a list
0028and you append to that list, changes to subclasses will effect
0029superclasses unless you add the attribute here.
0030
0031Also defines classinstancemethod, which acts as either a class method
0032or an instance method depending on where it is called.
0033"""
0034
0035import copy
0036import events
0037
0038import itertools
0039counter = itertools.count()
0040
0041__all__ = ('classinstancemethod', 'DeclarativeMeta', 'Declarative')
0042
0043
0044class classinstancemethod(object):
0045    """
0046    Acts like a class method when called from a class, like an
0047    instance method when called by an instance.  The method should
0048    take two arguments, 'self' and 'cls'; one of these will be None
0049    depending on how the method was called.
0050    """
0051
0052    def __init__(self, func):
0053        self.func = func
0054
0055    def __get__(self, obj, type=None):
0056        return _methodwrapper(self.func, obj=obj, type=type)
0057
0058class _methodwrapper(object):
0059
0060    def __init__(self, func, obj, type):
0061        self.func = func
0062        self.obj = obj
0063        self.type = type
0064
0065    def __call__(self, *args, **kw):
0066        assert not 'self' in kw and not 'cls' in kw, (
0067            "You cannot use 'self' or 'cls' arguments to a "
0068            "classinstancemethod")
0069        return self.func(*((self.obj, self.type) + args), **kw)
0070
0071    def __repr__(self):
0072        if self.obj is None:
0073            return ('<bound class method %s.%s>'
0074                    % (self.type.__name__, self.func.func_name))
0075        else:
0076            return ('<bound method %s.%s of %r>'
0077                    % (self.type.__name__, self.func.func_name, self.obj))
0078
0079class DeclarativeMeta(type):
0080
0081    def __new__(meta, class_name, bases, new_attrs):
0082        post_funcs = []
0083        early_funcs = []
0084        events.send(events.ClassCreateSignal,
0085                    bases[0], class_name, bases, new_attrs,
0086                    post_funcs, early_funcs)
0087        cls = type.__new__(meta, class_name, bases, new_attrs)
0088        for func in early_funcs:
0089            func(cls)
0090        if '__classinit__' in new_attrs:
0091            cls.__classinit__ = staticmethod(cls.__classinit__.im_func)
0092        cls.__classinit__(cls, new_attrs)
0093        for func in post_funcs:
0094            func(cls)
0095        return cls
0096
0097class Declarative(object):
0098
0099    __unpackargs__ = ()
0100
0101    __mutableattributes__ = ()
0102
0103    __metaclass__ = DeclarativeMeta
0104
0105    __restrict_attributes__ = None
0106
0107    def __classinit__(cls, new_attrs):
0108        cls.declarative_count = counter.next()
0109        for name in cls.__mutableattributes__:
0110            if name not in new_attrs:
0111                setattr(cls, copy.copy(getattr(cls, name)))
0112
0113    def __instanceinit__(self, new_attrs):
0114        if self.__restrict_attributes__ is not None:
0115            for name in new_attrs:
0116                if name not in self.__restrict_attributes__:
0117                    raise TypeError(
0118                        '%s() got an unexpected keyword argument %r'
0119                        % (self.__class__.__name__, name))
0120        for name, value in new_attrs.items():
0121            setattr(self, name, value)
0122        if 'declarative_count' not in new_attrs:
0123            self.declarative_count = counter.next()
0124
0125    def __init__(self, *args, **kw):
0126        if self.__unpackargs__ and self.__unpackargs__[0] == '*':
0127            assert len(self.__unpackargs__) == 2,                      "When using __unpackargs__ = ('*', varname), you must only provide a single variable name (you gave %r)" % self.__unpackargs__
0129            name = self.__unpackargs__[1]
0130            if name in kw:
0131                raise TypeError(
0132                    "keyword parameter '%s' was given by position and name"
0133                    % name)
0134            kw[name] = args
0135        else:
0136            if len(args) > len(self.__unpackargs__):
0137                raise TypeError(
0138                    '%s() takes at most %i arguments (%i given)'
0139                    % (self.__class__.__name__,
0140                       len(self.__unpackargs__),
0141                       len(args)))
0142            for name, arg in zip(self.__unpackargs__, args):
0143                if name in kw:
0144                    raise TypeError(
0145                        "keyword parameter '%s' was given by position and name"
0146                        % name)
0147                kw[name] = arg
0148        if '__alsocopy' in kw:
0149            for name, value in kw['__alsocopy'].items():
0150                if name not in kw:
0151                    if name in self.__mutableattributes__:
0152                        value = copy.copy(value)
0153                    kw[name] = value
0154            del kw['__alsocopy']
0155        self.__instanceinit__(kw)
0156
0157    def __call__(self, *args, **kw):
0158        kw['__alsocopy'] = self.__dict__
0159        return self.__class__(*args, **kw)
0160
0161    @classinstancemethod
0162    def singleton(self, cls):
0163        if self:
0164            return self
0165        name = '_%s__singleton' % cls.__name__
0166        if not hasattr(cls, name):
0167            setattr(cls, name, cls(declarative_count=cls.declarative_count))
0168        return getattr(cls, name)
0169
0170    @classinstancemethod
0171    def __repr__(self, cls):
0172        if self:
0173            name = '%s object' % self.__class__.__name__
0174            v = self.__dict__.copy()
0175        else:
0176            name = '%s class' % cls.__name__
0177            v = cls.__dict__.copy()
0178        if 'declarative_count' in v:
0179            name = '%s %i' % (name, v['declarative_count'])
0180            del v['declarative_count']
0181        # @@: simplifying repr:
0182        #v = {}
0183        names = v.keys()
0184        args = []
0185        for n in self._repr_vars(names):
0186            args.append('%s=%r' % (n, v[n]))
0187        if not args:
0188            return '<%s>' % name
0189        else:
0190            return '<%s %s>' % (name, ' '.join(args))
0191
0192    @staticmethod
0193    def _repr_vars(dictNames):
0194        names = [n for n in dictNames
0195                 if not n.startswith('_')
0196                 and n != 'declarative_count']
0197        names.sort()
0198        return names
0199
0200def setup_attributes(cls, new_attrs):
0201    for name, value in new_attrs.items():
0202        if hasattr(value, '__addtoclass__'):
0203            value.__addtoclass__(cls, name)