86 lines
3.7 KiB
Python
86 lines
3.7 KiB
Python
import collections
|
|
import warnings
|
|
|
|
class MetaBunch(type):
|
|
"""
|
|
Metaclass for new and improved "Bunch": implicitly defines
|
|
__slots__, __init__ and __repr__ from variables bound in
|
|
class scope.
|
|
A class statement for an instance of MetaBunch (i.e., for a
|
|
class whose metaclass is MetaBunch) must define only
|
|
class-scope data attributes (and possibly special methods, but
|
|
NOT __init__ and __repr__). MetaBunch removes the data
|
|
attributes from class scope, snuggles them instead as items in
|
|
a class-scope dict named __dflts__, and puts in the class a
|
|
__slots__ with those attributes' names, an __init__ that takes
|
|
as optional named arguments each of them (using the values in
|
|
__dflts__ 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 usual convention in the matter, if
|
|
each non-default-valued attribute respects the convention too).
|
|
|
|
In v3, the order of data attributes remains the same as in the
|
|
class body; in v2, there is no such guarantee.
|
|
"""
|
|
def __prepare__(name, *bases, **kwargs):
|
|
# precious in v3—harmless although useless in v2
|
|
return collections.OrderedDict()
|
|
|
|
def __new__(mcl, classname, bases, classdict):
|
|
""" Everything needs to be done in __new__, since
|
|
type.__new__ is where __slots__ are taken into account.
|
|
"""
|
|
# 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 in self.__dflts__:
|
|
setattr(self, k, self.__dflts__[k])
|
|
for k in kw:
|
|
setattr(self, k, kw[k])
|
|
def __repr__(self):
|
|
""" Clever __repr__: show only attributes that differ
|
|
from default values, for compactness.
|
|
"""
|
|
rep = ['{}={!r}'.format(k, getattr(self, k))
|
|
for k in self.__dflts__
|
|
if getattr(self, k) != self.__dflts__[k]
|
|
]
|
|
return '{}({})'.format(classname, ', '.join(rep))
|
|
# build the newdict that we'll use as class-dict for the
|
|
# new class
|
|
newdict = { '__slots__':[],
|
|
'__dflts__':collections.OrderedDict(),
|
|
'__init__':__init__, '__repr__':__repr__, }
|
|
for k in classdict:
|
|
if k.startswith('__') and k.endswith('__'):
|
|
# dunder methods: copy to newdict, or warn
|
|
# about conflicts
|
|
if k in newdict:
|
|
warnings.warn(
|
|
"Can't set attr {!r} in bunch-class {!r}".
|
|
format(k, classname))
|
|
else:
|
|
newdict[k] = classdict[k]
|
|
else:
|
|
# class variables, store name in __slots__, and
|
|
# name and value as an item in __dflts__
|
|
newdict['__slots__'].append(k)
|
|
newdict['__dflts__'][k] = classdict[k]
|
|
# finally delegate the rest of the work to type.__new__
|
|
return super(MetaBunch, mcl).__new__(
|
|
mcl, classname, bases, newdict)
|
|
|
|
class Bunch(metaclass=MetaBunch):
|
|
""" For convenience: inheriting from Bunch can be used to get
|
|
the new metaclass (same as defining metaclass= yourself).
|
|
|
|
In v2, remove the (metaclass=MetaBunch) above and add
|
|
instead __metaclass__=MetaBunch as the class body.
|
|
"""
|
|
pass
|