bpo-39491: Merge PEP 593 (typing.Annotated) support (#18260)

* bpo-39491: Merge PEP 593 (typing.Annotated) support

PEP 593 has been accepted some time ago. I got a green light for merging
this from Till, so I went ahead and combined the code contributed to
typing_extensions[1] and the documentation from the PEP 593 text[2].

My changes were limited to:

* removing code designed for typing_extensions to run on older Python
  versions
* removing some irrelevant parts of the PEP text when copying it over as
  documentation and otherwise changing few small bits to better serve
  the purpose
* changing the get_type_hints signature to match reality (parameter
  names)

I wasn't entirely sure how to go about crediting the authors but I used
my best judgment, let me know if something needs changing in this
regard.

[1] 8280de241f/typing_extensions/src_py3/typing_extensions.py
[2] 17710b8798/pep-0593.rst
This commit is contained in:
Jakub Stasiak 2020-02-05 02:10:19 +01:00 committed by GitHub
parent 89ae20b30e
commit cf5b109dbb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 467 additions and 6 deletions

View file

@ -22,6 +22,7 @@ from typing import NewType
from typing import NamedTuple, TypedDict
from typing import IO, TextIO, BinaryIO
from typing import Pattern, Match
from typing import Annotated
import abc
import typing
import weakref
@ -2891,6 +2892,64 @@ class GetTypeHintTests(BaseTestCase):
self.assertEqual(gth(ForRefExample.func), expects)
self.assertEqual(gth(ForRefExample.nested), expects)
def test_get_type_hints_annotated(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_annotated_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]}
)
class GetUtilitiesTestCase(TestCase):
def test_get_origin(self):
@ -2906,6 +2965,7 @@ class GetUtilitiesTestCase(TestCase):
self.assertIs(get_origin(Generic), Generic)
self.assertIs(get_origin(Generic[T]), Generic)
self.assertIs(get_origin(List[Tuple[T, T]][int]), list)
self.assertIs(get_origin(Annotated[T, 'thing']), Annotated)
def test_get_args(self):
T = TypeVar('T')
@ -2926,6 +2986,7 @@ class GetUtilitiesTestCase(TestCase):
(int, Callable[[Tuple[T, ...]], str]))
self.assertEqual(get_args(Tuple[int, ...]), (int, ...))
self.assertEqual(get_args(Tuple[()]), ((),))
self.assertEqual(get_args(Annotated[T, 'one', 2, ['three']]), (T, 'one', 2, ['three']))
class CollectionsAbcTests(BaseTestCase):
@ -3844,6 +3905,179 @@ class RETests(BaseTestCase):
"type 're.Match' is not an acceptable base type")
class AnnotatedTests(BaseTestCase):
def test_repr(self):
self.assertEqual(
repr(Annotated[int, 4, 5]),
"typing.Annotated[int, 4, 5]"
)
self.assertEqual(
repr(Annotated[List[int], 4, 5]),
"typing.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))
self.assertEqual(A.__origin__, int)
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",))
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.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_cannot_instantiate_type_var(self):
A = Annotated[T, (5, 6)]
with self.assertRaises(TypeError):
A(5)
def test_cannot_getattr_typevar(self):
with self.assertRaises(AttributeError):
Annotated[T, (5, 7)].x
def test_attr_passthrough(self):
class C:
classvar = 4
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)
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_check_instance(self):
with self.assertRaises(TypeError):
isinstance(5, Annotated[int, "positive"])
def test_cannot_check_subclass(self):
with self.assertRaises(TypeError):
issubclass(int, Annotated[int, "positive"])
def test_pickle(self):
samples = [typing.Any, typing.Union[int, str],
typing.Optional[str], Tuple[int, ...],
typing.Callable[[str], bytes]]
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)
global _Annotated_test_G
class _Annotated_test_G(Generic[T]):
x = 1
G = Annotated[_Annotated_test_G[int], "A decoration"]
G.foo = 42
G.bar = 'abc'
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)
def test_subst(self):
dec = "a decoration"
dec2 = "another decoration"
S = Annotated[T, dec2]
self.assertEqual(S[int], Annotated[int, dec2])
self.assertEqual(S[Annotated[int, dec]], Annotated[int, dec, dec2])
L = Annotated[List[T], dec]
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[typing.Dict[KT, VT], dec]
self.assertEqual(D[str, int], Annotated[typing.Dict[str, int], dec])
with self.assertRaises(TypeError):
D[int]
It = Annotated[int, dec]
with self.assertRaises(TypeError):
It[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]])
class AllTests(BaseTestCase):
"""Tests for __all__."""