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

MI can be difficult and confusing, and if the base classes don't cooperate -- well, cooperative MI won't work.

One way around this is to use composition instead. This class decorator will combine the source classes with the target class, ensuring that no duplications occur (raises a TypeError if there are any).

Python, 36 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
import new

class Compose(object):
    def __init__(self, *parts):
        self.parts = parts
    def __call__(self, cls):
        conflicts = dict()
        parts = self.parts + (cls,)
        for i, part1 in enumerate(parts):
            for partn in parts[i+1:]:
                for attr in dir(part1):
                    if attr[:2] == attr[-2:] == '__':
                        continue
                    if getattr(partn, attr, None):
                        if attr not in conflicts:
                            conflicts[attr] = [part1]
                        conflicts[attr].append(partn)
        if conflicts:
            text = []
            for key, lst in conflicts.items():
                text.append('    %s:' % key)
                for c in lst:
                    text.append('        %s' % c)
            text = '\n'.join(text)
            raise TypeError("Conflicts while composing:\n%s" % text)
        for part in self.parts:
            for attr in dir(part):
                if attr[:2] == attr[-2:] == '__':
                    continue
                thing = getattr(part, attr)
                thing = getattr(thing, '__func__', None) or thing
                if callable(thing):
                    setattr(cls, attr, new.instancemethod(thing, None, cls))
                else:
                    setattr(cls, attr, thing)
        return cls

Brief example:

import unittest from compose import Compose

class Eggs(object): def test_eggs_01(self): print('testing eggs_01') def test_eggs_02(self): print('testing eggs_02')

class Spam(object): def test_spam_01(self): print('testing spam_01') def test_spam_02(self): print('testing spam_02')

@Compose(Spam, Eggs) class TestAll(unittest.TestCase): def setUp(self): print('Setting up...') def tearDown(self): print('Tearing down...') def test_something(self): print('testing something')

if __name__ == '__main__': unittest.main()

1 comment

Ethan Furman (author) 10 years, 7 months ago  # | flag

Let's try that again. :)

import unittest
from compose import Compose

class Eggs(object):
    def test_eggs_01(self):
        print('testing eggs_01')
    def test_eggs_02(self):
        print('testing eggs_02')

class Spam(object):
    def test_spam_01(self):
        print('testing spam_01')
    def test_spam_02(self):
        print('testing spam_02')

@Compose(Spam, Eggs)
class TestAll(unittest.TestCase):
    def setUp(self):
        print('Setting up...')
    def tearDown(self):
        print('Tearing down...')
    def test_something(self):
        print('testing something')

if __name__ == '__main__':
    unittest.main()