Skip to content

Commit 0cdd287

Browse files
committed
[ty] support PEP 613 typing.TypeAlias
1 parent bfb0902 commit 0cdd287

File tree

20 files changed

+497
-96
lines changed

20 files changed

+497
-96
lines changed

crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Alias: TypeAlias = int
1616

1717
def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
1818
reveal_type(args) # revealed: tuple[@Todo(`Unpack[]` special form), ...]
19-
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
19+
reveal_type(Alias) # revealed: Alias
2020
return args
2121

2222
def g() -> TypeGuard[int]: ...

crates/ty_python_semantic/resources/mdtest/attributes.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2107,9 +2107,9 @@ reveal_type(False.real) # revealed: Literal[0]
21072107
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
21082108

21092109
```py
2110-
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[@Todo(Support for `typing.TypeAlias`)], /) -> bytes
2110+
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[Buffer], /) -> bytes
21112111
reveal_type(b"foo".join)
2112-
# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`) | tuple[@Todo(Support for `typing.TypeAlias`), ...], start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool
2112+
# revealed: bound method Literal[b"foo"].endswith(suffix: Buffer | tuple[Buffer, ...], start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool
21132113
reveal_type(b"foo".endswith)
21142114
```
21152115

crates/ty_python_semantic/resources/mdtest/binary/instances.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,7 @@ reveal_type(A() + "foo") # revealed: A
313313
reveal_type("foo" + A()) # revealed: A
314314

315315
reveal_type(A() + b"foo") # revealed: A
316-
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
317-
reveal_type(b"foo" + A()) # revealed: bytes
316+
reveal_type(b"foo" + A()) # revealed: A
318317

319318
reveal_type(A() + ()) # revealed: A
320319
reveal_type(() + A()) # revealed: A

crates/ty_python_semantic/resources/mdtest/binary/integers.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,8 @@ reveal_type(2**largest_u32) # revealed: int
5454

5555
def variable(x: int):
5656
reveal_type(x**2) # revealed: int
57-
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
58-
reveal_type(2**x) # revealed: int
59-
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
60-
reveal_type(x**x) # revealed: int
57+
reveal_type(2**x) # revealed: Any
58+
reveal_type(x**x) # revealed: Any
6159
```
6260

6361
If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but

crates/ty_python_semantic/resources/mdtest/expression/lambda.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ x = lambda y: y
127127
reveal_type(x.__code__) # revealed: CodeType
128128
reveal_type(x.__name__) # revealed: str
129129
reveal_type(x.__defaults__) # revealed: tuple[Any, ...] | None
130-
reveal_type(x.__annotations__) # revealed: dict[str, @Todo(Support for `typing.TypeAlias`)]
130+
reveal_type(x.__annotations__) # revealed: dict[str, AnnotationForm]
131131
reveal_type(x.__dict__) # revealed: dict[str, Any]
132132
reveal_type(x.__doc__) # revealed: str | None
133133
reveal_type(x.__kwdefaults__) # revealed: dict[str, Any] | None

crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,11 @@ def _(flag: bool):
146146
def _(flag: bool):
147147
x = 1 if flag else "a"
148148

149-
# TODO: this should cause us to emit a diagnostic during
150-
# type checking
149+
# error: [invalid-argument-type]
151150
if isinstance(x, "a"):
152151
reveal_type(x) # revealed: Literal[1, "a"]
153152

154-
# TODO: this should cause us to emit a diagnostic during
155-
# type checking
153+
# error: [invalid-argument-type]
156154
if isinstance(x, "int"):
157155
reveal_type(x) # revealed: Literal[1, "a"]
158156
```

crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -214,20 +214,13 @@ def flag() -> bool:
214214

215215
t = int if flag() else str
216216

217-
# TODO: this should cause us to emit a diagnostic during
218-
# type checking
217+
# error: [invalid-argument-type]
219218
if issubclass(t, "str"):
220219
reveal_type(t) # revealed: <class 'int'> | <class 'str'>
221220

222-
# TODO: this should cause us to emit a diagnostic during
223-
# type checking
221+
# TODO error: [invalid-argument-type]
224222
if issubclass(t, (bytes, "str")):
225223
reveal_type(t) # revealed: <class 'int'> | <class 'str'>
226-
227-
# TODO: this should cause us to emit a diagnostic during
228-
# type checking
229-
if issubclass(t, Any):
230-
reveal_type(t) # revealed: <class 'int'> | <class 'str'>
231224
```
232225

