Skip to content

Commit ca4fdf4

Browse files
authored
Create TypeVarInstance type for legacy typevars (#16538)
We are currently representing type variables using a `KnownInstance` variant, which wraps a `TypeVarInstance` that contains the information about the typevar (name, bounds, constraints, default type). We were previously only constructing that type for PEP 695 typevars. This PR constructs that type for legacy typevars as well. It also detects functions that are generic because they use legacy typevars in their parameter list. With the existing logic for inferring specializations of function calls (#17301), that means that we are correctly detecting that the definition of `reveal_type` in the typeshed is generic, and inferring the correct specialization of `_T` for each call site. This does not yet handle legacy generic classes; that will come in a follow-on PR.
1 parent 3c460a7 commit ca4fdf4

22 files changed

+1094
-185
lines changed

crates/red_knot_python_semantic/resources/mdtest/function/return_type.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ from typing import TypeVar
203203

204204
T = TypeVar("T")
205205

206-
# TODO: `invalid-return-type` error should be emitted
206+
# error: [invalid-return-type]
207207
def m(x: T) -> T: ...
208208
```
209209

crates/red_knot_python_semantic/resources/mdtest/generics/functions.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,39 @@ def f[T](x: list[T]) -> T:
7171
reveal_type(f([1.0, 2.0])) # revealed: Unknown
7272
```
7373

74+
## Inferring a bound typevar
75+
76+
<!-- snapshot-diagnostics -->
77+
78+
```py
79+
from typing_extensions import reveal_type
80+
81+
def f[T: int](x: T) -> T:
82+
return x
83+
84+
reveal_type(f(1)) # revealed: Literal[1]
85+
reveal_type(f(True)) # revealed: Literal[True]
86+
# error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper bound of type variable `T`"
87+
reveal_type(f("string")) # revealed: Unknown
88+
```
89+
90+
## Inferring a constrained typevar
91+
92+
<!-- snapshot-diagnostics -->
93+
94+
```py
95+
from typing_extensions import reveal_type
96+
97+
def f[T: (int, None)](x: T) -> T:
98+
return x
99+
100+
reveal_type(f(1)) # revealed: int
101+
reveal_type(f(True)) # revealed: int
102+
reveal_type(f(None)) # revealed: None
103+
# error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constraints of type variable `T`"
104+
reveal_type(f("string")) # revealed: Unknown
105+
```
106+
74107
## Typevar constraints
75108

76109
If a type parameter has an upper bound, that upper bound constrains which types can be used for that

crates/red_knot_python_semantic/resources/mdtest/generics/legacy.md

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ in newer Python releases.
1919
from typing import TypeVar
2020

2121
T = TypeVar("T")
22+
reveal_type(type(T)) # revealed: Literal[TypeVar]
23+
reveal_type(T) # revealed: typing.TypeVar
24+
reveal_type(T.__name__) # revealed: Literal["T"]
2225
```
2326

2427
### Directly assigned to a variable
@@ -29,7 +32,12 @@ T = TypeVar("T")
2932
```py
3033
from typing import TypeVar
3134

32-
# TODO: error
35+
T = TypeVar("T")
36+
# TODO: no error
37+
# error: [invalid-legacy-type-variable]
38+
U: TypeVar = TypeVar("U")
39+
40+
# error: [invalid-legacy-type-variable] "A legacy `typing.TypeVar` must be immediately assigned to a variable"
3341
TestList = list[TypeVar("W")]
3442
```
3543

@@ -40,7 +48,7 @@ TestList = list[TypeVar("W")]
4048
```py
4149
from typing import TypeVar
4250

43-
# TODO: error
51+
# error: [invalid-legacy-type-variable] "The name of a legacy `typing.TypeVar` (`Q`) must match the name of the variable it is assigned to (`T`)"
4452
T = TypeVar("Q")
4553
```
4654

@@ -57,6 +65,52 @@ T = TypeVar("T")
5765
T = TypeVar("T")
5866
```
5967

68+
### Type variables with a default
69+
70+
Note that the `__default__` property is only available in Python ≥3.13.
71+
72+
```toml
73+
[environment]
74+
python-version = "3.13"
75+
```
76+
77+
```py
78+
from typing import TypeVar
79+
80+
T = TypeVar("T", default=int)
81+
reveal_type(T.__default__) # revealed: int
82+
reveal_type(T.__bound__) # revealed: None
83+
reveal_type(T.__constraints__) # revealed: tuple[()]
84+
85+
S = TypeVar("S")
86+
reveal_type(S.__default__) # revealed: NoDefault
87+
```
88+
89+
### Type variables with an upper bound
90+
91+
```py
92+
from typing import TypeVar
93+
94+
T = TypeVar("T", bound=int)
95+
reveal_type(T.__bound__) # revealed: int
96+
reveal_type(T.__constraints__) # revealed: tuple[()]
97+
98+
S = TypeVar("S")
99+
reveal_type(S.__bound__) # revealed: None
100+
```
101+
102+
### Type variables with constraints
103+
104+
```py
105+
from typing import TypeVar
106+
107+
T = TypeVar("T", int, str)
108+
reveal_type(T.__constraints__) # revealed: tuple[int, str]
109+
110+
S = TypeVar("S")
111+
reveal_type(S.__constraints__) # revealed: tuple[()]
112+
```
113+
60114
### Cannot have only one constraint
61115

62116
> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should

crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,51 @@ instances of `typing.TypeVar`, just like legacy type variables.
1717
```py
1818
def f[T]():
1919
reveal_type(type(T)) # revealed: Literal[TypeVar]
20-
reveal_type(T) # revealed: T
20+
reveal_type(T) # revealed: typing.TypeVar
2121
reveal_type(T.__name__) # revealed: Literal["T"]
2222
```
2323

24+
### Type variables with a default
25+
26+
Note that the `__default__` property is only available in Python ≥3.13.
27+
28+
```toml
29+
[environment]
30+
python-version = "3.13"
31+
```
32+
33+
```py
34+
def f[T = int]():
35+
reveal_type(T.__default__) # revealed: int
36+
reveal_type(T.__bound__) # revealed: None
37+
reveal_type(T.__constraints__) # revealed: tuple[()]
38+
39+
def g[S]():
40+
reveal_type(S.__default__) # revealed: NoDefault
41+
```
42+
43+
### Type variables with an upper bound
44+
45+
```py
46+
def f[T: int]():
47+
reveal_type(T.__bound__) # revealed: int
48+
reveal_type(T.__constraints__) # revealed: tuple[()]
49+
50+
def g[S]():
51+
reveal_type(S.__bound__) # revealed: None
52+
```
53+
54+
### Type variables with constraints
55+
56+
```py
57+
def f[T: (int, str)]():
58+
reveal_type(T.__constraints__) # revealed: tuple[int, str]
59+
reveal_type(T.__bound__) # revealed: None
60+
61+
def g[S]():
62+
reveal_type(S.__constraints__) # revealed: tuple[()]
63+
```
64+
2465
### Cannot have only one constraint
2566

2667
> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should

crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,7 @@ class Legacy(Generic[T]):
142142
return y
143143

144144
legacy: Legacy[int] = Legacy()
145-
# TODO: revealed: str
146-
reveal_type(legacy.m(1, "string")) # revealed: @Todo(Support for `typing.TypeVar` instances in type expressions)
145+
reveal_type(legacy.m(1, "string")) # revealed: Literal["string"]
147146
```
148147

149148
With PEP 695 syntax, it is clearer that the method uses a separate typevar:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
source: crates/red_knot_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: functions.md - Generic functions - Inferring a bound typevar
7+
mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/functions.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | from typing_extensions import reveal_type
16+
2 |
17+
3 | def f[T: int](x: T) -> T:
18+
4 | return x
19+
5 |
20+
6 | reveal_type(f(1)) # revealed: Literal[1]
21+
7 | reveal_type(f(True)) # revealed: Literal[True]
22+
8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper bound of type variable `T`"
23+
9 | reveal_type(f("string")) # revealed: Unknown
24+
```
25+
26+
# Diagnostics
27+
28+
```
29+
info: revealed-type: Revealed type
30+
--> src/mdtest_snippet.py:6:1
31+
|
32+
4 | return x
33+
5 |
34+
6 | reveal_type(f(1)) # revealed: Literal[1]
35+
| ^^^^^^^^^^^^^^^^^ `Literal[1]`
36+
7 | reveal_type(f(True)) # revealed: Literal[True]
37+
8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper bo...
38+
|
39+
40+
```
41+
42+
```
43+
info: revealed-type: Revealed type
44+
--> src/mdtest_snippet.py:7:1
45+
|
46+
6 | reveal_type(f(1)) # revealed: Literal[1]
47+
7 | reveal_type(f(True)) # revealed: Literal[True]
48+
| ^^^^^^^^^^^^^^^^^^^^ `Literal[True]`
49+
8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper b...
50+
9 | reveal_type(f("string")) # revealed: Unknown
51+
|
52+
53+
```
54+
55+
```
56+
error: lint:invalid-argument-type: Argument to this function is incorrect
57+
--> src/mdtest_snippet.py:9:15
58+
|
59+
7 | reveal_type(f(True)) # revealed: Literal[True]
60+
8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper b...
61+
9 | reveal_type(f("string")) # revealed: Unknown
62+
| ^^^^^^^^ Argument type `Literal["string"]` does not satisfy upper bound of type variable `T`
63+
|
64+
info: Type variable defined here
65+
--> src/mdtest_snippet.py:3:7
66+
|
67+
1 | from typing_extensions import reveal_type
68+
2 |
69+
3 | def f[T: int](x: T) -> T:
70+
| ^^^^^^
71+
4 | return x
72+
|
73+
74+
```
75+
76+
```
77+
info: revealed-type: Revealed type
78+
--> src/mdtest_snippet.py:9:1
79+
|
80+
7 | reveal_type(f(True)) # revealed: Literal[True]
81+
8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper b...
82+
9 | reveal_type(f("string")) # revealed: Unknown
83+
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
84+
|
85+
86+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
---
2+
source: crates/red_knot_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: functions.md - Generic functions - Inferring a constrained typevar
7+
mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/functions.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | from typing_extensions import reveal_type
16+
2 |
17+
3 | def f[T: (int, None)](x: T) -> T:
18+
4 | return x
19+
5 |
20+
6 | reveal_type(f(1)) # revealed: int
21+
7 | reveal_type(f(True)) # revealed: int
22+
8 | reveal_type(f(None)) # revealed: None
23+
9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constraints of type variable `T`"
24+
10 | reveal_type(f("string")) # revealed: Unknown
25+
```
26+
27+
# Diagnostics
28+
29+
```
30+
info: revealed-type: Revealed type
31+
--> src/mdtest_snippet.py:6:1
32+
|
33+
4 | return x
34+
5 |
35+
6 | reveal_type(f(1)) # revealed: int
36+
| ^^^^^^^^^^^^^^^^^ `int`
37+
7 | reveal_type(f(True)) # revealed: int
38+
8 | reveal_type(f(None)) # revealed: None
39+
|
40+
41+
```
42+
43+
```
44+
info: revealed-type: Revealed type
45+
--> src/mdtest_snippet.py:7:1
46+
|
47+
6 | reveal_type(f(1)) # revealed: int
48+
7 | reveal_type(f(True)) # revealed: int
49+
| ^^^^^^^^^^^^^^^^^^^^ `int`
50+
8 | reveal_type(f(None)) # revealed: None
51+
9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra...
52+
|
53+
54+
```
55+
56+
```
57+
info: revealed-type: Revealed type
58+
--> src/mdtest_snippet.py:8:1
59+
|
60+
6 | reveal_type(f(1)) # revealed: int
61+
7 | reveal_type(f(True)) # revealed: int
62+
8 | reveal_type(f(None)) # revealed: None
63+
| ^^^^^^^^^^^^^^^^^^^^ `None`
64+
9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra...
65+
10 | reveal_type(f("string")) # revealed: Unknown
66+
|
67+
68+
```
69+
70+
```
71+
error: lint:invalid-argument-type: Argument to this function is incorrect
72+
--> src/mdtest_snippet.py:10:15
73+
|
74+
8 | reveal_type(f(None)) # revealed: None
75+
9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra...
76+
10 | reveal_type(f("string")) # revealed: Unknown
77+
| ^^^^^^^^ Argument type `Literal["string"]` does not satisfy constraints of type variable `T`
78+
|
79+
info: Type variable defined here
80+
--> src/mdtest_snippet.py:3:7
81+
|
82+
1 | from typing_extensions import reveal_type
83+
2 |
84+
3 | def f[T: (int, None)](x: T) -> T:
85+
| ^^^^^^^^^^^^^^
86+
4 | return x
87+
|
88+
89+
```
90+
91+
```
92+
info: revealed-type: Revealed type
93+
--> src/mdtest_snippet.py:10:1
94+
|
95+
8 | reveal_type(f(None)) # revealed: None
96+
9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra...
97+
10 | reveal_type(f("string")) # revealed: Unknown
98+
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
99+
|
100+
101+
```

0 commit comments

Comments
 (0)