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
0036from . import events
0037from sqlobject.compat import with_metaclass
0038
0039import itertools
0040counter = itertools.count()
0041
0042__all__ = ('classinstancemethod', 'DeclarativeMeta', 'Declarative')
0043
0044
0045class classinstancemethod(object):
0046    """
0047    Acts like a class method when called from a class, like an
0048    instance method when called by an instance.  The method should
0049    take two arguments, 'self' and 'cls'; one of these will be None
0050    depending on how the method was called.
0051    """
0052
0053    def __init__(self, func):
0054        self.func = func
0055
0056    def __get__(self, obj, type=None):
0057        return _methodwrapper(self.func, obj=obj, type=type)
0058
0059
0060class _methodwrapper(object):
0061
0062    def __init__(self, func, obj, type):
0063        self.func = func
0064        self.obj = obj
0065        self.type = type
0066
0067    def __call__(self, *args, **kw):
0068        assert 'self' not in kw and 'cls' not in kw, (
0069            "You cannot use 'self' or 'cls' arguments to a "
0070            "classinstancemethod")
0071        return self.func(*((self.obj, self.type) + args), **kw)
0072
0073    def __repr__(self):
0074        if self.obj is None:
0075            return ('<bound class method %s.%s>'
0076                    % (self.type.__name__, self.func.__name__))
0077        else:
0078            return ('<bound method %s.%s of %r>'
0079                    % (self.type.__name__, self.func.__name__, self.obj))
0080
0081
0082class DeclarativeMeta(type):
0083
0084    def __new__(meta, class_name, bases, new_attrs):
0085        post_funcs = []
0086        early_funcs = []
0087        events.send(events.ClassCreateSignal,
0088                    bases[0], class_name, bases, new_attrs,
0089                    post_funcs, early_funcs)
0090        cls = type.__new__(meta, class_name, bases, new_attrs)
0091        for func in early_funcs:
0092            func(cls)
0093        if '__classinit__' in new_attrs:
0094            if hasattr(cls.__classinit__, '__func__'):
0095                cls.__classinit__ = staticmethod(cls.__classinit__.__func__)
0096            else:
0097                cls.__classinit__ = staticmethod(cls.__classinit__)
0098        cls.__classinit__(cls, new_attrs)
0099        for func in post_funcs:
0100            func(cls)
0101        return cls
0102
0103
0104class Declarative(with_metaclass(DeclarativeMeta, object)):
0105
0106    __unpackargs__ = ()
0107
0108    __mutableattributes__ = ()
0109
0110    __restrict_attributes__ = None
0111
0112    def __classinit__(cls, new_attrs):
0113        cls.declarative_count = next(counter)
0114        for name in cls.__mutableattributes__:
0115            if name not in new_attrs:
0116                setattr(cls, copy.copy(getattr(cls, name)))
0117
0118    def __instanceinit__(self, new_attrs):
0119        if self.__restrict_attributes__ is not None:
0120            for name in new_attrs:
0121                if name not in self.__restrict_attributes__:
0122                    raise TypeError(
0123                        '%s() got an unexpected keyword argument %r'
0124                        % (self.__class__.__name__, name))
0125        for name, value in new_attrs.items():
0126            setattr(self, name, value)
0127        if 'declarative_count' not in new_attrs:
0128            self.declarative_count = next(counter)
0129
0130    def __init__(self, *args, **kw):
0131        if self.__unpackargs__ and self.__unpackargs__[0] == '*':
0132            assert len(self.__unpackargs__) == 2,                   "When using __unpackargs__ = ('*', varname), "                   "you must only provide a single variable name "                   "(you gave %r)" % self.__unpackargs__
0136            name = self.__unpackargs__[1]
0137            if name in kw:
0138                raise TypeError(
0139                    "keyword parameter '%s' was given by position and name"
0140                    % name)
0141            kw[name] = args
0142        else:
0143            if len(args) > len(self.__unpackargs__):
0144                raise TypeError(
0145                    '%s() takes at most %i arguments (%i given)'
0146                    % (self.__class__.__name__,
0147                       len(self.__unpackargs__),
0148                       len(args)))
0149            for name, arg in zip(self.__unpackargs__, args):
0150                if name in kw:
0151                    raise TypeError(
0152                        "keyword parameter '%s' was given by position and name"
0153                        % name)
0154                kw[name] = arg
0155        if '__alsocopy' in kw:
0156            for name, value in kw['__alsocopy'].items():
0157                if name not in kw:
0158                    if name in self.__mutableattributes__:
0159                        value = copy.copy(value)
0160                    kw[name] = value
0161            del kw['__alsocopy']
0162        self.__instanceinit__(kw)
0163
0164    def __call__(self, *args, **kw):
0165        kw['__alsocopy'] = self.__dict__
0166        return self.__class__(*args, **kw)
0167
0168    @classinstancemethod
0169    def singleton(self, cls):
0170        if self:
0171            return self
0172        name = '_%s__singleton' % cls.__name__
0173        if not hasattr(cls, name):
0174            setattr(cls, name, cls(declarative_count=cls.declarative_count))
0175        return getattr(cls, name)
0176
0177    @classinstancemethod
0178    def __repr__(self, cls):
0179        if self:
0180            name = '%s object' % self.__class__.__name__
0181            v = self.__dict__.copy()
0182        else:
0183            name = '%s class' % cls.__name__
0184            v = cls.__dict__.copy()
0185        if 'declarative_count' in v:
0186            name = '%s %i' % (name, v['declarative_count'])
0187            del v['declarative_count']
0188        # @@: simplifying repr:
0189        # v = {}
0190        names = v.keys()
0191        args = []
0192        for n in self._repr_vars(names):
0193            args.append('%s=%r' % (n, v[n]))
0194        if not args:
0195            return '<%s>' % name
0196        else:
0197            return '<%s %s>' % (name, ' '.join(args))
0198
0199    @staticmethod
0200    def _repr_vars(dictNames):
0201        names = [n for n in dictNames
0202                 if not n.startswith('_') and
0203                 n != 'declarative_count']
0204        names.sort()
0205        return names
0206
0207
0208def setup_attributes(cls, new_attrs):
0209    for name, value in new_attrs.items():
0210        if hasattr(value, '__addtoclass__'):
0211            value.__addtoclass__(cls, name)