Skip to content

Commit 241de48

Browse files
committed
[ty] support imported PEP 613 type aliases
1 parent 192c37d commit 241de48

File tree

22 files changed

+408
-263
lines changed

22 files changed

+408
-263
lines changed

crates/ruff_benchmark/benches/ty_walltime.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ static FREQTRADE: Benchmark = Benchmark::new(
143143
max_dep_date: "2025-06-17",
144144
python_version: PythonVersion::PY312,
145145
},
146-
525,
146+
600,
147147
);
148148

149149
static PANDAS: Benchmark = Benchmark::new(
@@ -163,7 +163,7 @@ static PANDAS: Benchmark = Benchmark::new(
163163
max_dep_date: "2025-06-17",
164164
python_version: PythonVersion::PY312,
165165
},
166-
3000,
166+
4000,
167167
);
168168

169169
static PYDANTIC: Benchmark = Benchmark::new(
@@ -181,7 +181,7 @@ static PYDANTIC: Benchmark = Benchmark::new(
181181
max_dep_date: "2025-06-17",
182182
python_version: PythonVersion::PY39,
183183
},
184-
5000,
184+
7000,
185185
);
186186

187187
static SYMPY: Benchmark = Benchmark::new(

crates/ty_ide/src/completion.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2136,10 +2136,10 @@ C.<CURSOR>
21362136
);
21372137

21382138
assert_snapshot!(
2139-
builder.skip_keywords().skip_builtins().type_signatures().build().snapshot(), @r"
2139+
builder.skip_keywords().skip_builtins().type_signatures().build().snapshot(), @r###"
21402140
meta_attr :: int
21412141
mro :: bound method <class 'C'>.mro() -> list[type]
2142-
__annotate__ :: @Todo | None
2142+
__annotate__ :: (() -> dict[str, Any]) | None
21432143
__annotations__ :: dict[str, Any]
21442144
__base__ :: type | None
21452145
__bases__ :: tuple[type, ...]
@@ -2182,7 +2182,7 @@ C.<CURSOR>
21822182
__text_signature__ :: str | None
21832183
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
21842184
__weakrefoffset__ :: int
2185-
");
2185+
"###);
21862186
}
21872187

21882188
#[test]
@@ -2331,14 +2331,14 @@ Quux.<CURSOR>
23312331
);
23322332

23332333
assert_snapshot!(
2334-
builder.skip_keywords().skip_builtins().type_signatures().build().snapshot(), @r"
2334+
builder.skip_keywords().skip_builtins().type_signatures().build().snapshot(), @r###"
23352335
mro :: bound method <class 'Quux'>.mro() -> list[type]
23362336
some_attribute :: int
23372337
some_class_method :: bound method <class 'Quux'>.some_class_method() -> int
23382338
some_method :: def some_method(self) -> int
23392339
some_property :: property
23402340
some_static_method :: def some_static_method(self) -> int
2341-
__annotate__ :: @Todo | None
2341+
__annotate__ :: (() -> dict[str, Any]) | None
23422342
__annotations__ :: dict[str, Any]
23432343
__base__ :: type | None
23442344
__bases__ :: tuple[type, ...]
@@ -2381,7 +2381,7 @@ Quux.<CURSOR>
23812381
__text_signature__ :: str | None
23822382
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
23832383
__weakrefoffset__ :: int
2384-
");
2384+
"###);
23852385
}
23862386

23872387
#[test]

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,8 @@ P = ParamSpec("P")
1212
Ts = TypeVarTuple("Ts")
1313
R_co = TypeVar("R_co", covariant=True)
1414

15-
Alias: TypeAlias = int
16-
1715
def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
1816
reveal_type(args) # revealed: tuple[@Todo(`Unpack[]` special form), ...]
19-
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
2017
return args
2118

2219
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
@@ -2208,9 +2208,9 @@ reveal_type(False.real) # revealed: Literal[0]
22082208
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
22092209

