Skip to content

Commit a15d786

Browse files
till-varoquauxilevkivskyi
authored andcommitted
Add Annotated in python2 (python#640)
The Python 2 version is very close to the Python 3.6 code.
1 parent 3269a69 commit a15d786

File tree

2 files changed

+274
-1
lines changed

2 files changed

+274
-1
lines changed

src_py2/test_typing_extensions.py

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
import types
99
from unittest import TestCase, main, skipUnless
1010

11-
from typing_extensions import NoReturn, ClassVar, Final, IntVar, Literal, TypedDict
11+
from typing_extensions import Annotated, NoReturn, ClassVar, Final, IntVar, Literal, TypedDict
1212
from typing_extensions import ContextManager, Counter, Deque, DefaultDict
1313
from typing_extensions import NewType, overload, Protocol, runtime
14+
from typing import Dict, List
1415
import typing
1516
import typing_extensions
1617

@@ -926,6 +927,147 @@ def test_total(self):
926927
self.assertEqual(D.__total__, False)
927928

928929

930+
class AnnotatedTests(BaseTestCase):
931+
932+
def test_repr(self):
933+
self.assertEqual(
934+
repr(Annotated[int, 4, 5]),
935+
"typing_extensions.Annotated[int, 4, 5]"
936+
)
937+
self.assertEqual(
938+
repr(Annotated[List[int], 4, 5]),
939+
"typing_extensions.Annotated[typing.List[int], 4, 5]"
940+
)
941+
self.assertEqual(repr(Annotated), "typing_extensions.Annotated")
942+
943+
def test_flatten(self):
944+
A = Annotated[Annotated[int, 4], 5]
945+
self.assertEqual(A, Annotated[int, 4, 5])
946+
self.assertEqual(A.__metadata__, (4, 5))
947+
948+
def test_specialize(self):
949+
L = Annotated[List[T], "my decoration"]
950+
LI = Annotated[List[int], "my decoration"]
951+
self.assertEqual(L[int], Annotated[List[int], "my decoration"])
952+
self.assertEqual(L[int].__metadata__, ("my decoration",))
953+
with self.assertRaises(TypeError):
954+
LI[int]
955+
with self.assertRaises(TypeError):
956+
L[int, float]
957+
958+
def test_hash_eq(self):
959+
self.assertEqual(len({Annotated[int, 4, 5], Annotated[int, 4, 5]}), 1)
960+
self.assertNotEqual(Annotated[int, 4, 5], Annotated[int, 5, 4])
961+
self.assertNotEqual(Annotated[int, 4, 5], Annotated[str, 4, 5])
962+
self.assertNotEqual(Annotated[int, 4], Annotated[int, 4, 4])
963+
self.assertEqual(
964+
{Annotated[int, 4, 5], Annotated[int, 4, 5], Annotated[T, 4, 5]},
965+
{Annotated[int, 4, 5], Annotated[T, 4, 5]}
966+
)
967+
968+
def test_instantiate(self):
969+
class C:
970+
classvar = 4
971+
972+
def __init__(self, x):
973+
self.x = x
974+
975+
def __eq__(self, other):
976+
if not isinstance(other, C):
977+
return NotImplemented
978+
return other.x == self.x
979+
980+
A = Annotated[C, "a decoration"]
981+
a = A(5)
982+
c = C(5)
983+
self.assertEqual(a, c)
984+
self.assertEqual(a.x, c.x)
985+
self.assertEqual(a.classvar, c.classvar)
986+
987+
def test_instantiate_generic(self):
988+
MyCount = Annotated[typing_extensions.Counter[T], "my decoration"]
989+
self.assertEqual(MyCount([4, 4, 5]), {4: 2, 5: 1})
990+
self.assertEqual(MyCount[int]([4, 4, 5]), {4: 2, 5: 1})
991+
992+
def test_cannot_instantiate_forward(self):
993+
A = Annotated["int", (5, 6)]
994+
with self.assertRaises(TypeError):
995+
A(5)
996+
997+
def test_cannot_instantiate_type_var(self):
998+
A = Annotated[T, (5, 6)]
999+
with self.assertRaises(TypeError):
1000+
A(5)
1001+
1002+
def test_cannot_getattr_typevar(self):
1003+
with self.assertRaises(AttributeError):
1004+
Annotated[T, (5, 7)].x
1005+
1006+
def test_attr_passthrough(self):
1007+
class C:
1008+
classvar = 4
1009+
1010+
A = Annotated[C, "a decoration"]
1011+
self.assertEqual(A.classvar, 4)
1012+
A.x = 5
1013+
self.assertEqual(C.x, 5)
1014+
1015+
def test_hash_eq(self):
1016+
self.assertEqual(len({Annotated[int, 4, 5], Annotated[int, 4, 5]}), 1)
1017+
self.assertNotEqual(Annotated[int, 4, 5], Annotated[int, 5, 4])
1018+
self.assertNotEqual(Annotated[int, 4, 5], Annotated[str, 4, 5])
1019+
self.assertNotEqual(Annotated[int, 4], Annotated[int, 4, 4])
1020+
self.assertEqual(
1021+
{Annotated[int, 4, 5], Annotated[int, 4, 5], Annotated[T, 4, 5]},
1022+
{Annotated[int, 4, 5], Annotated[T, 4, 5]}
1023+
)
1024+
1025+
def test_cannot_subclass(self):
1026+
with self.assertRaises(TypeError):
1027+
class C(Annotated):
1028+
pass
1029+
1030+
def test_cannot_check_instance(self):
1031+
with self.assertRaises(TypeError):
1032+
isinstance(5, Annotated[int, "positive"])
1033+
1034+
def test_cannot_check_subclass(self):
1035+
with self.assertRaises(TypeError):
1036+
issubclass(int, Annotated[int, "positive"])
1037+
1038+
def test_subst(self):
1039+
dec = "a decoration"
1040+
dec2 = "another decoration"
1041+
1042+
S = Annotated[T, dec2]
1043+
self.assertEqual(S[int], Annotated[int, dec2])
1044+
1045+
self.assertEqual(S[Annotated[int, dec]], Annotated[int, dec, dec2])
1046+
L = Annotated[List[T], dec]
1047+
1048+
self.assertEqual(L[int], Annotated[List[int], dec])
1049+
with self.assertRaises(TypeError):
1050+
L[int, int]
1051+
1052+
self.assertEqual(S[L[int]], Annotated[List[int], dec, dec2])
1053+
1054+
D = Annotated[Dict[KT, VT], dec]
1055+
self.assertEqual(D[str, int], Annotated[Dict[str, int], dec])
1056+
with self.assertRaises(TypeError):
1057+
D[int]
1058+
1059+
I = Annotated[int, dec]
1060+
with self.assertRaises(TypeError):
1061+
I[None]
1062+
1063+
LI = L[int]
1064+
with self.assertRaises(TypeError):
1065+
LI[None]
1066+
1067+
def test_annotated_in_other_types(self):
1068+
X = List[Annotated[T, 5]]
1069+
self.assertEqual(X[int], List[Annotated[int, 5]])
1070+
9291071
class AllTests(BaseTestCase):
9301072

9311073
def test_typing_extensions_includes_standard(self):

src_py2/typing_extensions.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,3 +605,134 @@ def __new__(cls, name, bases, ns, total=True):
605605
606606
Point2D = TypedDict('Point2D', x=int, y=int, label=str)
607607
"""
608+
609+
def _is_dunder(name):
610+
"""Returns True if name is a __dunder_variable_name__."""
611+
return len(name) > 4 and name.startswith('__') and name.endswith('__')
612+
613+
class AnnotatedMeta(GenericMeta):
614+
"""Metaclass for Annotated"""
615+
616+
def __new__(cls, name, bases, namespace, **kwargs):
617+
if any(b is not object for b in bases):
618+
raise TypeError("Cannot subclass %s" % Annotated)
619+
return super(AnnotatedMeta, cls).__new__(cls, name, bases, namespace, **kwargs)
620+
621+
@property
622+
def __metadata__(self):
623+
return self._subs_tree()[2]
624+
625+
def _tree_repr(self, tree):
626+
assert len(tree) == 3
627+
cls, origin, metadata = tree
628+
if not isinstance(origin, tuple):
629+
tp_repr = typing._type_repr(origin)
630+
else:
631+
tp_repr = origin[0]._tree_repr(origin)
632+
metadata_reprs = ", ".join(repr(arg) for arg in metadata)
633+
return '%s[%s, %s]' % (cls, tp_repr, metadata_reprs)
634+
635+
def _subs_tree(self, tvars=None, args=None):
636+
if self is Annotated:
637+
return Annotated
638+
res = super(AnnotatedMeta, self)._subs_tree(tvars=tvars, args=args)
639+
# Flatten nested Annotated
640+
if isinstance(res[1], tuple) and res[1][0] is Annotated:
641+
sub_tp = res[1][1]
642+
sub_annot = res[1][2]
643+
return (Annotated, sub_tp, sub_annot + res[2])
644+
return res
645+
646+
def _get_cons(self):
647+
"""Return the class used to create instance of this type."""
648+
if self.__origin__ is None:
649+
raise TypeError("Cannot get the underlying type of a non-specialized "
650+
"Annotated type.")
651+
tree = self._subs_tree()
652+
while isinstance(tree, tuple) and tree[0] is Annotated:
653+
tree = tree[1]
654+
if isinstance(tree, tuple):
655+
return tree[0]
656+
else:
657+
return tree
658+
659+
@_tp_cache
660+
def __getitem__(self, params):
661+
if not isinstance(params, tuple):
662+
params = (params,)
663+
if self.__origin__ is not None: # specializing an instantiated type
664+
return super(AnnotatedMeta, self).__getitem__(params)
665+
elif not isinstance(params, tuple) or len(params) < 2:
666+
raise TypeError("Annotated[...] should be instantiated with at "
667+
"least two arguments (a type and an annotation).")
668+
else:
669+
msg = "Annotated[t, ...]: t must be a type."
670+
tp = typing._type_check(params[0], msg)
671+
metadata = tuple(params[1:])
672+
return self.__class__(
673+
self.__name__,
674+
self.__bases__,
675+
dict(self.__dict__),
676+
tvars=_type_vars((tp,)),
677+
# Metadata is a tuple so it won't be touched by _replace_args et al.
678+
args=(tp, metadata),
679+
origin=self,
680+
)
681+
682+
def __call__(self, *args, **kwargs):
683+
cons = self._get_cons()
684+
result = cons(*args, **kwargs)
685+
try:
686+
result.__orig_class__ = self
687+
except AttributeError:
688+
pass
689+
return result
690+
691+
def __getattr__(self, attr):
692+
# For simplicity we just don't relay all dunder names
693+
if self.__origin__ is not None and not _is_dunder(attr):
694+
return getattr(self._get_cons(), attr)
695+
raise AttributeError(attr)
696+
697+
def __setattr__(self, attr, value):
698+
if _is_dunder(attr) or attr.startswith('_abc_'):
699+
super(AnnotatedMeta, self).__setattr__(attr, value)
700+
elif self.__origin__ is None:
701+
raise AttributeError(attr)
702+
else:
703+
setattr(self._get_cons(), attr, value)
704+
705+
706+
class Annotated(object):
707+
"""Add context specific metadata to a type.
708+
709+
Example: Annotated[int, runtime_check.Unsigned] indicates to the
710+
hypothetical runtime_check module that this type is an unsigned int.
711+
Every other consumer of this type can ignore this metadata and treat
712+
this type as int.
713+
714+
The first argument to Annotated must be a valid type, the remaining
715+
arguments are kept as a tuple in the __metadata__ field.
716+
717+
Details:
718+
719+
- It's an error to call `Annotated` with less than two arguments.
720+
- Nested Annotated are flattened::
721+
722+
Annotated[Annotated[int, Ann1, Ann2], Ann3] == Annotated[int, Ann1, Ann2, Ann3]
723+
724+
- Instantiating an annotated type is equivalent to instantiating the
725+
underlying type::
726+
727+
Annotated[C, Ann1](5) == C(5)
728+
729+
- Annotated can be used as a generic type alias::
730+
731+
Optimized = Annotated[T, runtime.Optimize()]
732+
Optimized[int] == Annotated[int, runtime.Optimize()]
733+
734+
OptimizedList = Annotated[List[T], runtime.Optimize()]
735+
OptimizedList[int] == Annotated[List[int], runtime.Optimize()]
736+
"""
737+
__metaclass__ = AnnotatedMeta
738+
__slots__ = ()

0 commit comments

Comments
 (0)