This code can be used in debugging phase to notice errors sooner. It is usually always desirable to visit all base class __init__-methods, but Python does nothing to ensure they will be visited. Set AssertInit as a metaclass in the base class of your class hierarchy, and all errors from not calling base class __init__s will be cleanly reported as AssertionErrors.
The solution should work properly with multiple inheritance and diamond-shaped inheritance graphs (see the example at the bottom of code).
It slows down object creation by a large amount (I'd assume but didn't do any tests).
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 | class AssertInit(type):
"""Assert that initializers get called.
Set this as a __metaclass__ for the root class
in a class hierarchy, and you will get AssertionError
if some of the base class initializers isn't called.
"""
def __new__(cls, classname, bases, classdict):
# if class has no real init method, it doesn't
# matter if it isn't called
classdict['_has_real_init_'] = ('__init__' in classdict)
old_init = classdict.get('__init__', lambda self: None)
def new_init(self, *args, **kwargs):
# here selfclass refers to the class in which
# this __init__ function lies (see below this function
# definition for the definition of selfclass)
# this is code that will be executed by
# all initializers except the first one:
if hasattr(self, '_visited_bases_'):
self._visited_bases_[selfclass] = True
old_init(self, *args, **kwargs)
return
# initialize _visited_bases_ by scanning *all* superclasses
# and by creating mappings from the class object to False
# if the base class needs to be visited, True otherwise.
self._visited_bases_ = vb = {}
def recurseBases(bases):
for claz in bases:
vb[claz] = (not hasattr(claz, '_has_real_init_') or
not claz.__dict__['_has_real_init_'])
recurseBases(claz.__bases__)
recurseBases(bases)
old_init(self, *args, **kwargs)
# scan _visited_bases_ to see which base class
# initializers didn't get visited
unvisited = ['%s.%s' % (claz.__module__, claz.__name__)
for claz, visited in vb.items() if not visited]
if unvisited:
fullClassName = '%s.%s' %\
(selfclass.__module__, selfclass.__name__)
raise AssertionError("Initializer%s in class%s (%s) not "
"visited when constructing object "
"from class %s" %
(len(unvisited) > 1 and 's' or '',
len(unvisited) > 1 and 'es' or '',
', '.join(unvisited),
fullClassName))
# ^def new_init
classdict['__init__'] = new_init
# the newly created class, selfclass, is referred to inside
# the new_init function, so it has to be put in a new variable
selfclass = super(AssertInit, cls).__new__\
(cls, classname, bases, classdict)
return selfclass
########### USAGE ############
def test():
class A(object):
__metaclass__ = AssertInit
def __init__(self):
print 'A init'
class B(A):
def __init__(self):
#A.__init__(self)
print 'B init'
class C(A):
pass
# a separate root class needs to set the __metaclass__ properly
class G(object):
__metaclass__ = AssertInit
def __init__(self):
print 'G init'
class D(C, B, G):
def __init__(self):
B.__init__(self)
#G.__init__(self)
print 'D init'
# This will raise an error for not calling two base
# class initializers: A and G.
# It properly sees that C.__init__ needs not be
# called and that B.__init__ was called.
D()
if __name__ == '__main__':
test()
|
The code may not be so useful after all, as the errors from not calling some base class initializer will soon be reported as NameErrors anyway. Usually. But this utility will report it neatly and as soon as the error happens.
I didn't do much more testing than the test included in the source code (with some variations) so I can't guarantee 100% workability.