Skip to content

Commit df710b6

Browse files
committed
Fix MixIn function and add unit tests
1 parent d8e40d8 commit df710b6

File tree

2 files changed

+221
-8
lines changed

2 files changed

+221
-8
lines changed

webware/MiscUtils/MixIn.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from types import MethodType
1+
from types import FunctionType, MethodType
22

33

44
def MixIn(pyClass, mixInClass, makeAncestor=False, mixInSuperMethods=False):
@@ -33,12 +33,15 @@ def MixIn(pyClass, mixInClass, makeAncestor=False, mixInSuperMethods=False):
3333
MiddleKit.Core.ModelUser.py, which install mix-ins for SQL interfaces.
3434
Search for "MixIn(".
3535
36-
If makeAncestor is 1, then a different technique is employed:
37-
the mixInClass is made the first base class of the pyClass.
36+
If makeAncestor is True, then a different technique is employed:
37+
a new class is created and returned that is the same as the given pyClass,
38+
but will have the mixInClass added as its first base class.
39+
Note that this is different from the behavior in legacy Webware
40+
versions, where the __bases__ attribute of the pyClass was changed.
3841
You probably don't need to use this and if you do, be aware that your
3942
mix-in can no longer override attributes/methods in pyClass.
4043
41-
If mixInSuperMethods is 1, then support will be enabled for you to
44+
If mixInSuperMethods is True, then support will be enabled for you to
4245
be able to call the original or "parent" method from the mixed-in method.
4346
This is done like so::
4447
@@ -51,12 +54,15 @@ def foo(self):
5154
raise TypeError('mixInClass is the same as pyClass')
5255
if makeAncestor:
5356
if mixInClass not in pyClass.__bases__:
54-
pyClass.__bases__ = (mixInClass,) + pyClass.__bases__
57+
return type(pyClass.__name__,
58+
(mixInClass,) + pyClass.__bases__,
59+
dict(pyClass.__dict__))
5560
else:
5661
# Recursively traverse the mix-in ancestor classes in order
5762
# to support inheritance
5863
for baseClass in reversed(mixInClass.__bases__):
59-
MixIn(pyClass, baseClass)
64+
if baseClass is not object:
65+
MixIn(pyClass, baseClass)
6066

6167
# Track the mix-ins made for a particular class
6268
attrName = 'mixInsFor' + pyClass.__name__
@@ -79,11 +85,10 @@ def foo(self):
7985
continue # built in or descriptor
8086
else:
8187
member = getattr(mixInClass, name)
82-
if isinstance(member, MethodType):
88+
if isinstance(member, (FunctionType, MethodType)):
8389
if mixInSuperMethods:
8490
if hasattr(pyClass, name):
8591
origMember = getattr(pyClass, name)
8692
setattr(mixInClass, 'mixInSuper' +
8793
name[0].upper() + name[1:], origMember)
88-
member = member.__func__
8994
setattr(pyClass, name, member)

