Python has multiple inheritence - a feature that's really handy because it allows the programing idiom called mixins - where you write a a bunch of methods on a "mixin" class and then later on attaching all of those methods onto another class simply by mixin-it-in. In Python, the process of mixin-it-in is simply to add it to the parent list of the class:
class MyObject(MyParent, MyMixin):
...
In Ruby, you can automatically mix in a mixin programmatically because the mix-it-in is done within the class definition, and because of its open classes. This is how the rails helpers are automatically found in a directory and mixed into the controllers for example.
How would you do this in Python? Let's say we simplify the problem to this: For a given Controller subclass named ProductController, for example, if there exists a helper class by naming convention: ProductControllerHelper, then we mix it in. Here's my solution:
class Meta(type):
def __new__(cls, name, bases, dct):
helper_mixin = globals().get('%sHelper' % name)
if helper_mixin:
bases = list(bases)
bases.append(helper_mixin)
return type.__new__(cls, name, tuple(bases), dct)
else:
return type.__new__(cls, name, bases, dct)
class Controller(object):
__metaclass__ = Meta
class ProductControllerHelper(object):
def method1(self):
print "method1 invoked"
class ProductController(Controller):
def index(self):
self.method1()
if __name__ == '__main__':
ProductController().index()
Output:
method1 invoked
It worked!
But! We are not done. What if what you want to mix in depends on how you configure the class inside the class definition? In my case, I wanted to mix in a bunch of classes when I write a DSL-like declaration like this:
class MyTestCase(TestCase):
all = fixture(SeleniumRCServer, BrowserSession, DBData, Login)
each = fixture(AddDelivery)
...
This is for a testing framework I am working on. I have a test case, and I want to specify its test context(fixtures) in the manner shown. Each object in the fixture(..) definition is a mixin. So I want to mix them all into MyTestCase.
This case is harder than the previous case because when __new__ is invoked, the class definition hasn't been processed yet, and therefore you won't have access to the class attributes all and each. The hack I came up with is this:
class TestCaseMetaClass(type):
def __init__(cls, name, bases, dct):
if len(bases) == 1 and bases[0] != unittest.TestCase:
bases = list(bases)
bases.extend(cls.all)
bases.extend(cls.each)
cls.__real__ = type(name, tuple(bases), dct)
else:
type.__init__(name, bases, dct)
def __call__(cls, *params, **kws):
if hasattr(cls, '__real__'):
return cls.__real__(*params, **kws)
else:
return type.__call__(cls, *params, **kws)
class TestCase(unittest.TestCase):
__metaclass__ = TestCaseMetaClass
Basically, I first override __init__ of the metaclass(rather than __new__) in order to record what was declared in all and each, and create a new class on the fly mixing in all the mixins defined in the fixture(..) definitions. We store that class in an attribute __real__ attached to the class. Then we override __call__ for the metaclass to return an instance of the class held in the __real__ attribute instead of the actual class. Oh yes, this is really ugly, but it worked, at least for what I was doing. If you can write to a classes' __bases__ attribute(which defines the parents of the class), then this would be much easier, but I haven't figured out how to do it if it can be done at all. But because of this limitation, it looks unlikely you can do something like Ruby's mixology in Python.
Update: the better solution to the second part is found:
class Meta2(type):
def __new__(cls, name, bases, dct):
mixins = []
all = dct.get('all')
if all:
mixins.extend(all)
each = dct.get('each')
if each:
mixins.extend(each)
bases = list(bases)
bases.extend(mixins)
return type.__new__(cls, name, tuple(bases), dct)
class TestCase(object):
__metaclass__ = Meta2
class Fixture1(object):
def method2(self):
print "method2 invoked"
class Fixture2(object):
def method3(self):
print "method3 invoked"
class BlahBlahTest(TestCase):
all = [Fixture1]
each = [Fixture2]
def blah_test(self):
self.method2()
self.method3()