From e98409e50beed03cd5127236b49bfe4370c00f81 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 29 Sep 2016 01:33:14 +0200 Subject: [PATCH 1/2] Extra bases for generics provide implementation --- src/test_typing.py | 44 ++++++++++++++++-------------- src/typing.py | 67 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 76 insertions(+), 35 deletions(-) diff --git a/src/test_typing.py b/src/test_typing.py index 6543005f0..c67130545 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -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): @@ -612,24 +612,12 @@ 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): - 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) + # 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_pickle(self): global C # pickle wants to reference the class by name @@ -1380,12 +1368,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 diff --git a/src/typing.py b/src/typing.py index 925d9e42d..5b1d1713a 100644 --- a/src/typing.py +++ b/src/typing.py @@ -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: @@ -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): @@ -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. From 7991496aa8302a440146a437f3b0cbbd017c07e8 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 29 Sep 2016 12:18:30 +0200 Subject: [PATCH 2/2] Backport implementation of implemetation to Python 2 --- python2/test_typing.py | 50 +++++++++++++++++++------------ python2/typing.py | 67 +++++++++++++++++++++++++++++++++--------- src/test_typing.py | 8 +++++ 3 files changed, 92 insertions(+), 33 deletions(-) diff --git a/python2/test_typing.py b/python2/test_typing.py index 25472955c..5e510d4fc 100644 --- a/python2/test_typing.py +++ b/python2/test_typing.py @@ -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): @@ -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 @@ -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 diff --git a/python2/typing.py b/python2/typing.py index ae85573d2..3b88f359b 100644 --- a/python2/typing.py +++ b/python2/typing.py @@ -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: @@ -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): @@ -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 diff --git a/src/test_typing.py b/src/test_typing.py index c67130545..6aa74db32 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -619,6 +619,14 @@ def __len__(self): 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 test_pickle(self): global C # pickle wants to reference the class by name T = TypeVar('T')