Skip to content

Commit

Permalink
Include __extra__ base in generics MRO (#287)
Browse files Browse the repository at this point in the history
Fixes #203.
  • Loading branch information
ilevkivskyi authored and gvanrossum committed Sep 29, 2016
1 parent 6f0ba61 commit 83d4b71
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 67 deletions.
50 changes: 31 additions & 19 deletions python2/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,8 +573,8 @@ class MyMapping(MutableMapping[str, str]): pass
self.assertNotIsInstance({}, MyMapping)
self.assertNotIsSubclass(dict, MyMapping)

def test_multiple_abc_bases(self):
class MM1(MutableMapping[str, str], collections_abc.MutableMapping):
def test_abc_bases(self):
class MM(MutableMapping[str, str]):
def __getitem__(self, k):
return None
def __setitem__(self, k, v):
Expand All @@ -585,24 +585,20 @@ def __iter__(self):
return iter(())
def __len__(self):
return 0
class MM2(collections_abc.MutableMapping, MutableMapping[str, str]):
def __getitem__(self, k):
return None
def __setitem__(self, k, v):
pass
def __delitem__(self, k):
# this should just work
MM().update()
self.assertIsInstance(MM(), collections_abc.MutableMapping)
self.assertIsInstance(MM(), MutableMapping)
self.assertNotIsInstance(MM(), List)
self.assertNotIsInstance({}, MM)

def test_multiple_bases(self):
class MM1(MutableMapping[str, str], collections_abc.MutableMapping):
pass
with self.assertRaises(TypeError):
# consistent MRO not possible
class MM2(collections_abc.MutableMapping, MutableMapping[str, str]):
pass
def __iter__(self):
return iter(())
def __len__(self):
return 0
# these two should just work
MM1().update()
MM2().update()
self.assertIsInstance(MM1(), collections_abc.MutableMapping)
self.assertIsInstance(MM1(), MutableMapping)
self.assertIsInstance(MM2(), collections_abc.MutableMapping)
self.assertIsInstance(MM2(), MutableMapping)

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

class MMC(MMA):
def __getitem__(self, k):
return None
def __setitem__(self, k, v):
pass
def __delitem__(self, k):
pass
def __iter__(self):
return iter(())
def __len__(self):
return 0

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

class MMB(typing.MutableMapping[KT, VT]):
def __getitem__(self, k):
return None
def __setitem__(self, k, v):
pass
def __delitem__(self, k):
pass
def __iter__(self):
return iter(())
def __len__(self):
return 0

Expand Down
67 changes: 53 additions & 14 deletions python2/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -974,11 +974,55 @@ def _next_in_mro(cls):
return next_in_mro


def _valid_for_check(cls):
if cls is Generic:
raise TypeError("Class %r cannot be used with class "
"or instance checks" % cls)
if (cls.__origin__ is not None and
sys._getframe(3).f_globals['__name__'] != 'abc'):
raise TypeError("Parameterized generics cannot be used with class "
"or instance checks")


def _make_subclasshook(cls):
"""Construct a __subclasshook__ callable that incorporates
the associated __extra__ class in subclass checks performed
against cls.
"""
if isinstance(cls.__extra__, abc.ABCMeta):
# The logic mirrors that of ABCMeta.__subclasscheck__.
# Registered classes need not be checked here because
# cls and its extra share the same _abc_registry.
def __extrahook__(cls, subclass):
_valid_for_check(cls)
res = cls.__extra__.__subclasshook__(subclass)
if res is not NotImplemented:
return res
if cls.__extra__ in subclass.__mro__:
return True
for scls in cls.__extra__.__subclasses__():
if isinstance(scls, GenericMeta):
continue
if issubclass(subclass, scls):
return True
return NotImplemented
else:
# For non-ABC extras we'll just call issubclass().
def __extrahook__(cls, subclass):
_valid_for_check(cls)
if cls.__extra__ and issubclass(subclass, cls.__extra__):
return True
return NotImplemented
return classmethod(__extrahook__)


class GenericMeta(TypingMeta, abc.ABCMeta):
"""Metaclass for generic types."""

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

if tvars is not None:
Expand Down Expand Up @@ -1027,6 +1071,13 @@ def __new__(cls, name, bases, namespace,
self.__extra__ = namespace.get('__extra__')
# Speed hack (https://github.com/python/typing/issues/196).
self.__next_in_mro__ = _next_in_mro(self)

# This allows unparameterized generic collections to be used
# with issubclass() and isinstance() in the same way as their
# collections.abc counterparts (e.g., isinstance([], Iterable)).
self.__subclasshook__ = _make_subclasshook(self)
if isinstance(extra, abc.ABCMeta):
self._abc_registry = extra._abc_registry
return self

def _get_type_vars(self, tvars):
Expand Down Expand Up @@ -1111,20 +1162,8 @@ def __instancecheck__(self, instance):
# latter, we must extend __instancecheck__ too. For simplicity
# we just skip the cache check -- instance checks for generic
# classes are supposed to be rare anyways.
return self.__subclasscheck__(instance.__class__)

def __subclasscheck__(self, cls):
if self is Generic:
raise TypeError("Class %r cannot be used with class "
"or instance checks" % self)
if (self.__origin__ is not None and
sys._getframe(1).f_globals['__name__'] != 'abc'):
raise TypeError("Parameterized generics cannot be used with class "
"or instance checks")
if super(GenericMeta, self).__subclasscheck__(cls):
return True
if self.__extra__ is not None:
return issubclass(cls, self.__extra__)
if not isinstance(instance, type):
return issubclass(instance.__class__, self)
return False


Expand Down
50 changes: 31 additions & 19 deletions src/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,8 +600,8 @@ class MyMapping(MutableMapping[str, str]): pass
self.assertNotIsInstance({}, MyMapping)
self.assertNotIsSubclass(dict, MyMapping)

def test_multiple_abc_bases(self):
class MM1(MutableMapping[str, str], collections_abc.MutableMapping):
def test_abc_bases(self):
class MM(MutableMapping[str, str]):
def __getitem__(self, k):
return None
def __setitem__(self, k, v):
Expand All @@ -612,24 +612,20 @@ def __iter__(self):
return iter(())
def __len__(self):
return 0
class MM2(collections_abc.MutableMapping, MutableMapping[str, str]):
def __getitem__(self, k):
return None
def __setitem__(self, k, v):
pass
def __delitem__(self, k):
# this should just work
MM().update()
self.assertIsInstance(MM(), collections_abc.MutableMapping)
self.assertIsInstance(MM(), MutableMapping)
self.assertNotIsInstance(MM(), List)
self.assertNotIsInstance({}, MM)

def test_multiple_bases(self):
class MM1(MutableMapping[str, str], collections_abc.MutableMapping):
pass
with self.assertRaises(TypeError):
# consistent MRO not possible
class MM2(collections_abc.MutableMapping, MutableMapping[str, str]):
pass
def __iter__(self):
return iter(())
def __len__(self):
return 0
# these two should just work
MM1().update()
MM2().update()
self.assertIsInstance(MM1(), collections_abc.MutableMapping)
self.assertIsInstance(MM1(), MutableMapping)
self.assertIsInstance(MM2(), collections_abc.MutableMapping)
self.assertIsInstance(MM2(), MutableMapping)

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

class MMC(MMA):
def __getitem__(self, k):
return None
def __setitem__(self, k, v):
pass
def __delitem__(self, k):
pass
def __iter__(self):
return iter(())
def __len__(self):
return 0

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

class MMB(typing.MutableMapping[KT, VT]):
def __getitem__(self, k):
return None
def __setitem__(self, k, v):
pass
def __delitem__(self, k):
pass
def __iter__(self):
return iter(())
def __len__(self):
return 0

Expand Down
67 changes: 52 additions & 15 deletions src/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,11 +894,55 @@ def _next_in_mro(cls):
return next_in_mro


def _valid_for_check(cls):
if cls is Generic:
raise TypeError("Class %r cannot be used with class "
"or instance checks" % cls)
if (cls.__origin__ is not None and
sys._getframe(3).f_globals['__name__'] != 'abc'):
raise TypeError("Parameterized generics cannot be used with class "
"or instance checks")


def _make_subclasshook(cls):
"""Construct a __subclasshook__ callable that incorporates
the associated __extra__ class in subclass checks performed
against cls.
"""
if isinstance(cls.__extra__, abc.ABCMeta):
# The logic mirrors that of ABCMeta.__subclasscheck__.
# Registered classes need not be checked here because
# cls and its extra share the same _abc_registry.
def __extrahook__(subclass):
_valid_for_check(cls)
res = cls.__extra__.__subclasshook__(subclass)
if res is not NotImplemented:
return res
if cls.__extra__ in subclass.__mro__:
return True
for scls in cls.__extra__.__subclasses__():
if isinstance(scls, GenericMeta):
continue
if issubclass(subclass, scls):
return True
return NotImplemented
else:
# For non-ABC extras we'll just call issubclass().
def __extrahook__(subclass):
_valid_for_check(cls)
if cls.__extra__ and issubclass(subclass, cls.__extra__):
return True
return NotImplemented
return __extrahook__


class GenericMeta(TypingMeta, abc.ABCMeta):
"""Metaclass for generic types."""

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

if tvars is not None:
Expand Down Expand Up @@ -947,6 +991,13 @@ def __new__(cls, name, bases, namespace,
self.__extra__ = extra
# Speed hack (https://github.com/python/typing/issues/196).
self.__next_in_mro__ = _next_in_mro(self)

# This allows unparameterized generic collections to be used
# with issubclass() and isinstance() in the same way as their
# collections.abc counterparts (e.g., isinstance([], Iterable)).
self.__subclasshook__ = _make_subclasshook(self)
if isinstance(extra, abc.ABCMeta):
self._abc_registry = extra._abc_registry
return self

def _get_type_vars(self, tvars):
Expand Down Expand Up @@ -1032,21 +1083,7 @@ def __instancecheck__(self, instance):
# latter, we must extend __instancecheck__ too. For simplicity
# we just skip the cache check -- instance checks for generic
# classes are supposed to be rare anyways.
return self.__subclasscheck__(instance.__class__)

def __subclasscheck__(self, cls):
if self is Generic:
raise TypeError("Class %r cannot be used with class "
"or instance checks" % self)
if (self.__origin__ is not None and
sys._getframe(1).f_globals['__name__'] != 'abc'):
raise TypeError("Parameterized generics cannot be used with class "
"or instance checks")
if super().__subclasscheck__(cls):
return True
if self.__extra__ is not None:
return issubclass(cls, self.__extra__)
return False
return issubclass(instance.__class__, self)


# Prevent checks for Generic to crash when defining Generic.
Expand Down

0 comments on commit 83d4b71

Please sign in to comment.