From 1ebd3f5963ace7241f3c7d0794e838fcbe824955 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 19 Aug 2017 12:27:47 +0200 Subject: [PATCH 01/35] Adapt Protocol and runtime from PR #417 --- .../src_py2/test_typing_extensions.py | 391 +++++++++++++- .../src_py2/typing_extensions.py | 268 +++++++++- .../src_py3/test_typing_extensions.py | 495 +++++++++++++++++- .../src_py3/typing_extensions.py | 264 ++++++++++ 4 files changed, 1413 insertions(+), 5 deletions(-) diff --git a/typing_extensions/src_py2/test_typing_extensions.py b/typing_extensions/src_py2/test_typing_extensions.py index 1597d47f..6a916e72 100644 --- a/typing_extensions/src_py2/test_typing_extensions.py +++ b/typing_extensions/src_py2/test_typing_extensions.py @@ -3,11 +3,12 @@ import abc import contextlib import collections +import pickle from unittest import TestCase, main, skipUnless from typing_extensions import NoReturn, ClassVar from typing_extensions import ContextManager, Counter, Deque, DefaultDict -from typing_extensions import NewType, overload +from typing_extensions import NewType, overload, Protocol, runtime import typing import typing_extensions @@ -211,6 +212,394 @@ def blah(): blah() +class ProtocolTests(BaseTestCase): + + def test_basic_protocol(self): + @runtime + class P(Protocol): + def meth(self): + pass + class C(object): pass + class D(object): + def meth(self): + pass + self.assertIsSubclass(D, P) + self.assertIsInstance(D(), P) + self.assertNotIsSubclass(C, P) + self.assertNotIsInstance(C(), P) + + def test_everything_implements_empty_protocol(self): + @runtime + class Empty(Protocol): pass + class C(object): pass + for thing in (object, type, tuple, C): + self.assertIsSubclass(thing, Empty) + for thing in (object(), 1, (), typing): + self.assertIsInstance(thing, Empty) + + def test_no_inheritance_from_nominal(self): + class C(object): pass + class BP(Protocol): pass + with self.assertRaises(TypeError): + class P(C, Protocol): + pass + with self.assertRaises(TypeError): + class P(Protocol, C): + pass + with self.assertRaises(TypeError): + class P(BP, C, Protocol): + pass + class D(BP, C): pass + class E(C, BP): pass + self.assertNotIsInstance(D(), E) + self.assertNotIsInstance(E(), D) + + def test_no_instantiation(self): + class P(Protocol): pass + with self.assertRaises(TypeError): + P() + class C(P): pass + self.assertIsInstance(C(), C) + T = typing.TypeVar('T') + class PG(Protocol[T]): pass + with self.assertRaises(TypeError): + PG() + with self.assertRaises(TypeError): + PG[int]() + with self.assertRaises(TypeError): + PG[T]() + class CG(PG[T]): pass + self.assertIsInstance(CG[int](), CG) + + def test_cannot_instantiate_abstract(self): + @runtime + class P(Protocol): + @abc.abstractmethod + def ameth(self): + raise NotImplementedError + class B(P): + pass + class C(B): + def ameth(self): + return 26 + with self.assertRaises(TypeError): + B() + self.assertIsInstance(C(), P) + + def test_subprotocols_extending(self): + class P1(Protocol): + def meth1(self): + pass + @runtime + class P2(P1, Protocol): + def meth2(self): + pass + class C(object): + def meth1(self): + pass + def meth2(self): + pass + class C1(object): + def meth1(self): + pass + class C2(object): + def meth2(self): + pass + self.assertNotIsInstance(C1(), P2) + self.assertNotIsInstance(C2(), P2) + self.assertNotIsSubclass(C1, P2) + self.assertNotIsSubclass(C2, P2) + self.assertIsInstance(C(), P2) + self.assertIsSubclass(C, P2) + + def test_subprotocols_merging(self): + class P1(Protocol): + def meth1(self): + pass + class P2(Protocol): + def meth2(self): + pass + @runtime + class P(P1, P2, Protocol): + pass + class C(object): + def meth1(self): + pass + def meth2(self): + pass + class C1(object): + def meth1(self): + pass + class C2(object): + def meth2(self): + pass + self.assertNotIsInstance(C1(), P) + self.assertNotIsInstance(C2(), P) + self.assertNotIsSubclass(C1, P) + self.assertNotIsSubclass(C2, P) + self.assertIsInstance(C(), P) + self.assertIsSubclass(C, P) + + def test_protocols_issubclass(self): + T = typing.TypeVar('T') + @runtime + class P(Protocol): + x = 1 + @runtime + class PG(Protocol[T]): + x = 1 + class BadP(Protocol): + x = 1 + class BadPG(Protocol[T]): + x = 1 + class C(object): + x = 1 + self.assertIsSubclass(C, P) + self.assertIsSubclass(C, PG) + self.assertIsSubclass(BadP, PG) + self.assertIsSubclass(PG[int], PG) + self.assertIsSubclass(BadPG[int], P) + self.assertIsSubclass(BadPG[T], PG) + with self.assertRaises(TypeError): + issubclass(C, PG[T]) + with self.assertRaises(TypeError): + issubclass(C, PG[C]) + with self.assertRaises(TypeError): + issubclass(C, BadP) + with self.assertRaises(TypeError): + issubclass(C, BadPG) + with self.assertRaises(TypeError): + issubclass(P, PG[T]) + with self.assertRaises(TypeError): + issubclass(PG, PG[int]) + + def test_protocols_isinstance(self): + T = typing.TypeVar('T') + @runtime + class P(Protocol): + def meth(x): pass + @runtime + class PG(Protocol[T]): + def meth(x): pass + class BadP(Protocol): + def meth(x): pass + class BadPG(Protocol[T]): + def meth(x): pass + class C(object): + def meth(x): pass + self.assertIsInstance(C(), P) + self.assertIsInstance(C(), PG) + with self.assertRaises(TypeError): + isinstance(C(), PG[T]) + with self.assertRaises(TypeError): + isinstance(C(), PG[C]) + with self.assertRaises(TypeError): + isinstance(C(), BadP) + with self.assertRaises(TypeError): + isinstance(C(), BadPG) + + def test_protocols_isinstance_init(self): + T = typing.TypeVar('T') + @runtime + class P(Protocol): + x = 1 + @runtime + class PG(Protocol[T]): + x = 1 + class C(object): + def __init__(self, x): + self.x = x + self.assertIsInstance(C(1), P) + self.assertIsInstance(C(1), PG) + + def test_protocols_support_register(self): + @runtime + class P(Protocol): + x = 1 + class PM(Protocol): + def meth(self): pass + class D(PM): pass + class C(object): pass + D.register(C) + P.register(C) + self.assertIsInstance(C(), P) + self.assertIsInstance(C(), D) + + def test_none_blocks_implementation(self): + @runtime + class P(Protocol): + x = 1 + class A(object): + x = 1 + class B(A): + x = None + class C(object): + def __init__(self): + self.x = None + self.assertNotIsInstance(B(), P) + self.assertNotIsInstance(C(), P) + + def test_non_protocol_subclasses(self): + class P(Protocol): + x = 1 + @runtime + class PR(Protocol): + def meth(self): pass + class NonP(P): + x = 1 + class NonPR(PR): pass + class C(object): + x = 1 + class D(object): + def meth(self): pass + self.assertNotIsInstance(C(), NonP) + self.assertNotIsInstance(D(), NonPR) + self.assertNotIsSubclass(C, NonP) + self.assertNotIsSubclass(D, NonPR) + self.assertIsInstance(NonPR(), PR) + self.assertIsSubclass(NonPR, PR) + + def test_custom_subclasshook(self): + class P(Protocol): + x = 1 + class OKClass(object): pass + class BadClass(object): + x = 1 + class C(P): + @classmethod + def __subclasshook__(cls, other): + return other.__name__.startswith("OK") + self.assertIsInstance(OKClass(), C) + self.assertNotIsInstance(BadClass(), C) + self.assertIsSubclass(OKClass, C) + self.assertNotIsSubclass(BadClass, C) + + def test_defining_generic_protocols(self): + T = typing.TypeVar('T') + S = typing.TypeVar('S') + @runtime + class PR(Protocol[T, S]): + def meth(self): pass + class P(PR[int, T], Protocol[T]): + y = 1 + self.assertIsSubclass(PR[int, T], PR) + self.assertIsSubclass(P[str], PR) + with self.assertRaises(TypeError): + PR[int] + with self.assertRaises(TypeError): + P[int, str] + with self.assertRaises(TypeError): + PR[int, 1] + with self.assertRaises(TypeError): + PR[int, ClassVar] + class C(PR[int, T]): pass + self.assertIsInstance(C[str](), C) + + def test_init_called(self): + T = typing.TypeVar('T') + class P(Protocol[T]): pass + class C(P[T]): + def __init__(self): + self.test = 'OK' + self.assertEqual(C[int]().test, 'OK') + + def test_protocols_bad_subscripts(self): + T = typing.TypeVar('T') + S = typing.TypeVar('S') + with self.assertRaises(TypeError): + class P(Protocol[T, T]): pass + with self.assertRaises(TypeError): + class P(Protocol[int]): pass + with self.assertRaises(TypeError): + class P(Protocol[T], Protocol[S]): pass + with self.assertRaises(TypeError): + class P(Protocol[T], typing.Mapping[T, S]): pass + + def test_generic_protocols_repr(self): + T = typing.TypeVar('T') + S = typing.TypeVar('S') + class P(Protocol[T, S]): pass + self.assertTrue(repr(P).endswith('P')) + self.assertTrue(repr(P[T, S]).endswith('P[~T, ~S]')) + self.assertTrue(repr(P[int, str]).endswith('P[int, str]')) + + def test_generic_protocols_eq(self): + T = typing.TypeVar('T') + S = typing.TypeVar('S') + class P(Protocol[T, S]): pass + self.assertEqual(P, P) + self.assertEqual(P[int, T], P[int, T]) + self.assertEqual(P[T, T][typing.Tuple[T, S]][int, str], + P[typing.Tuple[int, str], typing.Tuple[int, str]]) + + def test_generic_protocols_special_from_generic(self): + T = typing.TypeVar('T') + class P(Protocol[T]): pass + self.assertEqual(P.__parameters__, (T,)) + self.assertIs(P.__args__, None) + self.assertIs(P.__origin__, None) + self.assertEqual(P[int].__parameters__, ()) + self.assertEqual(P[int].__args__, (int,)) + self.assertIs(P[int].__origin__, P) + + def test_generic_protocols_special_from_protocol(self): + @runtime + class PR(Protocol): + x = 1 + class P(Protocol): + def meth(self): + pass + T = typing.TypeVar('T') + class PG(Protocol[T]): + x = 1 + def meth(self): + pass + self.assertTrue(P._is_protocol) + self.assertTrue(PR._is_protocol) + self.assertTrue(PG._is_protocol) + with self.assertRaises(AttributeError): + self.assertFalse(P._is_runtime_protocol) + self.assertTrue(PR._is_runtime_protocol) + self.assertTrue(PG[int]._is_protocol) + self.assertEqual(P._get_protocol_attrs(), {'meth'}) + self.assertEqual(PR._get_protocol_attrs(), {'x'}) + self.assertEqual(frozenset(PG._get_protocol_attrs()), + frozenset({'x', 'meth'})) + self.assertEqual(frozenset(PG[int]._get_protocol_attrs()), + frozenset({'x', 'meth'})) + + def test_no_runtime_deco_on_nominal(self): + with self.assertRaises(TypeError): + @runtime + class C(object): pass + + def test_protocols_pickleable(self): + global P, CP # pickle wants to reference the class by name + T = typing.TypeVar('T') + + @runtime + class P(Protocol[T]): + x = 1 + class CP(P[int]): + pass + + c = CP() + c.foo = 42 + c.bar = 'abc' + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + z = pickle.dumps(c, proto) + x = pickle.loads(z) + self.assertEqual(x.foo, 42) + self.assertEqual(x.bar, 'abc') + self.assertEqual(x.x, 1) + self.assertEqual(x.__dict__, {'foo': 42, 'bar': 'abc'}) + s = pickle.dumps(P) + D = pickle.loads(s) + class E(object): + x = 1 + self.assertIsInstance(E(), D) + + class AllTests(BaseTestCase): def test_typing_extensions_includes_standard(self): diff --git a/typing_extensions/src_py2/typing_extensions.py b/typing_extensions/src_py2/typing_extensions.py index 29858ca5..fdec8de5 100644 --- a/typing_extensions/src_py2/typing_extensions.py +++ b/typing_extensions/src_py2/typing_extensions.py @@ -1,10 +1,14 @@ import abc -import collections +import sys import typing from typing import ( - ClassVar, Type, - Counter, DefaultDict, Deque, + ClassVar, Type, Generic, Callable, GenericMeta, TypingMeta, + Counter, DefaultDict, Deque, TypeVar, Tuple, NewType, overload, Text, TYPE_CHECKING, + # We use internal typing helpers here, but this significantly reduces + # code duplication. (Also this is only until Protocol is in typing.) + _generic_new, _type_vars, _next_in_mro, _tp_cache, _type_check, + _TypingEllipsis, _TypingEmpty, _make_subclasshook, _check_generic ) # Please keep __all__ alphabetized within each category. @@ -12,6 +16,7 @@ # Super-special typing primitives. 'ClassVar', 'Type', + 'Protocol', # Concrete collection types. 'ContextManager', @@ -22,6 +27,7 @@ # One-off things. 'NewType', 'overload', + 'runtime', 'Text', 'TYPE_CHECKING', ] @@ -89,3 +95,259 @@ def __subclasshook__(cls, C): return True return NotImplemented + +def _collection_protocol(cls): + # Selected set of collections ABCs that are considered protocols. + name = cls.__name__ + return (name in ('ABC', 'Callable', 'Awaitable', + 'Iterable', 'Iterator', 'AsyncIterable', 'AsyncIterator', + 'Hashable', 'Sized', 'Container', 'Collection', 'Reversible', + 'Sequence', 'MutableSequence', 'Mapping', 'MutableMapping', + 'AbstractContextManager', 'ContextManager', + 'AbstractAsyncContextManager', 'AsyncContextManager',) and + cls.__module__ in ('collections.abc', 'typing', 'contextlib', + '_abcoll', 'abc')) + + +class ProtocolMeta(GenericMeta): + """Internal metaclass for Protocol. + + This exists so Protocol classes can be generic without deriving + from Generic. + """ + + def __new__(cls, name, bases, namespace, + tvars=None, args=None, origin=None, extra=None, orig_bases=None): + # This is just a version copied from GenericMeta.__new__ that + # includes "Protocol" special treatment. (Comments removed for brevity.) + if tvars is not None: + assert origin is not None + assert all(isinstance(t, TypeVar) for t in tvars), tvars + else: + tvars = _type_vars(bases) + gvars = None + for base in bases: + if base is Generic: + raise TypeError("Cannot inherit from plain Generic") + if (isinstance(base, GenericMeta) and + base.__origin__ in (Generic, Protocol)): + if gvars is not None: + raise TypeError( + "Cannot inherit from Generic[...] or" + " Protocol[...] multiple types.") + gvars = base.__parameters__ + if gvars is None: + gvars = tvars + else: + tvarset = set(tvars) + gvarset = set(gvars) + if not tvarset <= gvarset: + raise TypeError( + "Some type variables (%s) " + "are not listed in %s[%s]" % + (", ".join(str(t) for t in tvars if t not in gvarset), + "Generic" if any(b.__origin__ is Generic + for b in bases) else "Protocol", + ", ".join(str(g) for g in gvars))) + tvars = gvars + + initial_bases = bases + if extra is None: + extra = namespace.get('__extra__') + if extra is not None and type(extra) is abc.ABCMeta and extra not in bases: + bases = (extra,) + bases + bases = tuple(b._gorg if isinstance(b, GenericMeta) else b for b in bases) + + if any(isinstance(b, GenericMeta) and b is not Generic for b in bases): + bases = tuple(b for b in bases if b is not Generic) + namespace.update({'__origin__': origin, '__extra__': extra}) + self = abc.ABCMeta.__new__(cls, name, bases, namespace) + abc.ABCMeta.__setattr__(self, '_gorg', self if not origin else origin._gorg) + + self.__parameters__ = tvars + self.__args__ = tuple(Ellipsis if a is _TypingEllipsis else + () if a is _TypingEmpty else + a for a in args) if args else None + self.__next_in_mro__ = _next_in_mro(self) + if orig_bases is None: + self.__orig_bases__ = initial_bases + if ( + '__subclasshook__' not in namespace and extra or + getattr(self.__subclasshook__, '__name__', '') == '__extrahook__' + ): + self.__subclasshook__ = _make_subclasshook(self) + self.__tree_hash__ = (hash(self._subs_tree()) if origin else + abc.ABCMeta.__hash__(self)) + return self + + def __init__(cls, *args, **kwargs): + super(ProtocolMeta, cls).__init__(*args, **kwargs) + if not cls.__dict__.get('_is_protocol', None): + cls._is_protocol = any(b is Protocol or + isinstance(b, ProtocolMeta) and + b.__origin__ is Protocol + for b in cls.__bases__) + if cls._is_protocol: + for base in cls.__mro__[1:]: + if not (base in (object, Generic, Callable) or + isinstance(base, TypingMeta) and base._is_protocol or + isinstance(base, GenericMeta) and base.__origin__ is Generic or + _collection_protocol(base)): + raise TypeError('Protocols can only inherit from other protocols,' + ' got %r' % base) + + def _no_init(self, *args, **kwargs): + if type(self)._is_protocol: + raise TypeError('Protocols cannot be instantiated') + cls.__init__ = _no_init + + def _proto_hook(cls, other): + if not cls.__dict__.get('_is_protocol', None): + return NotImplemented + for attr in cls._get_protocol_attrs(): + for base in other.__mro__: + if attr in base.__dict__: + if base.__dict__[attr] is None: + return NotImplemented + break + else: + return NotImplemented + return True + if '__subclasshook__' not in cls.__dict__: + cls.__subclasshook__ = classmethod(_proto_hook) + + def __instancecheck__(self, instance): + # We need this method for situations where attributes are assigned in __init__ + if isinstance(instance, type): + # This looks like a fundamental limitation of Python 2. + # It cannot support runtime protocol metaclasses + return False + if issubclass(instance.__class__, self): + return True + if self._is_protocol: + return all(hasattr(instance, attr) and getattr(instance, attr) is not None + for attr in self._get_protocol_attrs()) + return False + + def __subclasscheck__(self, cls): + if (self.__dict__.get('_is_protocol', None) and + not self.__dict__.get('_is_runtime_protocol', None)): + if sys._getframe(1).f_globals['__name__'] in ['abc', 'functools']: + return False + raise TypeError("Instance and class checks can only be used with" + " @runtime protocols") + return super(ProtocolMeta, self).__subclasscheck__(cls) + + def _get_protocol_attrs(self): + attrs = set() + for base in self.__mro__[:-1]: # without object + if base.__name__ in ('Protocol', 'Generic'): + continue + annotations = getattr(base, '__annotations__', {}) + for attr in list(base.__dict__.keys()) + list(annotations.keys()): + if (not attr.startswith('_abc_') and attr not in ( + '__abstractmethods__', '__annotations__', '__weakref__', + '_is_protocol', '_is_runtime_protocol', '__dict__', + '__args__', '__slots__', '_get_protocol_attrs', + '__next_in_mro__', '__parameters__', '__origin__', + '__orig_bases__', '__extra__', '__tree_hash__', + '__doc__', '__subclasshook__', '__init__', '__new__', + '__module__', '_MutableMapping__marker', + '__metaclass__', '_gorg') and + getattr(base, attr, object()) is not None): + attrs.add(attr) + return attrs + + @_tp_cache + def __getitem__(self, params): + # We also need to copy this from GenericMeta.__getitem__ to get + # special treatment of "Protocol". (Comments removed for brevity.) + if not isinstance(params, tuple): + params = (params,) + if not params and self._gorg is not Tuple: + raise TypeError( + "Parameter list to %s[...] cannot be empty" % self.__qualname__) + msg = "Parameters to generic types must be types." + params = tuple(_type_check(p, msg) for p in params) + if self in (Generic, Protocol): + if not all(isinstance(p, TypeVar) for p in params): + raise TypeError( + "Parameters to %r[...] must all be type variables", self) + if len(set(params)) != len(params): + raise TypeError( + "Parameters to %r[...] must all be unique", self) + tvars = params + args = params + elif self in (Tuple, Callable): + tvars = _type_vars(params) + args = params + elif self.__origin__ in (Generic, Protocol): + raise TypeError("Cannot subscript already-subscripted %s" % + repr(self)) + else: + _check_generic(self, params) + tvars = _type_vars(params) + args = params + + prepend = (self,) if self.__origin__ is None else () + return self.__class__(self.__name__, + prepend + self.__bases__, + dict(self.__dict__), + tvars=tvars, + args=args, + origin=self, + extra=self.__extra__, + orig_bases=self.__orig_bases__) + + +class Protocol(object): + """Base class for protocol classes. Protocol classes are defined as:: + + class Proto(Protocol[T]): + def meth(self): + # type: () -> int + ... + + Such classes are primarily used with static type checkers that recognize + structural subtyping (static duck-typing), for example:: + + class C: + def meth(self): + # type: () -> int + return 0 + + def func(x): + # type: (Proto[int]) -> int + return x.meth() + + func(C()) # Passes static type check + + See PEP 544 for details. Protocol classes decorated with @typing_extensions.runtime + act as simple-minded runtime protocols that checks only the presence of + given attributes, ignoring their type signatures. + """ + + __metaclass__ = ProtocolMeta + __slots__ = () + _is_protocol = True + + def __new__(cls, *args, **kwds): + if cls._gorg is Protocol: + raise TypeError("Type Protocol cannot be instantiated; " + "it can be used only as a base class") + return _generic_new(cls.__next_in_mro__, cls, *args, **kwds) + + +def runtime(cls): + """Mark a protocol class as a runtime protocol, so that it + can be used with isinstance() and issubclass(). Raise TypeError + if applied to a non-protocol class. + + This allows a simple-minded structural check very similar to the + one-offs in collections.abc such as Hashable. + """ + if not isinstance(cls, ProtocolMeta) or not cls._is_protocol: + raise TypeError('@runtime can be only applied to protocol classes,' + ' got %r' % cls) + cls._is_runtime_protocol = True + return cls diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index 4e6f724f..d3fc9e2f 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -3,6 +3,7 @@ import abc import contextlib import collections +import pickle from unittest import TestCase, main, skipUnless from typing import TypeVar, Optional from typing import T, KT, VT # Not in __all__. @@ -10,7 +11,7 @@ from typing import Generic from typing import get_type_hints from typing import no_type_check -from typing_extensions import NoReturn, ClassVar, Type, NewType +from typing_extensions import NoReturn, ClassVar, Type, NewType, Protocol, runtime import typing import typing_extensions import collections.abc as collections_abc @@ -594,6 +595,498 @@ class D(UserName): pass +PY36_PROTOCOL_TESTS = """ +class Coordinate(Protocol): + x: int + y: int + +@runtime +class Point(Coordinate, Protocol): + label: str + +class MyPoint: + x: int + y: int + label: str + +class XAxis(Protocol): + x: int + +class YAxis(Protocol): + y: int + +@runtime +class Position(XAxis, YAxis, Protocol): + pass + +@runtime +class Proto(Protocol): + attr: int + def meth(self, arg: str) -> int: + ... + +class Concrete(Proto): + pass + +class Other: + attr: int = 1 + def meth(self, arg: str) -> int: + if arg == 'this': + return 1 + return 0 + +class NT(NamedTuple): + x: int + y: int +""" + +if PY36: + exec(PY36_PROTOCOL_TESTS) +else: + # fake names for the sake of static analysis + Coordinate = Point = MyPoint = BadPoint = NT = object + XAxis = YAxis = Position = Proto = Concrete = Other = object + + +class ProtocolTests(BaseTestCase): + + def test_basic_protocol(self): + @runtime + class P(Protocol): + def meth(self): + pass + class C: pass + class D: + def meth(self): + pass + self.assertIsSubclass(D, P) + self.assertIsInstance(D(), P) + self.assertNotIsSubclass(C, P) + self.assertNotIsInstance(C(), P) + + def test_everything_implements_empty_protocol(self): + @runtime + class Empty(Protocol): pass + class C: pass + for thing in (object, type, tuple, C): + self.assertIsSubclass(thing, Empty) + for thing in (object(), 1, (), typing): + self.assertIsInstance(thing, Empty) + + def test_no_inheritance_from_nominal(self): + class C: pass + class BP(Protocol): pass + with self.assertRaises(TypeError): + class P(C, Protocol): + pass + with self.assertRaises(TypeError): + class P(Protocol, C): + pass + with self.assertRaises(TypeError): + class P(BP, C, Protocol): + pass + class D(BP, C): pass + class E(C, BP): pass + self.assertNotIsInstance(D(), E) + self.assertNotIsInstance(E(), D) + + def test_no_instantiation(self): + class P(Protocol): pass + with self.assertRaises(TypeError): + P() + class C(P): pass + self.assertIsInstance(C(), C) + T = TypeVar('T') + class PG(Protocol[T]): pass + with self.assertRaises(TypeError): + PG() + with self.assertRaises(TypeError): + PG[int]() + with self.assertRaises(TypeError): + PG[T]() + class CG(PG[T]): pass + self.assertIsInstance(CG[int](), CG) + + def test_cannot_instantiate_abstract(self): + @runtime + class P(Protocol): + @abc.abstractmethod + def ameth(self) -> int: + raise NotImplementedError + class B(P): + pass + class C(B): + def ameth(self) -> int: + return 26 + with self.assertRaises(TypeError): + B() + self.assertIsInstance(C(), P) + + def test_subprotocols_extending(self): + class P1(Protocol): + def meth1(self): + pass + @runtime + class P2(P1, Protocol): + def meth2(self): + pass + class C: + def meth1(self): + pass + def meth2(self): + pass + class C1: + def meth1(self): + pass + class C2: + def meth2(self): + pass + self.assertNotIsInstance(C1(), P2) + self.assertNotIsInstance(C2(), P2) + self.assertNotIsSubclass(C1, P2) + self.assertNotIsSubclass(C2, P2) + self.assertIsInstance(C(), P2) + self.assertIsSubclass(C, P2) + + def test_subprotocols_merging(self): + class P1(Protocol): + def meth1(self): + pass + class P2(Protocol): + def meth2(self): + pass + @runtime + class P(P1, P2, Protocol): + pass + class C: + def meth1(self): + pass + def meth2(self): + pass + class C1: + def meth1(self): + pass + class C2: + def meth2(self): + pass + self.assertNotIsInstance(C1(), P) + self.assertNotIsInstance(C2(), P) + self.assertNotIsSubclass(C1, P) + self.assertNotIsSubclass(C2, P) + self.assertIsInstance(C(), P) + self.assertIsSubclass(C, P) + + def test_protocols_issubclass(self): + T = TypeVar('T') + @runtime + class P(Protocol): + x = 1 + @runtime + class PG(Protocol[T]): + x = 1 + class BadP(Protocol): + x = 1 + class BadPG(Protocol[T]): + x = 1 + class C: + x = 1 + self.assertIsSubclass(C, P) + self.assertIsSubclass(C, PG) + self.assertIsSubclass(BadP, PG) + self.assertIsSubclass(PG[int], PG) + self.assertIsSubclass(BadPG[int], P) + self.assertIsSubclass(BadPG[T], PG) + with self.assertRaises(TypeError): + issubclass(C, PG[T]) + with self.assertRaises(TypeError): + issubclass(C, PG[C]) + with self.assertRaises(TypeError): + issubclass(C, BadP) + with self.assertRaises(TypeError): + issubclass(C, BadPG) + with self.assertRaises(TypeError): + issubclass(P, PG[T]) + with self.assertRaises(TypeError): + issubclass(PG, PG[int]) + + @skipUnless(PY36, 'Python 3.6 required') + def test_protocols_issubclass_py36(self): + class OtherPoint: + x = 1 + y = 2 + label = 'other' + class Bad: pass + self.assertNotIsSubclass(MyPoint, Point) + self.assertIsSubclass(OtherPoint, Point) + self.assertNotIsSubclass(Bad, Point) + self.assertNotIsSubclass(MyPoint, Position) + self.assertIsSubclass(OtherPoint, Position) + self.assertIsSubclass(Concrete, Proto) + self.assertIsSubclass(Other, Proto) + self.assertNotIsSubclass(Concrete, Other) + self.assertNotIsSubclass(Other, Concrete) + self.assertIsSubclass(Point, Position) + self.assertIsSubclass(NT, Position) + + def test_protocols_isinstance(self): + T = TypeVar('T') + @runtime + class P(Protocol): + def meth(x): ... + @runtime + class PG(Protocol[T]): + def meth(x): ... + class BadP(Protocol): + def meth(x): ... + class BadPG(Protocol[T]): + def meth(x): ... + class C: + def meth(x): ... + self.assertIsInstance(C(), P) + self.assertIsInstance(C(), PG) + with self.assertRaises(TypeError): + isinstance(C(), PG[T]) + with self.assertRaises(TypeError): + isinstance(C(), PG[C]) + with self.assertRaises(TypeError): + isinstance(C(), BadP) + with self.assertRaises(TypeError): + isinstance(C(), BadPG) + + @skipUnless(PY36, 'Python 3.6 required') + def test_protocols_isinstance_py36(self): + class APoint: + def __init__(self, x, y, label): + self.x = x + self.y = y + self.label = label + class BPoint: + label = 'B' + def __init__(self, x, y): + self.x = x + self.y = y + class C: + def __init__(self, attr): + self.attr = attr + def meth(self, arg): + return 0 + class Bad: pass + self.assertIsInstance(APoint(1, 2, 'A'), Point) + self.assertIsInstance(BPoint(1, 2), Point) + self.assertNotIsInstance(MyPoint(), Point) + self.assertIsInstance(BPoint(1, 2), Position) + self.assertIsInstance(Other(), Proto) + self.assertIsInstance(Concrete(), Proto) + self.assertIsInstance(C(42), Proto) + self.assertNotIsInstance(Bad(), Proto) + self.assertNotIsInstance(Bad(), Point) + self.assertNotIsInstance(Bad(), Position) + self.assertNotIsInstance(Bad(), Concrete) + self.assertNotIsInstance(Other(), Concrete) + self.assertIsInstance(NT(1, 2), Position) + + def test_protocols_isinstance_init(self): + T = TypeVar('T') + @runtime + class P(Protocol): + x = 1 + @runtime + class PG(Protocol[T]): + x = 1 + class C: + def __init__(self, x): + self.x = x + self.assertIsInstance(C(1), P) + self.assertIsInstance(C(1), PG) + + def test_protocols_support_register(self): + @runtime + class P(Protocol): + x = 1 + class PM(Protocol): + def meth(self): pass + class D(PM): pass + class C: pass + D.register(C) + P.register(C) + self.assertIsInstance(C(), P) + self.assertIsInstance(C(), D) + + def test_none_blocks_implementation(self): + @runtime + class P(Protocol): + x = 1 + class A: + x = 1 + class B(A): + x = None + class C: + def __init__(self): + self.x = None + self.assertNotIsInstance(B(), P) + self.assertNotIsInstance(C(), P) + + def test_non_protocol_subclasses(self): + class P(Protocol): + x = 1 + @runtime + class PR(Protocol): + def meth(self): pass + class NonP(P): + x = 1 + class NonPR(PR): pass + class C: + x = 1 + class D: + def meth(self): pass + self.assertNotIsInstance(C(), NonP) + self.assertNotIsInstance(D(), NonPR) + self.assertNotIsSubclass(C, NonP) + self.assertNotIsSubclass(D, NonPR) + self.assertIsInstance(NonPR(), PR) + self.assertIsSubclass(NonPR, PR) + + def test_custom_subclasshook(self): + class P(Protocol): + x = 1 + class OKClass: pass + class BadClass: + x = 1 + class C(P): + @classmethod + def __subclasshook__(cls, other): + return other.__name__.startswith("OK") + self.assertIsInstance(OKClass(), C) + self.assertNotIsInstance(BadClass(), C) + self.assertIsSubclass(OKClass, C) + self.assertNotIsSubclass(BadClass, C) + + def test_defining_generic_protocols(self): + T = TypeVar('T') + S = TypeVar('S') + @runtime + class PR(Protocol[T, S]): + def meth(self): pass + class P(PR[int, T], Protocol[T]): + y = 1 + self.assertIsSubclass(PR[int, T], PR) + self.assertIsSubclass(P[str], PR) + with self.assertRaises(TypeError): + PR[int] + with self.assertRaises(TypeError): + P[int, str] + with self.assertRaises(TypeError): + PR[int, 1] + with self.assertRaises(TypeError): + PR[int, ClassVar] + class C(PR[int, T]): pass + self.assertIsInstance(C[str](), C) + + def test_init_called(self): + T = TypeVar('T') + class P(Protocol[T]): pass + class C(P[T]): + def __init__(self): + self.test = 'OK' + self.assertEqual(C[int]().test, 'OK') + + def test_protocols_bad_subscripts(self): + T = TypeVar('T') + S = TypeVar('S') + with self.assertRaises(TypeError): + class P(Protocol[T, T]): pass + with self.assertRaises(TypeError): + class P(Protocol[int]): pass + with self.assertRaises(TypeError): + class P(Protocol[T], Protocol[S]): pass + with self.assertRaises(TypeError): + class P(typing.Mapping[T, S], Protocol[T]): pass + + def test_generic_protocols_repr(self): + T = TypeVar('T') + S = TypeVar('S') + class P(Protocol[T, S]): pass + self.assertTrue(repr(P).endswith('P')) + self.assertTrue(repr(P[T, S]).endswith('P[~T, ~S]')) + self.assertTrue(repr(P[int, str]).endswith('P[int, str]')) + + def test_generic_protocols_eq(self): + T = TypeVar('T') + S = TypeVar('S') + class P(Protocol[T, S]): pass + self.assertEqual(P, P) + self.assertEqual(P[int, T], P[int, T]) + self.assertEqual(P[T, T][Tuple[T, S]][int, str], + P[Tuple[int, str], Tuple[int, str]]) + + def test_generic_protocols_special_from_generic(self): + T = TypeVar('T') + class P(Protocol[T]): pass + self.assertEqual(P.__parameters__, (T,)) + self.assertIs(P.__args__, None) + self.assertIs(P.__origin__, None) + self.assertEqual(P[int].__parameters__, ()) + self.assertEqual(P[int].__args__, (int,)) + self.assertIs(P[int].__origin__, P) + + def test_generic_protocols_special_from_protocol(self): + @runtime + class PR(Protocol): + x = 1 + class P(Protocol): + def meth(self): + pass + T = TypeVar('T') + class PG(Protocol[T]): + x = 1 + def meth(self): + pass + self.assertTrue(P._is_protocol) + self.assertTrue(PR._is_protocol) + self.assertTrue(PG._is_protocol) + with self.assertRaises(AttributeError): + self.assertFalse(P._is_runtime_protocol) + self.assertTrue(PR._is_runtime_protocol) + self.assertTrue(PG[int]._is_protocol) + self.assertEqual(P._get_protocol_attrs(), {'meth'}) + self.assertEqual(PR._get_protocol_attrs(), {'x'}) + self.assertEqual(frozenset(PG._get_protocol_attrs()), + frozenset({'x', 'meth'})) + self.assertEqual(frozenset(PG[int]._get_protocol_attrs()), + frozenset({'x', 'meth'})) + + def test_no_runtime_deco_on_nominal(self): + with self.assertRaises(TypeError): + @runtime + class C: pass + + def test_protocols_pickleable(self): + global P, CP # pickle wants to reference the class by name + T = TypeVar('T') + + @runtime + class P(Protocol[T]): + x = 1 + class CP(P[int]): + pass + + c = CP() + c.foo = 42 + c.bar = 'abc' + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + z = pickle.dumps(c, proto) + x = pickle.loads(z) + self.assertEqual(x.foo, 42) + self.assertEqual(x.bar, 'abc') + self.assertEqual(x.x, 1) + self.assertEqual(x.__dict__, {'foo': 42, 'bar': 'abc'}) + s = pickle.dumps(P) + D = pickle.loads(s) + class E: + x = 1 + self.assertIsInstance(E(), D) + + class AllTests(BaseTestCase): def test_typing_extensions_includes_standard(self): a = typing_extensions.__all__ diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 53e2def2..17a512f3 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -5,6 +5,15 @@ import typing import collections.abc as collections_abc +# These are used by Protocol implementation +# We use internal typing helpers here, but this significantly reduces +# code duplication. (Also this is only until Protocol is in typing.) +from typing import ( + GenericMeta, TypingMeta, Generic, Callable, TypeVar, Tuple, + _no_slots_copy, _type_vars, _next_in_mro, _tp_cache, _type_check, + _TypingEllipsis, _TypingEmpty, _make_subclasshook, _check_generic +) + if hasattr(typing, '_generic_new'): _generic_new = typing._generic_new else: @@ -54,6 +63,7 @@ def _check_methods_in_mro(C, *methods): # Super-special typing primitives. 'ClassVar', 'Type', + 'Protocol', # ABCs (from collections.abc). # The following are added depending on presence @@ -75,6 +85,7 @@ def _check_methods_in_mro(C, *methods): # One-off things. 'NewType', 'overload', + 'runtime', 'Text', 'TYPE_CHECKING', ] @@ -620,3 +631,256 @@ def new_type(x): else: # Constant that's True when type checking, but False here. TYPE_CHECKING = False + + +def _collection_protocol(cls): + # Selected set of collections ABCs that are considered protocols. + name = cls.__name__ + return (name in ('ABC', 'Callable', 'Awaitable', + 'Iterable', 'Iterator', 'AsyncIterable', 'AsyncIterator', + 'Hashable', 'Sized', 'Container', 'Collection', 'Reversible', + 'Sequence', 'MutableSequence', 'Mapping', 'MutableMapping', + 'AbstractContextManager', 'ContextManager', + 'AbstractAsyncContextManager', 'AsyncContextManager',) and + cls.__module__ in ('collections.abc', 'typing', 'contextlib', + '_abcoll', 'abc')) + + +class ProtocolMeta(GenericMeta): + """Internal metaclass for Protocol. + + This exists so Protocol classes can be generic without deriving + from Generic. + """ + def __new__(cls, name, bases, namespace, + tvars=None, args=None, origin=None, extra=None, orig_bases=None): + # This is just a version copied from GenericMeta.__new__ that + # includes "Protocol" special treatment. (Comments removed for brevity.) + if tvars is not None: + assert origin is not None + assert all(isinstance(t, TypeVar) for t in tvars), tvars + else: + tvars = _type_vars(bases) + gvars = None + for base in bases: + if base is Generic: + raise TypeError("Cannot inherit from plain Generic") + if (isinstance(base, GenericMeta) and + base.__origin__ in (Generic, Protocol)): + if gvars is not None: + raise TypeError( + "Cannot inherit from Generic[...] or" + " Protocol[...] multiple types.") + gvars = base.__parameters__ + if gvars is None: + gvars = tvars + else: + tvarset = set(tvars) + gvarset = set(gvars) + if not tvarset <= gvarset: + raise TypeError( + "Some type variables (%s) " + "are not listed in %s[%s]" % + (", ".join(str(t) for t in tvars if t not in gvarset), + "Generic" if any(b.__origin__ is Generic + for b in bases) else "Protocol", + ", ".join(str(g) for g in gvars))) + tvars = gvars + + initial_bases = bases + if extra is not None and type(extra) is abc.ABCMeta and extra not in bases: + bases = (extra,) + bases + bases = tuple(b._gorg if isinstance(b, GenericMeta) else b for b in bases) + if any(isinstance(b, GenericMeta) and b is not Generic for b in bases): + bases = tuple(b for b in bases if b is not Generic) + namespace.update({'__origin__': origin, '__extra__': extra}) + self = super(GenericMeta, cls).__new__(cls, name, bases, namespace, _root=True) + super(GenericMeta, self).__setattr__('_gorg', + self if not origin else origin._gorg) + self.__parameters__ = tvars + self.__args__ = tuple(... if a is _TypingEllipsis else + () if a is _TypingEmpty else + a for a in args) if args else None + self.__next_in_mro__ = _next_in_mro(self) + if orig_bases is None: + self.__orig_bases__ = initial_bases + if ( + '__subclasshook__' not in namespace and extra or + getattr(self.__subclasshook__, '__name__', '') == '__extrahook__' + ): + self.__subclasshook__ = _make_subclasshook(self) + if isinstance(extra, abc.ABCMeta): + self._abc_registry = extra._abc_registry + self._abc_cache = extra._abc_cache + elif origin is not None: + self._abc_registry = origin._abc_registry + self._abc_cache = origin._abc_cache + self.__tree_hash__ = (hash(self._subs_tree()) if origin else + super(GenericMeta, self).__hash__()) + return self + + def __init__(cls, *args, **kwargs): + super().__init__(*args, **kwargs) + if not cls.__dict__.get('_is_protocol', None): + cls._is_protocol = any(b is Protocol or + isinstance(b, ProtocolMeta) and + b.__origin__ is Protocol + for b in cls.__bases__) + if cls._is_protocol: + for base in cls.__mro__[1:]: + if not (base in (object, Generic, Callable) or + isinstance(base, TypingMeta) and base._is_protocol or + isinstance(base, GenericMeta) and base.__origin__ is Generic or + _collection_protocol(base)): + raise TypeError('Protocols can only inherit from other protocols,' + ' got %r' % base) + + def _no_init(self, *args, **kwargs): + if type(self)._is_protocol: + raise TypeError('Protocols cannot be instantiated') + cls.__init__ = _no_init + + def _proto_hook(other): + if not cls.__dict__.get('_is_protocol', None): + return NotImplemented + for attr in cls._get_protocol_attrs(): + for base in other.__mro__: + if attr in base.__dict__: + if base.__dict__[attr] is None: + return NotImplemented + break + if (attr in getattr(base, '__annotations__', {}) and + isinstance(other, ProtocolMeta) and other._is_protocol): + break + else: + return NotImplemented + return True + if '__subclasshook__' not in cls.__dict__: + cls.__subclasshook__ = _proto_hook + + def __instancecheck__(self, instance): + # We need this method for situations where attributes are assigned in __init__ + if issubclass(instance.__class__, self): + return True + if self._is_protocol: + return all(hasattr(instance, attr) and getattr(instance, attr) is not None + for attr in self._get_protocol_attrs()) + return False + + def __subclasscheck__(self, cls): + if (self.__dict__.get('_is_protocol', None) and + not self.__dict__.get('_is_runtime_protocol', None)): + if sys._getframe(1).f_globals['__name__'] in ['abc', 'functools']: + return False + raise TypeError("Instance and class checks can only be used with" + " @runtime protocols") + return super().__subclasscheck__(cls) + + def _get_protocol_attrs(self): + attrs = set() + for base in self.__mro__[:-1]: # without object + if base.__name__ in ('Protocol', 'Generic'): + continue + annotations = getattr(base, '__annotations__', {}) + for attr in list(base.__dict__.keys()) + list(annotations.keys()): + if (not attr.startswith('_abc_') and attr not in ( + '__abstractmethods__', '__annotations__', '__weakref__', + '_is_protocol', '_is_runtime_protocol', '__dict__', + '__args__', '__slots__', '_get_protocol_attrs', + '__next_in_mro__', '__parameters__', '__origin__', + '__orig_bases__', '__extra__', '__tree_hash__', + '__doc__', '__subclasshook__', '__init__', '__new__', + '__module__', '_MutableMapping__marker', '_gorg') and + getattr(base, attr, object()) is not None): + attrs.add(attr) + return attrs + + @_tp_cache + def __getitem__(self, params): + # We also need to copy this from GenericMeta.__getitem__ to get + # special treatment of "Protocol". (Comments removed for brevity.) + if not isinstance(params, tuple): + params = (params,) + if not params and self._gorg is not Tuple: + raise TypeError( + "Parameter list to %s[...] cannot be empty" % self.__qualname__) + msg = "Parameters to generic types must be types." + params = tuple(_type_check(p, msg) for p in params) + if self in (Generic, Protocol): + if not all(isinstance(p, TypeVar) for p in params): + raise TypeError( + "Parameters to %r[...] must all be type variables" % self) + if len(set(params)) != len(params): + raise TypeError( + "Parameters to %r[...] must all be unique" % self) + tvars = params + args = params + elif self in (Tuple, Callable): + tvars = _type_vars(params) + args = params + elif self.__origin__ in (Generic, Protocol): + raise TypeError("Cannot subscript already-subscripted %s" % + repr(self)) + else: + _check_generic(self, params) + tvars = _type_vars(params) + args = params + + prepend = (self,) if self.__origin__ is None else () + return self.__class__(self.__name__, + prepend + self.__bases__, + _no_slots_copy(self.__dict__), + tvars=tvars, + args=args, + origin=self, + extra=self.__extra__, + orig_bases=self.__orig_bases__) + + +class Protocol(metaclass=ProtocolMeta): + """Base class for protocol classes. Protocol classes are defined as:: + + class Proto(Protocol[T]): + def meth(self) -> T: + ... + + Such classes are primarily used with static type checkers that recognize + structural subtyping (static duck-typing), for example:: + + class C: + def meth(self) -> int: + return 0 + + def func(x: Proto[int]) -> int: + return x.meth() + + func(C()) # Passes static type check + + See PEP 544 for details. Protocol classes decorated with @typing_extensions.runtime + act as simple-minded runtime protocols that checks only the presence of + given attributes, ignoring their type signatures. + """ + + __slots__ = () + _is_protocol = True + + def __new__(cls, *args, **kwds): + if cls._gorg is Protocol: + raise TypeError("Type Protocol cannot be instantiated; " + "it can be used only as a base class") + return _generic_new(cls.__next_in_mro__, cls, *args, **kwds) + + +def runtime(cls): + """Mark a protocol class as a runtime protocol, so that it + can be used with isinstance() and issubclass(). Raise TypeError + if applied to a non-protocol class. + + This allows a simple-minded structural check very similar to the + one-offs in collections.abc such as Hashable. + """ + if not isinstance(cls, ProtocolMeta) or not cls._is_protocol: + raise TypeError('@runtime can be only applied to protocol classes,' + ' got %r' % cls) + cls._is_runtime_protocol = True + return cls From 37b6a7ba44e81a5ea63014c9f25f4a7a56bebff5 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 19 Aug 2017 12:46:46 +0200 Subject: [PATCH 02/35] An attempt to fix missing failure on some versions --- typing_extensions/src_py3/typing_extensions.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 17a512f3..d0a9b13a 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -768,13 +768,18 @@ def __instancecheck__(self, instance): return False def __subclasscheck__(self, cls): + if self.__origin__ is not None: + if sys._getframe(1).f_globals['__name__'] not in ['abc', 'functools']: + raise TypeError("Parameterized generics cannot be used with class " + "or instance checks") + return False if (self.__dict__.get('_is_protocol', None) and not self.__dict__.get('_is_runtime_protocol', None)): if sys._getframe(1).f_globals['__name__'] in ['abc', 'functools']: return False raise TypeError("Instance and class checks can only be used with" " @runtime protocols") - return super().__subclasscheck__(cls) + return super(GenericMeta, self).__subclasscheck__(cls) def _get_protocol_attrs(self): attrs = set() From bb3a8a9067594e1a0ae5d0d3a9ee2fa00e822ec8 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 19 Aug 2017 12:53:10 +0200 Subject: [PATCH 03/35] Fix missing import --- typing_extensions/src_py3/typing_extensions.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index d0a9b13a..a68ed524 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -10,9 +10,18 @@ # code duplication. (Also this is only until Protocol is in typing.) from typing import ( GenericMeta, TypingMeta, Generic, Callable, TypeVar, Tuple, - _no_slots_copy, _type_vars, _next_in_mro, _tp_cache, _type_check, + _type_vars, _next_in_mro, _tp_cache, _type_check, _TypingEllipsis, _TypingEmpty, _make_subclasshook, _check_generic ) +try: + from typing import _no_slots_copy +except ImportError: + def _no_slots_copy(dct): + dict_copy = dict(dct) + if '__slots__' in dict_copy: + for slot in dict_copy['__slots__']: + dict_copy.pop(slot, None) + return dict_copy if hasattr(typing, '_generic_new'): _generic_new = typing._generic_new From d8ae30ebf2332cf0e239fa7a58c052be5c425685 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 19 Aug 2017 12:59:06 +0200 Subject: [PATCH 04/35] Fix another missing import --- typing_extensions/src_py3/typing_extensions.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index a68ed524..8766e147 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -10,8 +10,8 @@ # code duplication. (Also this is only until Protocol is in typing.) from typing import ( GenericMeta, TypingMeta, Generic, Callable, TypeVar, Tuple, - _type_vars, _next_in_mro, _tp_cache, _type_check, - _TypingEllipsis, _TypingEmpty, _make_subclasshook, _check_generic + _type_vars, _next_in_mro, _type_check, _TypingEllipsis, _TypingEmpty, + _make_subclasshook, _check_generic ) try: from typing import _no_slots_copy @@ -22,6 +22,10 @@ def _no_slots_copy(dct): for slot in dict_copy['__slots__']: dict_copy.pop(slot, None) return dict_copy +try: + from typing import _tp_cache +except ImportError: + _tp_cache = lambda x: x if hasattr(typing, '_generic_new'): _generic_new = typing._generic_new From a4e68ff4515457c4f83b0c07030ed44b9ba4f95c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 19 Aug 2017 13:10:01 +0200 Subject: [PATCH 05/35] And another missing import --- typing_extensions/src_py3/typing_extensions.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 8766e147..f375bb6d 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -10,7 +10,7 @@ # code duplication. (Also this is only until Protocol is in typing.) from typing import ( GenericMeta, TypingMeta, Generic, Callable, TypeVar, Tuple, - _type_vars, _next_in_mro, _type_check, _TypingEllipsis, _TypingEmpty, + _type_vars, _next_in_mro, _type_check, _make_subclasshook, _check_generic ) try: @@ -26,6 +26,11 @@ def _no_slots_copy(dct): from typing import _tp_cache except ImportError: _tp_cache = lambda x: x +try: + from typing import _TypingEllipsis, _TypingEmpty +except ImportError: + class _TypingEllipsis: pass + class _TypingEmpty: pass if hasattr(typing, '_generic_new'): _generic_new = typing._generic_new @@ -721,7 +726,8 @@ def __new__(cls, name, bases, namespace, '__subclasshook__' not in namespace and extra or getattr(self.__subclasshook__, '__name__', '') == '__extrahook__' ): - self.__subclasshook__ = _make_subclasshook(self) + if _make_subclasshook: + self.__subclasshook__ = _make_subclasshook(self) if isinstance(extra, abc.ABCMeta): self._abc_registry = extra._abc_registry self._abc_cache = extra._abc_cache From c8076ee0dc40cca660f2c5ac2d089d40a0611ed0 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 19 Aug 2017 13:22:07 +0200 Subject: [PATCH 06/35] Abandon attempts to support protocols in 3.5.0 and 3.5.1, see #195 --- .../src_py3/test_typing_extensions.py | 14 +++++++++++++- .../src_py3/typing_extensions.py | 19 ++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index d3fc9e2f..7981119e 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -11,7 +11,11 @@ from typing import Generic from typing import get_type_hints from typing import no_type_check -from typing_extensions import NoReturn, ClassVar, Type, NewType, Protocol, runtime +from typing_extensions import NoReturn, ClassVar, Type, NewType +try: + from typing_extensions import Protocol, runtime +except ImportError: + pass import typing import typing_extensions import collections.abc as collections_abc @@ -48,6 +52,10 @@ # For checks reliant on Python 3.6 syntax changes (e.g. classvar) PY36 = sys.version_info[:2] >= (3, 6) +# It is very difficult to backport Protocols to these versions due to +# different generics system. See https://github.com/python/typing/pull/195 +NO_PROTOCOL = sys.version_info[:3] in [(3, 5, 0), (3, 5, 1)] + class BaseTestCase(TestCase): def assertIsSubclass(self, cls, class_or_tuple, msg=None): @@ -1087,6 +1095,10 @@ class E: self.assertIsInstance(E(), D) +if NO_PROTOCOL: + ProtocolTests = None + + class AllTests(BaseTestCase): def test_typing_extensions_includes_standard(self): a = typing_extensions.__all__ diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index f375bb6d..5c5b3b5a 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -8,11 +8,12 @@ # These are used by Protocol implementation # We use internal typing helpers here, but this significantly reduces # code duplication. (Also this is only until Protocol is in typing.) -from typing import ( - GenericMeta, TypingMeta, Generic, Callable, TypeVar, Tuple, - _type_vars, _next_in_mro, _type_check, - _make_subclasshook, _check_generic -) +from typing import GenericMeta, TypingMeta, Generic, Callable, TypeVar, Tuple +NO_PROTOCOL = False +try: + from typing import _type_vars, _next_in_mro, _type_check, _check_generic +except ImportError: + NO_PROTOCOL = True try: from typing import _no_slots_copy except ImportError: @@ -31,6 +32,10 @@ def _no_slots_copy(dct): except ImportError: class _TypingEllipsis: pass class _TypingEmpty: pass +try: + from typing import _make_subclasshook +except ImportError: + _make_subclasshook = None if hasattr(typing, '_generic_new'): _generic_new = typing._generic_new @@ -908,3 +913,7 @@ def runtime(cls): ' got %r' % cls) cls._is_runtime_protocol = True return cls + + +if NO_PROTOCOL: + del Protocol, runtime From 80e8014411e1d66a30a5b5684868d4fc270032a4 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 19 Aug 2017 13:35:24 +0200 Subject: [PATCH 07/35] Attempt to keep support of 3.5.2 --- .../src_py3/typing_extensions.py | 67 +++++++++++-------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 5c5b3b5a..d24f88d6 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -11,7 +11,7 @@ from typing import GenericMeta, TypingMeta, Generic, Callable, TypeVar, Tuple NO_PROTOCOL = False try: - from typing import _type_vars, _next_in_mro, _type_check, _check_generic + from typing import _type_vars, _next_in_mro, _type_check except ImportError: NO_PROTOCOL = True try: @@ -36,6 +36,17 @@ class _TypingEmpty: pass from typing import _make_subclasshook except ImportError: _make_subclasshook = None +try: + from typing import _check_generic +except ImportError: + def _check_generic(cls, parameters): + if not cls.__parameters__: + raise TypeError("%s is not a generic class" % repr(cls)) + alen = len(parameters) + elen = len(cls.__parameters__) + if alen != elen: + raise TypeError("Too %s parameters for %s; actual %s, expected %s" % + ("many" if alen > elen else "few", repr(cls), alen, elen)) if hasattr(typing, '_generic_new'): _generic_new = typing._generic_new @@ -865,39 +876,41 @@ def __getitem__(self, params): extra=self.__extra__, orig_bases=self.__orig_bases__) +if NO_PROTOCOL: + del ProtocolMeta +else: + class Protocol(metaclass=ProtocolMeta): + """Base class for protocol classes. Protocol classes are defined as:: -class Protocol(metaclass=ProtocolMeta): - """Base class for protocol classes. Protocol classes are defined as:: - - class Proto(Protocol[T]): - def meth(self) -> T: - ... + class Proto(Protocol[T]): + def meth(self) -> T: + ... - Such classes are primarily used with static type checkers that recognize - structural subtyping (static duck-typing), for example:: + Such classes are primarily used with static type checkers that recognize + structural subtyping (static duck-typing), for example:: - class C: - def meth(self) -> int: - return 0 + class C: + def meth(self) -> int: + return 0 - def func(x: Proto[int]) -> int: - return x.meth() + def func(x: Proto[int]) -> int: + return x.meth() - func(C()) # Passes static type check + func(C()) # Passes static type check - See PEP 544 for details. Protocol classes decorated with @typing_extensions.runtime - act as simple-minded runtime protocols that checks only the presence of - given attributes, ignoring their type signatures. - """ + See PEP 544 for details. Protocol classes decorated with @typing_extensions.runtime + act as simple-minded runtime protocols that checks only the presence of + given attributes, ignoring their type signatures. + """ - __slots__ = () - _is_protocol = True + __slots__ = () + _is_protocol = True - def __new__(cls, *args, **kwds): - if cls._gorg is Protocol: - raise TypeError("Type Protocol cannot be instantiated; " - "it can be used only as a base class") - return _generic_new(cls.__next_in_mro__, cls, *args, **kwds) + def __new__(cls, *args, **kwds): + if cls._gorg is Protocol: + raise TypeError("Type Protocol cannot be instantiated; " + "it can be used only as a base class") + return _generic_new(cls.__next_in_mro__, cls, *args, **kwds) def runtime(cls): @@ -916,4 +929,4 @@ def runtime(cls): if NO_PROTOCOL: - del Protocol, runtime + del runtime From a6a631a876992fd453004bd49b5ca1ee93599102 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 19 Aug 2017 13:40:40 +0200 Subject: [PATCH 08/35] Hopefully last backward compatibility nit --- typing_extensions/src_py3/typing_extensions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index d24f88d6..a7b19203 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -750,8 +750,9 @@ def __new__(cls, name, bases, namespace, elif origin is not None: self._abc_registry = origin._abc_registry self._abc_cache = origin._abc_cache - self.__tree_hash__ = (hash(self._subs_tree()) if origin else - super(GenericMeta, self).__hash__()) + if hasattr(self, '_subs_tree'): + self.__tree_hash__ = (hash(self._subs_tree()) if origin else + super(GenericMeta, self).__hash__()) return self def __init__(cls, *args, **kwargs): From 5913fe6f25418efbc5904415d363f42652255e2c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 19 Aug 2017 13:50:28 +0200 Subject: [PATCH 09/35] Skip some irrelevant tests --- typing_extensions/src_py3/test_typing_extensions.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index 7981119e..ccadcefc 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -985,8 +985,9 @@ class P(PR[int, T], Protocol[T]): P[int, str] with self.assertRaises(TypeError): PR[int, 1] - with self.assertRaises(TypeError): - PR[int, ClassVar] + if TYPING_3_5_3: + with self.assertRaises(TypeError): + PR[int, ClassVar] class C(PR[int, T]): pass self.assertIsInstance(C[str](), C) @@ -1010,6 +1011,7 @@ class P(Protocol[T], Protocol[S]): pass with self.assertRaises(TypeError): class P(typing.Mapping[T, S], Protocol[T]): pass + @skipUnless(TYPING_3_5_3, 'New style __repr__ and __eq__ only') def test_generic_protocols_repr(self): T = TypeVar('T') S = TypeVar('S') @@ -1018,6 +1020,7 @@ class P(Protocol[T, S]): pass self.assertTrue(repr(P[T, S]).endswith('P[~T, ~S]')) self.assertTrue(repr(P[int, str]).endswith('P[int, str]')) + @skipUnless(TYPING_3_5_3, 'New style __repr__ and __eq__ only') def test_generic_protocols_eq(self): T = TypeVar('T') S = TypeVar('S') From faf4ff08f2be895285252336bfc4c7e7143eb3b7 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 19 Aug 2017 14:05:57 +0200 Subject: [PATCH 10/35] Conditionally add Protocol and runtime to __all__ --- typing_extensions/src_py3/test_typing_extensions.py | 4 ++++ typing_extensions/src_py3/typing_extensions.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index ccadcefc..c8f80f9b 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -1127,6 +1127,10 @@ def test_typing_extensions_includes_standard(self): if PY36: self.assertIn('AsyncGenerator', a) + if TYPING_3_5_3: + self.assertIn('Protocol', a) + self.assertIn('runtime', a) + def test_typing_extensions_defers_when_possible(self): exclude = {'overload', 'Text', 'TYPE_CHECKING'} for item in typing_extensions.__all__: diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index a7b19203..708d3103 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -97,7 +97,6 @@ def _check_methods_in_mro(C, *methods): # Super-special typing primitives. 'ClassVar', 'Type', - 'Protocol', # ABCs (from collections.abc). # The following are added depending on presence @@ -119,11 +118,12 @@ def _check_methods_in_mro(C, *methods): # One-off things. 'NewType', 'overload', - 'runtime', 'Text', 'TYPE_CHECKING', ] +if not NO_PROTOCOL: + __all__.extend(['Protocol', 'runtime']) # TODO if hasattr(typing, 'NoReturn'): From c7f6b68c3a49416489c55a46e0cfe02660f48a20 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 21 Aug 2017 01:30:13 +0200 Subject: [PATCH 11/35] Tweaks following Jelle's comments --- typing_extensions/src_py2/typing_extensions.py | 14 +++++++------- typing_extensions/src_py3/typing_extensions.py | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/typing_extensions/src_py2/typing_extensions.py b/typing_extensions/src_py2/typing_extensions.py index fdec8de5..cebcb4b1 100644 --- a/typing_extensions/src_py2/typing_extensions.py +++ b/typing_extensions/src_py2/typing_extensions.py @@ -109,7 +109,7 @@ def _collection_protocol(cls): '_abcoll', 'abc')) -class ProtocolMeta(GenericMeta): +class _ProtocolMeta(GenericMeta): """Internal metaclass for Protocol. This exists so Protocol classes can be generic without deriving @@ -134,7 +134,7 @@ def __new__(cls, name, bases, namespace, if gvars is not None: raise TypeError( "Cannot inherit from Generic[...] or" - " Protocol[...] multiple types.") + " Protocol[...] multiple times.") gvars = base.__parameters__ if gvars is None: gvars = tvars @@ -181,10 +181,10 @@ def __new__(cls, name, bases, namespace, return self def __init__(cls, *args, **kwargs): - super(ProtocolMeta, cls).__init__(*args, **kwargs) + super(_ProtocolMeta, cls).__init__(*args, **kwargs) if not cls.__dict__.get('_is_protocol', None): cls._is_protocol = any(b is Protocol or - isinstance(b, ProtocolMeta) and + isinstance(b, _ProtocolMeta) and b.__origin__ is Protocol for b in cls.__bases__) if cls._is_protocol: @@ -236,7 +236,7 @@ def __subclasscheck__(self, cls): return False raise TypeError("Instance and class checks can only be used with" " @runtime protocols") - return super(ProtocolMeta, self).__subclasscheck__(cls) + return super(_ProtocolMeta, self).__subclasscheck__(cls) def _get_protocol_attrs(self): attrs = set() @@ -327,7 +327,7 @@ def func(x): given attributes, ignoring their type signatures. """ - __metaclass__ = ProtocolMeta + __metaclass__ = _ProtocolMeta __slots__ = () _is_protocol = True @@ -346,7 +346,7 @@ def runtime(cls): This allows a simple-minded structural check very similar to the one-offs in collections.abc such as Hashable. """ - if not isinstance(cls, ProtocolMeta) or not cls._is_protocol: + if not isinstance(cls, _ProtocolMeta) or not cls._is_protocol: raise TypeError('@runtime can be only applied to protocol classes,' ' got %r' % cls) cls._is_runtime_protocol = True diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 708d3103..14cd0cfd 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -680,7 +680,7 @@ def _collection_protocol(cls): '_abcoll', 'abc')) -class ProtocolMeta(GenericMeta): +class _ProtocolMeta(GenericMeta): """Internal metaclass for Protocol. This exists so Protocol classes can be generic without deriving @@ -704,7 +704,7 @@ def __new__(cls, name, bases, namespace, if gvars is not None: raise TypeError( "Cannot inherit from Generic[...] or" - " Protocol[...] multiple types.") + " Protocol[...] multiple times.") gvars = base.__parameters__ if gvars is None: gvars = tvars @@ -759,7 +759,7 @@ def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) if not cls.__dict__.get('_is_protocol', None): cls._is_protocol = any(b is Protocol or - isinstance(b, ProtocolMeta) and + isinstance(b, _ProtocolMeta) and b.__origin__ is Protocol for b in cls.__bases__) if cls._is_protocol: @@ -786,7 +786,7 @@ def _proto_hook(other): return NotImplemented break if (attr in getattr(base, '__annotations__', {}) and - isinstance(other, ProtocolMeta) and other._is_protocol): + isinstance(other, _ProtocolMeta) and other._is_protocol): break else: return NotImplemented @@ -878,9 +878,9 @@ def __getitem__(self, params): orig_bases=self.__orig_bases__) if NO_PROTOCOL: - del ProtocolMeta + del _ProtocolMeta else: - class Protocol(metaclass=ProtocolMeta): + class Protocol(metaclass=_ProtocolMeta): """Base class for protocol classes. Protocol classes are defined as:: class Proto(Protocol[T]): @@ -922,7 +922,7 @@ def runtime(cls): This allows a simple-minded structural check very similar to the one-offs in collections.abc such as Hashable. """ - if not isinstance(cls, ProtocolMeta) or not cls._is_protocol: + if not isinstance(cls, _ProtocolMeta) or not cls._is_protocol: raise TypeError('@runtime can be only applied to protocol classes,' ' got %r' % cls) cls._is_runtime_protocol = True From 8151355076da15585be9c57868ccf3f02e5ab461 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 2 Sep 2017 20:21:18 +0200 Subject: [PATCH 12/35] Allow Protocol tests on 3.5.1 --- .../src_py3/test_typing_extensions.py | 2 +- typing_extensions/src_py3/typing_extensions.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index c8f80f9b..cb7e808b 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -54,7 +54,7 @@ # It is very difficult to backport Protocols to these versions due to # different generics system. See https://github.com/python/typing/pull/195 -NO_PROTOCOL = sys.version_info[:3] in [(3, 5, 0), (3, 5, 1)] +NO_PROTOCOL = sys.version_info[:3] == (3, 5, 0) class BaseTestCase(TestCase): diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 14cd0cfd..837f6dbd 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -9,11 +9,11 @@ # We use internal typing helpers here, but this significantly reduces # code duplication. (Also this is only until Protocol is in typing.) from typing import GenericMeta, TypingMeta, Generic, Callable, TypeVar, Tuple -NO_PROTOCOL = False +OLD_GENERICS = False try: from typing import _type_vars, _next_in_mro, _type_check except ImportError: - NO_PROTOCOL = True + OLD_GENERICS = True try: from typing import _no_slots_copy except ImportError: @@ -754,6 +754,9 @@ def __new__(cls, name, bases, namespace, self.__tree_hash__ = (hash(self._subs_tree()) if origin else super(GenericMeta, self).__hash__()) return self + if OLD_GENERICS: + def __new__(*args, **kwargs): + return super().__new__(*args, **kwars) def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) @@ -877,6 +880,13 @@ def __getitem__(self, params): extra=self.__extra__, orig_bases=self.__orig_bases__) + if OLD_GENERICS: + def __getitem__(*args, **kwargs): + return super().__getitem__(*args, **kwars) + + +NO_PROTOCOL = sys.version_info[:3] == (3, 5, 0) + if NO_PROTOCOL: del _ProtocolMeta else: From 76a7ecd17d183fcbfbff6dd7a32dddb9f3fe38be Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 2 Sep 2017 20:24:47 +0200 Subject: [PATCH 13/35] Fix constannt NO_PROTOCOL definition --- typing_extensions/src_py3/typing_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 837f6dbd..0785d674 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -122,6 +122,8 @@ def _check_methods_in_mro(C, *methods): 'TYPE_CHECKING', ] +NO_PROTOCOL = sys.version_info[:3] == (3, 5, 0) + if not NO_PROTOCOL: __all__.extend(['Protocol', 'runtime']) @@ -885,8 +887,6 @@ def __getitem__(*args, **kwargs): return super().__getitem__(*args, **kwars) -NO_PROTOCOL = sys.version_info[:3] == (3, 5, 0) - if NO_PROTOCOL: del _ProtocolMeta else: From 95faebe9a82d41313d28ab66c28c73aaa759e154 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 2 Sep 2017 20:28:01 +0200 Subject: [PATCH 14/35] Add super() argument --- typing_extensions/src_py3/typing_extensions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 0785d674..4ffb7eb2 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -757,8 +757,8 @@ def __new__(cls, name, bases, namespace, super(GenericMeta, self).__hash__()) return self if OLD_GENERICS: - def __new__(*args, **kwargs): - return super().__new__(*args, **kwars) + def __new__(self, *args, **kwargs): + return super(self).__new__(*args, **kwars) def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) @@ -883,8 +883,8 @@ def __getitem__(self, params): orig_bases=self.__orig_bases__) if OLD_GENERICS: - def __getitem__(*args, **kwargs): - return super().__getitem__(*args, **kwars) + def __getitem__(self, *args, **kwargs): + return super(self).__getitem__(*args, **kwars) if NO_PROTOCOL: From 04f1c511dd2fd7cd814ae55d4d203e8c198540c5 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 2 Sep 2017 20:29:46 +0200 Subject: [PATCH 15/35] Fix typo --- typing_extensions/src_py3/typing_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 4ffb7eb2..18f86f73 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -758,7 +758,7 @@ def __new__(cls, name, bases, namespace, return self if OLD_GENERICS: def __new__(self, *args, **kwargs): - return super(self).__new__(*args, **kwars) + return super(self).__new__(*args, **kwargs) def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) @@ -884,7 +884,7 @@ def __getitem__(self, params): if OLD_GENERICS: def __getitem__(self, *args, **kwargs): - return super(self).__getitem__(*args, **kwars) + return super(self).__getitem__(*args, **kwargs) if NO_PROTOCOL: From ce4e04ffd3ef2dcecd5f91f6c34d14061d660dc1 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 2 Sep 2017 20:33:55 +0200 Subject: [PATCH 16/35] Fix super() --- typing_extensions/src_py3/typing_extensions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 18f86f73..be6543c5 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -757,8 +757,8 @@ def __new__(cls, name, bases, namespace, super(GenericMeta, self).__hash__()) return self if OLD_GENERICS: - def __new__(self, *args, **kwargs): - return super(self).__new__(*args, **kwargs) + def __new__(cls, *args, **kwargs): + return super(_ProtocolMeta, cls).__new__(*args, **kwargs) def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) @@ -884,7 +884,7 @@ def __getitem__(self, params): if OLD_GENERICS: def __getitem__(self, *args, **kwargs): - return super(self).__getitem__(*args, **kwargs) + return super(_ProtocolMeta, self).__getitem__(*args, **kwargs) if NO_PROTOCOL: From a7cf2fbe24317d9baa95203a0c4e67828b73a8c8 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 2 Sep 2017 20:36:45 +0200 Subject: [PATCH 17/35] Fix super() second attemt --- typing_extensions/src_py3/typing_extensions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index be6543c5..71a91576 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -757,8 +757,8 @@ def __new__(cls, name, bases, namespace, super(GenericMeta, self).__hash__()) return self if OLD_GENERICS: - def __new__(cls, *args, **kwargs): - return super(_ProtocolMeta, cls).__new__(*args, **kwargs) + def __new__(cls, name, bases, namespace, **kwargs): + return super(_ProtocolMeta, cls).__new__(name, bases, namespace, **kwargs) def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) @@ -883,8 +883,8 @@ def __getitem__(self, params): orig_bases=self.__orig_bases__) if OLD_GENERICS: - def __getitem__(self, *args, **kwargs): - return super(_ProtocolMeta, self).__getitem__(*args, **kwargs) + def __getitem__(self, params): + return super(_ProtocolMeta, self).__getitem__(params) if NO_PROTOCOL: From 8b50c605d278ea177a247d0653c32329c5783f80 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 2 Sep 2017 20:40:41 +0200 Subject: [PATCH 18/35] Attempt an alternative solution --- typing_extensions/src_py3/typing_extensions.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 71a91576..3aa443db 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -757,8 +757,7 @@ def __new__(cls, name, bases, namespace, super(GenericMeta, self).__hash__()) return self if OLD_GENERICS: - def __new__(cls, name, bases, namespace, **kwargs): - return super(_ProtocolMeta, cls).__new__(name, bases, namespace, **kwargs) + del __new__ def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) @@ -883,8 +882,7 @@ def __getitem__(self, params): orig_bases=self.__orig_bases__) if OLD_GENERICS: - def __getitem__(self, params): - return super(_ProtocolMeta, self).__getitem__(params) + del __getitem__ if NO_PROTOCOL: From 1dc1971d6a8a387cd76167f5acee7b68be402e94 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 2 Sep 2017 20:52:46 +0200 Subject: [PATCH 19/35] Fix _gorg in typing 3.5.1 --- .../src_py3/typing_extensions.py | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 3aa443db..7e022703 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -32,10 +32,6 @@ def _no_slots_copy(dct): except ImportError: class _TypingEllipsis: pass class _TypingEmpty: pass -try: - from typing import _make_subclasshook -except ImportError: - _make_subclasshook = None try: from typing import _check_generic except ImportError: @@ -669,6 +665,16 @@ def new_type(x): TYPE_CHECKING = False +def _gorg(cls): + """This function exists for compatibility with old typing versions""" + assert isinstance(cls, GenericMeta) + if hasattr(cls, '_gorg'): + return cls._gorg + while cls.__origin__ is not None: + cls = cls.__origin__ + return cls + + def _collection_protocol(cls): # Selected set of collections ABCs that are considered protocols. name = cls.__name__ @@ -692,6 +698,7 @@ def __new__(cls, name, bases, namespace, tvars=None, args=None, origin=None, extra=None, orig_bases=None): # This is just a version copied from GenericMeta.__new__ that # includes "Protocol" special treatment. (Comments removed for brevity.) + assert extra is None # Protocols should not have extra if tvars is not None: assert origin is not None assert all(isinstance(t, TypeVar) for t in tvars), tvars @@ -726,13 +733,13 @@ def __new__(cls, name, bases, namespace, initial_bases = bases if extra is not None and type(extra) is abc.ABCMeta and extra not in bases: bases = (extra,) + bases - bases = tuple(b._gorg if isinstance(b, GenericMeta) else b for b in bases) + bases = tuple(_gorg(b) if isinstance(b, GenericMeta) else b for b in bases) if any(isinstance(b, GenericMeta) and b is not Generic for b in bases): bases = tuple(b for b in bases if b is not Generic) namespace.update({'__origin__': origin, '__extra__': extra}) self = super(GenericMeta, cls).__new__(cls, name, bases, namespace, _root=True) super(GenericMeta, self).__setattr__('_gorg', - self if not origin else origin._gorg) + self if not origin else _gorg(origin)) self.__parameters__ = tvars self.__args__ = tuple(... if a is _TypingEllipsis else () if a is _TypingEmpty else @@ -740,15 +747,6 @@ def __new__(cls, name, bases, namespace, self.__next_in_mro__ = _next_in_mro(self) if orig_bases is None: self.__orig_bases__ = initial_bases - if ( - '__subclasshook__' not in namespace and extra or - getattr(self.__subclasshook__, '__name__', '') == '__extrahook__' - ): - if _make_subclasshook: - self.__subclasshook__ = _make_subclasshook(self) - if isinstance(extra, abc.ABCMeta): - self._abc_registry = extra._abc_registry - self._abc_cache = extra._abc_cache elif origin is not None: self._abc_registry = origin._abc_registry self._abc_cache = origin._abc_cache @@ -846,7 +844,7 @@ def __getitem__(self, params): # special treatment of "Protocol". (Comments removed for brevity.) if not isinstance(params, tuple): params = (params,) - if not params and self._gorg is not Tuple: + if not params and _gorg(self) is not Tuple: raise TypeError( "Parameter list to %s[...] cannot be empty" % self.__qualname__) msg = "Parameters to generic types must be types." @@ -916,7 +914,7 @@ def func(x: Proto[int]) -> int: _is_protocol = True def __new__(cls, *args, **kwds): - if cls._gorg is Protocol: + if _gorg(cls) is Protocol: raise TypeError("Type Protocol cannot be instantiated; " "it can be used only as a base class") return _generic_new(cls.__next_in_mro__, cls, *args, **kwds) From e25077f813c6f724314cd1388a99955c62122644 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 2 Sep 2017 21:19:32 +0200 Subject: [PATCH 20/35] Disable some tests for old-style generics and add one new instead --- .../src_py3/test_typing_extensions.py | 26 +++++++++++++++++++ .../src_py3/typing_extensions.py | 11 ++++++++ 2 files changed, 37 insertions(+) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index cb7e808b..46ed7c4f 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -19,6 +19,11 @@ import typing import typing_extensions import collections.abc as collections_abc +OLD_GENERICS = False +try: + from typing import _type_vars, _next_in_mro, _type_check +except ImportError: + OLD_GENERICS = True # We assume Python versions *below* 3.5.0 will have the most # up-to-date version of the typing module installed. Since @@ -969,6 +974,7 @@ def __subclasshook__(cls, other): self.assertIsSubclass(OKClass, C) self.assertNotIsSubclass(BadClass, C) + @skipUnless(not OLD_GENERICS, "New style generics required") def test_defining_generic_protocols(self): T = TypeVar('T') S = TypeVar('S') @@ -991,6 +997,24 @@ class P(PR[int, T], Protocol[T]): class C(PR[int, T]): pass self.assertIsInstance(C[str](), C) + def test_defining_generic_protocols_old_style(self): + T = TypeVar('T') + S = TypeVar('S') + @runtime + class PR(Protocol, Generic[T, S]): + def meth(self): pass + class P(PR[int, str], Protocol): + y = 1 + self.assertIsSubclass(PR[int, str], PR) + self.assertIsSubclass(P, PR) + with self.assertRaises(TypeError): + PR[int] + with self.assertRaises(TypeError): + PR[int, 1] + if TYPING_3_5_3: + with self.assertRaises(TypeError): + PR[int, ClassVar] + def test_init_called(self): T = TypeVar('T') class P(Protocol[T]): pass @@ -999,6 +1023,7 @@ def __init__(self): self.test = 'OK' self.assertEqual(C[int]().test, 'OK') + @skipUnless(not OLD_GENERICS, "New style generics required") def test_protocols_bad_subscripts(self): T = TypeVar('T') S = TypeVar('S') @@ -1030,6 +1055,7 @@ class P(Protocol[T, S]): pass self.assertEqual(P[T, T][Tuple[T, S]][int, str], P[Tuple[int, str], Tuple[int, str]]) + @skipUnless(not OLD_GENERICS, "New style generics required") def test_generic_protocols_special_from_generic(self): T = TypeVar('T') class P(Protocol[T]): pass diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 7e022703..1b710b9b 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -675,6 +675,15 @@ def _gorg(cls): return cls +if OLD_GENERICS: + def _next_in_mro(cls): + next_in_mro = object + for i, c in enumerate(cls.__mro__[:-1]): + if isinstance(c, GenericMeta) and _gorg(c) is Generic: + next_in_mro = cls.__mro__[i + 1] + return next_in_mro + + def _collection_protocol(cls): # Selected set of collections ABCs that are considered protocols. name = cls.__name__ @@ -917,6 +926,8 @@ def __new__(cls, *args, **kwds): if _gorg(cls) is Protocol: raise TypeError("Type Protocol cannot be instantiated; " "it can be used only as a base class") + if OLD_GENERICS: + return _generic_new(_next_in_mro(cls), cls, *args, **kwds) return _generic_new(cls.__next_in_mro__, cls, *args, **kwds) From 1c1d9fed02756ee68383afeb67c1cafada3e4c29 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 2 Sep 2017 21:34:26 +0200 Subject: [PATCH 21/35] Add some tests as proposed by Michael --- typing_extensions/src_py3/test_typing_extensions.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index 46ed7c4f..c48100e8 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -1011,6 +1011,18 @@ class P(PR[int, str], Protocol): PR[int] with self.assertRaises(TypeError): PR[int, 1] + class P1(Protocol, Generic[T]): + def bar(self, x: T) -> str: ... + class P2(Generic[T], Protocol): + def bar(self, x: T) -> str: ... + @runtime + class PSub(P1[str], Protocol): + x = 1 + class Test: + x = 1 + def bar(self, x: str) -> str: + return x + self.assertIsInstance(Test(), PSub) if TYPING_3_5_3: with self.assertRaises(TypeError): PR[int, ClassVar] From 9b22594235ffb9b1a881a14bab06a4aa17892c8e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 2 Sep 2017 21:52:31 +0200 Subject: [PATCH 22/35] More tests per Michael's CR; remove redundant extra; drop old-style classes on Python 2 --- .../src_py2/test_typing_extensions.py | 43 +++++++++++++++++++ .../src_py2/typing_extensions.py | 11 +++-- .../src_py3/test_typing_extensions.py | 14 ++++++ .../src_py3/typing_extensions.py | 3 ++ 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/typing_extensions/src_py2/test_typing_extensions.py b/typing_extensions/src_py2/test_typing_extensions.py index 6a916e72..41f208e9 100644 --- a/typing_extensions/src_py2/test_typing_extensions.py +++ b/typing_extensions/src_py2/test_typing_extensions.py @@ -474,6 +474,14 @@ def __subclasshook__(cls, other): self.assertIsSubclass(OKClass, C) self.assertNotIsSubclass(BadClass, C) + def test_issubclass_fails_correctly(self): + @runtime + class P(Protocol): + x = 1 + class C: pass + with self.assertRaises(TypeError): + issubclass(C(), P) + def test_defining_generic_protocols(self): T = typing.TypeVar('T') S = typing.TypeVar('S') @@ -495,6 +503,35 @@ class P(PR[int, T], Protocol[T]): class C(PR[int, T]): pass self.assertIsInstance(C[str](), C) + def test_defining_generic_protocols_old_style(self): + T = typing.TypeVar('T') + S = typing.TypeVar('S') + @runtime + class PR(Protocol, typing.Generic[T, S]): + def meth(self): pass + class P(PR[int, str], Protocol): + y = 1 + self.assertIsSubclass(PR[int, str], PR) + self.assertIsSubclass(P, PR) + with self.assertRaises(TypeError): + PR[int] + with self.assertRaises(TypeError): + PR[int, 1] + class P1(Protocol, typing.Generic[T]): + def bar(self, x): pass + class P2(typing.Generic[T], Protocol): + def bar(self, x): pass + @runtime + class PSub(P1[str], Protocol): + x = 1 + class Test(object): + x = 1 + def bar(self, x): + return x + self.assertIsInstance(Test(), PSub) + with self.assertRaises(TypeError): + PR[int, ClassVar] + def test_init_called(self): T = typing.TypeVar('T') class P(Protocol[T]): pass @@ -572,6 +609,12 @@ def test_no_runtime_deco_on_nominal(self): with self.assertRaises(TypeError): @runtime class C(object): pass + class Proto(Protocol): + x = 1 + with self.assertRaises(TypeError): + @runtime + class Concrete(Proto): + pass def test_protocols_pickleable(self): global P, CP # pickle wants to reference the class by name diff --git a/typing_extensions/src_py2/typing_extensions.py b/typing_extensions/src_py2/typing_extensions.py index cebcb4b1..336a4af4 100644 --- a/typing_extensions/src_py2/typing_extensions.py +++ b/typing_extensions/src_py2/typing_extensions.py @@ -8,7 +8,7 @@ # We use internal typing helpers here, but this significantly reduces # code duplication. (Also this is only until Protocol is in typing.) _generic_new, _type_vars, _next_in_mro, _tp_cache, _type_check, - _TypingEllipsis, _TypingEmpty, _make_subclasshook, _check_generic + _TypingEllipsis, _TypingEmpty, _check_generic ) # Please keep __all__ alphabetized within each category. @@ -120,6 +120,7 @@ def __new__(cls, name, bases, namespace, tvars=None, args=None, origin=None, extra=None, orig_bases=None): # This is just a version copied from GenericMeta.__new__ that # includes "Protocol" special treatment. (Comments removed for brevity.) + assert extra is None # Protocols should not have extra if tvars is not None: assert origin is not None assert all(isinstance(t, TypeVar) for t in tvars), tvars @@ -171,11 +172,6 @@ def __new__(cls, name, bases, namespace, self.__next_in_mro__ = _next_in_mro(self) if orig_bases is None: self.__orig_bases__ = initial_bases - if ( - '__subclasshook__' not in namespace and extra or - getattr(self.__subclasshook__, '__name__', '') == '__extrahook__' - ): - self.__subclasshook__ = _make_subclasshook(self) self.__tree_hash__ = (hash(self._subs_tree()) if origin else abc.ABCMeta.__hash__(self)) return self @@ -204,6 +200,9 @@ def _no_init(self, *args, **kwargs): def _proto_hook(cls, other): if not cls.__dict__.get('_is_protocol', None): return NotImplemented + if not isinstance(other, type): + # Same error as for issubclass(1, int) + raise TypeError('issubclass() arg 1 must be a new-style class') for attr in cls._get_protocol_attrs(): for base in other.__mro__: if attr in base.__dict__: diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index c48100e8..c4d59993 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -974,6 +974,14 @@ def __subclasshook__(cls, other): self.assertIsSubclass(OKClass, C) self.assertNotIsSubclass(BadClass, C) + def test_issubclass_fails_correctly(self): + @runtime + class P(Protocol): + x = 1 + class C: pass + with self.assertRaises(TypeError): + issubclass(C(), P) + @skipUnless(not OLD_GENERICS, "New style generics required") def test_defining_generic_protocols(self): T = TypeVar('T') @@ -1108,6 +1116,12 @@ def test_no_runtime_deco_on_nominal(self): with self.assertRaises(TypeError): @runtime class C: pass + class Proto(Protocol): + x = 1 + with self.assertRaises(TypeError): + @runtime + class Concrete(Proto): + pass def test_protocols_pickleable(self): global P, CP # pickle wants to reference the class by name diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 1b710b9b..9bf86b55 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -790,6 +790,9 @@ def _no_init(self, *args, **kwargs): def _proto_hook(other): if not cls.__dict__.get('_is_protocol', None): return NotImplemented + if not isinstance(other, type): + # Same error as for issubclass(1, int) + raise TypeError('issubclass() arg 1 must be a class') for attr in cls._get_protocol_attrs(): for base in other.__mro__: if attr in base.__dict__: From 458b232305a0a29a589d8620557c916cdef3485b Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 2 Sep 2017 21:57:43 +0200 Subject: [PATCH 23/35] Fix _gorg on Python 2; add 3.6.2 to Travis --- .travis.yml | 1 + typing_extensions/src_py2/typing_extensions.py | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0c5b465b..ac8a1c3e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ language: python python: - "nightly" - "3.7-dev" + - "3.6.2" - "3.6.1" - "3.6.0" - "3.5.3" diff --git a/typing_extensions/src_py2/typing_extensions.py b/typing_extensions/src_py2/typing_extensions.py index 336a4af4..fef77fc7 100644 --- a/typing_extensions/src_py2/typing_extensions.py +++ b/typing_extensions/src_py2/typing_extensions.py @@ -96,6 +96,16 @@ def __subclasshook__(cls, C): return NotImplemented +def _gorg(cls): + """This function exists for compatibility with old typing versions""" + assert isinstance(cls, GenericMeta) + if hasattr(cls, '_gorg'): + return cls._gorg + while cls.__origin__ is not None: + cls = cls.__origin__ + return cls + + def _collection_protocol(cls): # Selected set of collections ABCs that are considered protocols. name = cls.__name__ @@ -157,13 +167,13 @@ def __new__(cls, name, bases, namespace, extra = namespace.get('__extra__') if extra is not None and type(extra) is abc.ABCMeta and extra not in bases: bases = (extra,) + bases - bases = tuple(b._gorg if isinstance(b, GenericMeta) else b for b in bases) + bases = tuple(_gorg(b) if isinstance(b, GenericMeta) else b for b in bases) if any(isinstance(b, GenericMeta) and b is not Generic for b in bases): bases = tuple(b for b in bases if b is not Generic) namespace.update({'__origin__': origin, '__extra__': extra}) self = abc.ABCMeta.__new__(cls, name, bases, namespace) - abc.ABCMeta.__setattr__(self, '_gorg', self if not origin else origin._gorg) + abc.ABCMeta.__setattr__(self, '_gorg', self if not origin else _gorg(origin)) self.__parameters__ = tvars self.__args__ = tuple(Ellipsis if a is _TypingEllipsis else @@ -263,7 +273,7 @@ def __getitem__(self, params): # special treatment of "Protocol". (Comments removed for brevity.) if not isinstance(params, tuple): params = (params,) - if not params and self._gorg is not Tuple: + if not params and _gorg(self) is not Tuple: raise TypeError( "Parameter list to %s[...] cannot be empty" % self.__qualname__) msg = "Parameters to generic types must be types." @@ -331,7 +341,7 @@ def func(x): _is_protocol = True def __new__(cls, *args, **kwds): - if cls._gorg is Protocol: + if _gorg(cls) is Protocol: raise TypeError("Type Protocol cannot be instantiated; " "it can be used only as a base class") return _generic_new(cls.__next_in_mro__, cls, *args, **kwds) From dbc6defc218509209086aafca874803e172d0a2d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 2 Sep 2017 22:25:14 +0200 Subject: [PATCH 24/35] First part of Jukka's CR --- .../src_py2/test_typing_extensions.py | 7 +++++ .../src_py2/typing_extensions.py | 26 +++++-------------- .../src_py3/test_typing_extensions.py | 7 +++++ .../src_py3/typing_extensions.py | 22 +++------------- 4 files changed, 24 insertions(+), 38 deletions(-) diff --git a/typing_extensions/src_py2/test_typing_extensions.py b/typing_extensions/src_py2/test_typing_extensions.py index 41f208e9..1b7b2835 100644 --- a/typing_extensions/src_py2/test_typing_extensions.py +++ b/typing_extensions/src_py2/test_typing_extensions.py @@ -616,6 +616,13 @@ class Proto(Protocol): class Concrete(Proto): pass + def test_none_treated_correctly(self): + @runtime + class P(Protocol): + x = None # type: int + class B(object): pass + self.assertNotIsInstance(B(), P) + def test_protocols_pickleable(self): global P, CP # pickle wants to reference the class by name T = typing.TypeVar('T') diff --git a/typing_extensions/src_py2/typing_extensions.py b/typing_extensions/src_py2/typing_extensions.py index fef77fc7..0aa4525e 100644 --- a/typing_extensions/src_py2/typing_extensions.py +++ b/typing_extensions/src_py2/typing_extensions.py @@ -15,8 +15,8 @@ __all__ = [ # Super-special typing primitives. 'ClassVar', - 'Type', 'Protocol', + 'Type', # Concrete collection types. 'ContextManager', @@ -97,7 +97,7 @@ def __subclasshook__(cls, C): def _gorg(cls): - """This function exists for compatibility with old typing versions""" + """This function exists for compatibility with old typing versions.""" assert isinstance(cls, GenericMeta) if hasattr(cls, '_gorg'): return cls._gorg @@ -106,19 +106,6 @@ def _gorg(cls): return cls -def _collection_protocol(cls): - # Selected set of collections ABCs that are considered protocols. - name = cls.__name__ - return (name in ('ABC', 'Callable', 'Awaitable', - 'Iterable', 'Iterator', 'AsyncIterable', 'AsyncIterator', - 'Hashable', 'Sized', 'Container', 'Collection', 'Reversible', - 'Sequence', 'MutableSequence', 'Mapping', 'MutableMapping', - 'AbstractContextManager', 'ContextManager', - 'AbstractAsyncContextManager', 'AsyncContextManager',) and - cls.__module__ in ('collections.abc', 'typing', 'contextlib', - '_abcoll', 'abc')) - - class _ProtocolMeta(GenericMeta): """Internal metaclass for Protocol. @@ -197,8 +184,7 @@ def __init__(cls, *args, **kwargs): for base in cls.__mro__[1:]: if not (base in (object, Generic, Callable) or isinstance(base, TypingMeta) and base._is_protocol or - isinstance(base, GenericMeta) and base.__origin__ is Generic or - _collection_protocol(base)): + isinstance(base, GenericMeta) and base.__origin__ is Generic): raise TypeError('Protocols can only inherit from other protocols,' ' got %r' % base) @@ -211,7 +197,8 @@ def _proto_hook(cls, other): if not cls.__dict__.get('_is_protocol', None): return NotImplemented if not isinstance(other, type): - # Same error as for issubclass(1, int) + # Similar error as for issubclass(1, int) + # (also not a chance for old-style classes) raise TypeError('issubclass() arg 1 must be a new-style class') for attr in cls._get_protocol_attrs(): for base in other.__mro__: @@ -262,8 +249,7 @@ def _get_protocol_attrs(self): '__orig_bases__', '__extra__', '__tree_hash__', '__doc__', '__subclasshook__', '__init__', '__new__', '__module__', '_MutableMapping__marker', - '__metaclass__', '_gorg') and - getattr(base, attr, object()) is not None): + '__metaclass__', '_gorg')): attrs.add(attr) return attrs diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index c4d59993..b5eba63e 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -1123,6 +1123,13 @@ class Proto(Protocol): class Concrete(Proto): pass + def test_none_treated_correctly(self): + @runtime + class P(Protocol): + x = None # type: int + class B(object): pass + self.assertNotIsInstance(B(), P) + def test_protocols_pickleable(self): global P, CP # pickle wants to reference the class by name T = TypeVar('T') diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 9bf86b55..c14c2de2 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -666,7 +666,7 @@ def new_type(x): def _gorg(cls): - """This function exists for compatibility with old typing versions""" + """This function exists for compatibility with old typing versions.""" assert isinstance(cls, GenericMeta) if hasattr(cls, '_gorg'): return cls._gorg @@ -677,6 +677,7 @@ def _gorg(cls): if OLD_GENERICS: def _next_in_mro(cls): + """This function exists for compatibility with old typing versions.""" next_in_mro = object for i, c in enumerate(cls.__mro__[:-1]): if isinstance(c, GenericMeta) and _gorg(c) is Generic: @@ -684,19 +685,6 @@ def _next_in_mro(cls): return next_in_mro -def _collection_protocol(cls): - # Selected set of collections ABCs that are considered protocols. - name = cls.__name__ - return (name in ('ABC', 'Callable', 'Awaitable', - 'Iterable', 'Iterator', 'AsyncIterable', 'AsyncIterator', - 'Hashable', 'Sized', 'Container', 'Collection', 'Reversible', - 'Sequence', 'MutableSequence', 'Mapping', 'MutableMapping', - 'AbstractContextManager', 'ContextManager', - 'AbstractAsyncContextManager', 'AsyncContextManager',) and - cls.__module__ in ('collections.abc', 'typing', 'contextlib', - '_abcoll', 'abc')) - - class _ProtocolMeta(GenericMeta): """Internal metaclass for Protocol. @@ -777,8 +765,7 @@ def __init__(cls, *args, **kwargs): for base in cls.__mro__[1:]: if not (base in (object, Generic, Callable) or isinstance(base, TypingMeta) and base._is_protocol or - isinstance(base, GenericMeta) and base.__origin__ is Generic or - _collection_protocol(base)): + isinstance(base, GenericMeta) and base.__origin__ is Generic): raise TypeError('Protocols can only inherit from other protocols,' ' got %r' % base) @@ -845,8 +832,7 @@ def _get_protocol_attrs(self): '__next_in_mro__', '__parameters__', '__origin__', '__orig_bases__', '__extra__', '__tree_hash__', '__doc__', '__subclasshook__', '__init__', '__new__', - '__module__', '_MutableMapping__marker', '_gorg') and - getattr(base, attr, object()) is not None): + '__module__', '_MutableMapping__marker', '_gorg')): attrs.add(attr) return attrs From 31cbe0db5369bdfe2b1e7eac9bb8660948f62f02 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 2 Sep 2017 22:44:39 +0200 Subject: [PATCH 25/35] Second part of Jukka's CR: docstrings and comments --- typing_extensions/src_py2/typing_extensions.py | 16 ++++++++++++---- typing_extensions/src_py3/typing_extensions.py | 16 +++++++++++++--- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/typing_extensions/src_py2/typing_extensions.py b/typing_extensions/src_py2/typing_extensions.py index 0aa4525e..dcad4891 100644 --- a/typing_extensions/src_py2/typing_extensions.py +++ b/typing_extensions/src_py2/typing_extensions.py @@ -216,7 +216,8 @@ def __instancecheck__(self, instance): # We need this method for situations where attributes are assigned in __init__ if isinstance(instance, type): # This looks like a fundamental limitation of Python 2. - # It cannot support runtime protocol metaclasses + # It cannot support runtime protocol metaclasses, On Python 2 classes + # cannot be correctly inspected as instances of protocols. return False if issubclass(instance.__class__, self): return True @@ -298,10 +299,10 @@ def __getitem__(self, params): class Protocol(object): """Base class for protocol classes. Protocol classes are defined as:: - class Proto(Protocol[T]): + class Proto(Protocol): def meth(self): # type: () -> int - ... + pass Such classes are primarily used with static type checkers that recognize structural subtyping (static duck-typing), for example:: @@ -312,7 +313,7 @@ def meth(self): return 0 def func(x): - # type: (Proto[int]) -> int + # type: (Proto) -> int return x.meth() func(C()) # Passes static type check @@ -320,6 +321,13 @@ def func(x): See PEP 544 for details. Protocol classes decorated with @typing_extensions.runtime act as simple-minded runtime protocols that checks only the presence of given attributes, ignoring their type signatures. + + Protocol classes can be generic, they are defined as:: + + class GenProto(Protocol[T]): + def meth(self): + # type: () -> T + pass """ __metaclass__ = _ProtocolMeta diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index c14c2de2..dc19b648 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -118,6 +118,7 @@ def _check_methods_in_mro(C, *methods): 'TYPE_CHECKING', ] +# Protocols are hard to backport to the original version of typing 3.5.0 NO_PROTOCOL = sys.version_info[:3] == (3, 5, 0) if not NO_PROTOCOL: @@ -887,8 +888,8 @@ def __getitem__(self, params): class Protocol(metaclass=_ProtocolMeta): """Base class for protocol classes. Protocol classes are defined as:: - class Proto(Protocol[T]): - def meth(self) -> T: + class Proto(Protocol): + def meth(self) -> int: ... Such classes are primarily used with static type checkers that recognize @@ -898,7 +899,7 @@ class C: def meth(self) -> int: return 0 - def func(x: Proto[int]) -> int: + def func(x: Proto) -> int: return x.meth() func(C()) # Passes static type check @@ -906,6 +907,12 @@ def func(x: Proto[int]) -> int: See PEP 544 for details. Protocol classes decorated with @typing_extensions.runtime act as simple-minded runtime protocols that checks only the presence of given attributes, ignoring their type signatures. + + Protocol classes can be generic, they are defined as:: + + class GenProto({bases}): + def meth(self) -> T: + ... """ __slots__ = () @@ -919,6 +926,9 @@ def __new__(cls, *args, **kwds): return _generic_new(_next_in_mro(cls), cls, *args, **kwds) return _generic_new(cls.__next_in_mro__, cls, *args, **kwds) + Protocol.__doc__ = Protocol.__doc__.format(bases="Protocol, Generic[T]" if + OLD_GENERICS else "Protocol[T]") + def runtime(cls): """Mark a protocol class as a runtime protocol, so that it From 171b1e50e9fe57f419754397d0f16e819c186fbe Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 3 Sep 2017 20:10:59 +0200 Subject: [PATCH 26/35] Update one comment in test --- typing_extensions/src_py3/test_typing_extensions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index b5eba63e..152bad09 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -57,8 +57,7 @@ # For checks reliant on Python 3.6 syntax changes (e.g. classvar) PY36 = sys.version_info[:2] >= (3, 6) -# It is very difficult to backport Protocols to these versions due to -# different generics system. See https://github.com/python/typing/pull/195 +# Protocols are hard to backport to the original version of typing 3.5.0 NO_PROTOCOL = sys.version_info[:3] == (3, 5, 0) From fc0ec944d8e86d32e649b47979c0212819f1b01d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 9 Sep 2017 15:03:39 +0200 Subject: [PATCH 27/35] Don't support issubclass() for protocols with non-method members --- .../src_py3/test_typing_extensions.py | 39 ++++++++----------- .../src_py3/typing_extensions.py | 22 ++++++++--- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index 152bad09..354dbd7f 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -792,16 +792,16 @@ def test_protocols_issubclass(self): T = TypeVar('T') @runtime class P(Protocol): - x = 1 + def x(self): ... @runtime class PG(Protocol[T]): - x = 1 + def x(self): ... class BadP(Protocol): - x = 1 + def x(self): ... class BadPG(Protocol[T]): - x = 1 + def x(self): ... class C: - x = 1 + def x(self): ... self.assertIsSubclass(C, P) self.assertIsSubclass(C, PG) self.assertIsSubclass(BadP, PG) @@ -821,24 +821,19 @@ class C: with self.assertRaises(TypeError): issubclass(PG, PG[int]) - @skipUnless(PY36, 'Python 3.6 required') - def test_protocols_issubclass_py36(self): - class OtherPoint: + def test_protocols_issubclass_non_callable(self): + class C: x = 1 - y = 2 - label = 'other' - class Bad: pass - self.assertNotIsSubclass(MyPoint, Point) - self.assertIsSubclass(OtherPoint, Point) - self.assertNotIsSubclass(Bad, Point) - self.assertNotIsSubclass(MyPoint, Position) - self.assertIsSubclass(OtherPoint, Position) - self.assertIsSubclass(Concrete, Proto) - self.assertIsSubclass(Other, Proto) - self.assertNotIsSubclass(Concrete, Other) - self.assertNotIsSubclass(Other, Concrete) - self.assertIsSubclass(Point, Position) - self.assertIsSubclass(NT, Position) + @runtime + class PNonCall(Protocol): + x = 1 + with self.assertRaises(TypeError): + issubclass(C, PNonCall) + self.assertIsInstance(C(), PNonCall) + PNonCall.register(C) + with self.assertRaises(TypeError): + issubclass(C, PNonCall) + self.assertIsInstance(C(), PNonCall) def test_protocols_isinstance(self): T = TypeVar('T') diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index dc19b648..d5b44f5c 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -769,7 +769,8 @@ def __init__(cls, *args, **kwargs): isinstance(base, GenericMeta) and base.__origin__ is Generic): raise TypeError('Protocols can only inherit from other protocols,' ' got %r' % base) - + cls._callable_members_only = all(callable(getattr(cls, attr)) + for attr in cls._get_protocol_attrs()) def _no_init(self, *args, **kwargs): if type(self)._is_protocol: raise TypeError('Protocols cannot be instantiated') @@ -798,12 +799,15 @@ def _proto_hook(other): def __instancecheck__(self, instance): # We need this method for situations where attributes are assigned in __init__ - if issubclass(instance.__class__, self): + if ((not getattr(self, '_is_protocol', False) or + self._callable_members_only) and + issubclass(instance.__class__, self)): return True if self._is_protocol: - return all(hasattr(instance, attr) and getattr(instance, attr) is not None - for attr in self._get_protocol_attrs()) - return False + if all(hasattr(instance, attr) and getattr(instance, attr) is not None + for attr in self._get_protocol_attrs()): + return True + return super(GenericMeta, self).__instancecheck__(instance) def __subclasscheck__(self, cls): if self.__origin__ is not None: @@ -817,6 +821,11 @@ def __subclasscheck__(self, cls): return False raise TypeError("Instance and class checks can only be used with" " @runtime protocols") + if (self.__dict__.get('_is_runtime_protocol', None) and + not self._callable_members_only): + if sys._getframe(1).f_globals['__name__'] in ['abc', 'functools']: + return super(GenericMeta, self).__subclasscheck__(cls) + raise TypeError("Protocols with non-method members don't support issubclass()") return super(GenericMeta, self).__subclasscheck__(cls) def _get_protocol_attrs(self): @@ -833,7 +842,8 @@ def _get_protocol_attrs(self): '__next_in_mro__', '__parameters__', '__origin__', '__orig_bases__', '__extra__', '__tree_hash__', '__doc__', '__subclasshook__', '__init__', '__new__', - '__module__', '_MutableMapping__marker', '_gorg')): + '__module__', '_MutableMapping__marker', '_gorg', + '_callable_members_only')): attrs.add(attr) return attrs From f1f478a0fc2ab03471b2dc0196fa33fb97f12d56 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 9 Sep 2017 15:16:10 +0200 Subject: [PATCH 28/35] Add more tests --- typing_extensions/src_py3/test_typing_extensions.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index 354dbd7f..d47c1887 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -834,6 +834,15 @@ class PNonCall(Protocol): with self.assertRaises(TypeError): issubclass(C, PNonCall) self.assertIsInstance(C(), PNonCall) + # check that non-protocol subclasses are not affected + class D(PNonCall): ... + self.assertNotIsSubclass(C, D) + self.assertNotIsInstance(C(), D) + D.register(C) + self.assertIsSubclass(C, D) + self.assertIsInstance(C(), D) + with self.assertRaises(TypeError): + issubclass(D, PNonCall) def test_protocols_isinstance(self): T = TypeVar('T') From 8652cedf6ac3fa886df3fee9bb57b89a0a1dec59 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 9 Sep 2017 15:34:27 +0200 Subject: [PATCH 29/35] Only apply the None rule to callable attributes on class --- typing_extensions/src_py3/test_typing_extensions.py | 6 +++--- typing_extensions/src_py3/typing_extensions.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index d47c1887..6d3efbb9 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -928,7 +928,7 @@ class C: pass self.assertIsInstance(C(), P) self.assertIsInstance(C(), D) - def test_none_blocks_implementation(self): + def test_none_on_class_blocks_implementation(self): @runtime class P(Protocol): x = 1 @@ -939,8 +939,8 @@ class B(A): class C: def __init__(self): self.x = None - self.assertNotIsInstance(B(), P) - self.assertNotIsInstance(C(), P) + self.assertIsInstance(B(), P) + self.assertIsInstance(C(), P) def test_non_protocol_subclasses(self): class P(Protocol): diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index d5b44f5c..8c165d8a 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -804,7 +804,8 @@ def __instancecheck__(self, instance): issubclass(instance.__class__, self)): return True if self._is_protocol: - if all(hasattr(instance, attr) and getattr(instance, attr) is not None + if all(hasattr(instance, attr) and + (not callable(getattr(self, attr)) or getattr(instance, attr) is not None) for attr in self._get_protocol_attrs()): return True return super(GenericMeta, self).__instancecheck__(instance) From 8f0a778d48b894ff6dd4d86226445c25f5653029 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 9 Sep 2017 15:46:31 +0200 Subject: [PATCH 30/35] Add more tests --- .../src_py3/test_typing_extensions.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index 6d3efbb9..a85a0861 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -928,7 +928,7 @@ class C: pass self.assertIsInstance(C(), P) self.assertIsInstance(C(), D) - def test_none_on_class_blocks_implementation(self): + def test_none_on_non_callable_doesnt_block_implementation(self): @runtime class P(Protocol): x = 1 @@ -942,6 +942,20 @@ def __init__(self): self.assertIsInstance(B(), P) self.assertIsInstance(C(), P) + def test_none_on_callable_blocks_implementation(self): + @runtime + class P(Protocol): + def x(self): ... + class A: + def x(self): ... + class B(A): + x = None + class C: + def __init__(self): + self.x = None + self.assertNotIsInstance(B(), P) + self.assertNotIsInstance(C(), P) + def test_non_protocol_subclasses(self): class P(Protocol): x = 1 @@ -1132,6 +1146,20 @@ class P(Protocol): x = None # type: int class B(object): pass self.assertNotIsInstance(B(), P) + class C: + x = 1 + class D: + x = None + self.assertIsInstance(C(), P) + self.assertIsInstance(D(), P) + class CI: + def __init__(self): + self.x = 1 + class DI: + def __init__(self): + self.x = None + self.assertIsInstance(C(), P) + self.assertIsInstance(D(), P) def test_protocols_pickleable(self): global P, CP # pickle wants to reference the class by name From 23e80da6c1ae44f0c25c736c7200a8fe9dbe6679 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 9 Sep 2017 16:07:12 +0200 Subject: [PATCH 31/35] Backport latest changes and tests on Python 2 --- .../src_py2/test_typing_extensions.py | 63 +++++++++++++++++-- .../src_py2/typing_extensions.py | 22 +++++-- 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/typing_extensions/src_py2/test_typing_extensions.py b/typing_extensions/src_py2/test_typing_extensions.py index 1b7b2835..ec46b929 100644 --- a/typing_extensions/src_py2/test_typing_extensions.py +++ b/typing_extensions/src_py2/test_typing_extensions.py @@ -344,16 +344,16 @@ def test_protocols_issubclass(self): T = typing.TypeVar('T') @runtime class P(Protocol): - x = 1 + def x(self): pass @runtime class PG(Protocol[T]): - x = 1 + def x(self): pass class BadP(Protocol): - x = 1 + def x(self): pass class BadPG(Protocol[T]): - x = 1 + def x(self): pass class C(object): - x = 1 + def x(self): pass self.assertIsSubclass(C, P) self.assertIsSubclass(C, PG) self.assertIsSubclass(BadP, PG) @@ -373,6 +373,29 @@ class C(object): with self.assertRaises(TypeError): issubclass(PG, PG[int]) + def test_protocols_issubclass_non_callable(self): + class C(object): + x = 1 + @runtime + class PNonCall(Protocol): + x = 1 + with self.assertRaises(TypeError): + issubclass(C, PNonCall) + self.assertIsInstance(C(), PNonCall) + PNonCall.register(C) + with self.assertRaises(TypeError): + issubclass(C, PNonCall) + self.assertIsInstance(C(), PNonCall) + # check that non-protocol subclasses are not affected + class D(PNonCall): pass + self.assertNotIsSubclass(C, D) + self.assertNotIsInstance(C(), D) + D.register(C) + self.assertIsSubclass(C, D) + self.assertIsInstance(C(), D) + with self.assertRaises(TypeError): + issubclass(D, PNonCall) + def test_protocols_isinstance(self): T = typing.TypeVar('T') @runtime @@ -425,7 +448,7 @@ class C(object): pass self.assertIsInstance(C(), P) self.assertIsInstance(C(), D) - def test_none_blocks_implementation(self): + def test_none_on_non_callable_doesnt_block_implementation(self): @runtime class P(Protocol): x = 1 @@ -436,6 +459,20 @@ class B(A): class C(object): def __init__(self): self.x = None + self.assertIsInstance(B(), P) + self.assertIsInstance(C(), P) + + def test_none_on_callable_blocks_implementation(self): + @runtime + class P(Protocol): + def x(self): pass + class A(object): + def x(self): pass + class B(A): + x = None + class C(object): + def __init__(self): + self.x = None self.assertNotIsInstance(B(), P) self.assertNotIsInstance(C(), P) @@ -622,6 +659,20 @@ class P(Protocol): x = None # type: int class B(object): pass self.assertNotIsInstance(B(), P) + class C(object): + x = 1 + class D(object): + x = None + self.assertIsInstance(C(), P) + self.assertIsInstance(D(), P) + class CI(object): + def __init__(self): + self.x = 1 + class DI(object): + def __init__(self): + self.x = None + self.assertIsInstance(C(), P) + self.assertIsInstance(D(), P) def test_protocols_pickleable(self): global P, CP # pickle wants to reference the class by name diff --git a/typing_extensions/src_py2/typing_extensions.py b/typing_extensions/src_py2/typing_extensions.py index dcad4891..24288d09 100644 --- a/typing_extensions/src_py2/typing_extensions.py +++ b/typing_extensions/src_py2/typing_extensions.py @@ -187,7 +187,8 @@ def __init__(cls, *args, **kwargs): isinstance(base, GenericMeta) and base.__origin__ is Generic): raise TypeError('Protocols can only inherit from other protocols,' ' got %r' % base) - + cls._callable_members_only = all(callable(getattr(cls, attr)) + for attr in cls._get_protocol_attrs()) def _no_init(self, *args, **kwargs): if type(self)._is_protocol: raise TypeError('Protocols cannot be instantiated') @@ -219,12 +220,16 @@ def __instancecheck__(self, instance): # It cannot support runtime protocol metaclasses, On Python 2 classes # cannot be correctly inspected as instances of protocols. return False - if issubclass(instance.__class__, self): + if ((not getattr(self, '_is_protocol', False) or + self._callable_members_only) and + issubclass(instance.__class__, self)): return True if self._is_protocol: - return all(hasattr(instance, attr) and getattr(instance, attr) is not None - for attr in self._get_protocol_attrs()) - return False + if all(hasattr(instance, attr) and + (not callable(getattr(self, attr)) or getattr(instance, attr) is not None) + for attr in self._get_protocol_attrs()): + return True + return super(GenericMeta, self).__instancecheck__(instance) def __subclasscheck__(self, cls): if (self.__dict__.get('_is_protocol', None) and @@ -233,6 +238,11 @@ def __subclasscheck__(self, cls): return False raise TypeError("Instance and class checks can only be used with" " @runtime protocols") + if (self.__dict__.get('_is_runtime_protocol', None) and + not self._callable_members_only): + if sys._getframe(1).f_globals['__name__'] in ['abc', 'functools']: + return super(GenericMeta, self).__subclasscheck__(cls) + raise TypeError("Protocols with non-method members don't support issubclass()") return super(_ProtocolMeta, self).__subclasscheck__(cls) def _get_protocol_attrs(self): @@ -250,7 +260,7 @@ def _get_protocol_attrs(self): '__orig_bases__', '__extra__', '__tree_hash__', '__doc__', '__subclasshook__', '__init__', '__new__', '__module__', '_MutableMapping__marker', - '__metaclass__', '_gorg')): + '__metaclass__', '_gorg', '_callable_members_only')): attrs.add(attr) return attrs From dc01948fb0fb93b0be4094a8580ee054fe64d144 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 9 Sep 2017 16:14:19 +0200 Subject: [PATCH 32/35] Fix lint --- typing_extensions/src_py2/typing_extensions.py | 7 +++++-- typing_extensions/src_py3/typing_extensions.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/typing_extensions/src_py2/typing_extensions.py b/typing_extensions/src_py2/typing_extensions.py index 24288d09..4fc37636 100644 --- a/typing_extensions/src_py2/typing_extensions.py +++ b/typing_extensions/src_py2/typing_extensions.py @@ -189,6 +189,7 @@ def __init__(cls, *args, **kwargs): ' got %r' % base) cls._callable_members_only = all(callable(getattr(cls, attr)) for attr in cls._get_protocol_attrs()) + def _no_init(self, *args, **kwargs): if type(self)._is_protocol: raise TypeError('Protocols cannot be instantiated') @@ -226,7 +227,8 @@ def __instancecheck__(self, instance): return True if self._is_protocol: if all(hasattr(instance, attr) and - (not callable(getattr(self, attr)) or getattr(instance, attr) is not None) + (not callable(getattr(self, attr)) or + getattr(instance, attr) is not None) for attr in self._get_protocol_attrs()): return True return super(GenericMeta, self).__instancecheck__(instance) @@ -242,7 +244,8 @@ def __subclasscheck__(self, cls): not self._callable_members_only): if sys._getframe(1).f_globals['__name__'] in ['abc', 'functools']: return super(GenericMeta, self).__subclasscheck__(cls) - raise TypeError("Protocols with non-method members don't support issubclass()") + raise TypeError("Protocols with non-method members" + " don't support issubclass()") return super(_ProtocolMeta, self).__subclasscheck__(cls) def _get_protocol_attrs(self): diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 8c165d8a..3bc2d657 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -771,6 +771,7 @@ def __init__(cls, *args, **kwargs): ' got %r' % base) cls._callable_members_only = all(callable(getattr(cls, attr)) for attr in cls._get_protocol_attrs()) + def _no_init(self, *args, **kwargs): if type(self)._is_protocol: raise TypeError('Protocols cannot be instantiated') @@ -805,7 +806,8 @@ def __instancecheck__(self, instance): return True if self._is_protocol: if all(hasattr(instance, attr) and - (not callable(getattr(self, attr)) or getattr(instance, attr) is not None) + (not callable(getattr(self, attr)) or + getattr(instance, attr) is not None) for attr in self._get_protocol_attrs()): return True return super(GenericMeta, self).__instancecheck__(instance) @@ -826,7 +828,8 @@ def __subclasscheck__(self, cls): not self._callable_members_only): if sys._getframe(1).f_globals['__name__'] in ['abc', 'functools']: return super(GenericMeta, self).__subclasscheck__(cls) - raise TypeError("Protocols with non-method members don't support issubclass()") + raise TypeError("Protocols with non-method members" + " don't support issubclass()") return super(GenericMeta, self).__subclasscheck__(cls) def _get_protocol_attrs(self): From 733016df5261e19c93c77fa9cfc40cbf7091a1e8 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 9 Sep 2017 16:21:00 +0200 Subject: [PATCH 33/35] Fix Python 3.6+ tests --- typing_extensions/src_py3/typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 3bc2d657..a8e0e3de 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -769,7 +769,7 @@ def __init__(cls, *args, **kwargs): isinstance(base, GenericMeta) and base.__origin__ is Generic): raise TypeError('Protocols can only inherit from other protocols,' ' got %r' % base) - cls._callable_members_only = all(callable(getattr(cls, attr)) + cls._callable_members_only = all(callable(getattr(cls, attr, None)) for attr in cls._get_protocol_attrs()) def _no_init(self, *args, **kwargs): From 8f1268949cf21138d611ffff21f3593d9ff8524d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 9 Sep 2017 16:23:40 +0200 Subject: [PATCH 34/35] One more Python 3.6+ fix --- typing_extensions/src_py3/typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index a8e0e3de..2585c06d 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -806,7 +806,7 @@ def __instancecheck__(self, instance): return True if self._is_protocol: if all(hasattr(instance, attr) and - (not callable(getattr(self, attr)) or + (not callable(getattr(self, attr, None)) or getattr(instance, attr) is not None) for attr in self._get_protocol_attrs()): return True From a4d397ee2ec0836a914f69e2cc08478b9ab01f2c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 14 Sep 2017 21:59:36 +0200 Subject: [PATCH 35/35] Address CR --- typing_extensions/README.rst | 5 +- .../src_py3/test_typing_extensions.py | 4 +- .../src_py3/typing_extensions.py | 498 +++++++++--------- 3 files changed, 253 insertions(+), 254 deletions(-) diff --git a/typing_extensions/README.rst b/typing_extensions/README.rst index 9090402e..7f5ffc5e 100644 --- a/typing_extensions/README.rst +++ b/typing_extensions/README.rst @@ -17,7 +17,8 @@ able to take advantage of new types added to the ``typing`` module, such as The ``typing_extensions`` module contains both backports of these changes as well as experimental types that will eventually be added to the ``typing`` -module. +module, such as ``Protocol`` (see PEP 544 for details about protocols and +static duck typing). Users of other Python versions should continue to install and use use the ``typing`` module from PyPi instead of using this one unless @@ -40,6 +41,8 @@ All Python versions: - ``NewType`` - ``NoReturn`` - ``overload`` (note that older versions of ``typing`` only let you use ``overload`` in stubs) +- ``Protocol`` (except on Python 3.5.0) +- ``runtime`` (except on Python 3.5.0) - ``Text`` - ``Type`` - ``TYPE_CHECKING`` diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index a85a0861..b1b060ce 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -58,7 +58,7 @@ PY36 = sys.version_info[:2] >= (3, 6) # Protocols are hard to backport to the original version of typing 3.5.0 -NO_PROTOCOL = sys.version_info[:3] == (3, 5, 0) +HAVE_PROTOCOLS = sys.version_info[:3] != (3, 5, 0) class BaseTestCase(TestCase): @@ -1188,7 +1188,7 @@ class E: self.assertIsInstance(E(), D) -if NO_PROTOCOL: +if not HAVE_PROTOCOLS: ProtocolTests = None diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 2585c06d..44442eab 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -14,15 +14,6 @@ from typing import _type_vars, _next_in_mro, _type_check except ImportError: OLD_GENERICS = True -try: - from typing import _no_slots_copy -except ImportError: - def _no_slots_copy(dct): - dict_copy = dict(dct) - if '__slots__' in dict_copy: - for slot in dict_copy['__slots__']: - dict_copy.pop(slot, None) - return dict_copy try: from typing import _tp_cache except ImportError: @@ -32,17 +23,29 @@ def _no_slots_copy(dct): except ImportError: class _TypingEllipsis: pass class _TypingEmpty: pass -try: - from typing import _check_generic -except ImportError: - def _check_generic(cls, parameters): - if not cls.__parameters__: - raise TypeError("%s is not a generic class" % repr(cls)) - alen = len(parameters) - elen = len(cls.__parameters__) - if alen != elen: - raise TypeError("Too %s parameters for %s; actual %s, expected %s" % - ("many" if alen > elen else "few", repr(cls), alen, elen)) + + +# The two functions below are copies of typing internal helpers. +# They are needed by _ProtocolMeta + + +def _no_slots_copy(dct): + dict_copy = dict(dct) + if '__slots__' in dict_copy: + for slot in dict_copy['__slots__']: + dict_copy.pop(slot, None) + return dict_copy + + +def _check_generic(cls, parameters): + if not cls.__parameters__: + raise TypeError("%s is not a generic class" % repr(cls)) + alen = len(parameters) + elen = len(cls.__parameters__) + if alen != elen: + raise TypeError("Too %s parameters for %s; actual %s, expected %s" % + ("many" if alen > elen else "few", repr(cls), alen, elen)) + if hasattr(typing, '_generic_new'): _generic_new = typing._generic_new @@ -119,9 +122,9 @@ def _check_methods_in_mro(C, *methods): ] # Protocols are hard to backport to the original version of typing 3.5.0 -NO_PROTOCOL = sys.version_info[:3] == (3, 5, 0) +HAVE_PROTOCOLS = sys.version_info[:3] != (3, 5, 0) -if not NO_PROTOCOL: +if HAVE_PROTOCOLS: __all__.extend(['Protocol', 'runtime']) # TODO @@ -685,220 +688,219 @@ def _next_in_mro(cls): next_in_mro = cls.__mro__[i + 1] return next_in_mro +if HAVE_PROTOCOLS: + class _ProtocolMeta(GenericMeta): + """Internal metaclass for Protocol. -class _ProtocolMeta(GenericMeta): - """Internal metaclass for Protocol. + This exists so Protocol classes can be generic without deriving + from Generic. + """ + if not OLD_GENERICS: + def __new__(cls, name, bases, namespace, + tvars=None, args=None, origin=None, extra=None, orig_bases=None): + # This is just a version copied from GenericMeta.__new__ that + # includes "Protocol" special treatment. (Comments removed for brevity.) + assert extra is None # Protocols should not have extra + if tvars is not None: + assert origin is not None + assert all(isinstance(t, TypeVar) for t in tvars), tvars + else: + tvars = _type_vars(bases) + gvars = None + for base in bases: + if base is Generic: + raise TypeError("Cannot inherit from plain Generic") + if (isinstance(base, GenericMeta) and + base.__origin__ in (Generic, Protocol)): + if gvars is not None: + raise TypeError( + "Cannot inherit from Generic[...] or" + " Protocol[...] multiple times.") + gvars = base.__parameters__ + if gvars is None: + gvars = tvars + else: + tvarset = set(tvars) + gvarset = set(gvars) + if not tvarset <= gvarset: + raise TypeError( + "Some type variables (%s) " + "are not listed in %s[%s]" % + (", ".join(str(t) for t in tvars if t not in gvarset), + "Generic" if any(b.__origin__ is Generic + for b in bases) else "Protocol", + ", ".join(str(g) for g in gvars))) + tvars = gvars + + initial_bases = bases + if (extra is not None and type(extra) is abc.ABCMeta and + extra not in bases): + bases = (extra,) + bases + bases = tuple(_gorg(b) if isinstance(b, GenericMeta) else b + for b in bases) + if any(isinstance(b, GenericMeta) and b is not Generic for b in bases): + bases = tuple(b for b in bases if b is not Generic) + namespace.update({'__origin__': origin, '__extra__': extra}) + self = super(GenericMeta, cls).__new__(cls, name, bases, namespace, + _root=True) + super(GenericMeta, self).__setattr__('_gorg', + self if not origin else + _gorg(origin)) + self.__parameters__ = tvars + self.__args__ = tuple(... if a is _TypingEllipsis else + () if a is _TypingEmpty else + a for a in args) if args else None + self.__next_in_mro__ = _next_in_mro(self) + if orig_bases is None: + self.__orig_bases__ = initial_bases + elif origin is not None: + self._abc_registry = origin._abc_registry + self._abc_cache = origin._abc_cache + if hasattr(self, '_subs_tree'): + self.__tree_hash__ = (hash(self._subs_tree()) if origin else + super(GenericMeta, self).__hash__()) + return self - This exists so Protocol classes can be generic without deriving - from Generic. - """ - def __new__(cls, name, bases, namespace, - tvars=None, args=None, origin=None, extra=None, orig_bases=None): - # This is just a version copied from GenericMeta.__new__ that - # includes "Protocol" special treatment. (Comments removed for brevity.) - assert extra is None # Protocols should not have extra - if tvars is not None: - assert origin is not None - assert all(isinstance(t, TypeVar) for t in tvars), tvars - else: - tvars = _type_vars(bases) - gvars = None - for base in bases: - if base is Generic: - raise TypeError("Cannot inherit from plain Generic") - if (isinstance(base, GenericMeta) and - base.__origin__ in (Generic, Protocol)): - if gvars is not None: - raise TypeError( - "Cannot inherit from Generic[...] or" - " Protocol[...] multiple times.") - gvars = base.__parameters__ - if gvars is None: - gvars = tvars - else: - tvarset = set(tvars) - gvarset = set(gvars) - if not tvarset <= gvarset: - raise TypeError( - "Some type variables (%s) " - "are not listed in %s[%s]" % - (", ".join(str(t) for t in tvars if t not in gvarset), - "Generic" if any(b.__origin__ is Generic - for b in bases) else "Protocol", - ", ".join(str(g) for g in gvars))) - tvars = gvars - - initial_bases = bases - if extra is not None and type(extra) is abc.ABCMeta and extra not in bases: - bases = (extra,) + bases - bases = tuple(_gorg(b) if isinstance(b, GenericMeta) else b for b in bases) - if any(isinstance(b, GenericMeta) and b is not Generic for b in bases): - bases = tuple(b for b in bases if b is not Generic) - namespace.update({'__origin__': origin, '__extra__': extra}) - self = super(GenericMeta, cls).__new__(cls, name, bases, namespace, _root=True) - super(GenericMeta, self).__setattr__('_gorg', - self if not origin else _gorg(origin)) - self.__parameters__ = tvars - self.__args__ = tuple(... if a is _TypingEllipsis else - () if a is _TypingEmpty else - a for a in args) if args else None - self.__next_in_mro__ = _next_in_mro(self) - if orig_bases is None: - self.__orig_bases__ = initial_bases - elif origin is not None: - self._abc_registry = origin._abc_registry - self._abc_cache = origin._abc_cache - if hasattr(self, '_subs_tree'): - self.__tree_hash__ = (hash(self._subs_tree()) if origin else - super(GenericMeta, self).__hash__()) - return self - if OLD_GENERICS: - del __new__ - - def __init__(cls, *args, **kwargs): - super().__init__(*args, **kwargs) - if not cls.__dict__.get('_is_protocol', None): - cls._is_protocol = any(b is Protocol or - isinstance(b, _ProtocolMeta) and - b.__origin__ is Protocol - for b in cls.__bases__) - if cls._is_protocol: - for base in cls.__mro__[1:]: - if not (base in (object, Generic, Callable) or - isinstance(base, TypingMeta) and base._is_protocol or - isinstance(base, GenericMeta) and base.__origin__ is Generic): - raise TypeError('Protocols can only inherit from other protocols,' - ' got %r' % base) - cls._callable_members_only = all(callable(getattr(cls, attr, None)) - for attr in cls._get_protocol_attrs()) - - def _no_init(self, *args, **kwargs): - if type(self)._is_protocol: - raise TypeError('Protocols cannot be instantiated') - cls.__init__ = _no_init - - def _proto_hook(other): + def __init__(cls, *args, **kwargs): + super().__init__(*args, **kwargs) if not cls.__dict__.get('_is_protocol', None): - return NotImplemented - if not isinstance(other, type): - # Same error as for issubclass(1, int) - raise TypeError('issubclass() arg 1 must be a class') - for attr in cls._get_protocol_attrs(): - for base in other.__mro__: - if attr in base.__dict__: - if base.__dict__[attr] is None: - return NotImplemented - break - if (attr in getattr(base, '__annotations__', {}) and - isinstance(other, _ProtocolMeta) and other._is_protocol): - break - else: + cls._is_protocol = any(b is Protocol or + isinstance(b, _ProtocolMeta) and + b.__origin__ is Protocol + for b in cls.__bases__) + if cls._is_protocol: + for base in cls.__mro__[1:]: + if not (base in (object, Generic, Callable) or + isinstance(base, TypingMeta) and base._is_protocol or + isinstance(base, GenericMeta) and + base.__origin__ is Generic): + raise TypeError('Protocols can only inherit from other' + ' protocols, got %r' % base) + cls._callable_members_only = all(callable(getattr(cls, attr, None)) + for attr in cls._get_protocol_attrs()) + + def _no_init(self, *args, **kwargs): + if type(self)._is_protocol: + raise TypeError('Protocols cannot be instantiated') + cls.__init__ = _no_init + + def _proto_hook(other): + if not cls.__dict__.get('_is_protocol', None): return NotImplemented - return True - if '__subclasshook__' not in cls.__dict__: - cls.__subclasshook__ = _proto_hook - - def __instancecheck__(self, instance): - # We need this method for situations where attributes are assigned in __init__ - if ((not getattr(self, '_is_protocol', False) or - self._callable_members_only) and - issubclass(instance.__class__, self)): - return True - if self._is_protocol: - if all(hasattr(instance, attr) and - (not callable(getattr(self, attr, None)) or - getattr(instance, attr) is not None) - for attr in self._get_protocol_attrs()): + if not isinstance(other, type): + # Same error as for issubclass(1, int) + raise TypeError('issubclass() arg 1 must be a class') + for attr in cls._get_protocol_attrs(): + for base in other.__mro__: + if attr in base.__dict__: + if base.__dict__[attr] is None: + return NotImplemented + break + if (attr in getattr(base, '__annotations__', {}) and + isinstance(other, _ProtocolMeta) and other._is_protocol): + break + else: + return NotImplemented + return True + if '__subclasshook__' not in cls.__dict__: + cls.__subclasshook__ = _proto_hook + + def __instancecheck__(self, instance): + # We need this method for situations where attributes are + # assigned in __init__. + if ((not getattr(self, '_is_protocol', False) or + self._callable_members_only) and + issubclass(instance.__class__, self)): return True - return super(GenericMeta, self).__instancecheck__(instance) - - def __subclasscheck__(self, cls): - if self.__origin__ is not None: - if sys._getframe(1).f_globals['__name__'] not in ['abc', 'functools']: - raise TypeError("Parameterized generics cannot be used with class " - "or instance checks") - return False - if (self.__dict__.get('_is_protocol', None) and - not self.__dict__.get('_is_runtime_protocol', None)): - if sys._getframe(1).f_globals['__name__'] in ['abc', 'functools']: + if self._is_protocol: + if all(hasattr(instance, attr) and + (not callable(getattr(self, attr, None)) or + getattr(instance, attr) is not None) + for attr in self._get_protocol_attrs()): + return True + return super(GenericMeta, self).__instancecheck__(instance) + + def __subclasscheck__(self, cls): + if self.__origin__ is not None: + if sys._getframe(1).f_globals['__name__'] not in ['abc', 'functools']: + raise TypeError("Parameterized generics cannot be used with class " + "or instance checks") return False - raise TypeError("Instance and class checks can only be used with" - " @runtime protocols") - if (self.__dict__.get('_is_runtime_protocol', None) and - not self._callable_members_only): - if sys._getframe(1).f_globals['__name__'] in ['abc', 'functools']: - return super(GenericMeta, self).__subclasscheck__(cls) - raise TypeError("Protocols with non-method members" - " don't support issubclass()") - return super(GenericMeta, self).__subclasscheck__(cls) - - def _get_protocol_attrs(self): - attrs = set() - for base in self.__mro__[:-1]: # without object - if base.__name__ in ('Protocol', 'Generic'): - continue - annotations = getattr(base, '__annotations__', {}) - for attr in list(base.__dict__.keys()) + list(annotations.keys()): - if (not attr.startswith('_abc_') and attr not in ( - '__abstractmethods__', '__annotations__', '__weakref__', - '_is_protocol', '_is_runtime_protocol', '__dict__', - '__args__', '__slots__', '_get_protocol_attrs', - '__next_in_mro__', '__parameters__', '__origin__', - '__orig_bases__', '__extra__', '__tree_hash__', - '__doc__', '__subclasshook__', '__init__', '__new__', - '__module__', '_MutableMapping__marker', '_gorg', - '_callable_members_only')): - attrs.add(attr) - return attrs - - @_tp_cache - def __getitem__(self, params): - # We also need to copy this from GenericMeta.__getitem__ to get - # special treatment of "Protocol". (Comments removed for brevity.) - if not isinstance(params, tuple): - params = (params,) - if not params and _gorg(self) is not Tuple: - raise TypeError( - "Parameter list to %s[...] cannot be empty" % self.__qualname__) - msg = "Parameters to generic types must be types." - params = tuple(_type_check(p, msg) for p in params) - if self in (Generic, Protocol): - if not all(isinstance(p, TypeVar) for p in params): - raise TypeError( - "Parameters to %r[...] must all be type variables" % self) - if len(set(params)) != len(params): - raise TypeError( - "Parameters to %r[...] must all be unique" % self) - tvars = params - args = params - elif self in (Tuple, Callable): - tvars = _type_vars(params) - args = params - elif self.__origin__ in (Generic, Protocol): - raise TypeError("Cannot subscript already-subscripted %s" % - repr(self)) - else: - _check_generic(self, params) - tvars = _type_vars(params) - args = params - - prepend = (self,) if self.__origin__ is None else () - return self.__class__(self.__name__, - prepend + self.__bases__, - _no_slots_copy(self.__dict__), - tvars=tvars, - args=args, - origin=self, - extra=self.__extra__, - orig_bases=self.__orig_bases__) - - if OLD_GENERICS: - del __getitem__ - - -if NO_PROTOCOL: - del _ProtocolMeta -else: + if (self.__dict__.get('_is_protocol', None) and + not self.__dict__.get('_is_runtime_protocol', None)): + if sys._getframe(1).f_globals['__name__'] in ['abc', 'functools']: + return False + raise TypeError("Instance and class checks can only be used with" + " @runtime protocols") + if (self.__dict__.get('_is_runtime_protocol', None) and + not self._callable_members_only): + if sys._getframe(1).f_globals['__name__'] in ['abc', 'functools']: + return super(GenericMeta, self).__subclasscheck__(cls) + raise TypeError("Protocols with non-method members" + " don't support issubclass()") + return super(GenericMeta, self).__subclasscheck__(cls) + + def _get_protocol_attrs(self): + attrs = set() + for base in self.__mro__[:-1]: # without object + if base.__name__ in ('Protocol', 'Generic'): + continue + annotations = getattr(base, '__annotations__', {}) + for attr in list(base.__dict__.keys()) + list(annotations.keys()): + if (not attr.startswith('_abc_') and attr not in ( + '__abstractmethods__', '__annotations__', '__weakref__', + '_is_protocol', '_is_runtime_protocol', '__dict__', + '__args__', '__slots__', '_get_protocol_attrs', + '__next_in_mro__', '__parameters__', '__origin__', + '__orig_bases__', '__extra__', '__tree_hash__', + '__doc__', '__subclasshook__', '__init__', '__new__', + '__module__', '_MutableMapping__marker', '_gorg', + '_callable_members_only')): + attrs.add(attr) + return attrs + + if not OLD_GENERICS: + @_tp_cache + def __getitem__(self, params): + # We also need to copy this from GenericMeta.__getitem__ to get + # special treatment of "Protocol". (Comments removed for brevity.) + if not isinstance(params, tuple): + params = (params,) + if not params and _gorg(self) is not Tuple: + raise TypeError( + "Parameter list to %s[...] cannot be empty" % self.__qualname__) + msg = "Parameters to generic types must be types." + params = tuple(_type_check(p, msg) for p in params) + if self in (Generic, Protocol): + if not all(isinstance(p, TypeVar) for p in params): + raise TypeError( + "Parameters to %r[...] must all be type variables" % self) + if len(set(params)) != len(params): + raise TypeError( + "Parameters to %r[...] must all be unique" % self) + tvars = params + args = params + elif self in (Tuple, Callable): + tvars = _type_vars(params) + args = params + elif self.__origin__ in (Generic, Protocol): + raise TypeError("Cannot subscript already-subscripted %s" % + repr(self)) + else: + _check_generic(self, params) + tvars = _type_vars(params) + args = params + + prepend = (self,) if self.__origin__ is None else () + return self.__class__(self.__name__, + prepend + self.__bases__, + _no_slots_copy(self.__dict__), + tvars=tvars, + args=args, + origin=self, + extra=self.__extra__, + orig_bases=self.__orig_bases__) + class Protocol(metaclass=_ProtocolMeta): """Base class for protocol classes. Protocol classes are defined as:: @@ -918,9 +920,9 @@ def func(x: Proto) -> int: func(C()) # Passes static type check - See PEP 544 for details. Protocol classes decorated with @typing_extensions.runtime - act as simple-minded runtime protocols that checks only the presence of - given attributes, ignoring their type signatures. + See PEP 544 for details. Protocol classes decorated with + @typing_extensions.runtime act as simple-minded runtime protocol that checks + only the presence of given attributes, ignoring their type signatures. Protocol classes can be generic, they are defined as:: @@ -928,7 +930,6 @@ class GenProto({bases}): def meth(self) -> T: ... """ - __slots__ = () _is_protocol = True @@ -943,21 +944,16 @@ def __new__(cls, *args, **kwds): Protocol.__doc__ = Protocol.__doc__.format(bases="Protocol, Generic[T]" if OLD_GENERICS else "Protocol[T]") + def runtime(cls): + """Mark a protocol class as a runtime protocol, so that it + can be used with isinstance() and issubclass(). Raise TypeError + if applied to a non-protocol class. -def runtime(cls): - """Mark a protocol class as a runtime protocol, so that it - can be used with isinstance() and issubclass(). Raise TypeError - if applied to a non-protocol class. - - This allows a simple-minded structural check very similar to the - one-offs in collections.abc such as Hashable. - """ - if not isinstance(cls, _ProtocolMeta) or not cls._is_protocol: - raise TypeError('@runtime can be only applied to protocol classes,' - ' got %r' % cls) - cls._is_runtime_protocol = True - return cls - - -if NO_PROTOCOL: - del runtime + This allows a simple-minded structural check very similar to the + one-offs in collections.abc such as Hashable. + """ + if not isinstance(cls, _ProtocolMeta) or not cls._is_protocol: + raise TypeError('@runtime can be only applied to protocol classes,' + ' got %r' % cls) + cls._is_runtime_protocol = True + return cls