Skip to content

Commit dbac8c2

Browse files
authored
Fix typing_extensions to support PEP 560 (#549)
The main fix is rewriting Protocol class and its metaclass.
1 parent fcb6f4c commit dbac8c2

File tree

2 files changed

+247
-38
lines changed

2 files changed

+247
-38
lines changed

typing_extensions/src_py3/test_typing_extensions.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
import typing
2121
import typing_extensions
2222
import collections.abc as collections_abc
23+
24+
PEP_560 = sys.version_info[:3] >= (3, 7, 0)
25+
2326
OLD_GENERICS = False
2427
try:
2528
from typing import _type_vars, _next_in_mro, _type_check
@@ -471,7 +474,10 @@ def test_counter_instantiation(self):
471474
class C(typing_extensions.Counter[T]): ...
472475
if TYPING_3_5_3:
473476
self.assertIs(type(C[int]()), C)
474-
self.assertEqual(C.__bases__, (typing_extensions.Counter,))
477+
if not PEP_560:
478+
self.assertEqual(C.__bases__, (typing_extensions.Counter,))
479+
else:
480+
self.assertEqual(C.__bases__, (collections.Counter, typing.Generic))
475481

476482
def test_counter_subclass_instantiation(self):
477483

@@ -818,9 +824,10 @@ def x(self): ...
818824
self.assertIsSubclass(C, P)
819825
self.assertIsSubclass(C, PG)
820826
self.assertIsSubclass(BadP, PG)
821-
self.assertIsSubclass(PG[int], PG)
822-
self.assertIsSubclass(BadPG[int], P)
823-
self.assertIsSubclass(BadPG[T], PG)
827+
if not PEP_560:
828+
self.assertIsSubclass(PG[int], PG)
829+
self.assertIsSubclass(BadPG[int], P)
830+
self.assertIsSubclass(BadPG[T], PG)
824831
with self.assertRaises(TypeError):
825832
issubclass(C, PG[T])
826833
with self.assertRaises(TypeError):
@@ -1043,7 +1050,11 @@ class PR(Protocol, Generic[T, S]):
10431050
def meth(self): pass
10441051
class P(PR[int, str], Protocol):
10451052
y = 1
1046-
self.assertIsSubclass(PR[int, str], PR)
1053+
if not PEP_560:
1054+
self.assertIsSubclass(PR[int, str], PR)
1055+
else:
1056+
with self.assertRaises(TypeError):
1057+
self.assertIsSubclass(PR[int, str], PR)
10471058
self.assertIsSubclass(P, PR)
10481059
with self.assertRaises(TypeError):
10491060
PR[int]
@@ -1091,7 +1102,9 @@ def test_generic_protocols_repr(self):
10911102
T = TypeVar('T')
10921103
S = TypeVar('S')
10931104
class P(Protocol[T, S]): pass
1094-
self.assertTrue(repr(P).endswith('P'))
1105+
# After PEP 560 unsubscripted generics have a standard repr.
1106+
if not PEP_560:
1107+
self.assertTrue(repr(P).endswith('P'))
10951108
self.assertTrue(repr(P[T, S]).endswith('P[~T, ~S]'))
10961109
self.assertTrue(repr(P[int, str]).endswith('P[int, str]'))
10971110

@@ -1135,12 +1148,13 @@ def meth(self):
11351148
self.assertFalse(P._is_runtime_protocol)
11361149
self.assertTrue(PR._is_runtime_protocol)
11371150
self.assertTrue(PG[int]._is_protocol)
1138-
self.assertEqual(P._get_protocol_attrs(), {'meth'})
1139-
self.assertEqual(PR._get_protocol_attrs(), {'x'})
1140-
self.assertEqual(frozenset(PG._get_protocol_attrs()),
1141-
frozenset({'x', 'meth'}))
1142-
self.assertEqual(frozenset(PG[int]._get_protocol_attrs()),
1151+
self.assertEqual(typing_extensions._get_protocol_attrs(P), {'meth'})
1152+
self.assertEqual(typing_extensions._get_protocol_attrs(PR), {'x'})
1153+
self.assertEqual(frozenset(typing_extensions._get_protocol_attrs(PG)),
11431154
frozenset({'x', 'meth'}))
1155+
if not PEP_560:
1156+
self.assertEqual(frozenset(typing_extensions._get_protocol_attrs(PG[int])),
1157+
frozenset({'x', 'meth'}))
11441158

11451159
def test_no_runtime_deco_on_nominal(self):
11461160
with self.assertRaises(TypeError):

typing_extensions/src_py3/typing_extensions.py

Lines changed: 222 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@
55
import typing
66
import collections.abc as collections_abc
77

8+
# After PEP 560, internal typing API was substantially reworked.
9+
# This is especially important for Protocol class which uses internal APIs
10+
# quite extensivelly.
11+
PEP_560 = sys.version_info[:3] >= (3, 7, 0)
12+
813
# These are used by Protocol implementation
914
# We use internal typing helpers here, but this significantly reduces
1015
# code duplication. (Also this is only until Protocol is in typing.)
11-
from typing import GenericMeta, TypingMeta, Generic, Callable, TypeVar, Tuple
16+
from typing import Generic, Callable, TypeVar, Tuple
17+
if PEP_560:
18+
GenericMeta = TypingMeta = type
19+
else:
20+
from typing import GenericMeta, TypingMeta
1221
OLD_GENERICS = False
1322
try:
1423
from typing import _type_vars, _next_in_mro, _type_check
@@ -729,7 +738,31 @@ def _next_in_mro(cls):
729738
next_in_mro = cls.__mro__[i + 1]
730739
return next_in_mro
731740

732-
if HAVE_PROTOCOLS:
741+
742+
def _get_protocol_attrs(cls):
743+
attrs = set()
744+
for base in cls.__mro__[:-1]: # without object
745+
if base.__name__ in ('Protocol', 'Generic'):
746+
continue
747+
annotations = getattr(base, '__annotations__', {})
748+
for attr in list(base.__dict__.keys()) + list(annotations.keys()):
749+
if (not attr.startswith('_abc_') and attr not in (
750+
'__abstractmethods__', '__annotations__', '__weakref__',
751+
'_is_protocol', '_is_runtime_protocol', '__dict__',
752+
'__args__', '__slots__',
753+
'__next_in_mro__', '__parameters__', '__origin__',
754+
'__orig_bases__', '__extra__', '__tree_hash__',
755+
'__doc__', '__subclasshook__', '__init__', '__new__',
756+
'__module__', '_MutableMapping__marker', '_gorg')):
757+
attrs.add(attr)
758+
return attrs
759+
760+
761+
def _is_callable_members_only(cls):
762+
return all(callable(getattr(cls, attr, None)) for attr in _get_protocol_attrs(cls))
763+
764+
765+
if HAVE_PROTOCOLS and not PEP_560:
733766
class _ProtocolMeta(GenericMeta):
734767
"""Internal metaclass for Protocol.
735768
@@ -817,8 +850,6 @@ def __init__(cls, *args, **kwargs):
817850
base.__origin__ is Generic):
818851
raise TypeError('Protocols can only inherit from other'
819852
' protocols, got %r' % base)
820-
cls._callable_members_only = all(callable(getattr(cls, attr, None))
821-
for attr in cls._get_protocol_attrs())
822853

823854
def _no_init(self, *args, **kwargs):
824855
if type(self)._is_protocol:
@@ -831,7 +862,7 @@ def _proto_hook(other):
831862
if not isinstance(other, type):
832863
# Same error as for issubclass(1, int)
833864
raise TypeError('issubclass() arg 1 must be a class')
834-
for attr in cls._get_protocol_attrs():
865+
for attr in _get_protocol_attrs(cls):
835866
for base in other.__mro__:
836867
if attr in base.__dict__:
837868
if base.__dict__[attr] is None:
@@ -850,14 +881,14 @@ def __instancecheck__(self, instance):
850881
# We need this method for situations where attributes are
851882
# assigned in __init__.
852883
if ((not getattr(self, '_is_protocol', False) or
853-
self._callable_members_only) and
884+
_is_callable_members_only(self)) and
854885
issubclass(instance.__class__, self)):
855886
return True
856887
if self._is_protocol:
857888
if all(hasattr(instance, attr) and
858889
(not callable(getattr(self, attr, None)) or
859890
getattr(instance, attr) is not None)
860-
for attr in self._get_protocol_attrs()):
891+
for attr in _get_protocol_attrs(self)):
861892
return True
862893
return super(GenericMeta, self).__instancecheck__(instance)
863894

@@ -874,32 +905,13 @@ def __subclasscheck__(self, cls):
874905
raise TypeError("Instance and class checks can only be used with"
875906
" @runtime protocols")
876907
if (self.__dict__.get('_is_runtime_protocol', None) and
877-
not self._callable_members_only):
908+
not _is_callable_members_only(self)):
878909
if sys._getframe(1).f_globals['__name__'] in ['abc', 'functools', 'typing']:
879910
return super(GenericMeta, self).__subclasscheck__(cls)
880911
raise TypeError("Protocols with non-method members"
881912
" don't support issubclass()")
882913
return super(GenericMeta, self).__subclasscheck__(cls)
883914

884-
def _get_protocol_attrs(self):
885-
attrs = set()
886-
for base in self.__mro__[:-1]: # without object
887-
if base.__name__ in ('Protocol', 'Generic'):
888-
continue
889-
annotations = getattr(base, '__annotations__', {})
890-
for attr in list(base.__dict__.keys()) + list(annotations.keys()):
891-
if (not attr.startswith('_abc_') and attr not in (
892-
'__abstractmethods__', '__annotations__', '__weakref__',
893-
'_is_protocol', '_is_runtime_protocol', '__dict__',
894-
'__args__', '__slots__', '_get_protocol_attrs',
895-
'__next_in_mro__', '__parameters__', '__origin__',
896-
'__orig_bases__', '__extra__', '__tree_hash__',
897-
'__doc__', '__subclasshook__', '__init__', '__new__',
898-
'__module__', '_MutableMapping__marker', '_gorg',
899-
'_callable_members_only')):
900-
attrs.add(attr)
901-
return attrs
902-
903915
if not OLD_GENERICS:
904916
@_tp_cache
905917
def __getitem__(self, params):
@@ -985,6 +997,189 @@ def __new__(cls, *args, **kwds):
985997
Protocol.__doc__ = Protocol.__doc__.format(bases="Protocol, Generic[T]" if
986998
OLD_GENERICS else "Protocol[T]")
987999

1000+
1001+
elif PEP_560:
1002+
from typing import _type_check, _GenericAlias, _collect_type_vars
1003+
1004+
class _ProtocolMeta(abc.ABCMeta):
1005+
# This metaclass is a bit unfortunate and exists only because of the lack
1006+
# of __instancehook__.
1007+
def __instancecheck__(cls, instance):
1008+
# We need this method for situations where attributes are
1009+
# assigned in __init__.
1010+
if ((not getattr(cls, '_is_protocol', False) or
1011+
_is_callable_members_only(cls)) and
1012+
issubclass(instance.__class__, cls)):
1013+
return True
1014+
if cls._is_protocol:
1015+
if all(hasattr(instance, attr) and
1016+
(not callable(getattr(cls, attr, None)) or
1017+
getattr(instance, attr) is not None)
1018+
for attr in _get_protocol_attrs(cls)):
1019+
return True
1020+
return super().__instancecheck__(instance)
1021+
1022+
1023+
class Protocol(metaclass=_ProtocolMeta):
1024+
# There is quite a lot of overlapping code with typing.Generic.
1025+
# Unfortunately it is hard to avoid this while these live in two different modules.
1026+
# The duplicated code will be removed when Protocol is moved to typing.
1027+
"""Base class for protocol classes. Protocol classes are defined as::
1028+
1029+
class Proto(Protocol):
1030+
def meth(self) -> int:
1031+
...
1032+
1033+
Such classes are primarily used with static type checkers that recognize
1034+
structural subtyping (static duck-typing), for example::
1035+
1036+
class C:
1037+
def meth(self) -> int:
1038+
return 0
1039+
1040+
def func(x: Proto) -> int:
1041+
return x.meth()
1042+
1043+
func(C()) # Passes static type check
1044+
1045+
See PEP 544 for details. Protocol classes decorated with
1046+
@typing_extensions.runtime act as simple-minded runtime protocol that checks
1047+
only the presence of given attributes, ignoring their type signatures.
1048+
1049+
Protocol classes can be generic, they are defined as::
1050+
1051+
class GenProto(Protocol[T]):
1052+
def meth(self) -> T:
1053+
...
1054+
"""
1055+
__slots__ = ()
1056+
_is_protocol = True
1057+
1058+
def __new__(cls, *args, **kwds):
1059+
if cls is Protocol:
1060+
raise TypeError("Type Protocol cannot be instantiated; "
1061+
"it can only be used as a base class")
1062+
return super().__new__(cls)
1063+
1064+
@_tp_cache
1065+
def __class_getitem__(cls, params):
1066+
if not isinstance(params, tuple):
1067+
params = (params,)
1068+
if not params and cls is not Tuple:
1069+
raise TypeError(
1070+
"Parameter list to {}[...] cannot be empty".format(cls.__qualname__))
1071+
msg = "Parameters to generic types must be types."
1072+
params = tuple(_type_check(p, msg) for p in params)
1073+
if cls is Protocol:
1074+
# Generic can only be subscripted with unique type variables.
1075+
if not all(isinstance(p, TypeVar) for p in params):
1076+
i = 0
1077+
while isinstance(params[i], TypeVar):
1078+
i += 1
1079+
raise TypeError(
1080+
"Parameters to Protocol[...] must all be type variables."
1081+
" Parameter {} is {}".format(i + 1, params[i]))
1082+
if len(set(params)) != len(params):
1083+
raise TypeError(
1084+
"Parameters to Protocol[...] must all be unique")
1085+
else:
1086+
# Subscripting a regular Generic subclass.
1087+
_check_generic(cls, params)
1088+
return _GenericAlias(cls, params)
1089+
1090+
def __init_subclass__(cls, *args, **kwargs):
1091+
tvars = []
1092+
if '__orig_bases__' in cls.__dict__:
1093+
error = Generic in cls.__orig_bases__
1094+
else:
1095+
error = Generic in cls.__bases__
1096+
if error:
1097+
raise TypeError("Cannot inherit from plain Generic")
1098+
if '__orig_bases__' in cls.__dict__:
1099+
tvars = _collect_type_vars(cls.__orig_bases__)
1100+
# Look for Generic[T1, ..., Tn] or Protocol[T1, ..., Tn].
1101+
# If found, tvars must be a subset of it.
1102+
# If not found, tvars is it.
1103+
# Also check for and reject plain Generic,
1104+
# and reject multiple Generic[...] and/or Protocol[...].
1105+
gvars = None
1106+
for base in cls.__orig_bases__:
1107+
if (isinstance(base, _GenericAlias) and
1108+
base.__origin__ in (Generic, Protocol)):
1109+
# for error messages
1110+
the_base = 'Generic' if base.__origin__ is Generic else 'Protocol'
1111+
if gvars is not None:
1112+
raise TypeError(
1113+
"Cannot inherit from Generic[...]"
1114+
" and/or Protocol[...] multiple types.")
1115+
gvars = base.__parameters__
1116+
if gvars is None:
1117+
gvars = tvars
1118+
else:
1119+
tvarset = set(tvars)
1120+
gvarset = set(gvars)
1121+
if not tvarset <= gvarset:
1122+
s_vars = ', '.join(str(t) for t in tvars if t not in gvarset)
1123+
s_args = ', '.join(str(g) for g in gvars)
1124+
raise TypeError("Some type variables ({}) are"
1125+
" not listed in {}[{}]".format(s_vars, the_base, s_args))
1126+
tvars = gvars
1127+
cls.__parameters__ = tuple(tvars)
1128+
1129+
# Determine if this is a protocol or a concrete subclass.
1130+
if not cls.__dict__.get('_is_protocol', None):
1131+
cls._is_protocol = any(b is Protocol for b in cls.__bases__)
1132+
1133+
# Set (or override) the protocol subclass hook.
1134+
def _proto_hook(other):
1135+
if not cls.__dict__.get('_is_protocol', None):
1136+
return NotImplemented
1137+
if not getattr(cls, '_is_runtime_protocol', False):
1138+
if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']:
1139+
return NotImplemented
1140+
raise TypeError("Instance and class checks can only be used with"
1141+
" @runtime protocols")
1142+
if not _is_callable_members_only(cls):
1143+
if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']:
1144+
return NotImplemented
1145+
raise TypeError("Protocols with non-method members"
1146+
" don't support issubclass()")
1147+
if not isinstance(other, type):
1148+
# Same error as for issubclass(1, int)
1149+
raise TypeError('issubclass() arg 1 must be a class')
1150+
for attr in _get_protocol_attrs(cls):
1151+
for base in other.__mro__:
1152+
if attr in base.__dict__:
1153+
if base.__dict__[attr] is None:
1154+
return NotImplemented
1155+
break
1156+
if (attr in getattr(base, '__annotations__', {}) and
1157+
isinstance(other, _ProtocolMeta) and other._is_protocol):
1158+
break
1159+
else:
1160+
return NotImplemented
1161+
return True
1162+
if '__subclasshook__' not in cls.__dict__:
1163+
cls.__subclasshook__ = _proto_hook
1164+
1165+
# We have nothing more to do for non-protocols.
1166+
if not cls._is_protocol:
1167+
return
1168+
1169+
# Check consistency of bases.
1170+
for base in cls.__bases__:
1171+
if not (base in (object, Generic, Callable) or
1172+
isinstance(base, _ProtocolMeta) and base._is_protocol):
1173+
raise TypeError('Protocols can only inherit from other'
1174+
' protocols, got %r' % base)
1175+
1176+
def _no_init(self, *args, **kwargs):
1177+
if type(self)._is_protocol:
1178+
raise TypeError('Protocols cannot be instantiated')
1179+
cls.__init__ = _no_init
1180+
1181+
1182+
if HAVE_PROTOCOLS:
9881183
def runtime(cls):
9891184
"""Mark a protocol class as a runtime protocol, so that it
9901185
can be used with isinstance() and issubclass(). Raise TypeError

0 commit comments

Comments
 (0)