Skip to content

Commit ff376fc

Browse files
dcreagerAlexWaygoodcarljm
authored
[red-knot] Allow explicit specialization of generic classes (#17023)
This PR lets you explicitly specialize a generic class using a subscript expression. It introduces three new Rust types for representing classes: - `NonGenericClass` - `GenericClass` (not specialized) - `GenericAlias` (specialized) and two enum wrappers: - `ClassType` (a non-generic class or generic alias, represents a class _type_ at runtime) - `ClassLiteralType` (a non-generic class or generic class, represents a class body in the AST) We also add internal support for specializing callables, in particular function literals. (That is, the internal `Type` representation now attaches an optional specialization to a function literal.) This is used in this PR for the methods of a generic class, but should also give us most of what we need for specializing generic _functions_ (which this PR does not yet tackle). --------- Co-authored-by: Alex Waygood <[email protected]> Co-authored-by: Carl Meyer <[email protected]>
1 parent 7c81408 commit ff376fc

File tree

21 files changed

+1559
-435
lines changed

21 files changed

+1559
-435
lines changed

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

Lines changed: 87 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,13 @@ class C[T]: ...
1313
A class that inherits from a generic class, and fills its type parameters with typevars, is generic:
1414

1515
```py
16-
# TODO: no error
17-
# error: [non-subscriptable]
1816
class D[U](C[U]): ...
1917
```
2018

2119
A class that inherits from a generic class, but fills its type parameters with concrete types, is
2220
_not_ generic:
2321

2422
```py
25-
# TODO: no error
26-
# error: [non-subscriptable]
2723
class E(C[int]): ...
2824
```
2925

@@ -57,33 +53,85 @@ class D(C[T]): ...
5753

5854
(Examples `E` and `F` from above do not have analogues in the legacy syntax.)
5955

60-
## Inferring generic class parameters
56+
## Specializing generic classes explicitly
6157

6258
The type parameter can be specified explicitly:
6359

6460
```py
6561
class C[T]:
6662
x: T
6763

68-
# TODO: no error
69-
# TODO: revealed: C[int]
70-
# error: [non-subscriptable]
71-
reveal_type(C[int]()) # revealed: C
64+
reveal_type(C[int]()) # revealed: C[int]
65+
```
66+
67+
The specialization must match the generic types:
68+
69+
```py
70+
# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2"
71+
reveal_type(C[int, int]()) # revealed: Unknown
72+
```
73+
74+
If the type variable has an upper bound, the specialized type must satisfy that bound:
75+
76+
```py
77+
class Bounded[T: int]: ...
78+
class BoundedByUnion[T: int | str]: ...
79+
class IntSubclass(int): ...
80+
81+
reveal_type(Bounded[int]()) # revealed: Bounded[int]
82+
reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass]
83+
84+
# error: [invalid-argument-type] "Object of type `str` cannot be assigned to parameter 1 (`T`) of class `Bounded`; expected type `int`"
85+
reveal_type(Bounded[str]()) # revealed: Unknown
86+
87+
# error: [invalid-argument-type] "Object of type `int | str` cannot be assigned to parameter 1 (`T`) of class `Bounded`; expected type `int`"
88+
reveal_type(Bounded[int | str]()) # revealed: Unknown
89+
90+
reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int]
91+
reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion[IntSubclass]
92+
reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion[str]
93+
reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion[int | str]
7294
```
7395

96+
If the type variable is constrained, the specialized type must satisfy those constraints:
97+
98+
```py
99+
class Constrained[T: (int, str)]: ...
100+
101+
reveal_type(Constrained[int]()) # revealed: Constrained[int]
102+
103+
# TODO: error: [invalid-argument-type]
104+
# TODO: revealed: Constrained[Unknown]
105+
reveal_type(Constrained[IntSubclass]()) # revealed: Constrained[IntSubclass]
106+
107+
reveal_type(Constrained[str]()) # revealed: Constrained[str]
108+
109+
# TODO: error: [invalid-argument-type]
110+
# TODO: revealed: Unknown
111+
reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str]
112+
113+
# error: [invalid-argument-type] "Object of type `object` cannot be assigned to parameter 1 (`T`) of class `Constrained`; expected type `int | str`"
114+
reveal_type(Constrained[object]()) # revealed: Unknown
115+
```
116+
117+
## Inferring generic class parameters
118+
74119
We can infer the type parameter from a type context:
75120

76121
```py
122+
class C[T]:
123+
x: T
124+
77125
c: C[int] = C()
78126
# TODO: revealed: C[int]
79-
reveal_type(c) # revealed: C
127+
reveal_type(c) # revealed: C[Unknown]
80128
```
81129

82130
The typevars of a fully specialized generic class should no longer be visible:
83131

84132
```py
85133
# TODO: revealed: int
86-
reveal_type(c.x) # revealed: T
134+
reveal_type(c.x) # revealed: Unknown
87135
```
88136

89137
If the type parameter is not specified explicitly, and there are no constraints that let us infer a
@@ -92,15 +140,13 @@ specific type, we infer the typevar's default type:
92140
```py
93141
class D[T = int]: ...
94142

95-
# TODO: revealed: D[int]
96-
reveal_type(D()) # revealed: D
143+
reveal_type(D()) # revealed: D[int]
97144
```
98145

99146
If a typevar does not provide a default, we use `Unknown`:
100147

101148
```py
102-
# TODO: revealed: C[Unknown]
103-
reveal_type(C()) # revealed: C
149+
reveal_type(C()) # revealed: C[Unknown]
104150
```
105151

106152
If the type of a constructor parameter is a class typevar, we can use that to infer the type
@@ -111,17 +157,14 @@ class E[T]:
111157
def __init__(self, x: T) -> None: ...
112158

113159
# TODO: revealed: E[int] or E[Literal[1]]
114-
# TODO should not emit an error
115-
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`x`) of bound method `__init__`; expected type `T`"
116-
reveal_type(E(1)) # revealed: E
160+
reveal_type(E(1)) # revealed: E[Unknown]
117161
```
118162

119163
The types inferred from a type context and from a constructor parameter must be consistent with each
120164
other:
121165

122166
```py
123-
# TODO: the error should not leak the `T` typevar and should mention `E[int]`
124-
# error: [invalid-argument-type] "Object of type `Literal["five"]` cannot be assigned to parameter 2 (`x`) of bound method `__init__`; expected type `T`"
167+
# TODO: error: [invalid-argument-type]
125168
wrong_innards: E[int] = E("five")
126169
```
127170

@@ -134,17 +177,33 @@ propagate through:
134177
class Base[T]:
135178
x: T | None = None
136179

137-
# TODO: no error
138-
# error: [non-subscriptable]
139180
class Sub[U](Base[U]): ...
140181

182+
reveal_type(Base[int].x) # revealed: int | None
183+
reveal_type(Sub[int].x) # revealed: int | None
184+
```
185+
186+
## Generic methods
187+
188+
Generic classes can contain methods that are themselves generic. The generic methods can refer to
189+
the typevars of the enclosing generic class, and introduce new (distinct) typevars that are only in
190+
scope for the method.
191+
192+
```py
193+
class C[T]:
194+
def method[U](self, u: U) -> U:
195+
return u
196+
# error: [unresolved-reference]
197+
def cannot_use_outside_of_method(self, u: U): ...
198+
199+
# TODO: error
200+
def cannot_shadow_class_typevar[T](self, t: T): ...
201+
202+
c: C[int] = C[int]()
141203
# TODO: no error
142-
# TODO: revealed: int | None
143-
# error: [non-subscriptable]
144-
reveal_type(Base[int].x) # revealed: T | None
145-
# TODO: revealed: int | None
146-
# error: [non-subscriptable]
147-
reveal_type(Sub[int].x) # revealed: T | None
204+
# TODO: revealed: str or Literal["string"]
205+
# error: [invalid-argument-type]
206+
reveal_type(c.method("string")) # revealed: U
148207
```
149208

