Skip to content

bpo-32226: Implementation of PEP 560 (core components) #4732

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Dec 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
26afdc6
POC implementation of __base_subclass__
ilevkivskyi Jul 19, 2017
3285fb6
POC implementation of __class_getitem__
ilevkivskyi Jul 19, 2017
779f85d
Modify __base_subclass__ API to support dynamic evaluation and base r…
ilevkivskyi Jul 20, 2017
5d5211d
Make __base_subclass__ faster and safer
ilevkivskyi Jul 20, 2017
41fa7e9
Factor out base update in a separate helper
ilevkivskyi Jul 20, 2017
5aeebab
Formatting
ilevkivskyi Jul 20, 2017
e858ea7
Also make __class_getitem__ safer
ilevkivskyi Jul 20, 2017
5b8d453
Simplify some code
ilevkivskyi Jul 21, 2017
b3e52f1
Initial work on typing2
ilevkivskyi Jul 22, 2017
6e7b575
Remove test implementation for typing
ilevkivskyi Sep 3, 2017
a8437fa
Rename base_subclass to subclass_base
Sep 7, 2017
c2d8ac2
Start adding tests
Sep 7, 2017
bc06c6b
Add tests for __subclass_base__
Sep 8, 2017
86c5d61
Add tests for __class_getitem__
Sep 8, 2017
2cef78a
Fix trailing whitespace
ilevkivskyi Sep 9, 2017
b9bbf7c
Alternative implementation of __class_getitem__
ilevkivskyi Sep 10, 2017
2495e94
Simplify code and fix reference counting
ilevkivskyi Sep 10, 2017
9ca8dfc
Rename __base_subclass__ to __mro_entry__
ilevkivskyi Nov 11, 2017
74bd36f
Add types.resolve_bases
ilevkivskyi Nov 11, 2017
6f20c45
Make Python and C versions as similar as possible
ilevkivskyi Nov 11, 2017
bcfbcf6
More tests
ilevkivskyi Nov 11, 2017
d7bd630
Fail fast in type.__new__
ilevkivskyi Nov 11, 2017
29e54c3
Fix a test
ilevkivskyi Nov 11, 2017
cba2a58
Test error message
ilevkivskyi Nov 11, 2017
966a9ea
Test error message
ilevkivskyi Nov 11, 2017
734c7e9
Test error message
ilevkivskyi Nov 11, 2017
152d2f6
Allow expansion to multiple bases
Nov 13, 2017
1a218e0
Prohibit returning non-tuple
ilevkivskyi Nov 14, 2017
7cc8d8f
Rename to __mro_entries__
ilevkivskyi Nov 14, 2017
57f477c
Merge remote-tracking branch 'upstream/master' into base-subclass
ilevkivskyi Dec 5, 2017
d0c0889
Add news entry
ilevkivskyi Dec 5, 2017
bfea2f0
Fix indentation and refleak
ilevkivskyi Dec 6, 2017
978588c
Address CR (part 1)
ilevkivskyi Dec 8, 2017
7bff68d
Address CR (part 2) use a temporary tuple instead of modifying args i…
ilevkivskyi Dec 8, 2017
b3d85ff
Remove checks for callability of special methods
ilevkivskyi Dec 10, 2017
687e1e7
Some simplifications
ilevkivskyi Dec 11, 2017
4426e8c
More simplifications: use single pass
ilevkivskyi Dec 12, 2017
b7d28d8
Some fixes + performance
ilevkivskyi Dec 12, 2017
dce0218
Merge remote-tracking branch 'upstream/master' into base-subclass
ilevkivskyi Dec 12, 2017
caf9238
Re-organize code and fix formatting
Dec 12, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 252 additions & 0 deletions Lib/test/test_genericclass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import unittest


class TestMROEntry(unittest.TestCase):
def test_mro_entry_signature(self):
tested = []
class B: ...
class C:
def __mro_entries__(self, *args, **kwargs):
tested.extend([args, kwargs])
return (C,)
c = C()
self.assertEqual(tested, [])
class D(B, c): ...
self.assertEqual(tested[0], ((B, c),))
self.assertEqual(tested[1], {})

