From 2ef2a504304636ee7939c8b23c2f822e0331fabe Mon Sep 17 00:00:00 2001 From: Kalle Tuure Date: Sat, 16 Apr 2016 02:23:37 +0300 Subject: [PATCH 1/3] Prevent generic subclasses from inheriting __extra__ --- src/test_typing.py | 25 ++++++++++++++++++++++--- src/typing.py | 20 +++++++++----------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/test_typing.py b/src/test_typing.py index 47118edc..f5fa8202 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -1,4 +1,5 @@ import contextlib +import collections import pickle import re import sys @@ -1195,13 +1196,17 @@ def test_no_list_instantiation(self): with self.assertRaises(TypeError): typing.List[int]() - def test_list_subclass_instantiation(self): + def test_list_subclass(self): class MyList(typing.List[int]): pass a = MyList() assert isinstance(a, MyList) + assert isinstance(a, typing.Sequence) + + assert issubclass(MyList, list) + assert not issubclass(list, MyList) def test_no_dict_instantiation(self): with self.assertRaises(TypeError): @@ -1211,13 +1216,17 @@ def test_no_dict_instantiation(self): with self.assertRaises(TypeError): typing.Dict[str, int]() - def test_dict_subclass_instantiation(self): + def test_dict_subclass(self): class MyDict(typing.Dict[str, int]): pass d = MyDict() assert isinstance(d, MyDict) + assert isinstance(d, typing.MutableMapping) + + assert issubclass(MyDict, dict) + assert not issubclass(dict, MyDict) def test_no_defaultdict_instantiation(self): with self.assertRaises(TypeError): @@ -1227,7 +1236,7 @@ def test_no_defaultdict_instantiation(self): with self.assertRaises(TypeError): typing.DefaultDict[str, int]() - def test_defaultdict_subclass_instantiation(self): + def test_defaultdict_subclass(self): class MyDefDict(typing.DefaultDict[str, int]): pass @@ -1235,6 +1244,9 @@ class MyDefDict(typing.DefaultDict[str, int]): dd = MyDefDict() assert isinstance(dd, MyDefDict) + assert issubclass(MyDefDict, collections.defaultdict) + assert not issubclass(collections.defaultdict, MyDefDict) + def test_no_set_instantiation(self): with self.assertRaises(TypeError): typing.Set() @@ -1315,6 +1327,13 @@ def __len__(self): assert len(MMB[str, str]()) == 0 assert len(MMB[KT, VT]()) == 0 + assert not issubclass(dict, MMA) + assert not issubclass(dict, MMB) + + assert issubclass(MMA, typing.Mapping) + assert issubclass(MMB, typing.Mapping) + assert issubclass(MMC, typing.Mapping) + class OtherABCTests(TestCase): diff --git a/src/typing.py b/src/typing.py index d2750111..841e7786 100644 --- a/src/typing.py +++ b/src/typing.py @@ -894,8 +894,6 @@ def _next_in_mro(cls): class GenericMeta(TypingMeta, abc.ABCMeta): """Metaclass for generic types.""" - __extra__ = None - def __new__(cls, name, bases, namespace, tvars=None, args=None, origin=None, extra=None): self = super().__new__(cls, name, bases, namespace, _root=True) @@ -943,10 +941,7 @@ def __new__(cls, name, bases, namespace, self.__parameters__ = tvars self.__args__ = args self.__origin__ = origin - if extra is not None: - self.__extra__ = extra - # Else __extra__ is inherited, eventually from the - # (meta-)class default above. + self.__extra__ = extra # Speed hack (https://github.com/python/typing/issues/196). self.__next_in_mro__ = _next_in_mro(self) return self @@ -1307,6 +1302,7 @@ def _get_protocol_attrs(self): attr != '__next_in_mro__' and attr != '__parameters__' and attr != '__origin__' and + attr != '__extra__' and attr != '__module__'): attrs.add(attr) @@ -1470,7 +1466,7 @@ class ByteString(Sequence[int], extra=collections_abc.ByteString): ByteString.register(type(memoryview(b''))) -class List(list, MutableSequence[T]): +class List(list, MutableSequence[T], extra=list): def __new__(cls, *args, **kwds): if _geqv(cls, List): @@ -1479,7 +1475,7 @@ def __new__(cls, *args, **kwds): return list.__new__(cls, *args, **kwds) -class Set(set, MutableSet[T]): +class Set(set, MutableSet[T], extra=set): def __new__(cls, *args, **kwds): if _geqv(cls, Set): @@ -1502,7 +1498,8 @@ def __subclasscheck__(self, cls): return super().__subclasscheck__(cls) -class FrozenSet(frozenset, AbstractSet[T_co], metaclass=_FrozenSetMeta): +class FrozenSet(frozenset, AbstractSet[T_co], metaclass=_FrozenSetMeta, + extra=frozenset): __slots__ = () def __new__(cls, *args, **kwds): @@ -1538,7 +1535,7 @@ class ContextManager(Generic[T_co], extra=contextlib.AbstractContextManager): __all__.append('ContextManager') -class Dict(dict, MutableMapping[KT, VT]): +class Dict(dict, MutableMapping[KT, VT], extra=dict): def __new__(cls, *args, **kwds): if _geqv(cls, Dict): @@ -1546,7 +1543,8 @@ def __new__(cls, *args, **kwds): "use dict() instead") return dict.__new__(cls, *args, **kwds) -class DefaultDict(collections.defaultdict, MutableMapping[KT, VT]): +class DefaultDict(collections.defaultdict, MutableMapping[KT, VT], + extra=collections.defaultdict): def __new__(cls, *args, **kwds): if _geqv(cls, DefaultDict): From 0ec4dd2e1d3fef533a59a08cb8a0212ff4877c99 Mon Sep 17 00:00:00 2001 From: Kalle Tuure Date: Sun, 17 Apr 2016 19:50:54 +0300 Subject: [PATCH 2/3] Insert extras as bases, remove GenericMeta.__subclasscheck__ --- src/test_typing.py | 212 ++++++++++++++++++++++++++++++++++----------- src/typing.py | 124 +++++++++++++++++--------- 2 files changed, 247 insertions(+), 89 deletions(-) diff --git a/src/test_typing.py b/src/test_typing.py index f5fa8202..456a133e 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -1,3 +1,4 @@ +import abc import contextlib import collections import pickle @@ -14,6 +15,7 @@ from typing import Generic from typing import cast from typing import get_type_hints +from typing import is_compatible from typing import no_type_check, no_type_check_decorator from typing import NamedTuple from typing import IO, TextIO, BinaryIO @@ -81,15 +83,15 @@ def test_cannot_subscript(self): def test_any_is_subclass(self): # Any should be considered a subclass of everything. assert issubclass(Any, Any) - assert issubclass(Any, typing.List) - assert issubclass(Any, typing.List[int]) - assert issubclass(Any, typing.List[T]) - assert issubclass(Any, typing.Mapping) - assert issubclass(Any, typing.Mapping[str, int]) - assert issubclass(Any, typing.Mapping[KT, VT]) - assert issubclass(Any, Generic) - assert issubclass(Any, Generic[T]) - assert issubclass(Any, Generic[KT, VT]) + assert is_compatible(Any, typing.List) + assert is_compatible(Any, typing.List[int]) + assert is_compatible(Any, typing.List[T]) + assert is_compatible(Any, typing.Mapping) + assert is_compatible(Any, typing.Mapping[str, int]) + assert is_compatible(Any, typing.Mapping[KT, VT]) + assert is_compatible(Any, Generic) + assert is_compatible(Any, Generic[T]) + assert is_compatible(Any, Generic[KT, VT]) assert issubclass(Any, AnyStr) assert issubclass(Any, Union) assert issubclass(Any, Union[int, str]) @@ -794,13 +796,13 @@ class VarianceTests(TestCase): def test_invariance(self): # Because of invariance, List[subclass of X] is not a subclass # of List[X], and ditto for MutableSequence. - assert not issubclass(typing.List[Manager], typing.List[Employee]) - assert not issubclass(typing.MutableSequence[Manager], - typing.MutableSequence[Employee]) + assert not is_compatible(typing.List[Manager], typing.List[Employee]) + assert not is_compatible(typing.MutableSequence[Manager], + typing.MutableSequence[Employee]) # It's still reflexive. - assert issubclass(typing.List[Employee], typing.List[Employee]) - assert issubclass(typing.MutableSequence[Employee], - typing.MutableSequence[Employee]) + assert is_compatible(typing.List[Employee], typing.List[Employee]) + assert is_compatible(typing.MutableSequence[Employee], + typing.MutableSequence[Employee]) def test_covariance_tuple(self): # Check covariace for Tuple (which are really special cases). @@ -817,20 +819,20 @@ def test_covariance_tuple(self): def test_covariance_sequence(self): # Check covariance for Sequence (which is just a generic class # for this purpose, but using a covariant type variable). - assert issubclass(typing.Sequence[Manager], typing.Sequence[Employee]) - assert not issubclass(typing.Sequence[Employee], - typing.Sequence[Manager]) + assert is_compatible(typing.Sequence[Manager], typing.Sequence[Employee]) + assert not is_compatible(typing.Sequence[Employee], + typing.Sequence[Manager]) def test_covariance_mapping(self): # Ditto for Mapping (covariant in the value, invariant in the key). - assert issubclass(typing.Mapping[Employee, Manager], - typing.Mapping[Employee, Employee]) - assert not issubclass(typing.Mapping[Manager, Employee], - typing.Mapping[Employee, Employee]) - assert not issubclass(typing.Mapping[Employee, Manager], - typing.Mapping[Manager, Manager]) - assert not issubclass(typing.Mapping[Manager, Employee], - typing.Mapping[Manager, Manager]) + assert is_compatible(typing.Mapping[Employee, Manager], + typing.Mapping[Employee, Employee]) + assert not is_compatible(typing.Mapping[Manager, Employee], + typing.Mapping[Employee, Employee]) + assert not is_compatible(typing.Mapping[Employee, Manager], + typing.Mapping[Manager, Manager]) + assert not is_compatible(typing.Mapping[Manager, Employee], + typing.Mapping[Manager, Manager]) class CastTests(TestCase): @@ -1089,17 +1091,28 @@ def test_iterable(self): # path and could fail. So call this a few times. assert isinstance([], typing.Iterable) assert isinstance([], typing.Iterable) - assert isinstance([], typing.Iterable[int]) + assert not isinstance([], typing.Iterable[int]) assert not isinstance(42, typing.Iterable) # Just in case, also test issubclass() a few times. assert issubclass(list, typing.Iterable) assert issubclass(list, typing.Iterable) + assert is_compatible(list, typing.Iterable) + assert not issubclass(list, typing.Iterable[int]) + assert is_compatible(list, typing.Iterable[int]) + assert not is_compatible(int, typing.Iterable[int]) + assert issubclass(tuple, typing.Sequence) + assert is_compatible(tuple, typing.Sequence) + assert not issubclass(tuple, typing.Sequence[int]) + assert is_compatible(tuple, typing.Sequence[int]) def test_iterator(self): it = iter([]) assert isinstance(it, typing.Iterator) - assert isinstance(it, typing.Iterator[int]) + assert not isinstance(it, typing.Iterator[int]) assert not isinstance(42, typing.Iterator) + assert is_compatible(type(it), typing.Iterator) + assert is_compatible(type(it), typing.Iterator[int]) + assert not is_compatible(int, typing.Iterator[int]) @skipUnless(PY35, 'Python 3.5 required') def test_awaitable(self): @@ -1110,13 +1123,16 @@ def test_awaitable(self): globals(), ns) foo = ns['foo'] g = foo() - assert issubclass(type(g), typing.Awaitable[int]) assert isinstance(g, typing.Awaitable) assert not isinstance(foo, typing.Awaitable) - assert issubclass(typing.Awaitable[Manager], - typing.Awaitable[Employee]) - assert not issubclass(typing.Awaitable[Employee], - typing.Awaitable[Manager]) + assert is_compatible(type(g), typing.Awaitable[int]) + assert not is_compatible(type(foo), typing.Awaitable[int]) + assert not issubclass(typing.Awaitable[Manager], + typing.Awaitable[Employee]) + assert is_compatible(typing.Awaitable[Manager], + typing.Awaitable[Employee]) + assert not is_compatible(typing.Awaitable[Employee], + typing.Awaitable[Manager]) g.send(None) # Run foo() till completion, to avoid warning. @skipUnless(PY35, 'Python 3.5 required') @@ -1125,8 +1141,8 @@ def test_async_iterable(self): it = AsyncIteratorWrapper(base_it) assert isinstance(it, typing.AsyncIterable) assert isinstance(it, typing.AsyncIterable) - assert issubclass(typing.AsyncIterable[Manager], - typing.AsyncIterable[Employee]) + assert is_compatible(typing.AsyncIterable[Manager], + typing.AsyncIterable[Employee]) assert not isinstance(42, typing.AsyncIterable) @skipUnless(PY35, 'Python 3.5 required') @@ -1134,8 +1150,8 @@ def test_async_iterator(self): base_it = range(10) # type: Iterator[int] it = AsyncIteratorWrapper(base_it) assert isinstance(it, typing.AsyncIterator) - assert issubclass(typing.AsyncIterator[Manager], - typing.AsyncIterator[Employee]) + assert is_compatible(typing.AsyncIterator[Manager], + typing.AsyncIterator[Employee]) assert not isinstance(42, typing.AsyncIterator) def test_sized(self): @@ -1176,14 +1192,17 @@ def test_bytestring(self): def test_list(self): assert issubclass(list, typing.List) + assert is_compatible(list, typing.List) def test_set(self): assert issubclass(set, typing.Set) assert not issubclass(frozenset, typing.Set) + assert not is_compatible(frozenset, typing.Set) def test_frozenset(self): - assert issubclass(frozenset, typing.FrozenSet) + assert is_compatible(frozenset, typing.FrozenSet) assert not issubclass(set, typing.FrozenSet) + assert not is_compatible(set, typing.FrozenSet) def test_dict(self): assert issubclass(dict, typing.Dict) @@ -1204,9 +1223,11 @@ class MyList(typing.List[int]): a = MyList() assert isinstance(a, MyList) assert isinstance(a, typing.Sequence) + assert isinstance(a, collections.Sequence) assert issubclass(MyList, list) - assert not issubclass(list, MyList) + assert is_compatible(MyList, list) + assert not is_compatible(list, MyList) def test_no_dict_instantiation(self): with self.assertRaises(TypeError): @@ -1224,9 +1245,11 @@ class MyDict(typing.Dict[str, int]): d = MyDict() assert isinstance(d, MyDict) assert isinstance(d, typing.MutableMapping) + assert isinstance(d, collections.MutableMapping) assert issubclass(MyDict, dict) - assert not issubclass(dict, MyDict) + assert is_compatible(MyDict, dict) + assert not is_compatible(dict, MyDict) def test_no_defaultdict_instantiation(self): with self.assertRaises(TypeError): @@ -1245,7 +1268,7 @@ class MyDefDict(typing.DefaultDict[str, int]): assert isinstance(dd, MyDefDict) assert issubclass(MyDefDict, collections.defaultdict) - assert not issubclass(collections.defaultdict, MyDefDict) + assert not is_compatible(collections.defaultdict, MyDefDict) def test_no_set_instantiation(self): with self.assertRaises(TypeError): @@ -1292,10 +1315,11 @@ def foo(): yield 42 g = foo() assert issubclass(type(g), typing.Generator) - assert issubclass(typing.Generator[Manager, Employee, Manager], - typing.Generator[Employee, Manager, Employee]) - assert not issubclass(typing.Generator[Manager, Manager, Manager], - typing.Generator[Employee, Employee, Employee]) + assert not issubclass(int, typing.Generator) + assert is_compatible(typing.Generator[Manager, Employee, Manager], + typing.Generator[Employee, Manager, Employee]) + assert not is_compatible(typing.Generator[Manager, Manager, Manager], + typing.Generator[Employee, Employee, Employee]) def test_no_generator_instantiation(self): with self.assertRaises(TypeError): @@ -1314,25 +1338,115 @@ class MMA(typing.MutableMapping): MMA() class MMC(MMA): - def __len__(self): - return 0 + def __iter__(self): ... + def __len__(self): return 0 + def __getitem__(self, name): ... + def __setitem__(self, name, value): ... + def __delitem__(self, name): ... assert len(MMC()) == 0 + assert callable(MMC.update) + assert isinstance(MMC(), typing.Mapping) class MMB(typing.MutableMapping[KT, VT]): - def __len__(self): - return 0 + def __iter__(self): ... + def __len__(self): return 0 + def __getitem__(self, name): ... + def __setitem__(self, name, value): ... + def __delitem__(self, name): ... assert len(MMB()) == 0 assert len(MMB[str, str]()) == 0 assert len(MMB[KT, VT]()) == 0 + assert isinstance(MMB[KT, VT](), typing.Mapping) + assert isinstance(MMB[KT, VT](), collections.Mapping) assert not issubclass(dict, MMA) assert not issubclass(dict, MMB) + assert not is_compatible(dict, MMA) + assert not is_compatible(dict, MMB) assert issubclass(MMA, typing.Mapping) assert issubclass(MMB, typing.Mapping) assert issubclass(MMC, typing.Mapping) + assert issubclass(MMA, collections.Mapping) + assert issubclass(MMB, collections.Mapping) + assert issubclass(MMC, collections.Mapping) + assert is_compatible(MMC, typing.Mapping) + assert is_compatible(MMC, collections.Mapping) + + assert issubclass(MMB[str, str], typing.Mapping) + assert is_compatible(MMB[str, str], typing.Mapping) + + assert issubclass(MMC, MMA) + assert is_compatible(MMC, MMA) + + assert not issubclass(MMA, typing.Mapping[str, str]) + assert not issubclass(MMB, typing.Mapping[str, str]) + + class I(typing.Iterable): ... + assert not issubclass(list, I) + + class G(typing.Generator[int, int, int]): ... + def g(): yield 0 + assert issubclass(G, typing.Generator) + assert issubclass(G, typing.Iterable) + if hasattr(collections, 'Generator'): + assert issubclass(G, collections.Generator) + assert issubclass(G, collections.Iterable) + assert not issubclass(type(g), G) + + def test_subclassing_subclasshook(self): + + class Base: + @classmethod + def __subclasshook__(cls, other): + if other.__name__ == 'Foo': + return True + else: + return False + + class C(Base, typing.Iterable): ... + class Foo: ... + + assert issubclass(Foo, C) + + def test_subclassing_register(self): + + class A(typing.Container): ... + class B(A): ... + + class C: ... + A.register(C) + assert is_compatible(C, A) + assert not is_compatible(C, B) + + class D: ... + B.register(D) + assert is_compatible(D, A) + assert is_compatible(D, B) + + class M(): ... + collections.MutableMapping.register(M) + assert issubclass(M, typing.Mapping) + + def test_collections_as_base(self): + + class M(collections.Mapping): ... + assert issubclass(M, typing.Mapping) + assert issubclass(M, typing.Iterable) + + class S(collections.MutableSequence): ... + assert issubclass(S, typing.MutableSequence) + assert issubclass(S, typing.Iterable) + + class I(collections.Iterable): ... + assert issubclass(I, typing.Iterable) + + class A(collections.Mapping, metaclass=abc.ABCMeta): ... + class B: ... + A.register(B) + assert issubclass(B, typing.Mapping) class OtherABCTests(TestCase): diff --git a/src/typing.py b/src/typing.py index 841e7786..f018bae6 100644 --- a/src/typing.py +++ b/src/typing.py @@ -289,6 +289,28 @@ def _eval_type(t, globalns, localns): return t +def is_consistent(t1, t2): + """Returns a boolean indicating if `t1` is consistent with `t2`.""" + return t1 is t2 or t1 is Any or t2 is Any + + +def is_compatible(t1, t2): + """Returns a boolean indicating if `t1` is compatible with `t2`, + i.e., if `t1` is consistent with or a subtype of `t2`. + + This is similar to ``issubclass`` but adds support for the special + types ``Any``, ``Union``, etc. as well as parameterized generics + and the built-in analogues ``List``, ``Dict``, etc. + + If ``issubclass(D, C)`` is true, then ``is_compatible(D, C)`` is + also true, but the reverse doesn't necessarily hold. + """ + if isinstance(t2, GenericMeta): + return t2.__is_compatible__(t1) + else: + return is_consistent(t1, t2) or issubclass(t1, t2) + + def _type_check(arg, msg): """Check that the argument is a type, and return it. @@ -891,12 +913,42 @@ def _next_in_mro(cls): return next_in_mro +def _make_subclasshook(cls): + """Constructs 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): + 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): + if 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): - self = super().__new__(cls, name, bases, namespace, _root=True) if tvars is not None: # Called from __getitem__() below. @@ -938,12 +990,29 @@ def __new__(cls, name, bases, namespace, ", ".join(str(g) for g in gvars))) tvars = gvars + if extra and extra not in bases and extra.__module__ != 'builtins': + bases = (extra,) + bases + self = super().__new__(cls, name, bases, namespace, _root=True) self.__parameters__ = tvars self.__args__ = args self.__origin__ = origin self.__extra__ = extra # Speed hack (https://github.com/python/typing/issues/196). self.__next_in_mro__ = _next_in_mro(self) + + if extra and origin is None: + # 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 + else: + # Prevent parameterized and derived types from inheriting + # the __subclasshook__ created above. + if self.__subclasshook__.__name__ == '__extrahook__': + self.__subclasshook__ = object.__subclasshook__ + return self def _get_type_vars(self, tvars): @@ -1022,20 +1091,12 @@ def __getitem__(self, params): origin=self, extra=self.__extra__) - def __instancecheck__(self, instance): - # Since we extend ABC.__subclasscheck__ and - # ABC.__instancecheck__ inlines the cache checking done by the - # 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 cls is Any: + def __is_compatible__(self, cls): + if is_consistent(cls, self): return True if isinstance(cls, GenericMeta): # For a class C(Generic[T]) where T is co-variant, - # C[X] is a subclass of C[Y] iff X is a subclass of Y. + # C[X] is compatible with C[Y] iff X is compatible with Y. origin = self.__origin__ if origin is not None and origin is cls.__origin__: assert len(self.__args__) == len(origin.__parameters__) @@ -1045,30 +1106,28 @@ def __subclasscheck__(self, cls): origin.__parameters__): if isinstance(p_origin, TypeVar): if p_origin.__covariant__: - # Covariant -- p_cls must be a subclass of p_self. - if not issubclass(p_cls, p_self): + # Covariant -- p_cls must be compatible with p_self. + if not is_compatible(p_cls, p_self): break elif p_origin.__contravariant__: # Contravariant. I think it's the opposite. :-) - if not issubclass(p_self, p_cls): + if not is_compatible(p_self, p_cls): break else: - # Invariant -- p_cls and p_self must equal. - if p_self != p_cls: + # Invariant -- p_cls and p_self must be consistent. + if not is_consistent(p_self, p_cls): break else: # If the origin's parameter is not a typevar, # insist on invariance. - if p_self != p_cls: + if not is_consistent(p_self, p_cls): break else: return True - # If we break out of the loop, the superclass gets a chance. - if super().__subclasscheck__(cls): - return True - if self.__extra__ is None or isinstance(cls, GenericMeta): - return False - return issubclass(cls, self.__extra__) + return issubclass(cls, self) + # When cls is not a GenericMeta instance, we check against + # the ultimate origin because that one includes __extra__. + return issubclass(cls, _gorg(self)) # Prevent checks for Generic to crash when defining Generic. @@ -1484,22 +1543,7 @@ def __new__(cls, *args, **kwds): return set.__new__(cls, *args, **kwds) -class _FrozenSetMeta(GenericMeta): - """This metaclass ensures set is not a subclass of FrozenSet. - - Without this metaclass, set would be considered a subclass of - FrozenSet, because FrozenSet.__extra__ is collections.abc.Set, and - set is a subclass of that. - """ - - def __subclasscheck__(self, cls): - if issubclass(cls, Set): - return False - return super().__subclasscheck__(cls) - - -class FrozenSet(frozenset, AbstractSet[T_co], metaclass=_FrozenSetMeta, - extra=frozenset): +class FrozenSet(frozenset, AbstractSet[T_co], extra=frozenset): __slots__ = () def __new__(cls, *args, **kwds): From fd4a1b0790dcabc18d3e3c94e81ee3774ac0d9b5 Mon Sep 17 00:00:00 2001 From: Kalle Tuure Date: Tue, 19 Apr 2016 16:42:09 +0300 Subject: [PATCH 3/3] Drop issubclass() support from special types --- src/test_typing.py | 300 ++++++++++++++++++++++++--------------------- src/typing.py | 99 ++++++++------- 2 files changed, 219 insertions(+), 180 deletions(-) diff --git a/src/test_typing.py b/src/test_typing.py index 456a133e..b46c3376 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -41,20 +41,28 @@ class ManagingFounder(Manager, Founder): class AnyTests(TestCase): - def test_any_instance_type_error(self): + def test_any_type_errors(self): with self.assertRaises(TypeError): isinstance(42, Any) + with self.assertRaises(TypeError): + issubclass(Employee, Any) - def test_any_subclass(self): - self.assertTrue(issubclass(Employee, Any)) - self.assertTrue(issubclass(int, Any)) - self.assertTrue(issubclass(type(None), Any)) - self.assertTrue(issubclass(object, Any)) + def test_compatible_with_classes(self): + self.assertTrue(is_compatible(Employee, Any)) + self.assertTrue(is_compatible(int, Any)) + self.assertTrue(is_compatible(type(None), Any)) + self.assertTrue(is_compatible(object, Any)) + self.assertTrue(is_compatible(Any, Employee)) + self.assertTrue(is_compatible(Any, int)) + self.assertTrue(is_compatible(Any, type(None))) + self.assertTrue(is_compatible(Any, object)) - def test_others_any(self): + def test_not_a_subclass(self): self.assertFalse(issubclass(Any, Employee)) self.assertFalse(issubclass(Any, int)) self.assertFalse(issubclass(Any, type(None))) + self.assertFalse(issubclass(Any, typing.Generic)) + self.assertFalse(issubclass(Any, typing.Iterable)) # However, Any is a subclass of object (this can't be helped). self.assertTrue(issubclass(Any, object)) @@ -80,9 +88,9 @@ def test_cannot_subscript(self): with self.assertRaises(TypeError): Any[int] - def test_any_is_subclass(self): - # Any should be considered a subclass of everything. - assert issubclass(Any, Any) + def test_compatible_with_types(self): + # Any should be considered compatible with everything. + assert is_compatible(Any, Any) assert is_compatible(Any, typing.List) assert is_compatible(Any, typing.List[int]) assert is_compatible(Any, typing.List[T]) @@ -92,11 +100,14 @@ def test_any_is_subclass(self): assert is_compatible(Any, Generic) assert is_compatible(Any, Generic[T]) assert is_compatible(Any, Generic[KT, VT]) - assert issubclass(Any, AnyStr) - assert issubclass(Any, Union) - assert issubclass(Any, Union[int, str]) - assert issubclass(Any, typing.Match) - assert issubclass(Any, typing.Match[str]) + assert is_compatible(Any, AnyStr) + assert is_compatible(Any, Union) + assert is_compatible(Any, Union[int, str]) + assert is_compatible(Any, typing.Match) + assert is_compatible(Any, typing.Match[str]) + assert is_compatible(Generic, Any) + assert is_compatible(Union, Any) + assert is_compatible(Match, Any) # These expressions must simply not fail. typing.Match[Any] typing.Pattern[Any] @@ -107,31 +118,33 @@ class TypeVarTests(TestCase): def test_basic_plain(self): T = TypeVar('T') - # Every class is a subclass of T. - assert issubclass(int, T) - assert issubclass(str, T) + # Every class is a subtype of T. + assert is_compatible(int, T) + assert is_compatible(str, T) # T equals itself. assert T == T - # T is a subclass of itself. - assert issubclass(T, T) + # T is a subtype of itself. + assert is_compatible(T, T) # T is an instance of TypeVar assert isinstance(T, TypeVar) - def test_typevar_instance_type_error(self): + def test_typevar_type_errors(self): T = TypeVar('T') with self.assertRaises(TypeError): isinstance(42, T) + with self.assertRaises(TypeError): + issubclass(int, T) def test_basic_constrained(self): A = TypeVar('A', str, bytes) - # Only str and bytes are subclasses of A. - assert issubclass(str, A) - assert issubclass(bytes, A) - assert not issubclass(int, A) + # Only str and bytes are subtypes of A. + assert is_compatible(str, A) + assert is_compatible(bytes, A) + assert not is_compatible(int, A) # A equals itself. assert A == A - # A is a subclass of itself. - assert issubclass(A, A) + # A is a subtype of itself. + assert is_compatible(A, A) def test_constrained_error(self): with self.assertRaises(TypeError): @@ -170,16 +183,16 @@ def test_no_redefinition(self): def test_subclass_as_unions(self): # None of these are true -- each type var is its own world. - self.assertFalse(issubclass(TypeVar('T', int, str), - TypeVar('T', int, str))) - self.assertFalse(issubclass(TypeVar('T', int, float), - TypeVar('T', int, float, str))) - self.assertFalse(issubclass(TypeVar('T', int, str), - TypeVar('T', str, int))) + self.assertFalse(is_compatible(TypeVar('T', int, str), + TypeVar('T', int, str))) + self.assertFalse(is_compatible(TypeVar('T', int, float), + TypeVar('T', int, float, str))) + self.assertFalse(is_compatible(TypeVar('T', int, str), + TypeVar('T', str, int))) A = TypeVar('A', int, str) B = TypeVar('B', int, str, float) - self.assertFalse(issubclass(A, B)) - self.assertFalse(issubclass(B, A)) + self.assertFalse(is_compatible(A, B)) + self.assertFalse(is_compatible(B, A)) def test_cannot_subclass_vars(self): with self.assertRaises(TypeError): @@ -197,9 +210,9 @@ def test_cannot_instantiate_vars(self): def test_bound(self): X = TypeVar('X', bound=Employee) - assert issubclass(Employee, X) - assert issubclass(Manager, X) - assert not issubclass(int, X) + assert is_compatible(Employee, X) + assert is_compatible(Manager, X) + assert not is_compatible(int, X) def test_bound_errors(self): with self.assertRaises(TypeError): @@ -213,8 +226,8 @@ class UnionTests(TestCase): def test_basics(self): u = Union[int, float] self.assertNotEqual(u, Union) - self.assertTrue(issubclass(int, u)) - self.assertTrue(issubclass(float, u)) + self.assertTrue(is_compatible(int, u)) + self.assertTrue(is_compatible(float, u)) def test_union_any(self): u = Union[Any] @@ -245,15 +258,15 @@ def test_unordered(self): def test_subclass(self): u = Union[int, Employee] - self.assertTrue(issubclass(Manager, u)) + self.assertTrue(is_compatible(Manager, u)) def test_self_subclass(self): - self.assertTrue(issubclass(Union[KT, VT], Union)) - self.assertFalse(issubclass(Union, Union[KT, VT])) + self.assertTrue(is_compatible(Union[KT, VT], Union)) + self.assertFalse(is_compatible(Union, Union[KT, VT])) def test_multiple_inheritance(self): u = Union[int, Employee] - self.assertTrue(issubclass(ManagingFounder, u)) + self.assertTrue(is_compatible(ManagingFounder, u)) def test_single_class_disappears(self): t = Union[Employee] @@ -270,9 +283,9 @@ def test_base_class_disappears(self): def test_weird_subclasses(self): u = Union[Employee, int, float] v = Union[int, float] - self.assertTrue(issubclass(v, u)) + self.assertTrue(is_compatible(v, u)) w = Union[int, Manager] - self.assertTrue(issubclass(w, u)) + self.assertTrue(is_compatible(w, u)) def test_union_union(self): u = Union[int, float] @@ -310,13 +323,15 @@ def test_empty(self): with self.assertRaises(TypeError): Union[()] - def test_issubclass_union(self): - assert issubclass(Union[int, str], Union) - assert not issubclass(int, Union) + def test_is_compatible(self): + assert is_compatible(Union[int, str], Union) + assert not is_compatible(int, Union) - def test_union_instance_type_error(self): + def test_union_type_errors(self): with self.assertRaises(TypeError): isinstance(42, Union[int, str]) + with self.assertRaises(TypeError): + issubclass(int, Union[int, str]) def test_union_str_pattern(self): # Shouldn't crash; see http://bugs.python.org/issue25390 @@ -329,38 +344,38 @@ class TypeVarUnionTests(TestCase): def test_simpler(self): A = TypeVar('A', int, str, float) B = TypeVar('B', int, str) - assert issubclass(A, A) - assert issubclass(B, B) - assert not issubclass(B, A) - assert issubclass(A, Union[int, str, float]) - assert not issubclass(Union[int, str, float], A) - assert not issubclass(Union[int, str], B) - assert issubclass(B, Union[int, str]) - assert not issubclass(A, B) - assert not issubclass(Union[int, str, float], B) - assert not issubclass(A, Union[int, str]) - - def test_var_union_subclass(self): - self.assertTrue(issubclass(T, Union[int, T])) - self.assertTrue(issubclass(KT, Union[KT, VT])) + assert is_compatible(A, A) + assert is_compatible(B, B) + assert not is_compatible(B, A) + assert is_compatible(A, Union[int, str, float]) + assert not is_compatible(Union[int, str, float], A) + assert not is_compatible(Union[int, str], B) + assert is_compatible(B, Union[int, str]) + assert not is_compatible(A, B) + assert not is_compatible(Union[int, str, float], B) + assert not is_compatible(A, Union[int, str]) + + def test_var_union_subtype(self): + self.assertTrue(is_compatible(T, Union[int, T])) + self.assertTrue(is_compatible(KT, Union[KT, VT])) def test_var_union(self): TU = TypeVar('TU', Union[int, float], None) - assert issubclass(int, TU) - assert issubclass(float, TU) + assert is_compatible(int, TU) + assert is_compatible(float, TU) class TupleTests(TestCase): def test_basics(self): - self.assertTrue(issubclass(Tuple[int, str], Tuple)) - self.assertTrue(issubclass(Tuple[int, str], Tuple[int, str])) - self.assertFalse(issubclass(int, Tuple)) - self.assertFalse(issubclass(Tuple[float, str], Tuple[int, str])) - self.assertFalse(issubclass(Tuple[int, str, int], Tuple[int, str])) - self.assertFalse(issubclass(Tuple[int, str], Tuple[int, str, int])) - self.assertTrue(issubclass(tuple, Tuple)) - self.assertFalse(issubclass(Tuple, tuple)) # Can't have it both ways. + self.assertTrue(is_compatible(Tuple[int, str], Tuple)) + self.assertTrue(is_compatible(Tuple[int, str], Tuple[int, str])) + self.assertFalse(is_compatible(int, Tuple)) + self.assertFalse(is_compatible(Tuple[float, str], Tuple[int, str])) + self.assertFalse(is_compatible(Tuple[int, str, int], Tuple[int, str])) + self.assertFalse(is_compatible(Tuple[int, str], Tuple[int, str, int])) + self.assertTrue(is_compatible(tuple, Tuple)) + self.assertFalse(is_compatible(Tuple, tuple)) # Can't have it both ways. def test_equality(self): assert Tuple[int] == Tuple[int] @@ -371,13 +386,15 @@ def test_equality(self): def test_tuple_subclass(self): class MyTuple(tuple): pass - self.assertTrue(issubclass(MyTuple, Tuple)) + self.assertTrue(is_compatible(MyTuple, Tuple)) - def test_tuple_instance_type_error(self): + def test_tuple_type_errors(self): with self.assertRaises(TypeError): isinstance((0, 0), Tuple[int, int]) with self.assertRaises(TypeError): isinstance((0, 0), Tuple) + with self.assertRaises(TypeError): + issubclass(tuple, Tuple) def test_tuple_ellipsis_subclass(self): @@ -387,10 +404,10 @@ class B: class C(B): pass - assert not issubclass(Tuple[B], Tuple[B, ...]) - assert issubclass(Tuple[C, ...], Tuple[B, ...]) - assert not issubclass(Tuple[C, ...], Tuple[B]) - assert not issubclass(Tuple[C], Tuple[B, ...]) + assert not is_compatible(Tuple[B], Tuple[B, ...]) + assert is_compatible(Tuple[C, ...], Tuple[B, ...]) + assert not is_compatible(Tuple[C, ...], Tuple[B]) + assert not is_compatible(Tuple[C], Tuple[B, ...]) def test_repr(self): self.assertEqual(repr(Tuple), 'typing.Tuple') @@ -398,27 +415,21 @@ def test_repr(self): self.assertEqual(repr(Tuple[int, float]), 'typing.Tuple[int, float]') self.assertEqual(repr(Tuple[int, ...]), 'typing.Tuple[int, ...]') - def test_errors(self): - with self.assertRaises(TypeError): - issubclass(42, Tuple) - with self.assertRaises(TypeError): - issubclass(42, Tuple[int]) - class CallableTests(TestCase): def test_self_subclass(self): - self.assertTrue(issubclass(Callable[[int], int], Callable)) - self.assertFalse(issubclass(Callable, Callable[[int], int])) - self.assertTrue(issubclass(Callable[[int], int], Callable[[int], int])) - self.assertFalse(issubclass(Callable[[Employee], int], - Callable[[Manager], int])) - self.assertFalse(issubclass(Callable[[Manager], int], - Callable[[Employee], int])) - self.assertFalse(issubclass(Callable[[int], Employee], - Callable[[int], Manager])) - self.assertFalse(issubclass(Callable[[int], Manager], - Callable[[int], Employee])) + self.assertTrue(is_compatible(Callable[[int], int], Callable)) + self.assertFalse(is_compatible(Callable, Callable[[int], int])) + self.assertTrue(is_compatible(Callable[[int], int], Callable[[int], int])) + self.assertFalse(is_compatible(Callable[[Employee], int], + Callable[[Manager], int])) + self.assertFalse(is_compatible(Callable[[Manager], int], + Callable[[Employee], int])) + self.assertFalse(is_compatible(Callable[[int], Employee], + Callable[[int], Manager])) + self.assertFalse(is_compatible(Callable[[int], Manager], + Callable[[int], Employee])) def test_eq_hash(self): self.assertEqual(Callable[[int], int], Callable[[int], int]) @@ -447,23 +458,33 @@ def test_cannot_instantiate(self): with self.assertRaises(TypeError): c() - def test_callable_instance_works(self): + def test_callable_isinstance_works(self): def f(): pass assert isinstance(f, Callable) assert not isinstance(None, Callable) - def test_callable_instance_type_error(self): + def test_callable_issubclass_works(self): + def f(): + pass + assert issubclass(type(f), Callable) + assert not issubclass(type(None), Callable) + + def test_callable_type_errora(self): def f(): pass with self.assertRaises(TypeError): - assert isinstance(f, Callable[[], None]) + isinstance(f, Callable[[], None]) + with self.assertRaises(TypeError): + isinstance(f, Callable[[], Any]) + with self.assertRaises(TypeError): + isinstance(None, Callable[[], None]) with self.assertRaises(TypeError): - assert isinstance(f, Callable[[], Any]) + isinstance(None, Callable[[], Any]) with self.assertRaises(TypeError): - assert not isinstance(None, Callable[[], None]) + issubclass(type(f), Callable[[], None]) with self.assertRaises(TypeError): - assert not isinstance(None, Callable[[], Any]) + issubclass(type(f), Callable[[], Any]) def test_repr(self): ct0 = Callable[[], bool] @@ -519,12 +540,13 @@ def get(self, key: str, default=None): class ProtocolTests(TestCase): def test_supports_int(self): - assert issubclass(int, typing.SupportsInt) - assert not issubclass(str, typing.SupportsInt) + assert is_compatible(int, typing.SupportsInt) + assert not issubclass(int, typing.SupportsInt) + assert not is_compatible(str, typing.SupportsInt) def test_supports_float(self): - assert issubclass(float, typing.SupportsFloat) - assert not issubclass(str, typing.SupportsFloat) + assert is_compatible(float, typing.SupportsFloat) + assert not is_compatible(str, typing.SupportsFloat) def test_supports_complex(self): @@ -533,8 +555,8 @@ class C: def __complex__(self): return 0j - assert issubclass(C, typing.SupportsComplex) - assert not issubclass(str, typing.SupportsComplex) + assert is_compatible(C, typing.SupportsComplex) + assert not is_compatible(str, typing.SupportsComplex) def test_supports_bytes(self): @@ -543,23 +565,23 @@ class B: def __bytes__(self): return b'' - assert issubclass(B, typing.SupportsBytes) - assert not issubclass(str, typing.SupportsBytes) + assert is_compatible(B, typing.SupportsBytes) + assert not is_compatible(str, typing.SupportsBytes) def test_supports_abs(self): - assert issubclass(float, typing.SupportsAbs) - assert issubclass(int, typing.SupportsAbs) - assert not issubclass(str, typing.SupportsAbs) + assert is_compatible(float, typing.SupportsAbs) + assert is_compatible(int, typing.SupportsAbs) + assert not is_compatible(str, typing.SupportsAbs) def test_supports_round(self): - issubclass(float, typing.SupportsRound) - assert issubclass(float, typing.SupportsRound) - assert issubclass(int, typing.SupportsRound) - assert not issubclass(str, typing.SupportsRound) + is_compatible(float, typing.SupportsRound) + assert is_compatible(float, typing.SupportsRound) + assert is_compatible(int, typing.SupportsRound) + assert not is_compatible(str, typing.SupportsRound) def test_reversible(self): - assert issubclass(list, typing.Reversible) - assert not issubclass(int, typing.Reversible) + assert is_compatible(list, typing.Reversible) + assert not is_compatible(int, typing.Reversible) def test_protocol_instance_type_error(self): with self.assertRaises(TypeError): @@ -806,15 +828,15 @@ def test_invariance(self): def test_covariance_tuple(self): # Check covariace for Tuple (which are really special cases). - assert issubclass(Tuple[Manager], Tuple[Employee]) - assert not issubclass(Tuple[Employee], Tuple[Manager]) + assert is_compatible(Tuple[Manager], Tuple[Employee]) + assert not is_compatible(Tuple[Employee], Tuple[Manager]) # And pairwise. - assert issubclass(Tuple[Manager, Manager], Tuple[Employee, Employee]) - assert not issubclass(Tuple[Employee, Employee], - Tuple[Manager, Employee]) + assert is_compatible(Tuple[Manager, Manager], Tuple[Employee, Employee]) + assert not is_compatible(Tuple[Employee, Employee], + Tuple[Manager, Employee]) # And using ellipsis. - assert issubclass(Tuple[Manager, ...], Tuple[Employee, ...]) - assert not issubclass(Tuple[Employee, ...], Tuple[Manager, ...]) + assert is_compatible(Tuple[Manager, ...], Tuple[Employee, ...]) + assert not is_compatible(Tuple[Employee, ...], Tuple[Manager, ...]) def test_covariance_sequence(self): # Check covariance for Sequence (which is just a generic class @@ -1531,24 +1553,24 @@ class RETests(TestCase): def test_basics(self): pat = re.compile('[a-z]+', re.I) - assert issubclass(pat.__class__, Pattern) - assert issubclass(type(pat), Pattern) - assert issubclass(type(pat), Pattern[str]) + assert is_compatible(pat.__class__, Pattern) + assert is_compatible(type(pat), Pattern) + assert is_compatible(type(pat), Pattern[str]) mat = pat.search('12345abcde.....') - assert issubclass(mat.__class__, Match) - assert issubclass(mat.__class__, Match[str]) - assert issubclass(mat.__class__, Match[bytes]) # Sad but true. - assert issubclass(type(mat), Match) - assert issubclass(type(mat), Match[str]) + assert is_compatible(mat.__class__, Match) + assert is_compatible(mat.__class__, Match[str]) + assert is_compatible(mat.__class__, Match[bytes]) # Sad but true. + assert is_compatible(type(mat), Match) + assert is_compatible(type(mat), Match[str]) p = Pattern[Union[str, bytes]] - assert issubclass(Pattern[str], Pattern) - assert issubclass(Pattern[str], p) + assert is_compatible(Pattern[str], Pattern) + assert is_compatible(Pattern[str], p) m = Match[Union[bytes, str]] - assert issubclass(Match[bytes], Match) - assert issubclass(Match[bytes], m) + assert is_compatible(Match[bytes], Match) + assert is_compatible(Match[bytes], m) def test_errors(self): with self.assertRaises(TypeError): diff --git a/src/typing.py b/src/typing.py index f018bae6..23136381 100644 --- a/src/typing.py +++ b/src/typing.py @@ -180,6 +180,9 @@ def __instancecheck__(self, obj): raise TypeError("Forward references cannot be used with isinstance().") def __subclasscheck__(self, cls): + raise TypeError("Forward references cannot be used with issubclass().") + + def __is_compatible__(self, cls): if not self.__forward_evaluated__: globalns = self.__forward_frame__.f_globals localns = self.__forward_frame__.f_locals @@ -187,7 +190,7 @@ def __subclasscheck__(self, cls): self._eval_type(globalns, localns) except NameError: return False # Too early. - return issubclass(cls, self.__forward_value__) + return is_compatible(cls, self.__forward_value__) def __repr__(self): return '_ForwardRef(%r)' % (self.__forward_arg__,) @@ -196,11 +199,10 @@ def __repr__(self): class _TypeAlias: """Internal helper class for defining generic variants of concrete types. - Note that this is not a type; let's call it a pseudo-type. It can - be used in instance and subclass checks, e.g. isinstance(m, Match) - or issubclass(type(m), Match). However, it cannot be itself the - target of an issubclass() call; e.g. issubclass(Match, C) (for - some arbitrary class C) raises TypeError rather than returning + Note that this is not a type; let's call it a pseudo-type. It can be + used in subtype checks, e.g., is_compatible(type(m), Match). However, + it cannot be itself the subject of such a check; is_compatible(Match, C) + (for some arbitrary class C) raises TypeError rather than returning False. """ @@ -247,7 +249,7 @@ def __getitem__(self, parameter): if not isinstance(self.type_var, TypeVar): raise TypeError("%s cannot be further parameterized." % self) if self.type_var.__constraints__: - if not issubclass(parameter, Union[self.type_var.__constraints__]): + if not is_compatible(parameter, Union[self.type_var.__constraints__]): raise TypeError("%s is not a valid substitution for %s." % (parameter, self.type_var)) return self.__class__(self.name, parameter, @@ -257,17 +259,20 @@ def __instancecheck__(self, obj): raise TypeError("Type aliases cannot be used with isinstance().") def __subclasscheck__(self, cls): - if cls is Any: + raise TypeError("Type aliases cannot be used with issubclass().") + + def __is_compatible__(self, cls): + if is_consistent(cls, self): return True if isinstance(cls, _TypeAlias): # Covariance. For now, we compare by name. return (cls.name == self.name and - issubclass(cls.type_var, self.type_var)) + is_compatible(cls.type_var, self.type_var)) else: # Note that this is too lenient, because the # implementation type doesn't carry information about # whether it is about bytes or str (for example). - return issubclass(cls, self.impl_type) + return is_compatible(cls, self.impl_type) def _get_type_vars(types, tvars): @@ -305,7 +310,7 @@ def is_compatible(t1, t2): If ``issubclass(D, C)`` is true, then ``is_compatible(D, C)`` is also true, but the reverse doesn't necessarily hold. """ - if isinstance(t2, GenericMeta): + if isinstance(t2, (TypingMeta, _TypeAlias)): return t2.__is_compatible__(t1) else: return is_consistent(t1, t2) or issubclass(t1, t2) @@ -360,17 +365,17 @@ def __instancecheck__(self, obj): raise TypeError("Any cannot be used with isinstance().") def __subclasscheck__(self, cls): - if not isinstance(cls, type): + raise TypeError("Any cannot be used with issubclass().") + + def __is_compatible__(self, cls): + if not isinstance(cls, (type, _TypeAlias)): return super().__subclasscheck__(cls) # To TypeError. return True class Any(Final, metaclass=AnyMeta, _root=True): - """Special type indicating an unconstrained type. - - - Any object is an instance of Any. - - Any class is a subclass of Any. - - As a special case, Any and object are subclasses of each other. + """Special type indicating an unconstrained type. Any type is + consistent with Any. """ __slots__ = () @@ -402,10 +407,10 @@ def longest(x: A, y: A) -> A: that if the arguments are instances of some subclass of str, the return type is still plain str. - At runtime, isinstance(x, T) will raise TypeError. However, - issubclass(C, T) is true for any class C, and issubclass(str, A) - and issubclass(bytes, A) are true, and issubclass(int, A) is - false. (TODO: Why is this needed? This may change. See #136.) + At runtime, issubclass(x, T) will raise TypeError. However, + - is_compatible(C, T) is true for any class C + - is_compatible(str, A) and is_compatible(bytes, A) are true + - is_compatible(int, A) is false. Type variables may be marked covariant or contravariant by passing covariant=True or contravariant=True. See PEP 484 for more @@ -456,15 +461,15 @@ def __instancecheck__(self, instance): raise TypeError("Type variables cannot be used with isinstance().") def __subclasscheck__(self, cls): - # TODO: Make this raise TypeError too? - if cls is self: - return True - if cls is Any: + raise TypeError("Type variables cannot be used with issubclass().") + + def __is_compatible__(self, cls): + if is_consistent(cls, self): return True if self.__bound__ is not None: - return issubclass(cls, self.__bound__) + return is_compatible(cls, self.__bound__) if self.__constraints__: - return any(issubclass(cls, c) for c in self.__constraints__) + return any(is_compatible(cls, c) for c in self.__constraints__) return True @@ -522,7 +527,7 @@ def __new__(cls, name, bases, namespace, parameters=None, _root=False): if isinstance(t1, _TypeAlias): # _TypeAlias is not a real class. continue - if any(issubclass(t1, t2) + if any(is_compatible(t1, t2) for t2 in all_params - {t1} if not isinstance(t2, TypeVar)): all_params.remove(t1) # It's not a union if there's only one type left. @@ -577,22 +582,25 @@ def __instancecheck__(self, obj): raise TypeError("Unions cannot be used with isinstance().") def __subclasscheck__(self, cls): - if cls is Any: + raise TypeError("Unions cannot be used with issubclass().") + + def __is_compatible__(self, cls): + if is_consistent(cls, self): return True if self.__union_params__ is None: return isinstance(cls, UnionMeta) elif isinstance(cls, UnionMeta): if cls.__union_params__ is None: return False - return all(issubclass(c, self) for c in (cls.__union_params__)) + return all(is_compatible(c, self) for c in (cls.__union_params__)) elif isinstance(cls, TypeVar): if cls in self.__union_params__: return True if cls.__constraints__: - return issubclass(Union[cls.__constraints__], self) + return is_compatible(Union[cls.__constraints__], self) return False else: - return any(issubclass(cls, t) for t in self.__union_params__) + return any(is_compatible(cls, t) for t in self.__union_params__) class Union(Final, metaclass=UnionMeta, _root=True): @@ -621,7 +629,7 @@ class Union(Final, metaclass=UnionMeta, _root=True): Union[int, str] == Union[str, int] - - When two arguments have a subclass relationship, the least + - When two arguments have a subtype relationship, the least derived argument is kept, e.g.:: class Employee: pass @@ -737,14 +745,17 @@ def __instancecheck__(self, obj): raise TypeError("Tuples cannot be used with isinstance().") def __subclasscheck__(self, cls): - if cls is Any: + raise TypeError("Tuples cannot be used with issubclass().") + + def __is_compatible__(self, cls): + if is_consistent(cls, self): return True if not isinstance(cls, type): return super().__subclasscheck__(cls) # To TypeError. if issubclass(cls, tuple): return True # Special case. if not isinstance(cls, TupleMeta): - return super().__subclasscheck__(cls) # False. + return False if self.__tuple_params__ is None: return True if cls.__tuple_params__ is None: @@ -753,7 +764,7 @@ def __subclasscheck__(self, cls): return False # Covariance. return (len(self.__tuple_params__) == len(cls.__tuple_params__) and - all(issubclass(x, p) + all(is_compatible(x, p) for x, p in zip(cls.__tuple_params__, self.__tuple_params__))) @@ -849,10 +860,16 @@ def __instancecheck__(self, obj): if self.__args__ is None and self.__result__ is None: return isinstance(obj, collections_abc.Callable) else: - raise TypeError("Callable[] cannot be used with isinstance().") + raise TypeError("Callable[...] cannot be used with isinstance().") def __subclasscheck__(self, cls): - if cls is Any: + if self.__args__ is None and self.__result__ is None: + return issubclass(cls, collections_abc.Callable) + else: + raise TypeError("Callable[...] cannot be used with issubclass().") + + def __is_compatible__(self, cls): + if is_consistent(cls, self): return True if not isinstance(cls, CallableMeta): return super().__subclasscheck__(cls) @@ -1317,10 +1334,10 @@ class _ProtocolMeta(GenericMeta): def __instancecheck__(self, obj): raise TypeError("Protocols cannot be used with isinstance().") - def __subclasscheck__(self, cls): + def __is_compatible__(self, cls): if not self._is_protocol: # No structural checks since this isn't a protocol. - return NotImplemented + return super().__is_compatible__(cls) if self is _Protocol: # Every class is a subclass of the empty protocol. @@ -1371,7 +1388,7 @@ def _get_protocol_attrs(self): class _Protocol(metaclass=_ProtocolMeta): """Internal base class for protocol classes. - This implements a simple-minded structural isinstance check + This implements a simple-minded structural type check (similar but more general than the one-offs in collections.abc such as Hashable). """