Skip to content

Commit 83d4b71

Browse files
ilevkivskyigvanrossum
authored andcommitted
Include __extra__ base in generics MRO (#287)
Fixes #203.
1 parent 6f0ba61 commit 83d4b71

File tree

4 files changed

+167
-67
lines changed

4 files changed

+167
-67
lines changed

python2/test_typing.py

+31-19
Original file line numberDiff line numberDiff line change
@@ -573,8 +573,8 @@ class MyMapping(MutableMapping[str, str]): pass
573573
self.assertNotIsInstance({}, MyMapping)
574574
self.assertNotIsSubclass(dict, MyMapping)
575575

576-
def test_multiple_abc_bases(self):
577-
class MM1(MutableMapping[str, str], collections_abc.MutableMapping):
576+
def test_abc_bases(self):
577+
class MM(MutableMapping[str, str]):
578578
def __getitem__(self, k):
579579
return None
580580
def __setitem__(self, k, v):
@@ -585,24 +585,20 @@ def __iter__(self):
585585
return iter(())
586586
def __len__(self):
587587
return 0
588-
class MM2(collections_abc.MutableMapping, MutableMapping[str, str]):
589-
def __getitem__(self, k):
590-
return None
591-
def __setitem__(self, k, v):
592-
pass
593-
def __delitem__(self, k):
588+
# this should just work
589+
MM().update()
590+
self.assertIsInstance(MM(), collections_abc.MutableMapping)
591+
self.assertIsInstance(MM(), MutableMapping)
592+
self.assertNotIsInstance(MM(), List)
593+
self.assertNotIsInstance({}, MM)
594+
595+
def test_multiple_bases(self):
596+
class MM1(MutableMapping[str, str], collections_abc.MutableMapping):
597+
pass
598+
with self.assertRaises(TypeError):
599+
# consistent MRO not possible
600+
class MM2(collections_abc.MutableMapping, MutableMapping[str, str]):
594601
pass
595-
def __iter__(self):
596-
return iter(())
597-
def __len__(self):
598-
return 0
599-
# these two should just work
600-
MM1().update()
601-
MM2().update()
602-
self.assertIsInstance(MM1(), collections_abc.MutableMapping)
603-
self.assertIsInstance(MM1(), MutableMapping)
604-
self.assertIsInstance(MM2(), collections_abc.MutableMapping)
605-
self.assertIsInstance(MM2(), MutableMapping)
606602

607603
def test_pickle(self):
608604
global C # pickle wants to reference the class by name
@@ -1054,12 +1050,28 @@ class MMA(typing.MutableMapping):
10541050
MMA()
10551051

10561052
class MMC(MMA):
1053+
def __getitem__(self, k):
1054+
return None
1055+
def __setitem__(self, k, v):
1056+
pass
1057+
def __delitem__(self, k):
1058+
pass
1059+
def __iter__(self):
1060+
return iter(())
10571061
def __len__(self):
10581062
return 0
10591063

10601064
self.assertEqual(len(MMC()), 0)
10611065

10621066
class MMB(typing.MutableMapping[KT, VT]):
1067+
def __getitem__(self, k):
1068+
return None
1069+
def __setitem__(self, k, v):
1070+
pass
1071+
def __delitem__(self, k):
1072+
pass
1073+
def __iter__(self):
1074+
return iter(())
10631075
def __len__(self):
10641076
return 0
10651077

python2/typing.py

+53-14
Original file line numberDiff line numberDiff line change
@@ -974,11 +974,55 @@ def _next_in_mro(cls):
974974
return next_in_mro
975975

976976

977+
def _valid_for_check(cls):
978+
if cls is Generic:
979+
raise TypeError("Class %r cannot be used with class "
980+
"or instance checks" % cls)
981+
if (cls.__origin__ is not None and
982+
sys._getframe(3).f_globals['__name__'] != 'abc'):
983+
raise TypeError("Parameterized generics cannot be used with class "
984+
"or instance checks")
985+
986+
987+
def _make_subclasshook(cls):
988+
"""Construct a __subclasshook__ callable that incorporates
989+
the associated __extra__ class in subclass checks performed
990+
against cls.
991+
"""
992+
if isinstance(cls.__extra__, abc.ABCMeta):
993+
# The logic mirrors that of ABCMeta.__subclasscheck__.
994+
# Registered classes need not be checked here because
995+
# cls and its extra share the same _abc_registry.
996+
def __extrahook__(cls, subclass):
997+
_valid_for_check(cls)
998+
res = cls.__extra__.__subclasshook__(subclass)
999+
if res is not NotImplemented:
1000+
return res
1001+
if cls.__extra__ in subclass.__mro__:
1002+
return True
1003+
for scls in cls.__extra__.__subclasses__():
1004+
if isinstance(scls, GenericMeta):
1005+
continue
1006+
if issubclass(subclass, scls):
1007+
return True
1008+
return NotImplemented
1009+
else:
1010+
# For non-ABC extras we'll just call issubclass().
1011+
def __extrahook__(cls, subclass):
1012+
_valid_for_check(cls)
1013+
if cls.__extra__ and issubclass(subclass, cls.__extra__):
1014+
return True
1015+
return NotImplemented
1016+
return classmethod(__extrahook__)
1017+
1018+
9771019
class GenericMeta(TypingMeta, abc.ABCMeta):
9781020
"""Metaclass for generic types."""
9791021

9801022
def __new__(cls, name, bases, namespace,
9811023
tvars=None, args=None, origin=None, extra=None):
1024+
if extra is not None and type(extra) is abc.ABCMeta and extra not in bases:
1025+
bases = (extra,) + bases
9821026
self = super(GenericMeta, cls).__new__(cls, name, bases, namespace)
9831027

9841028
if tvars is not None:
@@ -1027,6 +1071,13 @@ def __new__(cls, name, bases, namespace,
10271071
self.__extra__ = namespace.get('__extra__')
10281072
# Speed hack (https://github.com/python/typing/issues/196).
10291073
self.__next_in_mro__ = _next_in_mro(self)
1074+
1075+
# This allows unparameterized generic collections to be used
1076+
# with issubclass() and isinstance() in the same way as their
1077+
# collections.abc counterparts (e.g., isinstance([], Iterable)).
1078+
self.__subclasshook__ = _make_subclasshook(self)
1079+
if isinstance(extra, abc.ABCMeta):
1080+
self._abc_registry = extra._abc_registry
10301081
return self
10311082

10321083
def _get_type_vars(self, tvars):
@@ -1111,20 +1162,8 @@ def __instancecheck__(self, instance):
11111162
# latter, we must extend __instancecheck__ too. For simplicity
11121163
# we just skip the cache check -- instance checks for generic
11131164
# classes are supposed to be rare anyways.
1114-
return self.__subclasscheck__(instance.__class__)
1115-
1116-
def __subclasscheck__(self, cls):
1117-
if self is Generic:
1118-
raise TypeError("Class %r cannot be used with class "
1119-
"or instance checks" % self)
1120-
if (self.__origin__ is not None and
1121-
sys._getframe(1).f_globals['__name__'] != 'abc'):
1122-
raise TypeError("Parameterized generics cannot be used with class "
1123-
"or instance checks")
1124-
if super(GenericMeta, self).__subclasscheck__(cls):
1125-
return True
1126-
if self.__extra__ is not None:
1127-
return issubclass(cls, self.__extra__)
1165+
if not isinstance(instance, type):
1166+
return issubclass(instance.__class__, self)
11281167
return False
11291168

11301169

src/test_typing.py

+31-19
Original file line numberDiff line numberDiff line change
@@ -600,8 +600,8 @@ class MyMapping(MutableMapping[str, str]): pass
600600
self.assertNotIsInstance({}, MyMapping)
601601
self.assertNotIsSubclass(dict, MyMapping)
602602

603-
def test_multiple_abc_bases(self):
604-
class MM1(MutableMapping[str, str], collections_abc.MutableMapping):
603+
def test_abc_bases(self):
604+
class MM(MutableMapping[str, str]):
605605
def __getitem__(self, k):
606606
return None
607607
def __setitem__(self, k, v):
@@ -612,24 +612,20 @@ def __iter__(self):
612612
return iter(())
613613
def __len__(self):
614614
return 0
615-
class MM2(collections_abc.MutableMapping, MutableMapping[str, str]):
616-
def __getitem__(self, k):
617-
return None
618-
def __setitem__(self, k, v):
619-
pass
620-
def __delitem__(self, k):
615+
# this should just work
616+
MM().update()
617+
self.assertIsInstance(MM(), collections_abc.MutableMapping)
618+
self.assertIsInstance(MM(), MutableMapping)
619+
self.assertNotIsInstance(MM(), List)
620+
self.assertNotIsInstance({}, MM)
621+
622+
def test_multiple_bases(self):
623+
class MM1(MutableMapping[str, str], collections_abc.MutableMapping):
624+
pass
625+
with self.assertRaises(TypeError):
626+
# consistent MRO not possible
627+
class MM2(collections_abc.MutableMapping, MutableMapping[str, str]):
621628
pass
622-
def __iter__(self):
623-
return iter(())
624-
def __len__(self):
625-
return 0
626-
# these two should just work
627-
MM1().update()
628-
MM2().update()
629-
self.assertIsInstance(MM1(), collections_abc.MutableMapping)
630-
self.assertIsInstance(MM1(), MutableMapping)
631-
self.assertIsInstance(MM2(), collections_abc.MutableMapping)
632-
self.assertIsInstance(MM2(), MutableMapping)
633629

634630
def test_pickle(self):
635631
global C # pickle wants to reference the class by name
@@ -1380,12 +1376,28 @@ class MMA(typing.MutableMapping):
13801376
MMA()
13811377

13821378
class MMC(MMA):
1379+
def __getitem__(self, k):
1380+
return None
1381+
def __setitem__(self, k, v):
1382+
pass
1383+
def __delitem__(self, k):
1384+
pass
1385+
def __iter__(self):
1386+
return iter(())
13831387
def __len__(self):
13841388
return 0
13851389

13861390
self.assertEqual(len(MMC()), 0)
13871391

13881392
class MMB(typing.MutableMapping[KT, VT]):
1393+
def __getitem__(self, k):
1394+
return None
1395+
def __setitem__(self, k, v):
1396+
pass
1397+
def __delitem__(self, k):
1398+
pass
1399+
def __iter__(self):
1400+
return iter(())
13891401
def __len__(self):
13901402
return 0
13911403

src/typing.py

+52-15
Original file line numberDiff line numberDiff line change
@@ -894,11 +894,55 @@ def _next_in_mro(cls):
894894
return next_in_mro
895895

896896

897+
def _valid_for_check(cls):
898+
if cls is Generic:
899+
raise TypeError("Class %r cannot be used with class "
900+
"or instance checks" % cls)
901+
if (cls.__origin__ is not None and
902+
sys._getframe(3).f_globals['__name__'] != 'abc'):
903+
raise TypeError("Parameterized generics cannot be used with class "
904+
"or instance checks")
905+
906+
907+
def _make_subclasshook(cls):
908+
"""Construct a __subclasshook__ callable that incorporates
909+
the associated __extra__ class in subclass checks performed
910+
against cls.
911+
"""
912+
if isinstance(cls.__extra__, abc.ABCMeta):
913+
# The logic mirrors that of ABCMeta.__subclasscheck__.
914+
# Registered classes need not be checked here because
915+
# cls and its extra share the same _abc_registry.
916+
def __extrahook__(subclass):
917+
_valid_for_check(cls)
918+
res = cls.__extra__.__subclasshook__(subclass)
919+
if res is not NotImplemented:
920+
return res
921+
if cls.__extra__ in subclass.__mro__:
922+
return True
923+
for scls in cls.__extra__.__subclasses__():
924+
if isinstance(scls, GenericMeta):
925+
continue
926+
if issubclass(subclass, scls):
927+
return True
928+
return NotImplemented
929+
else:
930+
# For non-ABC extras we'll just call issubclass().
931+
def __extrahook__(subclass):
932+
_valid_for_check(cls)
933+
if cls.__extra__ and issubclass(subclass, cls.__extra__):
934+
return True
935+
return NotImplemented
936+
return __extrahook__
937+
938+
897939
class GenericMeta(TypingMeta, abc.ABCMeta):
898940
"""Metaclass for generic types."""
899941

900942
def __new__(cls, name, bases, namespace,
901943
tvars=None, args=None, origin=None, extra=None):
944+
if extra is not None and type(extra) is abc.ABCMeta and extra not in bases:
945+
bases = (extra,) + bases
902946
self = super().__new__(cls, name, bases, namespace, _root=True)
903947

904948
if tvars is not None:
@@ -947,6 +991,13 @@ def __new__(cls, name, bases, namespace,
947991
self.__extra__ = extra
948992
# Speed hack (https://github.com/python/typing/issues/196).
949993
self.__next_in_mro__ = _next_in_mro(self)
994+
995+
# This allows unparameterized generic collections to be used
996+
# with issubclass() and isinstance() in the same way as their
997+
# collections.abc counterparts (e.g., isinstance([], Iterable)).
998+
self.__subclasshook__ = _make_subclasshook(self)
999+
if isinstance(extra, abc.ABCMeta):
1000+
self._abc_registry = extra._abc_registry
9501001
return self
9511002

9521003
def _get_type_vars(self, tvars):
@@ -1032,21 +1083,7 @@ def __instancecheck__(self, instance):
10321083
# latter, we must extend __instancecheck__ too. For simplicity
10331084
# we just skip the cache check -- instance checks for generic
10341085
# classes are supposed to be rare anyways.
1035-
return self.__subclasscheck__(instance.__class__)
1036-
1037-
def __subclasscheck__(self, cls):
1038-
if self is Generic:
1039-
raise TypeError("Class %r cannot be used with class "
1040-
"or instance checks" % self)
1041-
if (self.__origin__ is not None and
1042-
sys._getframe(1).f_globals['__name__'] != 'abc'):
1043-
raise TypeError("Parameterized generics cannot be used with class "
1044-
"or instance checks")
1045-
if super().__subclasscheck__(cls):
1046-
return True
1047-
if self.__extra__ is not None:
1048-
return issubclass(cls, self.__extra__)
1049-
return False
1086+
return issubclass(instance.__class__, self)
10501087

10511088

10521089
# Prevent checks for Generic to crash when defining Generic.

0 commit comments

Comments
 (0)