def test_mro_entry(self):
tested = []
class A: ...
class B: ...
class C:
def __mro_entries__(self, bases):
tested.append(bases)
return (self.__class__,)
c = C()
self.assertEqual(tested, [])
class D(A, c, B): ...
self.assertEqual(tested[-1], (A, c, B))
self.assertEqual(D.__bases__, (A, C, B))
self.assertEqual(D.__orig_bases__, (A, c, B))
self.assertEqual(D.__mro__, (D, A, C, B, object))
d = D()
class E(d): ...
self.assertEqual(tested[-1], (d,))
self.assertEqual(E.__bases__, (D,))

def test_mro_entry_none(self):
tested = []
class A: ...
class B: ...
class C:
def __mro_entries__(self, bases):
tested.append(bases)
return ()
c = C()
self.assertEqual(tested, [])
class D(A, c, B): ...
self.assertEqual(tested[-1], (A, c, B))
self.assertEqual(D.__bases__, (A, B))
self.assertEqual(D.__orig_bases__, (A, c, B))
self.assertEqual(D.__mro__, (D, A, B, object))
class E(c): ...
self.assertEqual(tested[-1], (c,))
self.assertEqual(E.__bases__, (object,))
self.assertEqual(E.__orig_bases__, (c,))
self.assertEqual(E.__mro__, (E, object))

def test_mro_entry_with_builtins(self):
tested = []
class A: ...
class C:
def __mro_entries__(self, bases):
tested.append(bases)
return (dict,)
c = C()
self.assertEqual(tested, [])
class D(A, c): ...
self.assertEqual(tested[-1], (A, c))
self.assertEqual(D.__bases__, (A, dict))
self.assertEqual(D.__orig_bases__, (A, c))
self.assertEqual(D.__mro__, (D, A, dict, object))

def test_mro_entry_with_builtins_2(self):
tested = []
class C:
def __mro_entries__(self, bases):
tested.append(bases)
return (C,)
c = C()
self.assertEqual(tested, [])
class D(c, dict): ...
self.assertEqual(tested[-1], (c, dict))
self.assertEqual(D.__bases__, (C, dict))
self.assertEqual(D.__orig_bases__, (c, dict))
self.assertEqual(D.__mro__, (D, C, dict, object))

def test_mro_entry_errors(self):
class C_too_many:
def __mro_entries__(self, bases, something, other):
return ()
c = C_too_many()
with self.assertRaises(TypeError):
class D(c): ...
class C_too_few:
def __mro_entries__(self):
return ()
d = C_too_few()
with self.assertRaises(TypeError):
class D(d): ...

def test_mro_entry_errors_2(self):
class C_not_callable:
__mro_entries__ = "Surprise!"
c = C_not_callable()
with self.assertRaises(TypeError):
class D(c): ...
class C_not_tuple:
def __mro_entries__(self):
return object
c = C_not_tuple()
with self.assertRaises(TypeError):
class D(c): ...

def test_mro_entry_metaclass(self):
meta_args = []
class Meta(type):
def __new__(mcls, name, bases, ns):
meta_args.extend([mcls, name, bases, ns])
return super().__new__(mcls, name, bases, ns)
class A: ...
class C:
def __mro_entries__(self, bases):
return (A,)
c = C()
class D(c, metaclass=Meta):
x = 1
self.assertEqual(meta_args[0], Meta)
self.assertEqual(meta_args[1], 'D')
self.assertEqual(meta_args[2], (A,))
self.assertEqual(meta_args[3]['x'], 1)
self.assertEqual(D.__bases__, (A,))
self.assertEqual(D.__orig_bases__, (c,))
self.assertEqual(D.__mro__, (D, A, object))
self.assertEqual(D.__class__, Meta)