22102210
```py
2211-
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[@Todo(Support for `typing.TypeAlias`)], /) -> bytes
2211+
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[Buffer], /) -> bytes
22122212
reveal_type(b"foo".join)
2213-
# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`) | tuple[@Todo(Support for `typing.TypeAlias`), ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> bool
2213+
# revealed: bound method Literal[b"foo"].endswith(suffix: Buffer | tuple[Buffer, ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> bool
22142214
reveal_type(b"foo".endswith)
22152215
```
22162216

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/call/methods.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -598,9 +598,9 @@ from typing_extensions import Self
598598

599599
reveal_type(object.__new__) # revealed: def __new__(cls) -> Self@__new__
600600
reveal_type(object().__new__) # revealed: def __new__(cls) -> Self@__new__
601-
# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
601+
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
602602
reveal_type(int.__new__)
603-
# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
603+
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
604604
reveal_type((42).__new__)
605605

606606
class X:

crates/ty_python_semantic/resources/mdtest/call/open.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import pickle
1010

1111
reveal_type(open("")) # revealed: TextIOWrapper[_WrappedBuffer]
1212
reveal_type(open("", "r")) # revealed: TextIOWrapper[_WrappedBuffer]
13-
reveal_type(open("", "rb")) # revealed: @Todo(`builtins.open` return type)
13+
reveal_type(open("", "rb")) # revealed: BufferedReader[_BufferedReaderStream]
1414

1515
with open("foo.pickle", "rb") as f:
1616
x = pickle.load(f) # fine
1717

1818
def _(mode: str):
19-
reveal_type(open("", mode)) # revealed: @Todo(`builtins.open` return type)
19+
reveal_type(open("", mode)) # revealed: IO[Any]
2020
```
2121

2222
## `os.fdopen`
@@ -29,7 +29,7 @@ import os
2929

3030
reveal_type(os.fdopen(0)) # revealed: TextIOWrapper[_WrappedBuffer]
3131
reveal_type(os.fdopen(0, "r")) # revealed: TextIOWrapper[_WrappedBuffer]
32-
reveal_type(os.fdopen(0, "rb")) # revealed: @Todo(`os.fdopen` return type)
32+
reveal_type(os.fdopen(0, "rb")) # revealed: BufferedReader[_BufferedReaderStream]
3333

3434
with os.fdopen(0, "rb") as f:
3535
x = pickle.load(f) # fine
@@ -43,9 +43,9 @@ And similarly for `Path.open()`:
4343
from pathlib import Path
4444
import pickle
4545

46-
reveal_type(Path("").open()) # revealed: @Todo(`Path.open` return type)
47-
reveal_type(Path("").open("r")) # revealed: @Todo(`Path.open` return type)
48-
reveal_type(Path("").open("rb")) # revealed: @Todo(`Path.open` return type)
46+
reveal_type(Path("").open()) # revealed: TextIOWrapper[_WrappedBuffer]
47+
reveal_type(Path("").open("r")) # revealed: TextIOWrapper[_WrappedBuffer]
48+
reveal_type(Path("").open("rb")) # revealed: BufferedReader[_BufferedReaderStream]
4949

5050
with Path("foo.pickle").open("rb") as f:
5151
x = pickle.load(f) # fine
@@ -61,7 +61,7 @@ import pickle
6161

6262
reveal_type(NamedTemporaryFile()) # revealed: _TemporaryFileWrapper[bytes]
6363
reveal_type(NamedTemporaryFile("r")) # revealed: _TemporaryFileWrapper[str]
64-
reveal_type(NamedTemporaryFile("rb")) # revealed: @Todo(`tempfile.NamedTemporaryFile` return type)
64+
reveal_type(NamedTemporaryFile("rb")) # revealed: _TemporaryFileWrapper[bytes]
6565

6666
with NamedTemporaryFile("rb") as f:
6767
x = pickle.load(f) # fine

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, Any]
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/pep613_type_aliases.md

Lines changed: 164 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,124 @@
11
# PEP 613 type aliases
22

3-
## No panics
3+
PEP 613 type aliases are simple assignment statements, annotated with `typing.TypeAlias` to mark
4+
them as a type alias. At runtime, they behave the same as implicit type aliases. Our support for
5+
them is currently the same as for implicit type aliases, but we don't reproduce the full
6+
implicit-type-alias test suite here, just some particularly interesting cases.
47

