From 05dd8ee470d966b12d8dc571ee187d10d836c6f9 Mon Sep 17 00:00:00 2001 From: Till Varoquaux Date: Sun, 2 Jun 2019 00:36:33 -0400 Subject: [PATCH 1/4] rename _Annotated to _AnnotatedAlias --- .../src_py3/typing_extensions.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 81f60ac96..5f53acc2a 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -1612,7 +1612,7 @@ class Point2D(TypedDict): if HAVE_ANNOTATED: - class _Annotated(typing._GenericAlias, _root=True): + class _AnnotatedAlias(typing._GenericAlias, _root=True): """Runtime representation of an annotated type. At its core 'Annotated[t, dec1, dec2, ...]' is an alias for the type 't' @@ -1621,7 +1621,7 @@ class _Annotated(typing._GenericAlias, _root=True): it to types is also the same. """ def __init__(self, origin, metadata): - if isinstance(origin, _Annotated): + if isinstance(origin, _AnnotatedAlias): metadata = origin.__metadata__ + metadata origin = origin.__origin__ super().__init__(origin, origin) @@ -1630,7 +1630,7 @@ def __init__(self, origin, metadata): def copy_with(self, params): assert len(params) == 1 new_type = params[0] - return _Annotated(new_type, self.__metadata__) + return _AnnotatedAlias(new_type, self.__metadata__) def __repr__(self): return "typing_extensions.Annotated[{}, {}]".format( @@ -1644,7 +1644,7 @@ def __reduce__(self): ) def __eq__(self, other): - if not isinstance(other, _Annotated): + if not isinstance(other, _AnnotatedAlias): return NotImplemented if self.__origin__ != other.__origin__: return False @@ -1680,11 +1680,11 @@ class Annotated: - Annotated can be used as a generic type alias:: - Optimized = Annotated[T, runtime.Optimize] - Optimized[int] == Annotated[int, runtime.Optimize] + Optimized = Annotated[T, runtime.Optimize()] + Optimized[int] == Annotated[int, runtime.Optimize()] - OptimizedList = Annotated[List[T], runtime.Optimize] - OptimizedList[int] == Annotated[List[int], runtime.Optimize] + OptimizedList = Annotated[List[T], runtime.Optimize()] + OptimizedList[int] == Annotated[List[int], runtime.Optimize()] """ __slots__ = () @@ -1701,7 +1701,7 @@ def __class_getitem__(cls, params): msg = "Annotated[t, ...]: t must be a type." origin = typing._type_check(params[0], msg) metadata = tuple(params[1:]) - return _Annotated(origin, metadata) + return _AnnotatedAlias(origin, metadata) def __init_subclass__(cls, *args, **kwargs): raise TypeError("Cannot inherit from Annotated") @@ -1709,7 +1709,7 @@ def __init_subclass__(cls, *args, **kwargs): def _strip_annotations(t): """Strips the annotations from a given type. """ - if isinstance(t, _Annotated): + if isinstance(t, _AnnotatedAlias): return _strip_annotations(t.__origin__) if isinstance(t, typing._GenericAlias): stripped_args = tuple(_strip_annotations(a) for a in t.__args__) From 7f2e69719cc9195838a58c0bd54606bd8293dc3c Mon Sep 17 00:00:00 2001 From: Till Varoquaux Date: Thu, 30 May 2019 17:06:28 -0400 Subject: [PATCH 2/4] Add Annotated for python 3 pre PEP-560. --- .../src_py3/test_typing_extensions.py | 385 +++++++++++------- .../src_py3/typing_extensions.py | 199 ++++++++- 2 files changed, 417 insertions(+), 167 deletions(-) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index e209a3f06..b62964fc7 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -11,13 +11,21 @@ from typing import T, KT, VT # Not in __all__. from typing import Tuple, List, Dict from typing import Generic -from typing import get_type_hints from typing import no_type_check from typing_extensions import NoReturn, ClassVar, Final, IntVar, Literal, Type, NewType, TypedDict try: from typing_extensions import Protocol, runtime except ImportError: pass +try: + from typing_extensions import Annotated +except ImportError: + pass +try: + from typing_extensions import get_type_hints +except ImportError: + from typing import get_type_hints + import typing import typing_extensions import collections.abc as collections_abc @@ -65,9 +73,6 @@ # Protocols are hard to backport to the original version of typing 3.5.0 HAVE_PROTOCOLS = sys.version_info[:3] != (3, 5, 0) -# Not backported to older versions yet -HAVE_ANNOTATED = PEP_560 - class BaseTestCase(TestCase): def assertIsSubclass(self, cls, class_or_tuple, msg=None): if not issubclass(cls, class_or_tuple): @@ -1474,179 +1479,244 @@ def test_total(self): self.assertEqual(Options.__total__, False) -if HAVE_ANNOTATED: - from typing_extensions import Annotated, get_type_hints +@skipUnless(TYPING_3_5_3, "Python >= 3.5.3 required") +class AnnotatedTests(BaseTestCase): - class AnnotatedTests(BaseTestCase): + def test_repr(self): + self.assertEqual( + repr(Annotated[int, 4, 5]), + "typing_extensions.Annotated[int, 4, 5]" + ) + self.assertEqual( + repr(Annotated[List[int], 4, 5]), + "typing_extensions.Annotated[typing.List[int], 4, 5]" + ) + + def test_flatten(self): + A = Annotated[Annotated[int, 4], 5] + self.assertEqual(A, Annotated[int, 4, 5]) + self.assertEqual(A.__metadata__, (4, 5)) + if PEP_560: + self.assertEqual(A.__origin__, int) - def test_repr(self): - self.assertEqual( - repr(Annotated[int, 4, 5]), - "typing_extensions.Annotated[int, 4, 5]" - ) + def test_specialize(self): + L = Annotated[List[T], "my decoration"] + LI = Annotated[List[int], "my decoration"] + self.assertEqual(L[int], Annotated[List[int], "my decoration"]) + self.assertEqual(L[int].__metadata__, ("my decoration",)) + if PEP_560: + self.assertEqual(L[int].__origin__, List[int]) + with self.assertRaises(TypeError): + LI[int] + with self.assertRaises(TypeError): + L[int, float] + + def test_hash_eq(self): + self.assertEqual(len({Annotated[int, 4, 5], Annotated[int, 4, 5]}), 1) + self.assertNotEqual(Annotated[int, 4, 5], Annotated[int, 5, 4]) + self.assertNotEqual(Annotated[int, 4, 5], Annotated[str, 4, 5]) + self.assertNotEqual(Annotated[int, 4], Annotated[int, 4, 4]) + self.assertEqual( + {Annotated[int, 4, 5], Annotated[int, 4, 5], Annotated[T, 4, 5]}, + {Annotated[int, 4, 5], Annotated[T, 4, 5]} + ) + + def test_instantiate(self): + class C: + classvar = 4 + + def __init__(self, x): + self.x = x + + def __eq__(self, other): + if not isinstance(other, C): + return NotImplemented + return other.x == self.x + + A = Annotated[C, "a decoration"] + a = A(5) + c = C(5) + self.assertEqual(a, c) + self.assertEqual(a.x, c.x) + self.assertEqual(a.classvar, c.classvar) + + def test_instantiate_generic(self): + MyCount = Annotated[typing_extensions.Counter[T], "my decoration"] + self.assertEqual(MyCount([4, 4, 5]), {4: 2, 5: 1}) + self.assertEqual(MyCount[int]([4, 4, 5]), {4: 2, 5: 1}) + + def test_cannot_instantiate_forward(self): + A = Annotated["int", (5, 6)] + with self.assertRaises(TypeError): + A(5) - def test_flatten(self): - A = Annotated[Annotated[int, 4], 5] - self.assertEqual(A, Annotated[int, 4, 5]) - self.assertEqual(A.__metadata__, (4, 5)) - self.assertEqual(A.__origin__, int) + def test_cannot_instantiate_type_var(self): + A = Annotated[T, (5, 6)] + with self.assertRaises(TypeError): + A(5) - def test_hash_eq(self): - self.assertEqual(len({Annotated[int, 4, 5], Annotated[int, 4, 5]}), 1) - self.assertNotEqual(Annotated[int, 4, 5], Annotated[int, 5, 4]) - self.assertNotEqual(Annotated[int, 4, 5], Annotated[str, 4, 5]) - self.assertNotEqual(Annotated[int, 4], Annotated[int, 4, 4]) - self.assertEqual( - {Annotated[int, 4, 5], Annotated[int, 4, 5], Annotated[T, 4, 5]}, - {Annotated[int, 4, 5], Annotated[T, 4, 5]} - ) - - def test_instantiate(self): - class C: - classvar = 4 + def test_cannot_getattr_typevar(self): + with self.assertRaises(Exception): + Annotated[T, (5, 7)].x - def __init__(self, x): - self.x = x - def __eq__(self, other): - if not isinstance(other, C): - return NotImplemented - return other.x == self.x + def test_attr_passthrough(self): + class C: + classvar = 4 - A = Annotated[C, "a decoration"] - a = A(5) - c = C(5) - self.assertEqual(a, c) - self.assertEqual(a.x, c.x) - self.assertEqual(A.classvar, C.classvar) + A = Annotated[C, "a decoration"] + self.assertEqual(A.classvar, 4) - MyCount = Annotated[typing_extensions.Counter[T], "my decoration"] - self.assertEqual(MyCount([4, 4, 5]), {4: 2, 5: 1}) - self.assertEqual(MyCount[int]([4, 4, 5]), {4: 2, 5: 1}) + def test_hash_eq(self): + self.assertEqual(len({Annotated[int, 4, 5], Annotated[int, 4, 5]}), 1) + self.assertNotEqual(Annotated[int, 4, 5], Annotated[int, 5, 4]) + self.assertNotEqual(Annotated[int, 4, 5], Annotated[str, 4, 5]) + self.assertNotEqual(Annotated[int, 4], Annotated[int, 4, 4]) + self.assertEqual( + {Annotated[int, 4, 5], Annotated[int, 4, 5], Annotated[T, 4, 5]}, + {Annotated[int, 4, 5], Annotated[T, 4, 5]} + ) + def test_cannot_subclass(self): + with self.assertRaisesRegex(TypeError, "Cannot subclass .*Annotated"): + class C(Annotated): + pass - def test_cannot_subclass(self): - with self.assertRaises(TypeError): - class C(Annotated): - pass + def test_cannot_check_instance(self): + with self.assertRaisesRegex(TypeError, "Annotated cannot be used with isinstance()"): + isinstance(5, Annotated[int, "positive"]) - def test_pickle(self): - samples = [typing.Any, typing.Union[int, str], - typing.Optional[str], Tuple[int, ...], - typing.Callable[[str], bytes]] + def test_cannot_check_subclass(self): + with self.assertRaisesRegex(TypeError, "Annotated cannot be used with issubclass()"): + issubclass(int, Annotated[int, "positive"]) - for t in samples: - x = Annotated[t, "a"] - for prot in range(pickle.HIGHEST_PROTOCOL + 1): - with self.subTest(protocol=prot, type=t): - pickled = pickle.dumps(x, prot) - restored = pickle.loads(pickled) - self.assertEqual(x, restored) + @skipUnless(PEP_560, "pickle support was added with pep_560") + def test_pickle(self): + samples = [typing.Any, typing.Union[int, str], + typing.Optional[str], Tuple[int, ...], + typing.Callable[[str], bytes]] - global _Annotated_test_G + for t in samples: + x = Annotated[t, "a"] - class _Annotated_test_G(Generic[T]): - x = 1 + for prot in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(protocol=prot, type=t): + pickled = pickle.dumps(x, prot) + restored = pickle.loads(pickled) + self.assertEqual(x, restored) - G = Annotated[_Annotated_test_G[int], "A decoration"] - G.foo = 42 - G.bar = 'abc' + global _Annotated_test_G - for proto in range(pickle.HIGHEST_PROTOCOL + 1): - z = pickle.dumps(G, proto) - x = pickle.loads(z) - self.assertEqual(x.foo, 42) - self.assertEqual(x.bar, 'abc') - self.assertEqual(x.x, 1) + class _Annotated_test_G(Generic[T]): + x = 1 - def test_subst(self): - dec = "a decoration" + G = Annotated[_Annotated_test_G[int], "A decoration"] + G.foo = 42 + G.bar = 'abc' - S = Annotated[T, dec] - self.assertEqual(S[int], Annotated[int, dec]) + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + z = pickle.dumps(G, proto) + x = pickle.loads(z) + self.assertEqual(x.foo, 42) + self.assertEqual(x.bar, 'abc') + self.assertEqual(x.x, 1) - L = Annotated[List[T], dec] - self.assertEqual(L[int], Annotated[List[int], dec]) - with self.assertRaises(TypeError): - L[int, int] + def test_subst(self): + dec = "a decoration" + dec2 = "another decoration" - D = Annotated[Dict[KT, VT], dec] - self.assertEqual(D[str, int], Annotated[Dict[str, int], dec]) - with self.assertRaises(TypeError): - D[int] + S = Annotated[T, dec2] + self.assertEqual(S[int], Annotated[int, dec2]) - I = Annotated[int, dec] - with self.assertRaises(TypeError): - I[None] + self.assertEqual(S[Annotated[int, dec]], Annotated[int, dec, dec2]) + L = Annotated[List[T], dec] - LI = L[int] - with self.assertRaises(TypeError): - LI[None] - - def test_annotated_in_other_types(self): - X = List[Annotated[T, 5]] - self.assertEqual(X[int], List[Annotated[int, 5]]) - - - class GetTypeHintsTests(BaseTestCase): - def test_get_type_hints(self): - def foobar(x: List['X']): ... - X = Annotated[int, (1, 10)] - self.assertEqual( - get_type_hints(foobar, globals(), locals()), - {'x': List[int]} - ) - self.assertEqual( - get_type_hints(foobar, globals(), locals(), include_extras=True), - {'x': List[Annotated[int, (1, 10)]]} - ) - BA = Tuple[Annotated[T, (1, 0)], ...] - def barfoo(x: BA): ... - self.assertEqual(get_type_hints(barfoo, globals(), locals())['x'], Tuple[T, ...]) - self.assertIs( - get_type_hints(barfoo, globals(), locals(), include_extras=True)['x'], - BA - ) - def barfoo2(x: typing.Callable[..., Annotated[List[T], "const"]], - y: typing.Union[int, Annotated[T, "mutable"]]): ... - self.assertEqual( - get_type_hints(barfoo2, globals(), locals()), - {'x': typing.Callable[..., List[T]], 'y': typing.Union[int, T]} - ) - BA2 = typing.Callable[..., List[T]] - def barfoo3(x: BA2): ... - self.assertIs( - get_type_hints(barfoo3, globals(), locals(), include_extras=True)["x"], - BA2 - ) - - def test_get_type_hints_refs(self): - - Const = Annotated[T, "Const"] - - class MySet(Generic[T]): - - def __ior__(self, other: "Const[MySet[T]]") -> "MySet[T]": - ... - - def __iand__(self, other: Const["MySet[T]"]) -> "MySet[T]": - ... - - self.assertEqual( - get_type_hints(MySet.__iand__, globals(), locals()), - {'other': MySet[T], 'return': MySet[T]} - ) - - self.assertEqual( - get_type_hints(MySet.__iand__, globals(), locals(), include_extras=True), - {'other': Const[MySet[T]], 'return': MySet[T]} - ) - - self.assertEqual( - get_type_hints(MySet.__ior__, globals(), locals()), - {'other': MySet[T], 'return': MySet[T]} - ) + self.assertEqual(L[int], Annotated[List[int], dec]) + with self.assertRaises(TypeError): + L[int, int] + + self.assertEqual(S[L[int]], Annotated[List[int], dec, dec2]) + + + + D = Annotated[Dict[KT, VT], dec] + self.assertEqual(D[str, int], Annotated[Dict[str, int], dec]) + with self.assertRaises(TypeError): + D[int] + + I = Annotated[int, dec] + with self.assertRaises(TypeError): + I[None] + + LI = L[int] + with self.assertRaises(TypeError): + LI[None] + + def test_annotated_in_other_types(self): + X = List[Annotated[T, 5]] + self.assertEqual(X[int], List[Annotated[int, 5]]) + + +@skipUnless(PEP_560, "python 3.7 required") +class GetTypeHintsTests(BaseTestCase): + def test_get_type_hints(self): + def foobar(x: List['X']): ... + X = Annotated[int, (1, 10)] + self.assertEqual( + get_type_hints(foobar, globals(), locals()), + {'x': List[int]} + ) + self.assertEqual( + get_type_hints(foobar, globals(), locals(), include_extras=True), + {'x': List[Annotated[int, (1, 10)]]} + ) + BA = Tuple[Annotated[T, (1, 0)], ...] + def barfoo(x: BA): ... + self.assertEqual(get_type_hints(barfoo, globals(), locals())['x'], Tuple[T, ...]) + self.assertIs( + get_type_hints(barfoo, globals(), locals(), include_extras=True)['x'], + BA + ) + def barfoo2(x: typing.Callable[..., Annotated[List[T], "const"]], + y: typing.Union[int, Annotated[T, "mutable"]]): ... + self.assertEqual( + get_type_hints(barfoo2, globals(), locals()), + {'x': typing.Callable[..., List[T]], 'y': typing.Union[int, T]} + ) + BA2 = typing.Callable[..., List[T]] + def barfoo3(x: BA2): ... + self.assertIs( + get_type_hints(barfoo3, globals(), locals(), include_extras=True)["x"], + BA2 + ) + + def test_get_type_hints_refs(self): + + Const = Annotated[T, "Const"] + + class MySet(Generic[T]): + + def __ior__(self, other: "Const[MySet[T]]") -> "MySet[T]": + ... + + def __iand__(self, other: Const["MySet[T]"]) -> "MySet[T]": + ... + + self.assertEqual( + get_type_hints(MySet.__iand__, globals(), locals()), + {'other': MySet[T], 'return': MySet[T]} + ) + + self.assertEqual( + get_type_hints(MySet.__iand__, globals(), locals(), include_extras=True), + {'other': Const[MySet[T]], 'return': MySet[T]} + ) + + self.assertEqual( + get_type_hints(MySet.__ior__, globals(), locals()), + {'other': MySet[T], 'return': MySet[T]} + ) @@ -1665,6 +1735,10 @@ def test_typing_extensions_includes_standard(self): self.assertIn('overload', a) self.assertIn('Text', a) self.assertIn('TYPE_CHECKING', a) + if TYPING_3_5_3: + self.assertIn('Annotated', a) + if PEP_560: + self.assertIn('get_type_hints', a) if ASYNCIO: self.assertIn('Awaitable', a) @@ -1680,9 +1754,6 @@ def test_typing_extensions_includes_standard(self): self.assertIn('Protocol', a) self.assertIn('runtime', a) - if HAVE_ANNOTATED: - self.assertIn('Annotated', a) - self.assertIn('get_type_hints', a) def test_typing_extensions_defers_when_possible(self): exclude = {'overload', 'Text', 'TYPE_CHECKING', 'Final', 'get_type_hints'} diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 5f53acc2a..15c9c1842 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -24,6 +24,11 @@ from typing import _type_vars, _next_in_mro, _type_check except ImportError: OLD_GENERICS = True +try: + from typing import _subs_tree + SUBS_TREE = True +except ImportError: + SUBS_TREE = False try: from typing import _tp_cache except ImportError: @@ -135,19 +140,22 @@ def _check_methods_in_mro(C, *methods): 'TYPE_CHECKING', ] +# Annotated relies on substitution trees of pep 560. It will not work for +# versions of typing older than 3.5.3 +HAVE_ANNOTATED = PEP_560 or SUBS_TREE + +if PEP_560: + __all__.append("get_type_hints") + +if HAVE_ANNOTATED: + __all__.append("Annotated") + # Protocols are hard to backport to the original version of typing 3.5.0 HAVE_PROTOCOLS = sys.version_info[:3] != (3, 5, 0) if HAVE_PROTOCOLS: __all__.extend(['Protocol', 'runtime']) -# Annotations were implemented under tight time constraints; this keeps the -# implementation simple for now -HAVE_ANNOTATED = PEP_560 - -if HAVE_ANNOTATED: - __all__.extend(['Annotated', 'get_type_hints']) - # TODO if hasattr(typing, 'NoReturn'): @@ -1611,7 +1619,7 @@ class Point2D(TypedDict): """ -if HAVE_ANNOTATED: +if PEP_560: class _AnnotatedAlias(typing._GenericAlias, _root=True): """Runtime representation of an annotated type. @@ -1653,6 +1661,11 @@ def __eq__(self, other): def __hash__(self): return hash((self.__origin__, self.__metadata__)) + def __instancecheck__(self, obj): + raise TypeError("Annotated cannot be used with isinstance().") + + def __subclasscheck__(self, cls): + raise TypeError("Annotated cannot be used with issubclass().") class Annotated: """Add context specific metadata to a type. @@ -1685,6 +1698,8 @@ class Annotated: OptimizedList = Annotated[List[T], runtime.Optimize()] OptimizedList[int] == Annotated[List[int], runtime.Optimize()] + + NOTE: the __type__ field is only available in python 3.7+. """ __slots__ = () @@ -1692,7 +1707,7 @@ class Annotated: def __new__(cls, *args, **kwargs): raise TypeError("Type Annotated cannot be instantiated.") - @typing._tp_cache + @_tp_cache def __class_getitem__(cls, params): if not isinstance(params, tuple) or len(params) < 2: raise TypeError("Annotated[...] should be used " @@ -1704,7 +1719,9 @@ def __class_getitem__(cls, params): return _AnnotatedAlias(origin, metadata) def __init_subclass__(cls, *args, **kwargs): - raise TypeError("Cannot inherit from Annotated") + raise TypeError( + "Cannot subclass {}.Annotated".format(cls.__module__) + ) def _strip_annotations(t): """Strips the annotations from a given type. @@ -1755,3 +1772,165 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): if include_extras: return hint return {k: _strip_annotations(t) for k, t in hint.items()} + +elif HAVE_ANNOTATED: + + # Prior to python 3.7 types did not have `copy_with`. A lot of the equality + # checks, argument expansion etc.. are done on the _subs_tree. + # + # >> U = Union[Optional[float], int] + # >> U1 = Union[T, int] + # >> U2 = U1[Optional[float]] + # >> U == U2 + # True + # >> U2.__args__, U.__args__ + # (typing.Union[float, NoneType],) (, , ) + # + # Here U and U2 have different shapes but have the same _subs_tree. + # + # As a result we can't provide a get_type_hints function that strips out + # annotations or a '__type__' field on the Annotated class. + + class AnnotatedMeta(typing.GenericMeta): + """Metaclass for Annotated""" + + def __new__(cls, name, bases, namespace, **kwargs): + if any(b is not object for b in bases): + raise TypeError("Cannot subclass " + str(Annotated)) + return super().__new__(cls, name, bases, namespace, **kwargs) + + @property + def __metadata__(self): + return self._subs_tree()[2] + + def _tree_repr(self, tree): + assert len(tree) == 3 + # First argument is this class, second is __type__, 3rd is __metadata__ + tp_tree = tree[1] + if not isinstance(tp_tree, tuple): + tp_repr = typing._type_repr(tp_tree) + else: + tp_repr = tp_tree[0]._tree_repr(tp_tree) + metadata_reprs = ", ".join(repr(arg) for arg in tree[2]) + return repr(tree[0]) + '[%s, %s]' % (tp_repr, metadata_reprs) + + def _subs_tree(self, tvars=None, args=None): + if self is Annotated: + return Annotated + res = super()._subs_tree(tvars=tvars, args=args) + # Flatten nested Annotated + if isinstance(res[1], tuple) and res[1][0] is Annotated: + sub_tp = res[1][1] + sub_annot = res[1][2] + return (Annotated, sub_tp, sub_annot + res[2]) + return res + + + def _get_cons(self, msg): + if self.__origin__ is None: + raise TypeError(msg + " a non specialized Annotated type") + tree = self._subs_tree() + while isinstance(tree, tuple) and tree[0] is Annotated: + tree = tree[1] + if isinstance(tree, tuple): + cons = tree[0] + else: + cons = tree + type_error = None + if isinstance(cons, typing._ForwardRef): + type_error = "a forward reference." + elif isinstance(cons, typing.TypeVar): + type_error = "a type variable." + + if type_error: + raise TypeError( + "{} {}: the annotated type is {}".format( + msg, + self, + type_error + ) + ) + + return cons + + @_tp_cache + def __getitem__(self, params): + if not isinstance(params, tuple): + params = (params,) + if self.__origin__ is not None: # specializing an instantiated type + return super().__getitem__(params) + elif not isinstance(params, tuple) or len(params) < 2: + raise TypeError("Annotated[...] should be instantiated " + "with at least two arguments (a type and an " + "annotation).") + else: + msg = "Annotated[t, ...]: t must be a type." + tp = typing._type_check(params[0], msg) + metadata = tuple(params[1:]) + return self.__class__( + self.__name__, + self.__bases__, + _no_slots_copy(self.__dict__), + tvars=_type_vars((tp,)), + # Metadata is a tuple so it won't be touched by _replace_args et al. + args=(tp, metadata), + origin=self, + ) + + def __call__(self, *args, **kwargs): + cons = self._get_cons("Cannot create an instance of") + result = cons(*args, **kwargs) + try: + result.__orig_class__ = self + except AttributeError: + pass + return result + + def __getattr__(self, attr): + # We are careful for copy and pickle. + # Also for simplicity we just don't relay all dunder names + if self.__origin__ is not None and not ( + attr.startswith('__') and attr.endswith('__') + ): + return getattr(self._get_cons("Cannot access properties on"), attr) + raise AttributeError(attr) + + def __instancecheck__(self, obj): + raise TypeError("Annotated cannot be used with isinstance().") + + def __subclasscheck__(self, cls): + raise TypeError("Annotated cannot be used with issubclass().") + + + + class Annotated(metaclass=AnnotatedMeta): + """Add context specific metadata to a type. + + Example: Annotated[int, runtime_check.Unsigned] indicates to the + hypothetical runtime_check module that this type is an unsigned int. + Every other consumer of this type can ignore this metadata and treat + this type as int. + + The first argument to Annotated must be a valid type, the remaining + arguments are kept as a tuple in the __metadata__ field. + + Details: + + - It's an error to call `Annotated` with less than two arguments. + - Nested Annotated are flattened:: + + Annotated[Annotated[int, Ann1, Ann2], Ann3] == Annotated[int, Ann1, Ann2, Ann3] + + - Instantiating an annotated type is equivalent to instantiating the + underlying type:: + + Annotated[C, Ann1](5) == C(5) + + - Annotated can be used as a generic type alias:: + + Optimized = Annotated[T, runtime.Optimize()] + Optimized[int] == Annotated[int, runtime.Optimize()] + + OptimizedList = Annotated[List[T], runtime.Optimize()] + OptimizedList[int] == Annotated[List[int], runtime.Optimize()] + """ From d78a6b36a09bee26b43badb6d06ef119d48d55de Mon Sep 17 00:00:00 2001 From: Till Varoquaux Date: Mon, 3 Jun 2019 10:57:38 -0400 Subject: [PATCH 3/4] Annotated: retrofitted setattr for Python < 3.7 and cleaned up tests and docs --- .../src_py3/test_typing_extensions.py | 8 ++- .../src_py3/typing_extensions.py | 67 ++++++++++--------- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index b62964fc7..8c69b572c 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -1556,7 +1556,7 @@ def test_cannot_instantiate_type_var(self): A(5) def test_cannot_getattr_typevar(self): - with self.assertRaises(Exception): + with self.assertRaises(AttributeError): Annotated[T, (5, 7)].x @@ -1566,6 +1566,8 @@ class C: A = Annotated[C, "a decoration"] self.assertEqual(A.classvar, 4) + A.x = 5 + self.assertEqual(C.x, 5) def test_hash_eq(self): self.assertEqual(len({Annotated[int, 4, 5], Annotated[int, 4, 5]}), 1) @@ -1591,7 +1593,7 @@ def test_cannot_check_subclass(self): issubclass(int, Annotated[int, "positive"]) - @skipUnless(PEP_560, "pickle support was added with pep_560") + @skipUnless(PEP_560, "pickle support was added with PEP 560") def test_pickle(self): samples = [typing.Any, typing.Union[int, str], typing.Optional[str], Tuple[int, ...], @@ -1658,7 +1660,7 @@ def test_annotated_in_other_types(self): self.assertEqual(X[int], List[Annotated[int, 5]]) -@skipUnless(PEP_560, "python 3.7 required") +@skipUnless(PEP_560, "Python 3.7 required") class GetTypeHintsTests(BaseTestCase): def test_get_type_hints(self): def foobar(x: List['X']): ... diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 15c9c1842..1b8500c69 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -1698,8 +1698,6 @@ class Annotated: OptimizedList = Annotated[List[T], runtime.Optimize()] OptimizedList[int] == Annotated[List[int], runtime.Optimize()] - - NOTE: the __type__ field is only available in python 3.7+. """ __slots__ = () @@ -1775,21 +1773,9 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): elif HAVE_ANNOTATED: - # Prior to python 3.7 types did not have `copy_with`. A lot of the equality - # checks, argument expansion etc.. are done on the _subs_tree. - # - # >> U = Union[Optional[float], int] - # >> U1 = Union[T, int] - # >> U2 = U1[Optional[float]] - # >> U == U2 - # True - # >> U2.__args__, U.__args__ - # (typing.Union[float, NoneType],) (, , ) - # - # Here U and U2 have different shapes but have the same _subs_tree. - # - # As a result we can't provide a get_type_hints function that strips out - # annotations or a '__type__' field on the Annotated class. + # Prior to Python 3.7 types did not have `copy_with`. A lot of the equality + # checks, argument expansion etc. are done on the _subs_tre. As a result we + # can't provide a get_type_hints function that strips out annotations. class AnnotatedMeta(typing.GenericMeta): """Metaclass for Annotated""" @@ -1805,7 +1791,7 @@ def __metadata__(self): def _tree_repr(self, tree): assert len(tree) == 3 - # First argument is this class, second is __type__, 3rd is __metadata__ + # First argument is this class, second is __origin__, 3rd is __metadata__ tp_tree = tree[1] if not isinstance(tp_tree, tuple): tp_repr = typing._type_repr(tp_tree) @@ -1825,8 +1811,13 @@ def _subs_tree(self, tvars=None, args=None): return (Annotated, sub_tp, sub_annot + res[2]) return res + def _get_cons(self, msg, exc_type): + """ Return the class used to create instance of this type. - def _get_cons(self, msg): + The msg and exc_type argument are used to control the exceptions + that get raised when there's no underlying constructor available ( + e.g.: we're in an unspecialized class). + """ if self.__origin__ is None: raise TypeError(msg + " a non specialized Annotated type") tree = self._subs_tree() @@ -1843,13 +1834,8 @@ def _get_cons(self, msg): type_error = "a type variable." if type_error: - raise TypeError( - "{} {}: the annotated type is {}".format( - msg, - self, - type_error - ) - ) + error = "{} {}: the annotated type is {}".format(msg, self, type_error) + raise exc_type(error) return cons @@ -1878,7 +1864,7 @@ def __getitem__(self, params): ) def __call__(self, *args, **kwargs): - cons = self._get_cons("Cannot create an instance of") + cons = self._get_cons("Cannot create an instance of", TypeError) result = cons(*args, **kwargs) try: result.__orig_class__ = self @@ -1887,22 +1873,39 @@ def __call__(self, *args, **kwargs): return result def __getattr__(self, attr): - # We are careful for copy and pickle. - # Also for simplicity we just don't relay all dunder names + # For simplicity we just don't relay all dunder names if self.__origin__ is not None and not ( attr.startswith('__') and attr.endswith('__') ): - return getattr(self._get_cons("Cannot access properties on"), attr) + return getattr( + self._get_cons("Cannot access properties on", AttributeError), + attr + ) raise AttributeError(attr) + def __setattr__(self, attr, value): + if ( + attr.startswith('__') and attr.endswith('__') + or attr.startswith('_abc_') + ): + super().__setattr__(attr, value) + elif self.__origin__ is None: + raise AttributeError(attr) + else: + setattr( + self._get_cons("Cannot set properties on", AttributeError), + attr, + value + ) + + # We overload this because the super() error message is confusing: + # Parametrized generics cannot be used ... def __instancecheck__(self, obj): raise TypeError("Annotated cannot be used with isinstance().") def __subclasscheck__(self, cls): raise TypeError("Annotated cannot be used with issubclass().") - - class Annotated(metaclass=AnnotatedMeta): """Add context specific metadata to a type. From 68e15c6e4f1606f408488a9f17aba09f7298bf7e Mon Sep 17 00:00:00 2001 From: Till Varoquaux Date: Tue, 4 Jun 2019 23:40:47 -0400 Subject: [PATCH 4/4] Simplify error code and reformat a bit --- .../src_py3/test_typing_extensions.py | 7 +-- .../src_py3/typing_extensions.py | 46 ++++--------------- 2 files changed, 11 insertions(+), 42 deletions(-) diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index 8c69b572c..57d77e65d 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -1559,7 +1559,6 @@ def test_cannot_getattr_typevar(self): with self.assertRaises(AttributeError): Annotated[T, (5, 7)].x - def test_attr_passthrough(self): class C: classvar = 4 @@ -1585,11 +1584,11 @@ class C(Annotated): pass def test_cannot_check_instance(self): - with self.assertRaisesRegex(TypeError, "Annotated cannot be used with isinstance()"): + with self.assertRaises(TypeError): isinstance(5, Annotated[int, "positive"]) def test_cannot_check_subclass(self): - with self.assertRaisesRegex(TypeError, "Annotated cannot be used with issubclass()"): + with self.assertRaises(TypeError): issubclass(int, Annotated[int, "positive"]) @@ -1640,8 +1639,6 @@ def test_subst(self): self.assertEqual(S[L[int]], Annotated[List[int], dec, dec2]) - - D = Annotated[Dict[KT, VT], dec] self.assertEqual(D[str, int], Annotated[Dict[str, int], dec]) with self.assertRaises(TypeError): diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 1b8500c69..dc5d2db9c 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -1661,12 +1661,6 @@ def __eq__(self, other): def __hash__(self): return hash((self.__origin__, self.__metadata__)) - def __instancecheck__(self, obj): - raise TypeError("Annotated cannot be used with isinstance().") - - def __subclasscheck__(self, cls): - raise TypeError("Annotated cannot be used with issubclass().") - class Annotated: """Add context specific metadata to a type. @@ -1811,33 +1805,18 @@ def _subs_tree(self, tvars=None, args=None): return (Annotated, sub_tp, sub_annot + res[2]) return res - def _get_cons(self, msg, exc_type): - """ Return the class used to create instance of this type. - - The msg and exc_type argument are used to control the exceptions - that get raised when there's no underlying constructor available ( - e.g.: we're in an unspecialized class). - """ + def _get_cons(self): + """Return the class used to create instance of this type.""" if self.__origin__ is None: - raise TypeError(msg + " a non specialized Annotated type") + raise TypeError("Cannot get the underlying type of a " + "non-specialized Annotated type.") tree = self._subs_tree() while isinstance(tree, tuple) and tree[0] is Annotated: tree = tree[1] if isinstance(tree, tuple): - cons = tree[0] + return tree[0] else: - cons = tree - type_error = None - if isinstance(cons, typing._ForwardRef): - type_error = "a forward reference." - elif isinstance(cons, typing.TypeVar): - type_error = "a type variable." - - if type_error: - error = "{} {}: the annotated type is {}".format(msg, self, type_error) - raise exc_type(error) - - return cons + return tree @_tp_cache def __getitem__(self, params): @@ -1864,7 +1843,7 @@ def __getitem__(self, params): ) def __call__(self, *args, **kwargs): - cons = self._get_cons("Cannot create an instance of", TypeError) + cons = self._get_cons() result = cons(*args, **kwargs) try: result.__orig_class__ = self @@ -1877,10 +1856,7 @@ def __getattr__(self, attr): if self.__origin__ is not None and not ( attr.startswith('__') and attr.endswith('__') ): - return getattr( - self._get_cons("Cannot access properties on", AttributeError), - attr - ) + return getattr(self._get_cons(), attr) raise AttributeError(attr) def __setattr__(self, attr, value): @@ -1892,11 +1868,7 @@ def __setattr__(self, attr, value): elif self.__origin__ is None: raise AttributeError(attr) else: - setattr( - self._get_cons("Cannot set properties on", AttributeError), - attr, - value - ) + setattr(self._get_cons(), attr, value) # We overload this because the super() error message is confusing: # Parametrized generics cannot be used ...