def test_mro_entry_type_call(self):
# Substitution should _not_ happen in direct type call
class C:
def __mro_entries__(self, bases):
return ()
c = C()
with self.assertRaisesRegex(TypeError,
"MRO entry resolution; "
"use types.new_class()"):
type('Bad', (c,), {})


class TestClassGetitem(unittest.TestCase):
def test_class_getitem(self):
getitem_args = []
class C:
def __class_getitem__(*args, **kwargs):
getitem_args.extend([args, kwargs])
return None
C[int, str]
self.assertEqual(getitem_args[0], (C, (int, str)))
self.assertEqual(getitem_args[1], {})

def test_class_getitem(self):
class C:
def __class_getitem__(cls, item):
return f'C[{item.__name__}]'
self.assertEqual(C[int], 'C[int]')
self.assertEqual(C[C], 'C[C]')

def test_class_getitem_inheritance(self):
class C:
def __class_getitem__(cls, item):
return f'{cls.__name__}[{item.__name__}]'
class D(C): ...
self.assertEqual(D[int], 'D[int]')
self.assertEqual(D[D], 'D[D]')

def test_class_getitem_inheritance_2(self):
class C:
def __class_getitem__(cls, item):
return 'Should not see this'
class D(C):
def __class_getitem__(cls, item):
return f'{cls.__name__}[{item.__name__}]'
self.assertEqual(D[int], 'D[int]')
self.assertEqual(D[D], 'D[D]')

def test_class_getitem_patched(self):
class C:
def __init_subclass__(cls):
def __class_getitem__(cls, item):
return f'{cls.__name__}[{item.__name__}]'
cls.__class_getitem__ = __class_getitem__
class D(C): ...
self.assertEqual(D[int], 'D[int]')
self.assertEqual(D[D], 'D[D]')

def test_class_getitem_with_builtins(self):
class A(dict):
called_with = None

def __class_getitem__(cls, item):
cls.called_with = item
class B(A):
pass
self.assertIs(B.called_with, None)
B[int]
self.assertIs(B.called_with, int)

def test_class_getitem_errors(self):
class C_too_few:
def __class_getitem__(cls):
return None
with self.assertRaises(TypeError):
C_too_few[int]
class C_too_many:
def __class_getitem__(cls, one, two):
return None
with self.assertRaises(TypeError):
C_too_many[int]

def test_class_getitem_errors_2(self):
class C:
def __class_getitem__(cls, item):
return None
with self.assertRaises(TypeError):
C()[int]
class E: ...
e = E()
e.__class_getitem__ = lambda cls, item: 'This will not work'
with self.assertRaises(TypeError):
e[int]
class C_not_callable:
__class_getitem__ = "Surprise!"
with self.assertRaises(TypeError):
C_not_callable[int]

def test_class_getitem_metaclass(self):
class Meta(type):
def __class_getitem__(cls, item):
return f'{cls.__name__}[{item.__name__}]'
self.assertEqual(Meta[int], 'Meta[int]')

def test_class_getitem_metaclass_2(self):
class Meta(type):
def __getitem__(cls, item):
return 'from metaclass'
class C(metaclass=Meta):
def __class_getitem__(cls, item):
return 'from __class_getitem__'
self.assertEqual(C[int], 'from metaclass')


if __name__ == "__main__":
unittest.main()
84 changes: 84 additions & 0 deletions Lib/test/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,68 @@ def func(ns):
self.assertEqual(C.y, 1)
self.assertEqual(C.z, 2)

def test_new_class_with_mro_entry(self):
class A: pass
class C:
def __mro_entries__(self, bases):
return (A,)
c = C()
D = types.new_class('D', (c,), {})
self.assertEqual(D.__bases__, (A,))
self.assertEqual(D.__orig_bases__, (c,))
self.assertEqual(D.__mro__, (D, A, object))