5-
We do not fully support PEP 613 type aliases yet. For now, just make sure that we don't panic:
8+
## Basic
9+
10+
### as `TypeAlias`
11+
12+
```py
13+
from typing import TypeAlias
14+
15+
IntOrStr: TypeAlias = int | str
16+
17+
def _(x: IntOrStr):
18+
reveal_type(x) # revealed: int | str
19+
```
20+
21+
### as `typing.TypeAlias`
22+
23+
```py
24+
import typing
25+
26+
IntOrStr: typing.TypeAlias = int | str
27+
28+
def _(x: IntOrStr):
29+
reveal_type(x) # revealed: int | str
30+
```
31+
32+
## Can be used as value
33+
34+
Because PEP 613 type aliases are just annotated assignments, they can be used as values, like a
35+
legacy type expression (and unlike a PEP 695 type alias). We might prefer this wasn't allowed, but
36+
people do use it.
37+
38+
```py
39+
from typing import TypeAlias
40+
41+
MyExc: TypeAlias = Exception
42+
43+
try:
44+
raise MyExc("error")
45+
except MyExc as e:
46+
reveal_type(e) # revealed: Exception
47+
```
48+
49+
## Unknown type in PEP 604 union
50+
51+
If we run into an unexpected type in a PEP 604 union in the RHS of a PEP 613 type alias, we still
52+
understand it as a union type, just with an unknown element.
53+
54+
```py
55+
from typing import TypeAlias
56+
from nonexistent import unknown_type # error: [unresolved-import]
57+
58+
MyAlias: TypeAlias = int | unknown_type | str
59+
60+
def _(x: MyAlias):
61+
reveal_type(x) # revealed: int | Unknown | str
62+
```
63+
64+
## Callable type in union
65+
66+
```py
67+
from typing import TypeAlias, Callable
68+
69+
MyAlias: TypeAlias = int | Callable[[str], int]
70+
71+
def _(x: MyAlias):
72+
# TODO: int | (str) -> int
73+
reveal_type(x) # revealed: int | ((str, /) -> int)
74+
```
75+
76+
## Subscripted generic alias in union
77+
78+
```py
79+
from typing import TypeAlias, TypeVar
80+
81+
T = TypeVar("T")
82+
83+
Alias1: TypeAlias = list[T] | set[T]
84+
MyAlias: TypeAlias = int | Alias1[str]
85+
86+
def _(x: MyAlias):
87+
# TODO: int | list[str] | set[str]
88+
reveal_type(x) # revealed: int | @Todo(Specialization of union type alias)
89+
```
90+
91+
## Imported
92+
93+
`alias.py`:
94+
95+
```py
96+
from typing import TypeAlias
97+
98+
MyAlias: TypeAlias = int | str
99+
```
100+
101+
`main.py`:
102+
103+
```py
104+
from alias import MyAlias
105+
106+
def _(x: MyAlias):
107+
reveal_type(x) # revealed: int | str
108+
```
109+
110+
## String literal in RHS
111+
112+
```py
113+
from typing import TypeAlias
114+
115+
IntOrStr: TypeAlias = "int | str"
116+
117+
def _(x: IntOrStr):
118+
reveal_type(x) # revealed: int | str
119+
```
120+
121+
## Cyclic
6122

7123
```py
8124
from typing import TypeAlias
@@ -18,6 +134,26 @@ def _(rec: RecursiveHomogeneousTuple):
18134
reveal_type(rec) # revealed: tuple[Divergent, ...]
19135
```
20136

137+
## Conditionally imported on Python < 3.10
138+
139+
```toml
140+
[environment]
141+
python-version = "3.9"
142+
```
143+
144+
```py
145+
try:
146+
# error: [unresolved-import]
147+
from typing import TypeAlias
148+
except ImportError:
149+
from typing_extensions import TypeAlias
150+
151+
MyAlias: TypeAlias = int
152+
153+
def _(x: MyAlias):
154+
reveal_type(x) # revealed: int
155+
```
156+
21157
## PEP-613 aliases in stubs are deferred
22158

23159
Although the right-hand side of a PEP-613 alias is a value expression, inference of this value is
@@ -46,7 +182,31 @@ f(stub.B())
46182

47183
class Unrelated: ...
48184

49-
# TODO: we should emit `[invalid-argument-type]` here
50-
# (the alias is a `@Todo` because it's imported from another file)
185+
# error: [invalid-argument-type]
51186
f(Unrelated())
52187
```
188+
189+
## Invalid position
190+
191+
`typing.TypeAlias` must be used as the sole annotation in an annotated assignment. Use in any other
192+
context is an error.
193+
194+
```py
195+
from typing import TypeAlias
196+
197+
# error: [invalid-type-form]
198+
def _(x: TypeAlias):
199+
reveal_type(x) # revealed: Unknown
200+
201+
# error: [invalid-type-form]
202+
y: list[TypeAlias] = []
203+
```
204+
205+
## RHS is required
206+
207+
```py
208+
from typing import TypeAlias
209+
210+
# error: [invalid-type-form]
211+
Empty: TypeAlias
212+
```

0 commit comments

Comments
 (0)