Skip to content
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

Include __extra__ base in generics MRO #287

Merged
merged 2 commits into from
Sep 29, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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