Skip to content

Commit 2b5fd1e

Browse files
authored
bpo-32226: Implementation of PEP 560 (core components) (#4732)
This part of the PEP implementation adds support for __mro_entries__ and __class_getitem__ by updating __build_class__ and PyObject_GetItem.
1 parent 15a8728 commit 2b5fd1e

File tree

7 files changed

+492
-5
lines changed

7 files changed

+492
-5
lines changed

Lib/test/test_genericclass.py

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import unittest
2+
3+
4+
class TestMROEntry(unittest.TestCase):
5+
def test_mro_entry_signature(self):
6+
tested = []
7+
class B: ...
8+
class C:
9+
def __mro_entries__(self, *args, **kwargs):
10+
tested.extend([args, kwargs])
11+
return (C,)
12+
c = C()
13+
self.assertEqual(tested, [])
14+
class D(B, c): ...
15+
self.assertEqual(tested[0], ((B, c),))
16+
self.assertEqual(tested[1], {})
17+
18+
def test_mro_entry(self):
19+
tested = []
20+
class A: ...
21+
class B: ...
22+
class C:
23+
def __mro_entries__(self, bases):
24+
tested.append(bases)
25+
return (self.__class__,)
26+
c = C()
27+
self.assertEqual(tested, [])
28+
class D(A, c, B): ...
29+
self.assertEqual(tested[-1], (A, c, B))
30+
self.assertEqual(D.__bases__, (A, C, B))
31+
self.assertEqual(D.__orig_bases__, (A, c, B))
32+
self.assertEqual(D.__mro__, (D, A, C, B, object))
33+
d = D()
34+
class E(d): ...
35+
self.assertEqual(tested[-1], (d,))
36+
self.assertEqual(E.__bases__, (D,))
37+
38+
def test_mro_entry_none(self):
39+
tested = []
40+
class A: ...
41+
class B: ...
42+
class C:
43+
def __mro_entries__(self, bases):
44+
tested.append(bases)
45+
return ()
46+
c = C()
47+
self.assertEqual(tested, [])
48+
class D(A, c, B): ...
49+
self.assertEqual(tested[-1], (A, c, B))
50+
self.assertEqual(D.__bases__, (A, B))
51+
self.assertEqual(D.__orig_bases__, (A, c, B))
52+
self.assertEqual(D.__mro__, (D, A, B, object))
53+
class E(c): ...
54+
self.assertEqual(tested[-1], (c,))
55+
self.assertEqual(E.__bases__, (object,))
56+
self.assertEqual(E.__orig_bases__, (c,))
57+
self.assertEqual(E.__mro__, (E, object))
58+
59+
def test_mro_entry_with_builtins(self):
60+
tested = []
61+
class A: ...
62+
class C:
63+
def __mro_entries__(self, bases):
64+
tested.append(bases)
65+
return (dict,)
66+
c = C()
67+
self.assertEqual(tested, [])
68+
class D(A, c): ...
69+
self.assertEqual(tested[-1], (A, c))
70+
self.assertEqual(D.__bases__, (A, dict))
71+
self.assertEqual(D.__orig_bases__, (A, c))
72+
self.assertEqual(D.__mro__, (D, A, dict, object))
73+
74+
def test_mro_entry_with_builtins_2(self):
75+
tested = []
76+
class C:
77+
def __mro_entries__(self, bases):
78+
tested.append(bases)
79+
return (C,)
80+
c = C()
81+
self.assertEqual(tested, [])
82+
class D(c, dict): ...
83+
self.assertEqual(tested[-1], (c, dict))
84+
self.assertEqual(D.__bases__, (C, dict))
85+
self.assertEqual(D.__orig_bases__, (c, dict))
86+
self.assertEqual(D.__mro__, (D, C, dict, object))
87+
88+
def test_mro_entry_errors(self):
89+
class C_too_many:
90+
def __mro_entries__(self, bases, something, other):
91+
return ()
92+
c = C_too_many()
93+
with self.assertRaises(TypeError):
94+
class D(c): ...
95+
class C_too_few:
96+
def __mro_entries__(self):
97+
return ()
98+
d = C_too_few()
99+
with self.assertRaises(TypeError):
100+
class D(d): ...
101+
102+
def test_mro_entry_errors_2(self):
103+
class C_not_callable:
104+
__mro_entries__ = "Surprise!"
105+
c = C_not_callable()
106+
with self.assertRaises(TypeError):
107+
class D(c): ...
108+
class C_not_tuple:
109+
def __mro_entries__(self):
110+
return object
111+
c = C_not_tuple()
112+
with self.assertRaises(TypeError):
113+
class D(c): ...
114+
115+
def test_mro_entry_metaclass(self):
116+
meta_args = []
117+
class Meta(type):
118+
def __new__(mcls, name, bases, ns):
119+
meta_args.extend([mcls, name, bases, ns])
120+
return super().__new__(mcls, name, bases, ns)
121+
class A: ...
122+
class C:
123+
def __mro_entries__(self, bases):
124+
return (A,)
125+
c = C()
126+
class D(c, metaclass=Meta):
127+
x = 1
128+
self.assertEqual(meta_args[0], Meta)
129+
self.assertEqual(meta_args[1], 'D')
130+
self.assertEqual(meta_args[2], (A,))
131+
self.assertEqual(meta_args[3]['x'], 1)
132+
self.assertEqual(D.__bases__, (A,))
133+
self.assertEqual(D.__orig_bases__, (c,))
134+
self.assertEqual(D.__mro__, (D, A, object))
135+
self.assertEqual(D.__class__, Meta)
136+
137+
def test_mro_entry_type_call(self):
138+
# Substitution should _not_ happen in direct type call
139+
class C:
140+
def __mro_entries__(self, bases):
141+
return ()
142+
c = C()
143+
with self.assertRaisesRegex(TypeError,
144+
"MRO entry resolution; "
145+
"use types.new_class()"):
146+
type('Bad', (c,), {})
147+
148+
149+
class TestClassGetitem(unittest.TestCase):
150+
def test_class_getitem(self):
151+
getitem_args = []
152+
class C:
153+
def __class_getitem__(*args, **kwargs):
154+
getitem_args.extend([args, kwargs])
155+
return None
156+
C[int, str]
157+
self.assertEqual(getitem_args[0], (C, (int, str)))
158+
self.assertEqual(getitem_args[1], {})
159+
160+
def test_class_getitem(self):
161+
class C:
162+
def __class_getitem__(cls, item):
163+
return f'C[{item.__name__}]'
164+
self.assertEqual(C[int], 'C[int]')
165+
self.assertEqual(C[C], 'C[C]')
166+
167+
def test_class_getitem_inheritance(self):
168+
class C:
169+
def __class_getitem__(cls, item):
170+
return f'{cls.__name__}[{item.__name__}]'
171+
class D(C): ...
172+
self.assertEqual(D[int], 'D[int]')
173+
self.assertEqual(D[D], 'D[D]')
174+
175+
def test_class_getitem_inheritance_2(self):
176+
class C:
177+
def __class_getitem__(cls, item):
178+
return 'Should not see this'
179+
class D(C):
180+
def __class_getitem__(cls, item):
181+
return f'{cls.__name__}[{item.__name__}]'
182+
self.assertEqual(D[int], 'D[int]')
183+
self.assertEqual(D[D], 'D[D]')
184+
185+
def test_class_getitem_patched(self):
186+
class C:
187+
def __init_subclass__(cls):
188+
def __class_getitem__(cls, item):
189+
return f'{cls.__name__}[{item.__name__}]'
190+
cls.__class_getitem__ = __class_getitem__
191+
class D(C): ...
192+
self.assertEqual(D[int], 'D[int]')
193+
self.assertEqual(D[D], 'D[D]')
194+
195+
def test_class_getitem_with_builtins(self):
196+
class A(dict):
197+
called_with = None
198+
199+
def __class_getitem__(cls, item):
200+
cls.called_with = item
201+
class B(A):
202+
pass
203+
self.assertIs(B.called_with, None)
204+
B[int]
205+
self.assertIs(B.called_with, int)
206+
207+
def test_class_getitem_errors(self):
208+
class C_too_few:
209+
def __class_getitem__(cls):
210+
return None
211+
with self.assertRaises(TypeError):
212+
C_too_few[int]
213+
class C_too_many:
214+
def __class_getitem__(cls, one, two):
215+
return None
216+
with self.assertRaises(TypeError):
217+
C_too_many[int]
218+
219+
def test_class_getitem_errors_2(self):
220+
class C:
221+
def __class_getitem__(cls, item):
222+
return None
223+
with self.assertRaises(TypeError):
224+
C()[int]
225+
class E: ...
226+
e = E()
227+
e.__class_getitem__ = lambda cls, item: 'This will not work'
228+
with self.assertRaises(TypeError):
229+
e[int]
230+
class C_not_callable:
231+
__class_getitem__ = "Surprise!"
232+
with self.assertRaises(TypeError):
233+
C_not_callable[int]
234+
235+
def test_class_getitem_metaclass(self):
236+
class Meta(type):
237+
def __class_getitem__(cls, item):
238+
return f'{cls.__name__}[{item.__name__}]'
239+
self.assertEqual(Meta[int], 'Meta[int]')
240+
241+
def test_class_getitem_metaclass_2(self):
242+
class Meta(type):
243+
def __getitem__(cls, item):
244+
return 'from metaclass'
245+
class C(metaclass=Meta):
246+
def __class_getitem__(cls, item):
247+
return 'from __class_getitem__'
248+
self.assertEqual(C[int], 'from metaclass')
249+
250+
251+
if __name__ == "__main__":
252+
unittest.main()

Lib/test/test_types.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,68 @@ def func(ns):
844844
self.assertEqual(C.y, 1)
845845
self.assertEqual(C.z, 2)
846846

847+
def test_new_class_with_mro_entry(self):
848+
class A: pass
849+
class C:
850+
def __mro_entries__(self, bases):
851+
return (A,)
852+
c = C()
853+
D = types.new_class('D', (c,), {})
854+
self.assertEqual(D.__bases__, (A,))
855+
self.assertEqual(D.__orig_bases__, (c,))
856+
self.assertEqual(D.__mro__, (D, A, object))
857+
858+
def test_new_class_with_mro_entry_none(self):
859+
class A: pass
860+
class B: pass
861+
class C:
862+
def __mro_entries__(self, bases):
863+
return ()
864+
c = C()
865+
D = types.new_class('D', (A, c, B), {})
866+
self.assertEqual(D.__bases__, (A, B))
867+
self.assertEqual(D.__orig_bases__, (A, c, B))
868+
self.assertEqual(D.__mro__, (D, A, B, object))
869+
870+
def test_new_class_with_mro_entry_error(self):
871+
class A: pass
872+
class C:
873+
def __mro_entries__(self, bases):
874+
return A
875+
c = C()
876+
with self.assertRaises(TypeError):
877+
types.new_class('D', (c,), {})
878+
879+
def test_new_class_with_mro_entry_multiple(self):
880+
class A1: pass
881+
class A2: pass
882+
class B1: pass
883+
class B2: pass
884+
class A:
885+
def __mro_entries__(self, bases):
886+
return (A1, A2)
887+
class B:
888+
def __mro_entries__(self, bases):
889+
return (B1, B2)
890+
D = types.new_class('D', (A(), B()), {})
891+
self.assertEqual(D.__bases__, (A1, A2, B1, B2))
892+
893+
def test_new_class_with_mro_entry_multiple_2(self):
894+
class A1: pass
895+
class A2: pass
896+
class A3: pass
897+
class B1: pass
898+
class B2: pass
899+
class A:
900+
def __mro_entries__(self, bases):
901+
return (A1, A2, A3)
902+
class B:
903+
def __mro_entries__(self, bases):
904+
return (B1, B2)
905+
class C: pass
906+
D = types.new_class('D', (A(), C, B()), {})
907+
self.assertEqual(D.__bases__, (A1, A2, A3, C, B1, B2))
908+
847909
# Many of the following tests are derived from test_descr.py
848910
def test_prepare_class(self):
849911
# Basic test of metaclass derivation
@@ -886,6 +948,28 @@ def __prepare__(*args):
886948
class Bar(metaclass=BadMeta()):
887949
pass
888950

951+
def test_resolve_bases(self):
952+
class A: pass
953+
class B: pass
954+
class C:
955+
def __mro_entries__(self, bases):
956+
if A in bases:
957+
return ()
958+
return (A,)
959+
c = C()
960+
self.assertEqual(types.resolve_bases(()), ())
961+
self.assertEqual(types.resolve_bases((c,)), (A,))
962+
self.assertEqual(types.resolve_bases((C,)), (C,))
963+
self.assertEqual(types.resolve_bases((A, C)), (A, C))
964+
self.assertEqual(types.resolve_bases((c, A)), (A,))
965+
self.assertEqual(types.resolve_bases((A, c)), (A,))
966+
x = (A,)
967+
y = (C,)
968+
z = (A, C)
969+
t = (A, C, B)
970+
for bases in [x, y, z, t]:
971+
self.assertIs(types.resolve_bases(bases), bases)
972+
889973
def test_metaclass_derivation(self):
890974
# issue1294232: correct metaclass calculation
891975
new_calls = [] # to check the order of __new__ calls

Lib/types.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,34 @@ def _m(self): pass
6060
# Provide a PEP 3115 compliant mechanism for class creation
6161
def new_class(name, bases=(), kwds=None, exec_body=None):
6262
"""Create a class object dynamically using the appropriate metaclass."""
63-
meta, ns, kwds = prepare_class(name, bases, kwds)
63+
resolved_bases = resolve_bases(bases)
64+
meta, ns, kwds = prepare_class(name, resolved_bases, kwds)
6465
if exec_body is not None:
6566
exec_body(ns)
66-
return meta(name, bases, ns, **kwds)
67+
if resolved_bases is not bases:
68+
ns['__orig_bases__'] = bases
69+
return meta(name, resolved_bases, ns, **kwds)
70+
71+
def resolve_bases(bases):
72+
"""Resolve MRO entries dynamically as specified by PEP 560."""
73+
new_bases = list(bases)
74+
updated = False
75+
shift = 0
76+
for i, base in enumerate(bases):
77+
if isinstance(base, type):
78+
continue
79+
if not hasattr(base, "__mro_entries__"):
80+
continue
81+
new_base = base.__mro_entries__(bases)
82+
updated = True
83+
if not isinstance(new_base, tuple):
84+
raise TypeError("__mro_entries__ must return a tuple")
85+
else:
86+
new_bases[i+shift:i+shift+1] = new_base
87+
shift += len(new_base) - 1
88+
if not updated:
89+
return bases
90+
return tuple(new_bases)
6791

6892
def prepare_class(name, bases=(), kwds=None):
6993
"""Call the __prepare__ method of the appropriate metaclass.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
PEP 560: Add support for __mro_entries__ and __class_getitem__. Implemented
2+
by Ivan Levkivskyi.

0 commit comments

Comments
 (0)