Welcome, guest | Sign In | My Account | Store | Cart

Provide a way to construct class __init__, __slots__, __eq__, __ne__, __repr__ for the class and makes explicit which attributes each instance will have (and providing defaults).

The __main__ session shows an example of how it should be used.

Python, 171 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
'''Bunch utility class.

Developed at ESSS (http://esss.com.br) and provided under the MIT license.

@author: Bruno Oliveira 
@author: Fabio Zadrozny

Based on implementation by Alex Martelli (http://mail.python.org/pipermail
/python-list/2002-July/112007.html).

To use:

class Point(Bunch):
    x = 0
    y = 0

p0 = Point()
assert p0.x == 0
assert p0.y == 0

p1 = Point(x=10, y=20)
assert p1.x == 10
assert p1.y == 20

'''

import copy
from types import NoneType

#===============================================================================
# MetaBunch
#===============================================================================
class MetaBunch(type):
    """
    metaclass for new and improved "Bunch": implicitly defines 
    __slots__, __init__ and __repr__ from variables bound in class scope.

    An instance of metaMetaBunch (a class whose metaclass is metaMetaBunch)
    defines only class-scope variables (and possibly special methods, but
    NOT __init__ and __repr__!).  metaMetaBunch removes those variables from
    class scope, snuggles them instead as items in a class-scope dict named
    __defaults__, and puts in the class a __slots__ listing those variables'
    names, an __init__ that takes as optional keyword arguments each of
    them (using the values in __defaults__ as defaults for missing ones), and
    a __repr__ that shows the repr of each attribute that differs from its
    default value (the output of __repr__ can be passed to __eval__ to make
    an equal instance, as per the usual convention in the matter).
    """

    def __new__(cls, classname, bases, classdict):
        """ Everything needs to be done in __new__, since type.__new__ is
            where __slots__ are taken into account.
        """
        import inspect

        # define as local functions the __init__ and __repr__ that we'll
        # use in the new class

        def __init__(self, **kw):
            """ Simplistic __init__: first set all attributes to default
                values, then override those explicitly passed in kw.
            """
            for k, (value, copy_op) in self.__defaults__.iteritems():
                if k not in kw: #No need to set value to be overridden later on.
                    if copy_op is None:
                        #No copy op (immutable value)
                        setattr(self, k, value)
                    else:
                        setattr(self, k, copy_op(value))
            for k, value in kw.iteritems():
                setattr(self, k, value)

        def __repr__(self):
            """ 
            repr operator.
            """
            rep = ['%s=%r' % (k, getattr(self, k))
                for k in sorted(self.__defaults__)]
            return '%s(%s)' % (classname, ', '.join(rep))


        def __eq__(self, other):
            '''Basic __eq__.
            '''
            if not isinstance(other, type(self)):
                return False
            for attr in self.__defaults__:
                if getattr(self, attr) != getattr(other, attr):
                    return False
            return True

        def __ne__(self, other):
            return not self == other

        # build the newdict that we'll use as class-dict for the new class
        newdict = dict(
            __slots__=classdict.pop('__slots__', []),
            __defaults__={},
            __init__=__init__,
            __repr__=__repr__,
            __eq__=__eq__,
            __ne__=__ne__,
        )

        # update the dafaults dict with the contents of the bases's defaults, so
        # they're properly initialized during __init__
        for base in bases:
            newdict['__defaults__'].update(getattr(base, '__defaults__', {}))


        for k, value in classdict.iteritems():
            if k.startswith('__') or inspect.isfunction(value) or \
                type(value) is property:
                # methods: copy to newdict
                newdict[k] = value
            else:
                # class variables, store name in __slots__ and name and
                # value as an item in __defaults__

                #Default for each value is deepcopy, but we may optimize that
                #(if the copy op is None, the value is considered immutable).
                copy_op = copy.deepcopy

                if value.__class__ in (
                    bool, int, float, long, str, NoneType
                    ):
                    copy_op = None
                else:
                    if value.__class__ in (tuple, frozenset):
                        if not value:
                            copy_op = None

                    if value.__class__ in (list, set, dict):
                        if not value:
                            copy_op = copy.copy


                newdict['__slots__'].append(k)
                newdict['__defaults__'][k] = (value, copy_op)

        # finally delegate the rest of the work to type.__new__
        return type.__new__(cls, classname, bases, newdict)



#===============================================================================
# Bunch
#===============================================================================
class Bunch(object):
    """ For convenience: inheriting from Bunch can be used to get
        the new metaclass (same as defining __metaclass__ yourself).
    """
    __metaclass__ = MetaBunch



#===============================================================================
# main
#===============================================================================
if __name__ == '__main__':
    class Point(Bunch):
        x = 0
        y = 0

    p0 = Point()
    assert p0.x == 0
    assert p0.y == 0

    p1 = Point(x=10, y=20)
    assert p1.x == 10
    assert p1.y == 20