def test_new_class_with_mro_entry_none(self):
class A: pass
class B: pass
class C:
def __mro_entries__(self, bases):
return ()
c = C()
D = types.new_class('D', (A, c, B), {})
self.assertEqual(D.__bases__, (A, B))
self.assertEqual(D.__orig_bases__, (A, c, B))
self.assertEqual(D.__mro__, (D, A, B, object))

def test_new_class_with_mro_entry_error(self):
class A: pass
class C:
def __mro_entries__(self, bases):
return A
c = C()
with self.assertRaises(TypeError):
types.new_class('D', (c,), {})

def test_new_class_with_mro_entry_multiple(self):
class A1: pass
class A2: pass
class B1: pass
class B2: pass
class A:
def __mro_entries__(self, bases):
return (A1, A2)
class B:
def __mro_entries__(self, bases):
return (B1, B2)
D = types.new_class('D', (A(), B()), {})
self.assertEqual(D.__bases__, (A1, A2, B1, B2))

def test_new_class_with_mro_entry_multiple_2(self):
class A1: pass
class A2: pass
class A3: pass
class B1: pass
class B2: pass
class A:
def __mro_entries__(self, bases):
return (A1, A2, A3)
class B:
def __mro_entries__(self, bases):
return (B1, B2)
class C: pass
D = types.new_class('D', (A(), C, B()), {})
self.assertEqual(D.__bases__, (A1, A2, A3, C, B1, B2))

# Many of the following tests are derived from test_descr.py
def test_prepare_class(self):
# Basic test of metaclass derivation
Expand Down Expand Up @@ -886,6 +948,28 @@ def __prepare__(*args):
class Bar(metaclass=BadMeta()):
pass

def test_resolve_bases(self):
class A: pass
class B: pass
class C:
def __mro_entries__(self, bases):
if A in bases:
return ()
return (A,)
c = C()
self.assertEqual(types.resolve_bases(()), ())
self.assertEqual(types.resolve_bases((c,)), (A,))
self.assertEqual(types.resolve_bases((C,)), (C,))
self.assertEqual(types.resolve_bases((A, C)), (A, C))
self.assertEqual(types.resolve_bases((c, A)), (A,))
self.assertEqual(types.resolve_bases((A, c)), (A,))
x = (A,)
y = (C,)
z = (A, C)
t = (A, C, B)
for bases in [x, y, z, t]:
self.assertIs(types.resolve_bases(bases), bases)

def test_metaclass_derivation(self):
# issue1294232: correct metaclass calculation
new_calls = [] # to check the order of __new__ calls
Expand Down
28 changes: 26 additions & 2 deletions Lib/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,34 @@ def _m(self): pass
# Provide a PEP 3115 compliant mechanism for class creation
def new_class(name, bases=(), kwds=None, exec_body=None):
"""Create a class object dynamically using the appropriate metaclass."""
meta, ns, kwds = prepare_class(name, bases, kwds)
resolved_bases = resolve_bases(bases)
meta, ns, kwds = prepare_class(name, resolved_bases, kwds)
if exec_body is not None:
exec_body(ns)
return meta(name, bases, ns, **kwds)
if resolved_bases is not bases:
ns['__orig_bases__'] = bases
return meta(name, resolved_bases, ns, **kwds)

def resolve_bases(bases):
"""Resolve MRO entries dynamically as specified by PEP 560."""
new_bases = list(bases)
updated = False
shift = 0
for i, base in enumerate(bases):
if isinstance(base, type):
continue
if not hasattr(base, "__mro_entries__"):
continue
new_base = base.__mro_entries__(bases)
updated = True
if not isinstance(new_base, tuple):
raise TypeError("__mro_entries__ must return a tuple")
else:
new_bases[i+shift:i+shift+1] = new_base
shift += len(new_base) - 1
if not updated:
return bases
return tuple(new_bases)

def prepare_class(name, bases=(), kwds=None):
"""Call the __prepare__ method of the appropriate metaclass.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
PEP 560: Add support for __mro_entries__ and __class_getitem__. Implemented
by Ivan Levkivskyi.
Loading