150209
## Cyclic class definition
@@ -158,8 +217,6 @@ Here, `Sub` is not a generic class, since it fills its superclass's type paramet
158217

159218
```pyi
160219
class Base[T]: ...
161-
# TODO: no error
162-
# error: [non-subscriptable]
163220
class Sub(Base[Sub]): ...
164221

165222
reveal_type(Sub) # revealed: Literal[Sub]
@@ -171,9 +228,6 @@ A similar case can work in a non-stub file, if forward references are stringifie
171228

172229
```py
173230
class Base[T]: ...
174-
175-
# TODO: no error
176-
# error: [non-subscriptable]
177231
class Sub(Base["Sub"]): ...
178232

179233
reveal_type(Sub) # revealed: Literal[Sub]
@@ -186,8 +240,6 @@ In a non-stub file, without stringified forward references, this raises a `NameE
186240
```py
187241
class Base[T]: ...
188242

189-
# TODO: the unresolved-reference error is correct, the non-subscriptable is not
190-
# error: [non-subscriptable]
191243
# error: [unresolved-reference]
192244
class Sub(Base[Sub]): ...
193245
```

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

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,51 @@ class C[T]:
8282
def m2(self, x: T) -> T:
8383
return x
8484

85-
c: C[int] = C()
86-
# TODO: no error
87-
# error: [invalid-argument-type]
85+
c: C[int] = C[int]()
8886
c.m1(1)
89-
# TODO: no error
90-
# error: [invalid-argument-type]
9187
c.m2(1)
92-
# TODO: expected type `int`
93-
# error: [invalid-argument-type] "Object of type `Literal["string"]` cannot be assigned to parameter 2 (`x`) of bound method `m2`; expected type `T`"
88+
# error: [invalid-argument-type] "Object of type `Literal["string"]` cannot be assigned to parameter 2 (`x`) of bound method `m2`; expected type `int`"
9489
c.m2("string")
9590
```
9691