233226
### Do not narrow if there are keyword arguments
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
# PEP 613 explicit type aliases
2+
3+
```toml
4+
[environment]
5+
python-version = "3.10"
6+
```
7+
8+
Explicit type aliases were introduced in PEP 613. They are defined using an annotated-assignment
9+
statement, annotated with `typing.TypeAlias`:
10+
11+
## Basic
12+
13+
```py
14+
from typing import TypeAlias
15+
16+
MyInt: TypeAlias = int
17+
18+
def f(x: MyInt):
19+
reveal_type(x) # revealed: int
20+
21+
f(1)
22+
```
23+
24+
## Union
25+
26+
For more complex type aliases, such as those involving unions or generics, the inferred value type
27+
of the right-hand side is not a valid type for use in a type expression, and we need to infer it as
28+
a type expression.
29+
30+
### Old syntax
31+
32+
```py
33+
from typing import TypeAlias, Union
34+
35+
IntOrStr: TypeAlias = Union[int, str]
36+
37+
def f(x: IntOrStr):
38+
reveal_type(x) # revealed: int | str
39+
if isinstance(x, int):
40+
reveal_type(x) # revealed: int
41+
else:
42+
reveal_type(x) # revealed: str
43+
44+
f(1)
45+
f("foo")
46+
```
47+
48+
### New syntax
49+
50+
```py
51+
from typing import TypeAlias
52+
53+
IntOrStr: TypeAlias = int | str
54+
55+
def f(x: IntOrStr):
56+
reveal_type(x) # revealed: int | str
57+
if isinstance(x, int):
58+
reveal_type(x) # revealed: int
59+
else:
60+
reveal_type(x) # revealed: str
61+
62+
f(1)
63+
f("foo")
64+
```
65+
66+
### Name resolution is not deferred
67+
68+
Unlike with a PEP 695 type alias, the right-hand side of a PEP 613 type alias is evaluated
69+
immediately, name resolution is not deferred.
70+
71+
```py
72+
from typing import TypeAlias
73+
74+
A: TypeAlias = B | None # error: [unresolved-reference]
75+
B: TypeAlias = int
76+
77+
def _(a: A):
78+
reveal_type(a) # revealed: Unknown | None
79+
```
80+
81+
## Multiple layers of union aliases
82+
83+
```py
84+
from typing import TypeAlias
85+
86+
class A: ...
87+
class B: ...
88+
class C: ...
89+
class D: ...
90+
91+
W: TypeAlias = A | B
92+
X: TypeAlias = C | D
93+
Y: TypeAlias = W | X
94+
95+
from ty_extensions import is_equivalent_to, static_assert
96+
97+
static_assert(is_equivalent_to(Y, A | B | C | D))
98+
```
99+
100+
## Cycles
101+
102+
We also support cyclic type aliases:
103+
104+
### Old syntax
105+
106+
```py
107+
from typing import Union, TypeAlias
108+
109+
MiniJSON: TypeAlias = Union[int, str, list["MiniJSON"]]
110+
111+
def f(x: MiniJSON):
112+
reveal_type(x) # revealed: int | str | list[MiniJSON]
113+
if isinstance(x, int):
114+
reveal_type(x) # revealed: int
115+
elif isinstance(x, str):
116+
reveal_type(x) # revealed: str
117+
else:
118+
reveal_type(x) # revealed: list[MiniJSON]
119+
120+
f(1)
121+
f("foo")
122+
f([1, "foo"])
123+
```
124+
125+
### New syntax
126+
127+
```py
128+
from typing import TypeAlias
129+
130+
MiniJSON: TypeAlias = int | str | list["MiniJSON"]
131+
132+
def f(x: MiniJSON):
133+
reveal_type(x) # revealed: int | str | list[MiniJSON]
134+
if isinstance(x, int):
135+
reveal_type(x) # revealed: int
136+
elif isinstance(x, str):
137+
reveal_type(x) # revealed: str
138+
else:
139+
reveal_type(x) # revealed: list[MiniJSON]
140+
141+
f(1)
142+
f("foo")
143+
f([1, "foo"])
144+
```
145+
146+
### Generic
147+
148+
```py
149+
from typing import TypeAlias, Generic, TypeVar, Union
150+
151+
T = TypeVar("T")
152+
153+
Alias: TypeAlias = Union[list["Alias"], int]
154+
155+
class A(Generic[T]):
156+
pass
157+
158+
class B(A[Alias]):
159+
pass
160+
```
161+
162+
### Mutually recursive
163+
164+
```py
165+
from typing import TypeAlias
166+
167+
A: TypeAlias = tuple["B"] | None
168+
B: TypeAlias = tuple[A] | None
169+
170+
def f(x: A):
171+
if x is not None:
172+
reveal_type(x) # revealed: tuple[B]
173+
y = x[0]
174+
if y is not None:
175+
reveal_type(y) # revealed: tuple[A]
176+
177+
def g(x: A | B):
178+
reveal_type(x) # revealed: tuple[B] | None
179+
180+
from ty_extensions import Intersection
181+
182+
def h(x: Intersection[A, B]):
183+
reveal_type(x) # revealed: tuple[B] | None
184+
```
185+
186+
### Self-recursive callable type
187+
188+
```py
189+
from typing import Callable, TypeAlias
190+
191+
C: TypeAlias = Callable[[], "C" | None]
192+
193+
def _(x: C):
194+
reveal_type(x) # revealed: () -> C | None
195+
```
196+
197+
### Union inside generic
198+
199+
#### With old-style union
200+
201+
```py
202+
from typing import Union, TypeAlias
203+
204+
A: TypeAlias = list[Union["A", str]]
205+
206+
def f(x: A):
207+
reveal_type(x) # revealed: list[A | str]
208+
for item in x:
209+
reveal_type(item) # revealed: list[A | str] | str
210+
```
211+
212+
#### With new-style union
213+
214+
```py
215+
from typing import TypeAlias
216+
217+
A: TypeAlias = list["A" | str]
218+
219+
def f(x: A):
220+
reveal_type(x) # revealed: list[A | str]
221+
for item in x:
222+
reveal_type(item) # revealed: list[A | str] | str
223+
```
224+
225+
#### With Optional
226+
227+
```py
228+
from typing import Optional, Union, TypeAlias
229+
230+
A: TypeAlias = list[Optional[Union["A", str]]]
231+
232+
def f(x: A):
233+
reveal_type(x) # revealed: list[A | str | None]
234+
for item in x:
235+
reveal_type(item) # revealed: list[A | str | None] | str | None
236+
```
237+
238+
### Invalid examples
239+
240+
#### No value
241+
242+
```py
243+
from typing import TypeAlias
244+
245+
# TODO: error
246+
Bad: TypeAlias
247+
248+
# Nested function so we don't emit unresolved-reference for `Bad`:
249+
def _():
250+
def f(x: Bad):
251+
reveal_type(x) # revealed: Unknown
252+
```
253+
254+
#### No value, in stub
255+
256+
`stub.pyi`:
257+
258+
```pyi
259+
from typing import TypeAlias
260+
261+
# TODO: error
262+
Bad: TypeAlias
263+
```
264+
265+
`main.py`:
266+
267+
```py
268+
from stub import Bad
269+
270+
def f(x: Bad):
271+
reveal_type(x) # revealed: Unknown
272+
```

crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def f() -> None:
2828
```py
2929
type IntOrStr = int | str
3030

31-
reveal_type(IntOrStr.__value__) # revealed: @Todo(Support for `typing.TypeAlias`)
31+
reveal_type(IntOrStr.__value__) # revealed: Any
3232
```
3333

3434
## Invalid assignment

crates/ty_python_semantic/resources/mdtest/protocols.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,9 @@ def f(
267267
Nonetheless, `Protocol` can still be used as the second argument to `issubclass()` at runtime:
268268

269269
```py
270+
# TODO should not error
270271
# Could also be `Literal[True]`, but `bool` is fine:
272+
# error: [invalid-argument-type]
271273
reveal_type(issubclass(MyProtocol, Protocol)) # revealed: bool
272274
```
273275

0 commit comments

Comments
 (0)