2021-09-10 12:34:39 -03:00

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