Skip to content

Commit 787bcd1

Browse files
authored
[red-knot] Handle explicit class specialization in type expressions (#17434)
You can now use subscript expressions in a type expression to explicitly specialize generic classes, just like you could already do in value expressions. This still does not implement bidirectional checking, so a type annotation on an assignment does not influence how we infer a specialization for a (not explicitly specialized) constructor call. You might get an `invalid-assignment` error if (a) we cannot infer a class specialization from the constructor call (in which case you end up e.g. trying to assign `C[Unknown]` to `C[int]`) or if (b) we can infer a specialization, but it doesn't match the annotation. Closes #17432
1 parent 5853eb2 commit 787bcd1

File tree

19 files changed

+588
-62
lines changed

19 files changed

+588
-62
lines changed

crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def _(
8989
reveal_type(k) # revealed: Unknown
9090
reveal_type(p) # revealed: Unknown
9191
reveal_type(q) # revealed: int | Unknown
92-
reveal_type(r) # revealed: @Todo(generics)
92+
reveal_type(r) # revealed: @Todo(unknown type subscript)
9393
```
9494

9595
## Invalid Collection based AST nodes

crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ from other import Literal
137137
a1: Literal[26]
138138

139139
def f():
140-
reveal_type(a1) # revealed: @Todo(generics)
140+
reveal_type(a1) # revealed: @Todo(unknown type subscript)
141141
```
142142

143143
## Detecting typing_extensions.Literal

crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
6161
reveal_type(e) # revealed: @Todo(full tuple[...] support)
6262
reveal_type(f) # revealed: @Todo(full tuple[...] support)
6363
reveal_type(g) # revealed: @Todo(full tuple[...] support)
64-
reveal_type(h) # revealed: tuple[@Todo(generics), @Todo(generics)]
64+
reveal_type(h) # revealed: tuple[@Todo(specialized non-generic class), @Todo(specialized non-generic class)]
6565

6666
reveal_type(i) # revealed: tuple[str | int, str | int]
6767
reveal_type(j) # revealed: tuple[str | int]

crates/red_knot_python_semantic/resources/mdtest/attributes.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1665,7 +1665,7 @@ functions are instances of that class:
16651665
def f(): ...
16661666

16671667
reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None
1668-
reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
1668+
reveal_type(f.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None
16691669
```
16701670

16711671
Some attributes are special-cased, however:
@@ -1716,7 +1716,8 @@ reveal_type(False.real) # revealed: Literal[0]
17161716
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
17171717

17181718
```py
1719-
reveal_type(b"foo".join) # revealed: bound method Literal[b"foo"].join(iterable_of_bytes: @Todo(generics), /) -> bytes
1719+
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: @Todo(specialized non-generic class), /) -> bytes
1720+
reveal_type(b"foo".join)
17201721
# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`), start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool
17211722
reveal_type(b"foo".endswith)
17221723
```

crates/red_knot_python_semantic/resources/mdtest/call/methods.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ function object. We model this explicitly, which means that we can access `__kwd
9494
methods, even though it is not available on `types.MethodType`:
9595

9696
```py
97-
reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(generics) | None
97+
reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None
9898
```
9999

100100
## Basic method calls on class objects and instances

crates/red_knot_python_semantic/resources/mdtest/decorators.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,10 @@ def f(x: int) -> int:
145145
return x**2
146146

147147
# TODO: Should be `_lru_cache_wrapper[int]`
148-
reveal_type(f) # revealed: @Todo(generics)
148+
reveal_type(f) # revealed: @Todo(specialized non-generic class)
149149

150150
# TODO: Should be `int`
151-
reveal_type(f(1)) # revealed: @Todo(generics)
151+
reveal_type(f(1)) # revealed: @Todo(specialized non-generic class)
152152
```
153153

154154
## Lambdas as decorators

crates/red_knot_python_semantic/resources/mdtest/directives/cast.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ from knot_extensions import Unknown
6161

6262
def f(x: Any, y: Unknown, z: Any | str | int):
6363
a = cast(dict[str, Any], x)
64-
reveal_type(a) # revealed: @Todo(generics)
64+
reveal_type(a) # revealed: @Todo(specialized non-generic class)
6565

6666
b = cast(Any, y)
6767
reveal_type(b) # revealed: Any

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,12 @@ consistent with each other.
164164

165165
```py
166166
class C[T]:
167-
def __new__(cls, x: T) -> "C"[T]:
167+
def __new__(cls, x: T) -> "C[T]":
168168
return object.__new__(cls)
169169

170170
reveal_type(C(1)) # revealed: C[Literal[1]]
171171

172-
# TODO: error: [invalid-argument-type]
172+
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
173173
wrong_innards: C[int] = C("five")
174174
```
175175

@@ -181,48 +181,48 @@ class C[T]:
181181

182182
reveal_type(C(1)) # revealed: C[Literal[1]]
183183

184-
# TODO: error: [invalid-argument-type]
184+
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
185185
wrong_innards: C[int] = C("five")
186186
```
187187

188188
## Identical `__new__` and `__init__` signatures
189189

190190
```py
191191
class C[T]:
192-
def __new__(cls, x: T) -> "C"[T]:
192+
def __new__(cls, x: T) -> "C[T]":
193193
return object.__new__(cls)
194194

195195
def __init__(self, x: T) -> None: ...
196196

197197
reveal_type(C(1)) # revealed: C[Literal[1]]
198198

199-
# TODO: error: [invalid-argument-type]
199+
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
200200
wrong_innards: C[int] = C("five")
201201
```
202202

203203
## Compatible `__new__` and `__init__` signatures
204204

205205
```py
206206
class C[T]:
207-
def __new__(cls, *args, **kwargs) -> "C"[T]:
207+
def __new__(cls, *args, **kwargs) -> "C[T]":
208208
return object.__new__(cls)
209209

210210
def __init__(self, x: T) -> None: ...
211211

212212
reveal_type(C(1)) # revealed: C[Literal[1]]
213213

214-
# TODO: error: [invalid-argument-type]
214+
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
215215
wrong_innards: C[int] = C("five")
216216

217217
class D[T]:
218-
def __new__(cls, x: T) -> "D"[T]:
218+
def __new__(cls, x: T) -> "D[T]":
219219
return object.__new__(cls)
220220

221221
def __init__(self, *args, **kwargs) -> None: ...
222222

223223
reveal_type(D(1)) # revealed: D[Literal[1]]
224224

225-
# TODO: error: [invalid-argument-type]
225+
# error: [invalid-assignment] "Object of type `D[Literal["five"]]` is not assignable to `D[int]`"
226226
wrong_innards: D[int] = D("five")
227227
```
228228

@@ -247,7 +247,7 @@ reveal_type(C(1, "string")) # revealed: C[Unknown]
247247
# error: [invalid-argument-type]
248248
reveal_type(C(1, True)) # revealed: C[Unknown]
249249

250-
# TODO: error for the correct reason
250+
# TODO: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
251251
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `S`, found `Literal[1]`"
252252
wrong_innards: C[int] = C("five", 1)
253253
```

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

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,19 @@ is.)
6464
from knot_extensions import is_fully_static, static_assert
6565
from typing import Any
6666

67-
def unbounded_unconstrained[T](t: list[T]) -> None:
67+
def unbounded_unconstrained[T](t: T) -> None:
6868
static_assert(is_fully_static(T))
6969

70-
def bounded[T: int](t: list[T]) -> None:
70+
def bounded[T: int](t: T) -> None:
7171
static_assert(is_fully_static(T))
7272

73-
def bounded_by_gradual[T: Any](t: list[T]) -> None:
73+
def bounded_by_gradual[T: Any](t: T) -> None:
7474
static_assert(not is_fully_static(T))
7575

76-
def constrained[T: (int, str)](t: list[T]) -> None:
76+
def constrained[T: (int, str)](t: T) -> None:
7777
static_assert(is_fully_static(T))
7878

79-
def constrained_by_gradual[T: (int, Any)](t: list[T]) -> None:
79+
def constrained_by_gradual[T: (int, Any)](t: T) -> None:
8080
static_assert(not is_fully_static(T))
8181
```
8282

@@ -99,7 +99,7 @@ class Base(Super): ...
9999
class Sub(Base): ...
100100
class Unrelated: ...
101101

102-
def unbounded_unconstrained[T, U](t: list[T], u: list[U]) -> None:
102+
def unbounded_unconstrained[T, U](t: T, u: U) -> None:
103103
static_assert(is_assignable_to(T, T))
104104
static_assert(is_assignable_to(T, object))
105105
static_assert(not is_assignable_to(T, Super))
@@ -129,7 +129,7 @@ is a final class, since the typevar can still be specialized to `Never`.)
129129
from typing import Any
130130
from typing_extensions import final
131131

132-
def bounded[T: Super](t: list[T]) -> None:
132+
def bounded[T: Super](t: T) -> None:
133133
static_assert(is_assignable_to(T, Super))
134134
static_assert(not is_assignable_to(T, Sub))
135135
static_assert(not is_assignable_to(Super, T))
@@ -140,7 +140,7 @@ def bounded[T: Super](t: list[T]) -> None:
140140
static_assert(not is_subtype_of(Super, T))
141141
static_assert(not is_subtype_of(Sub, T))
142142

143-
def bounded_by_gradual[T: Any](t: list[T]) -> None:
143+
def bounded_by_gradual[T: Any](t: T) -> None:
144144
static_assert(is_assignable_to(T, Any))
145145
static_assert(is_assignable_to(Any, T))
146146
static_assert(is_assignable_to(T, Super))
@@ -158,7 +158,7 @@ def bounded_by_gradual[T: Any](t: list[T]) -> None:
158158
@final
159159
class FinalClass: ...
160160

161-
def bounded_final[T: FinalClass](t: list[T]) -> None:
161+
def bounded_final[T: FinalClass](t: T) -> None:
162162
static_assert(is_assignable_to(T, FinalClass))
163163
static_assert(not is_assignable_to(FinalClass, T))
164164

@@ -172,14 +172,14 @@ true even if both typevars are bounded by the same final class, since you can sp
172172
typevars to `Never` in addition to that final class.
173173

174174
```py
175-
def two_bounded[T: Super, U: Super](t: list[T], u: list[U]) -> None:
175+
def two_bounded[T: Super, U: Super](t: T, u: U) -> None:
176176
static_assert(not is_assignable_to(T, U))
177177
static_assert(not is_assignable_to(U, T))
178178

179179
static_assert(not is_subtype_of(T, U))
180180
static_assert(not is_subtype_of(U, T))
181181

182-
def two_final_bounded[T: FinalClass, U: FinalClass](t: list[T], u: list[U]) -> None:
182+
def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None:
183183
static_assert(not is_assignable_to(T, U))
184184
static_assert(not is_assignable_to(U, T))
185185

@@ -194,7 +194,7 @@ intersection of all of its constraints is a subtype of the typevar.
194194
```py
195195
from knot_extensions import Intersection
196196

197-
def constrained[T: (Base, Unrelated)](t: list[T]) -> None:
197+
def constrained[T: (Base, Unrelated)](t: T) -> None:
198198
static_assert(not is_assignable_to(T, Super))
199199
static_assert(not is_assignable_to(T, Base))
200200
static_assert(not is_assignable_to(T, Sub))
@@ -219,7 +219,7 @@ def constrained[T: (Base, Unrelated)](t: list[T]) -> None:
219219
static_assert(not is_subtype_of(Super | Unrelated, T))
220220
static_assert(is_subtype_of(Intersection[Base, Unrelated], T))
221221

222-
def constrained_by_gradual[T: (Base, Any)](t: list[T]) -> None:
222+
def constrained_by_gradual[T: (Base, Any)](t: T) -> None:
223223
static_assert(is_assignable_to(T, Super))
224224
static_assert(is_assignable_to(T, Base))
225225
static_assert(not is_assignable_to(T, Sub))
@@ -261,7 +261,7 @@ distinct constraints, meaning that there is (still) no guarantee that they will
261261
the same type.
262262

263263
```py
264-
def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> None:
264+
def two_constrained[T: (int, str), U: (int, str)](t: T, u: U) -> None:
265265
static_assert(not is_assignable_to(T, U))
266266
static_assert(not is_assignable_to(U, T))
267267

@@ -271,7 +271,7 @@ def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> Non
271271
@final
272272
class AnotherFinalClass: ...
273273

274-
def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: list[T], u: list[U]) -> None:
274+
def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None:
275275
static_assert(not is_assignable_to(T, U))
276276
static_assert(not is_assignable_to(U, T))
277277

@@ -290,7 +290,7 @@ non-singleton type.
290290
```py
291291
from knot_extensions import is_singleton, is_single_valued, static_assert
292292

293-
def unbounded_unconstrained[T](t: list[T]) -> None:
293+
def unbounded_unconstrained[T](t: T) -> None:
294294
static_assert(not is_singleton(T))
295295
static_assert(not is_single_valued(T))
296296
```
@@ -299,7 +299,7 @@ A bounded typevar is not a singleton, even if its bound is a singleton, since it
299299
specialized to `Never`.
300300

301301
```py
302-
def bounded[T: None](t: list[T]) -> None:
302+
def bounded[T: None](t: T) -> None:
303303
static_assert(not is_singleton(T))
304304
static_assert(not is_single_valued(T))
305305
```
@@ -310,14 +310,14 @@ specialize a constrained typevar to a subtype of a constraint.)
310310
```py
311311
from typing_extensions import Literal
312312

313-
def constrained_non_singletons[T: (int, str)](t: list[T]) -> None:
313+
def constrained_non_singletons[T: (int, str)](t: T) -> None:
314314
static_assert(not is_singleton(T))
315315
static_assert(not is_single_valued(T))
316316

317-
def constrained_singletons[T: (Literal[True], Literal[False])](t: list[T]) -> None:
317+
def constrained_singletons[T: (Literal[True], Literal[False])](t: T) -> None:
318318
static_assert(is_singleton(T))
319319

320-
def constrained_single_valued[T: (Literal[True], tuple[()])](t: list[T]) -> None:
320+
def constrained_single_valued[T: (Literal[True], tuple[()])](t: T) -> None:
321321
static_assert(is_single_valued(T))
322322
```
323323

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,13 @@ S = TypeVar("S")
174174

175175
def f(x: T) -> None:
176176
x: list[T] = []
177-
# TODO: error
177+
# TODO: invalid-assignment error
178178
y: list[S] = []
179179

180180
# TODO: no error
181181
# error: [invalid-base]
182182
class C(Generic[T]):
183-
# TODO: error
183+
# TODO: error: cannot use S if it's not in the current generic context
184184
x: list[S] = []
185185

186186
# This is not an error, as shown in the previous test
@@ -200,11 +200,11 @@ S = TypeVar("S")
200200

201201
def f[T](x: T) -> None:
202202
x: list[T] = []
203-
# TODO: error
203+
# TODO: invalid assignment error
204204
y: list[S] = []
205205

206206
class C[T]:
207-
# TODO: error
207+
# TODO: error: cannot use S if it's not in the current generic context
208208
x: list[S] = []
209209

210210
def m1(self, x: S) -> S:
@@ -288,7 +288,7 @@ class C[T]:
288288
ok1: list[T] = []
289289

290290
class Bad:
291-
# TODO: error
291+
# TODO: error: cannot refer to T in nested scope
292292
bad: list[T] = []
293293

294294
class Inner[S]: ...

0 commit comments

Comments
 (0)