Skip to content

Commit 372fc3d

Browse files
committed
support __mro_entries__ as per PEP 560
1 parent fefd829 commit 372fc3d

File tree

2 files changed

+161
-2
lines changed

2 files changed

+161
-2
lines changed

graalpython/com.oracle.graal.python.test/src/tests/test_genericclass.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,140 @@
55
import unittest
66

77

8+
class TestMROEntry(unittest.TestCase):
9+
def test_mro_entry_signature(self):
10+
tested = []
11+
class B: ...
12+
class C:
13+
def __mro_entries__(self, *args, **kwargs):
14+
tested.extend([args, kwargs])
15+
return (C,)
16+
c = C()
17+
self.assertEqual(tested, [])
18+
class D(B, c): ...
19+
self.assertEqual(tested[0], ((B, c),))
20+
self.assertEqual(tested[1], {})
21+
22+
def test_mro_entry(self):
23+
tested = []
24+
class A: ...
25+
class B: ...
26+
class C:
27+
def __mro_entries__(self, bases):
28+
tested.append(bases)
29+
return (self.__class__,)
30+
c = C()
31+
self.assertEqual(tested, [])
32+
class D(A, c, B): ...
33+
self.assertEqual(tested[-1], (A, c, B))
34+
self.assertEqual(D.__bases__, (A, C, B))
35+
self.assertEqual(D.__orig_bases__, (A, c, B))
36+
self.assertEqual(D.__mro__, (D, A, C, B, object))
37+
d = D()
38+
class E(d): ...
39+
self.assertEqual(tested[-1], (d,))
40+
self.assertEqual(E.__bases__, (D,))
41+
42+
def test_mro_entry_none(self):
43+
tested = []
44+
class A: ...
45+
class B: ...
46+
class C:
47+
def __mro_entries__(self, bases):
48+
tested.append(bases)
49+
return ()
50+
c = C()
51+
self.assertEqual(tested, [])
52+
class D(A, c, B): ...
53+
self.assertEqual(tested[-1], (A, c, B))
54+
self.assertEqual(D.__bases__, (A, B))
55+
self.assertEqual(D.__orig_bases__, (A, c, B))
56+
self.assertEqual(D.__mro__, (D, A, B, object))
57+
class E(c): ...
58+
self.assertEqual(tested[-1], (c,))
59+
self.assertEqual(E.__bases__, (object,))
60+
self.assertEqual(E.__orig_bases__, (c,))
61+
self.assertEqual(E.__mro__, (E, object))
62+
63+
def test_mro_entry_with_builtins(self):
64+
tested = []
65+
class A: ...
66+
class C:
67+
def __mro_entries__(self, bases):
68+
tested.append(bases)
69+
return (dict,)
70+
c = C()
71+
self.assertEqual(tested, [])
72+
class D(A, c): ...
73+
self.assertEqual(tested[-1], (A, c))
74+
self.assertEqual(D.__bases__, (A, dict))
75+
self.assertEqual(D.__orig_bases__, (A, c))
76+
self.assertEqual(D.__mro__, (D, A, dict, object))
77+
78+
def test_mro_entry_with_builtins_2(self):
79+
tested = []
80+
class C:
81+
def __mro_entries__(self, bases):
82+
tested.append(bases)
83+
return (C,)
84+
c = C()
85+
self.assertEqual(tested, [])
86+
class D(c, dict): ...
87+
self.assertEqual(tested[-1], (c, dict))
88+
self.assertEqual(D.__bases__, (C, dict))
89+
self.assertEqual(D.__orig_bases__, (c, dict))
90+
self.assertEqual(D.__mro__, (D, C, dict, object))
91+
92+
def test_mro_entry_errors(self):
93+
class C_too_many:
94+
def __mro_entries__(self, bases, something, other):
95+
return ()
96+
c = C_too_many()
97+
with self.assertRaises(TypeError):
98+
class D(c): ...
99+
class C_too_few:
100+
def __mro_entries__(self):
101+
return ()
102+
d = C_too_few()
103+
with self.assertRaises(TypeError):
104+
class D(d): ...
105+
106+
def test_mro_entry_errors_2(self):
107+
class C_not_callable:
108+
__mro_entries__ = "Surprise!"
109+
c = C_not_callable()
110+
with self.assertRaises(TypeError):
111+
class D(c): ...
112+
class C_not_tuple:
113+
def __mro_entries__(self):
114+
return object
115+
c = C_not_tuple()
116+
with self.assertRaises(TypeError):
117+
class D(c): ...
118+
119+
def test_mro_entry_metaclass(self):
120+
meta_args = []
121+
class Meta(type):
122+
def __new__(mcls, name, bases, ns):
123+
meta_args.extend([mcls, name, bases, ns])
124+
return super().__new__(mcls, name, bases, ns)
125+
class A: ...
126+
class C:
127+
def __mro_entries__(self, bases):
128+
return (A,)
129+
c = C()
130+
class D(c, metaclass=Meta):
131+
x = 1
132+
self.assertEqual(meta_args[0], Meta)
133+
self.assertEqual(meta_args[1], 'D')
134+
self.assertEqual(meta_args[2], (A,))
135+
self.assertEqual(meta_args[3]['x'], 1)
136+
self.assertEqual(D.__bases__, (A,))
137+
self.assertEqual(D.__orig_bases__, (c,))
138+
self.assertEqual(D.__mro__, (D, A, object))
139+
self.assertEqual(D.__class__, Meta)
140+
141+
8142
class TestClassGetitem(unittest.TestCase):
9143
def test_class_getitem(self):
10144
getitem_args = []

graalpython/lib-graalpython/classes.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,35 @@
4444
# Provide a PEP 3115 compliant mechanism for class creation
4545
def new_class(name, bases=(), kwds=None, exec_body=None):
4646
"""Create a class object dynamically using the appropriate metaclass."""
47-
meta, ns, kwds = prepare_class(name, bases, kwds)
47+
resolved_bases = resolve_bases(bases)
48+
meta, ns, kwds = prepare_class(name, resolved_bases, kwds)
4849
if exec_body is not None:
4950
exec_body(ns)
50-
return meta(name, bases, ns, **kwds)
51+
if resolved_bases is not bases:
52+
ns['__orig_bases__'] = bases
53+
return meta(name, resolved_bases, ns, **kwds)
54+
55+
56+
def resolve_bases(bases):
57+
"""Resolve MRO entries dynamically as specified by PEP 560."""
58+
new_bases = list(bases)
59+
updated = False
60+
shift = 0
61+
for i, base in enumerate(bases):
62+
if isinstance(base, type):
63+
continue
64+
if not hasattr(base, "__mro_entries__"):
65+
continue
66+
new_base = base.__mro_entries__(bases)
67+
updated = True
68+
if not isinstance(new_base, tuple):
69+
raise TypeError("__mro_entries__ must return a tuple")
70+
else:
71+
new_bases[i+shift:i+shift+1] = new_base
72+
shift += len(new_base) - 1
73+
if not updated:
74+
return bases
75+
return tuple(new_bases)
5176

5277

5378
def prepare_class(name, bases=(), kwds=None):

0 commit comments

Comments
 (0)