Skip to content

Commit aa5d665

Browse files
authored
[ty] Add support for generic PEP695 type aliases (#20219)
## Summary Adds support for generic PEP695 type aliases, e.g., ```python type A[T] = T reveal_type(A[int]) # A[int] ``` Resolves astral-sh/ty#677.
1 parent d55edb3 commit aa5d665

File tree

8 files changed

+510
-70
lines changed

8 files changed

+510
-70
lines changed

crates/ty_ide/src/hover.rs

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -312,14 +312,14 @@ mod tests {
312312
This is such a great class!!
313313
314314
Don't you know?
315-
315+
316316
Everyone loves my class!!
317317
318318
'''
319319
def __init__(self, val):
320320
"""initializes MyClass (perfectly)"""
321321
self.val = val
322-
322+
323323
def my_method(self, a, b):
324324
'''This is such a great func!!
325325
@@ -379,14 +379,14 @@ mod tests {
379379
This is such a great class!!
380380
381381
Don't you know?
382-
382+
383383
Everyone loves my class!!
384384
385385
'''
386386
def __init__(self, val):
387387
"""initializes MyClass (perfectly)"""
388388
self.val = val
389-
389+
390390
def my_method(self, a, b):
391391
'''This is such a great func!!
392392
@@ -444,14 +444,14 @@ mod tests {
444444
This is such a great class!!
445445
446446
Don't you know?
447-
447+
448448
Everyone loves my class!!
449449
450450
'''
451451
def __init__(self, val):
452452
"""initializes MyClass (perfectly)"""
453453
self.val = val
454-
454+
455455
def my_method(self, a, b):
456456
'''This is such a great func!!
457457
@@ -505,7 +505,7 @@ mod tests {
505505
This is such a great class!!
506506
507507
Don't you know?
508-
508+
509509
Everyone loves my class!!
510510
511511
'''
@@ -562,13 +562,13 @@ mod tests {
562562
This is such a great class!!
563563
564564
Don't you know?
565-
565+
566566
Everyone loves my class!!
567567
568568
'''
569569
def __init__(self, val):
570570
self.val = val
571-
571+
572572
def my_method(self, a, b):
573573
'''This is such a great func!!
574574
@@ -628,14 +628,14 @@ mod tests {
628628
This is such a great class!!
629629
630630
Don't you know?
631-
631+
632632
Everyone loves my class!!
633633
634634
'''
635635
def __init__(self, val):
636636
"""initializes MyClass (perfectly)"""
637637
self.val = val
638-
638+
639639
def my_method(self, a, b):
640640
'''This is such a great func!!
641641
@@ -1589,12 +1589,11 @@ def ab(a: int, *, c: int):
15891589
"#,
15901590
);
15911591

1592-
// TODO: This should render T@Alias once we create GenericContexts for type alias scopes.
15931592
assert_snapshot!(test.hover(), @r"
1594-
typing.TypeVar
1593+
T@Alias
15951594
---------------------------------------------
15961595
```python
1597-
typing.TypeVar
1596+
T@Alias
15981597
```
15991598
---------------------------------------------
16001599
info[hover]: Hovered content is
@@ -1875,9 +1874,9 @@ def ab(a: int, *, c: int):
18751874
def foo(a: str | None, b):
18761875
'''
18771876
My cool func
1878-
1877+
18791878
Args:
1880-
a: hopefully a string, right?!
1879+
a: hopefully a string, right?!
18811880
'''
18821881
if a is not None:
18831882
print(a<CURSOR>)
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Generic type aliases: PEP 695 syntax
2+
3+
```toml
4+
[environment]
5+
python-version = "3.13"
6+
```
7+
8+
## Defining a generic alias
9+
10+
At its simplest, to define a type alias using PEP 695 syntax, you add a list of `TypeVar`s,
11+
`ParamSpec`s or `TypeVarTuple`s after the alias name.
12+
13+
```py
14+
from ty_extensions import generic_context
15+
16+
type SingleTypevar[T] = ...
17+
type MultipleTypevars[T, S] = ...
18+
type SingleParamSpec[**P] = ...
19+
type TypeVarAndParamSpec[T, **P] = ...
20+
type SingleTypeVarTuple[*Ts] = ...
21+
type TypeVarAndTypeVarTuple[T, *Ts] = ...
22+
23+
# revealed: tuple[T@SingleTypevar]
24+
reveal_type(generic_context(SingleTypevar))
25+
# revealed: tuple[T@MultipleTypevars, S@MultipleTypevars]
26+
reveal_type(generic_context(MultipleTypevars))
27+
28+
# TODO: support `ParamSpec`/`TypeVarTuple` properly
29+
# (these should include the `ParamSpec`s and `TypeVarTuple`s in their generic contexts)
30+
reveal_type(generic_context(SingleParamSpec)) # revealed: tuple[()]
31+
reveal_type(generic_context(TypeVarAndParamSpec)) # revealed: tuple[T@TypeVarAndParamSpec]
32+
reveal_type(generic_context(SingleTypeVarTuple)) # revealed: tuple[()]
33+
reveal_type(generic_context(TypeVarAndTypeVarTuple)) # revealed: tuple[T@TypeVarAndTypeVarTuple]
34+
```
35+
36+
You cannot use the same typevar more than once.
37+
38+
```py
39+
# error: [invalid-syntax] "duplicate type parameter"
40+
type RepeatedTypevar[T, T] = ...
41+
```
42+
43+
## Specializing type aliases explicitly
44+
45+
The type parameter can be specified explicitly:
46+
47+
```py
48+
from typing import Literal
49+
50+
type C[T] = T
51+
52+
def _(a: C[int], b: C[Literal[5]]):
53+
reveal_type(a) # revealed: int
54+
reveal_type(b) # revealed: Literal[5]
55+
```
56+
57+
The specialization must match the generic types:
58+
59+
```py
60+
# error: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2"
61+
reveal_type(C[int, int]) # revealed: Unknown
62+
```
63+
64+
And non-generic types cannot be specialized:
65+
66+
```py
67+
type B = ...
68+
69+
# error: [non-subscriptable] "Cannot subscript non-generic type alias"
70+
reveal_type(B[int]) # revealed: Unknown
71+
72+
# error: [non-subscriptable] "Cannot subscript non-generic type alias"
73+
def _(b: B[int]): ...
74+
```
75+
76+
If the type variable has an upper bound, the specialized type must satisfy that bound:
77+
78+
```py
79+
type Bounded[T: int] = ...
80+
type BoundedByUnion[T: int | str] = ...
81+
82+
class IntSubclass(int): ...
83+
84+
reveal_type(Bounded[int]) # revealed: Bounded[int]
85+
reveal_type(Bounded[IntSubclass]) # revealed: Bounded[IntSubclass]
86+
87+
# TODO: update this diagnostic to talk about type parameters and specializations
88+
# error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `str`"
89+
reveal_type(Bounded[str]) # revealed: Unknown
90+
91+
# TODO: update this diagnostic to talk about type parameters and specializations
92+
# error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `int | str`"
93+
reveal_type(Bounded[int | str]) # revealed: Unknown
94+
95+
reveal_type(BoundedByUnion[int]) # revealed: BoundedByUnion[int]
96+
reveal_type(BoundedByUnion[IntSubclass]) # revealed: BoundedByUnion[IntSubclass]
97+
reveal_type(BoundedByUnion[str]) # revealed: BoundedByUnion[str]
98+
reveal_type(BoundedByUnion[int | str]) # revealed: BoundedByUnion[int | str]
99+
```
100+
101+
If the type variable is constrained, the specialized type must satisfy those constraints:
102+
103+
```py
104+
type Constrained[T: (int, str)] = ...
105+
106+
reveal_type(Constrained[int]) # revealed: Constrained[int]
107+
108+
# TODO: error: [invalid-argument-type]
109+
# TODO: revealed: Constrained[Unknown]
110+
reveal_type(Constrained[IntSubclass]) # revealed: Constrained[IntSubclass]
111+
112+
reveal_type(Constrained[str]) # revealed: Constrained[str]
113+
114+
# TODO: error: [invalid-argument-type]
115+
# TODO: revealed: Unknown
116+
reveal_type(Constrained[int | str]) # revealed: Constrained[int | str]
117+
118+
# TODO: update this diagnostic to talk about type parameters and specializations
119+
# error: [invalid-argument-type] "Argument is incorrect: Expected `int | str`, found `object`"
120+
reveal_type(Constrained[object]) # revealed: Unknown
121+
```
122+
123+
If the type variable has a default, it can be omitted:
124+
125+
```py
126+
type WithDefault[T, U = int] = ...
127+
128+
reveal_type(WithDefault[str, str]) # revealed: WithDefault[str, str]
129+
reveal_type(WithDefault[str]) # revealed: WithDefault[str, int]
130+
```
131+
132+
If the type alias is not specialized explicitly, it is implicitly specialized to `Unknown`:
133+
134+
```py
135+
type G[T] = list[T]
136+
137+
def _(g: G):
138+
reveal_type(g) # revealed: list[Unknown]
139+
```
140+
141+
Unless a type default was provided:
142+
143+
```py
144+
type G[T = int] = list[T]
145+
146+
def _(g: G):
147+
reveal_type(g) # revealed: list[int]
148+
```
149+
150+
## Aliases are not callable
151+
152+
```py
153+
type A = int
154+
type B[T] = T
155+
156+
# error: [call-non-callable] "Object of type `TypeAliasType` is not callable"
157+
reveal_type(A()) # revealed: Unknown
158+
159+
# error: [call-non-callable] "Object of type `GenericAlias` is not callable"
160+
reveal_type(B[int]()) # revealed: Unknown
161+
```
162+
163+
## Recursive Truthiness
164+
165+
Make sure we handle cycles correctly when computing the truthiness of a generic type alias:
166+
167+
```py
168+
type X[T: X] = T
169+
170+
def _(x: X):
171+
assert x
172+
```

crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,6 @@ You cannot use the same typevar more than once.
4040
class RepeatedTypevar[T, T]: ...
4141
```
4242

43-
You can only use typevars (TODO: or param specs or typevar tuples) in the class's generic context.
44-
45-
```py
46-
# TODO: error
47-
class GenericOfType[int]: ...
48-
```
49-
5043
You can also define a generic class by inheriting from some _other_ generic class, and specializing
5144
it with typevars. With PEP 695 syntax, you must explicitly list all of the typevars that you use in
5245
your base classes.

crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ T = TypeVar("T")
188188
IntAnd = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,))
189189

190190
def f(x: IntAnd[str]) -> None:
191-
reveal_type(x) # revealed: @Todo(Generic PEP-695 type alias)
191+
reveal_type(x) # revealed: @Todo(Generic manual PEP-695 type alias)
192192
```
193193

194194
### Error cases

0 commit comments

Comments
 (0)