webware/MiscUtils/Tests/TestMixIn.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import unittest
2+
3+
from MiscUtils.MixIn import MixIn
4+
5+
6+
class TestMixIn(unittest.TestCase):
7+
8+
def setUp(self):
9+
class Base:
10+
attr0 = 'this is Base.attr0'
11+
attr1 = 'this is Base.attr1'
12+
13+
def foo(self):
14+
return 'this is Base.foo'
15+
16+
class PyClass(Base):
17+
attr1 = 'this is PyClass.attr1'
18+
attr2 = 'this is PyClass.attr2'
19+
attr3 = 'this is PyClass.attr3'
20+
21+
def foo(self):
22+
return 'this is PyClass.foo'
23+
24+
@classmethod
25+
def cFoo(cls):
26+
return 'this is PyClass.cFoo'
27+
28+
@staticmethod
29+
def sFoo():
30+
return 'this is PyClass.sFoo'
31+
32+
class MixIn1:
33+
attr3 = 'this is MixIn1.attr3'
34+
attr4 = 'this is MixIn1.attr4'
35+
36+
def foo(self):
37+
return 'this is MixIn1.foo'
38+
39+
@classmethod
40+
def cFoo(cls):
41+
return 'this is MixIn1.cFoo'
42+
43+
@staticmethod
44+
def sFoo():
45+
return 'this is MixIn1.sFoo'
46+
47+
class MixIn2Base:
48+
attr5 = 'this is MixIn2Base.attr5'
49+
50+
def bar(self):
51+
return 'this is MixIn2Base.bar'
52+
53+
def baz(self):
54+
return 'this is MixIn2Base.baz'
55+
56+
class MixIn2(MixIn2Base):
57+
attr5 = 'this is MixIn2.attr5'
58+
attr6 = 'this is MixIn2.attr6'
59+
60+
def bar(self):
61+
return 'this is MixIn2.bar'
62+
63+
self.base = Base
64+
self.pyClass = PyClass
65+
self.mixIn1 = MixIn1
66+
self.mixIn2Base = MixIn2Base
67+
self.mixIn2 = MixIn2
68+
69+
def testMixInSameAsClass(self):
70+
self.assertRaisesRegex(
71+
TypeError, 'mixInClass is the same as pyClass',
72+
MixIn, self.pyClass, self.pyClass)
73+
74+
# pylint: disable=no-member
75+
76+
def testMakeAncestor1(self):
77+
cls = MixIn(self.pyClass, self.mixIn1, makeAncestor=True)
78+
self.assertEqual(cls.__bases__, (self.mixIn1, self.base))
79+
self.assertFalse(hasattr(cls, 'mixInsForPyClass'))
80+
self.assertFalse(hasattr(self.mixIn1, 'mixInSuperFoo'))
81+
self.assertEqual(cls.attr0, 'this is Base.attr0')
82+
self.assertEqual(cls.attr1, 'this is PyClass.attr1')
83+
self.assertEqual(cls.attr2, 'this is PyClass.attr2')
84+
self.assertEqual(cls.attr3, 'this is PyClass.attr3')
85+
self.assertEqual(cls.attr4, 'this is MixIn1.attr4')
86+
self.assertEqual(cls.cFoo(), 'this is PyClass.cFoo')
87+
self.assertEqual(cls.sFoo(), 'this is PyClass.sFoo')
88+
obj = cls()
89+
self.assertEqual(obj.foo(), 'this is PyClass.foo')
90+
91+
def testMakeAncestor2(self):
92+
cls = MixIn(self.pyClass, self.mixIn2, makeAncestor=True)
93+
self.assertEqual(cls.__bases__, (self.mixIn2, self.base))
94+
self.assertFalse(hasattr(cls, 'mixInsForPyClass'))
95+
self.assertFalse(hasattr(self.mixIn2, 'mixInSuperFoo'))
96+
self.assertEqual(cls.attr0, 'this is Base.attr0')
97+
self.assertEqual(cls.attr1, 'this is PyClass.attr1')
98+
self.assertEqual(cls.attr2, 'this is PyClass.attr2')
99+
self.assertEqual(cls.attr3, 'this is PyClass.attr3')
100+
self.assertEqual(cls.attr5, 'this is MixIn2.attr5')
101+
self.assertEqual(cls.attr6, 'this is MixIn2.attr6')
102+
self.assertEqual(cls.cFoo(), 'this is PyClass.cFoo')
103+
self.assertEqual(cls.sFoo(), 'this is PyClass.sFoo')
104+
obj = cls()
105+
self.assertEqual(obj.foo(), 'this is PyClass.foo')
106+
self.assertEqual(obj.bar(), 'this is MixIn2.bar')
107+
self.assertEqual(obj.baz(), 'this is MixIn2Base.baz')
108+
109+
def testMixIn1(self):
110+
cls = self.pyClass
111+
MixIn(cls, self.mixIn1)
112+
self.assertEqual(cls.__bases__, (self.base,))
113+
self.assertEqual(cls.mixInsForPyClass, [self.mixIn1])
114+
self.assertFalse(hasattr(self.mixIn1, 'mixInSuperFoo'))
115+
self.assertEqual(cls.attr0, 'this is Base.attr0')
116+
self.assertEqual(cls.attr1, 'this is PyClass.attr1')
117+
self.assertEqual(cls.attr2, 'this is PyClass.attr2')
118+
self.assertEqual(cls.attr3, 'this is MixIn1.attr3')
119+
self.assertEqual(cls.attr4, 'this is MixIn1.attr4')
120+
self.assertEqual(cls.cFoo(), 'this is MixIn1.cFoo')
121+
self.assertEqual(cls.sFoo(), 'this is MixIn1.sFoo')
122+
obj = cls()
123+
self.assertEqual(obj.foo(), 'this is MixIn1.foo')
124+
125+
def testMixIn2(self):
126+
cls = self.pyClass
127+
MixIn(cls, self.mixIn2)
128+
self.assertEqual(cls.__bases__, (self.base,))
129+
self.assertEqual(cls.mixInsForPyClass, [self.mixIn2Base, self.mixIn2])
130+
self.assertFalse(hasattr(self.mixIn2, 'mixInSuperFoo'))
131+
self.assertEqual(cls.attr0, 'this is Base.attr0')
132+
self.assertEqual(cls.attr1, 'this is PyClass.attr1')
133+
self.assertEqual(cls.attr2, 'this is PyClass.attr2')
134+
self.assertEqual(cls.attr3, 'this is PyClass.attr3')
135+
self.assertEqual(cls.attr5, 'this is MixIn2.attr5')
136+
self.assertEqual(cls.attr6, 'this is MixIn2.attr6')
137+
self.assertEqual(cls.cFoo(), 'this is PyClass.cFoo')
138+
self.assertEqual(cls.sFoo(), 'this is PyClass.sFoo')
139+
obj = cls()
140+
self.assertEqual(obj.foo(), 'this is PyClass.foo')
141+
self.assertEqual(obj.bar(), 'this is MixIn2.bar')
142+
self.assertEqual(obj.baz(), 'this is MixIn2Base.baz')
143+
144+
def testMixIn1And2(self):
145+
cls = self.pyClass
146+
MixIn(cls, self.mixIn1)
147+
MixIn(cls, self.mixIn2)
148+
self.assertEqual(cls.__bases__, (self.base,))
149+
self.assertEqual(cls.mixInsForPyClass, [
150+
self.mixIn1, self.mixIn2Base, self.mixIn2])
151+
self.assertFalse(hasattr(self.mixIn1, 'mixInSuperFoo'))
152+
self.assertFalse(hasattr(self.mixIn2, 'mixInSuperFoo'))
153+
self.assertEqual(cls.attr0, 'this is Base.attr0')
154+
self.assertEqual(cls.attr1, 'this is PyClass.attr1')
155+
self.assertEqual(cls.attr2, 'this is PyClass.attr2')
156+
self.assertEqual(cls.attr3, 'this is MixIn1.attr3')
157+
self.assertEqual(cls.attr4, 'this is MixIn1.attr4')
158+
self.assertEqual(cls.attr5, 'this is MixIn2.attr5')
159+
self.assertEqual(cls.attr6, 'this is MixIn2.attr6')
160+
self.assertEqual(cls.cFoo(), 'this is MixIn1.cFoo')
161+
self.assertEqual(cls.sFoo(), 'this is MixIn1.sFoo')
162+
obj = cls()
163+
self.assertEqual(obj.foo(), 'this is MixIn1.foo')
164+
self.assertEqual(obj.bar(), 'this is MixIn2.bar')
165+
self.assertEqual(obj.baz(), 'this is MixIn2Base.baz')
166+
167+
def testMixIn1WithSuper(self):
168+
cls, mixIn = self.pyClass, self.mixIn1
169+
MixIn(cls, mixIn, mixInSuperMethods=True)
170+
self.assertEqual(cls.__bases__, (self.base,))
171+
self.assertEqual(cls.mixInsForPyClass, [mixIn])
172+
self.assertTrue(hasattr(mixIn, 'mixInSuperFoo'))
173+
self.assertTrue(hasattr(mixIn, 'mixInSuperCFoo'))
174+
self.assertTrue(hasattr(mixIn, 'mixInSuperSFoo'))
175+
self.assertEqual(cls.attr0, 'this is Base.attr0')
176+
self.assertEqual(cls.attr1, 'this is PyClass.attr1')
177+
self.assertEqual(cls.attr2, 'this is PyClass.attr2')
178+
self.assertEqual(cls.attr3, 'this is MixIn1.attr3')
179+
self.assertEqual(cls.attr4, 'this is MixIn1.attr4')
180+
self.assertEqual(cls.cFoo(), 'this is MixIn1.cFoo')
181+
self.assertEqual(cls.sFoo(), 'this is MixIn1.sFoo')
182+
self.assertEqual(mixIn.mixInSuperCFoo(), 'this is PyClass.cFoo')
183+
self.assertEqual(mixIn.mixInSuperSFoo(), 'this is PyClass.sFoo')
184+
obj = cls()
185+
self.assertEqual(obj.foo(), 'this is MixIn1.foo')
186+
superFoo = mixIn.mixInSuperFoo
187+
self.assertEqual(superFoo(self), 'this is PyClass.foo')
188+
189+
def testMixIn2WithSuper(self):
190+
cls, mixIn = self.pyClass, self.mixIn2
191+
MixIn(cls, mixIn, mixInSuperMethods=True)
192+
self.assertEqual(cls.__bases__, (self.base,))
193+
self.assertEqual(cls.mixInsForPyClass, [self.mixIn2Base, mixIn])
194+
self.assertFalse(hasattr(mixIn, 'mixInSuperFoo'))
195+
self.assertFalse(hasattr(mixIn, 'mixInSuperCFoo'))
196+
self.assertFalse(hasattr(mixIn, 'mixInSuperSFoo'))
197+
self.assertEqual(cls.attr0, 'this is Base.attr0')
198+
self.assertEqual(cls.attr1, 'this is PyClass.attr1')
199+
self.assertEqual(cls.attr2, 'this is PyClass.attr2')
200+
self.assertEqual(cls.attr3, 'this is PyClass.attr3')
201+
self.assertEqual(cls.attr5, 'this is MixIn2.attr5')
202+
self.assertEqual(cls.attr6, 'this is MixIn2.attr6')
203+
self.assertEqual(cls.cFoo(), 'this is PyClass.cFoo')
204+
self.assertEqual(cls.sFoo(), 'this is PyClass.sFoo')
205+
obj = cls()
206+
self.assertEqual(obj.foo(), 'this is PyClass.foo')
207+
self.assertEqual(obj.bar(), 'this is MixIn2.bar')
208+
self.assertEqual(obj.baz(), 'this is MixIn2Base.baz')

0 commit comments

Comments
 (0)