92+
## Functions on generic classes are descriptors
93+
94+
This repeats the tests in the [Functions as descriptors](./call/methods.md) test suite, but on a
95+
generic class. This ensures that we are carrying any specializations through the entirety of the
96+
descriptor protocol, which is how `self` parameters are bound to instance methods.
97+
98+
```py
99+
from inspect import getattr_static
100+
101+
class C[T]:
102+
def f(self, x: T) -> str:
103+
return "a"
104+
105+
reveal_type(getattr_static(C[int], "f")) # revealed: Literal[f[int]]
106+
reveal_type(getattr_static(C[int], "f").__get__) # revealed: <method-wrapper `__get__` of `f[int]`>
107+
reveal_type(getattr_static(C[int], "f").__get__(None, C[int])) # revealed: Literal[f[int]]
108+
# revealed: <bound method `f` of `C[int]`>
109+
reveal_type(getattr_static(C[int], "f").__get__(C[int](), C[int]))
110+
111+
reveal_type(C[int].f) # revealed: Literal[f[int]]
112+
reveal_type(C[int]().f) # revealed: <bound method `f` of `C[int]`>
113+
114+
bound_method = C[int]().f
115+
reveal_type(bound_method.__self__) # revealed: C[int]
116+
reveal_type(bound_method.__func__) # revealed: Literal[f[int]]
117+
118+
reveal_type(C[int]().f(1)) # revealed: str
119+
reveal_type(bound_method(1)) # revealed: str
120+
121+
C[int].f(1) # error: [missing-argument]
122+
reveal_type(C[int].f(C[int](), 1)) # revealed: str
123+
124+
class D[U](C[U]):
125+
pass
126+
127+
reveal_type(D[int]().f) # revealed: <bound method `f` of `D[int]`>
128+
```
129+
97130
## Methods can mention other typevars
98131

99132
> A type variable used in a method that does not match any of the variables that parameterize the
@@ -127,7 +160,6 @@ c: C[int] = C()
127160
# TODO: no errors
128161
# TODO: revealed: str
129162
# error: [invalid-argument-type]
130-
# error: [invalid-argument-type]
131163
reveal_type(c.m(1, "string")) # revealed: S
132164
```
133165

crates/red_knot_python_semantic/resources/mdtest/narrow/type.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,24 @@ def _(x: A | B):
109109
reveal_type(x) # revealed: A
110110
```
111111

112+
## Narrowing for generic classes
113+
114+
Note that `type` returns the runtime class of an object, which does _not_ include specializations in
115+
the case of a generic class. (The typevars are erased.) That means we cannot narrow the type to the
116+
specialization that we compare with; we must narrow to an unknown specialization of the generic
117+
class.
118+
119+
```py
120+
class A[T = int]: ...
121+
class B: ...
122+
123+
def _[T](x: A | B):
124+
if type(x) is A[str]:
125+
reveal_type(x) # revealed: A[int] & A[Unknown] | B & A[Unknown]
126+
else:
127+
reveal_type(x) # revealed: A[int] | B
128+
```
129+
112130
## Limitations
113131

114132
```py

crates/red_knot_python_semantic/resources/mdtest/stubs/class.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,10 @@ In type stubs, classes can reference themselves in their base class definitions.
88
```pyi
99
class Foo[T]: ...
1010

11-
# TODO: actually is subscriptable
12-
# error: [non-subscriptable]
1311
class Bar(Foo[Bar]): ...
1412

1513
reveal_type(Bar) # revealed: Literal[Bar]
16-
# TODO: Instead of `Literal[Foo]`, we might eventually want to show a type that involves the type parameter.
17-
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo], Literal[object]]
14+
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo[Bar]], Literal[object]]
1815
```
1916

2017
## Access to attributes declared in stubs

crates/red_knot_python_semantic/src/symbol.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,17 @@ impl<'db> SymbolAndQualifiers<'db> {
425425
self.qualifiers.contains(TypeQualifiers::CLASS_VAR)
426426
}
427427

428+
#[must_use]
429+
pub(crate) fn map_type(
430+
self,
431+
f: impl FnOnce(Type<'db>) -> Type<'db>,
432+
) -> SymbolAndQualifiers<'db> {
433+
SymbolAndQualifiers {
434+
symbol: self.symbol.map_type(f),
435+
qualifiers: self.qualifiers,
436+
}
437+
}
438+
428439
/// Transform symbol and qualifiers into a [`LookupResult`],
429440
/// a [`Result`] type in which the `Ok` variant represents a definitely bound symbol
430441
/// and the `Err` variant represents a symbol that is either definitely or possibly unbound.

0 commit comments

Comments
 (0)