Skip to content

Commit 125dff3

Browse files
committed
[ty] defer inference of legacy TypeVar bound/constraints/defaults
1 parent 70f51e9 commit 125dff3

File tree

12 files changed

+708
-369
lines changed

12 files changed

+708
-369
lines changed

crates/ty_ide/src/goto_type_definition.rs

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -308,26 +308,8 @@ mod tests {
308308
"#,
309309
);
310310

311-
assert_snapshot!(test.goto_type_definition(), @r#"
312-
info[goto-type-definition]: Type definition
313-
--> main.py:4:1
314-
|
315-
2 | from typing_extensions import TypeAliasType
316-
3 |
317-
4 | Alias = TypeAliasType("Alias", tuple[int, int])
318-
| ^^^^^
319-
5 |
320-
6 | Alias
321-
|
322-
info: Source
323-
--> main.py:6:1
324-
|
325-
4 | Alias = TypeAliasType("Alias", tuple[int, int])
326-
5 |
327-
6 | Alias
328-
| ^^^^^
329-
|
330-
"#);
311+
// TODO: This should jump to the definition of `Alias` above.
312+
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
331313
}
332314

333315
#[test]

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Generic classes: Legacy syntax
22

3+
We use TypeVar defaults here, which was added in Python 3.13 for legacy typevars.
4+
5+
```toml
6+
[environment]
7+
python-version = "3.13"
8+
```
9+
310
## Defining a generic class
411

512
At its simplest, to define a generic class using the legacy syntax, you inherit from the

crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md

Lines changed: 186 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,12 @@ reveal_type(T.__name__) # revealed: Literal["T"]
3333
from typing import TypeVar
3434

3535
T = TypeVar("T")
36-
# TODO: no error
3736
# error: [invalid-legacy-type-variable]
3837
U: TypeVar = TypeVar("U")
3938

40-
# error: [invalid-legacy-type-variable] "A legacy `typing.TypeVar` must be immediately assigned to a variable"
41-
# error: [invalid-type-form] "Function calls are not allowed in type expressions"
42-
TestList = list[TypeVar("W")]
39+
# error: [invalid-legacy-type-variable]
40+
tuple_with_typevar = ("foo", TypeVar("W"))
41+
reveal_type(tuple_with_typevar[1]) # revealed: TypeVar
4342
```
4443

4544
### `TypeVar` parameter must match variable name
@@ -49,7 +48,7 @@ TestList = list[TypeVar("W")]
4948
```py
5049
from typing import TypeVar
5150

52-
# 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`)"
51+
# error: [invalid-legacy-type-variable]
5352
T = TypeVar("Q")
5453
```
5554

@@ -66,6 +65,22 @@ T = TypeVar("T")
6665
T = TypeVar("T")
6766
```
6867

68+
### No variadic arguments
69+
70+
```py
71+
from typing import TypeVar
72+
73+
types = (int, str)
74+
75+
# error: [invalid-legacy-type-variable]
76+
T = TypeVar("T", *types)
77+
reveal_type(T) # revealed: TypeVar
78+
79+
# error: [invalid-legacy-type-variable]
80+
S = TypeVar("S", **{"bound": int})
81+
reveal_type(S) # revealed: TypeVar
82+
```
83+
6984
### Type variables with a default
7085

7186
Note that the `__default__` property is only available in Python ≥3.13.
@@ -91,6 +106,11 @@ reveal_type(S.__default__) # revealed: NoDefault
91106

92107
### Using other typevars as a default
93108

109+
```toml
110+
[environment]
111+
python-version = "3.13"
112+
```
113+
94114
```py
95115
from typing import Generic, TypeVar, Union
96116

@@ -122,6 +142,20 @@ reveal_type(T.__constraints__) # revealed: tuple[()]
122142

123143
S = TypeVar("S")
124144
reveal_type(S.__bound__) # revealed: None
145+
146+
from typing import TypedDict
147+
148+
# error: [invalid-type-form]
149+
T = TypeVar("T", bound=TypedDict)
150+
```
151+
152+
The upper bound must be a valid type expression:
153+
154+
```py
155+
from typing import TypedDict
156+
157+
# error: [invalid-type-form]
158+
T = TypeVar("T", bound=TypedDict)
125159
```
126160

127161
### Type variables with constraints
@@ -138,6 +172,16 @@ S = TypeVar("S")
138172
reveal_type(S.__constraints__) # revealed: tuple[()]
139173
```
140174

175+
Constraints are not simplified relative to each other, even if one is a subtype of the other:
176+
177+
```py
178+
T = TypeVar("T", int, bool)
179+
reveal_type(T.__constraints__) # revealed: tuple[int, bool]
180+
181+
S = TypeVar("S", float, str)
182+
reveal_type(S.__constraints__) # revealed: tuple[int | float, str]
183+
```
184+
141185
### Cannot have only one constraint
142186

143187
> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should
@@ -146,10 +190,19 @@ reveal_type(S.__constraints__) # revealed: tuple[()]
146190
```py
147191
from typing import TypeVar
148192

149-
# TODO: error: [invalid-type-variable-constraints]
193+
# error: [invalid-legacy-type-variable]
150194
T = TypeVar("T", int)
151195
```
152196

197+
### Cannot have both bound and constraint
198+
199+
```py
200+
from typing import TypeVar
201+
202+
# error: [invalid-legacy-type-variable]
203+
T = TypeVar("T", int, str, bound=bytes)
204+
```
205+
153206
### Cannot be both covariant and contravariant
154207

155208
> To facilitate the declaration of container types where covariant or contravariant type checking is
@@ -178,6 +231,66 @@ T = TypeVar("T", covariant=cond())
178231
U = TypeVar("U", contravariant=cond())
179232
```
180233

234+
### Invalid keyword arguments
235+
236+
```py
237+
from typing import TypeVar
238+
239+
# error: [invalid-legacy-type-variable]
240+
T = TypeVar("T", invalid_keyword=True)
241+
```
242+
243+
```pyi
244+
from typing import TypeVar
245+
246+
# error: [invalid-legacy-type-variable]
247+
T = TypeVar("T", invalid_keyword=True)
248+
249+
```
250+
251+
### Constructor signature versioning
252+
253+
#### For `typing.TypeVar`
254+
255+
```toml
256+
[environment]
257+
python-version = "3.10"
258+
```
259+
260+
In a stub file, features from the latest supported Python version can be used on any version:
261+
262+
```pyi
263+
from typing import TypeVar
264+
T = TypeVar("T", default=int)
265+
```
266+
267+
But this raises an error in a non-stub file:
268+
269+
```py
270+
from typing import TypeVar
271+
272+
# error: [invalid-legacy-type-variable]
273+
T = TypeVar("T", default=int)
274+
```
275+
276+
#### For `typing_extensions.TypeVar`
277+
278+
`typing_extensions.TypeVar` always supports the latest features, on any Python version.
279+
280+
```toml
281+
[environment]
282+
python-version = "3.10"
283+
```
284+
285+
```py
286+
from typing_extensions import TypeVar
287+
288+
T = TypeVar("T", default=int)
289+
# TODO: should not error, should reveal `int`
290+
# error: [unresolved-attribute]
291+
reveal_type(T.__default__) # revealed: Unknown
292+
```
293+
181294
## Callability
182295

183296
A typevar bound to a Callable type is callable:
@@ -231,4 +344,71 @@ def constrained(x: T_constrained):
231344
reveal_type(type(x)) # revealed: type[int] | type[str]
232345
```
233346

347+
## Cycles
348+
349+
### Bounds and constraints
350+
351+
A typevar's bounds and constraints cannot be generic, cyclic or otherwise:
352+
353+
```py
354+
from typing import Any, TypeVar
355+
356+
S = TypeVar("S")
357+
358+
# TODO: error
359+
T = TypeVar("T", bound=list[S])
360+
361+
# TODO: error
362+
U = TypeVar("U", list["T"], str)
363+
364+
# TODO: error
365+
V = TypeVar("V", list["V"], str)
366+
```
367+
368+
However, they are lazily evaluated and can cyclically refer to their own type:
369+
370+
```py
371+
from typing import TypeVar, Generic
372+
373+
T = TypeVar("T", bound=list["G"])
374+
375+
class G(Generic[T]):
376+
x: T
377+
378+
reveal_type(G[list[G]]().x) # revealed: list[G[Unknown]]
379+
```
380+
381+
### Defaults
382+
383+
```toml
384+
[environment]
385+
python-version = "3.13"
386+
```
387+
388+
Defaults can be generic, but can only refer to earlier typevars:
389+
390+
```py
391+
from typing import Generic, TypeVar
392+
393+
T = TypeVar("T")
394+
U = TypeVar("U", default=T)
395+
396+
class C(Generic[T, U]):
397+
x: T
398+
y: U
399+
400+
reveal_type(C[int, str]().x) # revealed: int
401+
reveal_type(C[int, str]().y) # revealed: str
402+
reveal_type(C[int]().x) # revealed: int
403+
reveal_type(C[int]().y) # revealed: int
404+
405+
# TODO: error
406+
V = TypeVar("V", default="V")
407+
408+
class D(Generic[V]):
409+
x: V
410+
411+
reveal_type(D().x) # revealed: V@D
412+
```
413+
234414
[generics]: https://typing.python.org/en/latest/spec/generics.html

crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,9 @@ from typing_extensions import TypeAliasType, TypeVar
197197

198198
T = TypeVar("T")
199199

200-
IntAnd = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,))
200+
IntAndT = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,))
201201

202-
def f(x: IntAnd[str]) -> None:
202+
def f(x: IntAndT[str]) -> None:
203203
reveal_type(x) # revealed: @Todo(Generic manual PEP-695 type alias)
204204
```
205205

crates/ty_python_semantic/src/ast_node_ref.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use ruff_text_size::Ranged;
3030
/// This means that changes to expressions in other scopes don't invalidate the expression's id, giving
3131
/// us some form of scope-stable identity for expressions. Only queries accessing the node field
3232
/// run on every AST change. All other queries only run when the expression's identity changes.
33-
#[derive(Clone)]
33+
#[derive(Clone, PartialEq)]
3434
pub struct AstNodeRef<T> {
3535
/// The index of the node in the AST.
3636
index: NodeIndex,
@@ -40,15 +40,14 @@ pub struct AstNodeRef<T> {
4040
kind: ruff_python_ast::NodeKind,
4141
#[cfg(debug_assertions)]
4242
range: ruff_text_size::TextRange,
43-
// Note that because the module address is not stored in release builds, `AstNodeRef`
44-
// cannot implement `Eq`, as indices are only unique within a given instance of the
45-
// AST.
4643
#[cfg(debug_assertions)]
4744
module_addr: usize,
4845

4946
_node: PhantomData<T>,
5047
}
5148

49+
impl<T> Eq for AstNodeRef<T> where T: PartialEq {}
50+
5251
impl<T> AstNodeRef<T> {
5352
pub(crate) fn index(&self) -> NodeIndex {
5453
self.index

crates/ty_python_semantic/src/semantic_index/builder.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,10 +1165,6 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
11651165
target: &'ast ast::Expr,
11661166
value: Expression<'db>,
11671167
) {
1168-
// We only handle assignments to names and unpackings here, other targets like
1169-
// attribute and subscript are handled separately as they don't create a new
1170-
// definition.
1171-
11721168
let current_assignment = match target {
11731169
ast::Expr::List(_) | ast::Expr::Tuple(_) => {
11741170
if matches!(unpackable, Unpackable::Comprehension { .. }) {
@@ -1628,10 +1624,22 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
16281624
debug_assert_eq!(&self.current_assignments, &[]);
16291625

16301626
self.visit_expr(&node.value);
1631-
let value = self.add_standalone_assigned_expression(&node.value, node);
16321627

1633-
for target in &node.targets {
1634-
self.add_unpackable_assignment(&Unpackable::Assign(node), target, value);
1628+
// Optimization for the common case: if there's just one target, and it's not an
1629+
// unpacking, and the target is a simple name, we don't need the RHS to be a
1630+
// standalone expression at all.
1631+
if let [target] = &node.targets[..]
1632+
&& target.is_name_expr()
1633+
{
1634+
self.push_assignment(CurrentAssignment::Assign { node, unpack: None });
1635+
self.visit_expr(target);
1636+
self.pop_assignment();
1637+
} else {
1638+
let value = self.add_standalone_assigned_expression(&node.value, node);
1639+
1640+
for target in &node.targets {
1641+
self.add_unpackable_assignment(&Unpackable::Assign(node), target, value);
1642+
}
16351643
}
16361644
}
16371645
ast::Stmt::AnnAssign(node) => {

crates/ty_python_semantic/src/semantic_index/definition.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -706,13 +706,6 @@ impl DefinitionKind<'_> {
706706
matches!(self, DefinitionKind::Assignment(_))
707707
}
708708

709-
pub(crate) fn as_typevar(&self) -> Option<&AstNodeRef<ast::TypeParamTypeVar>> {
710-
match self {
711-
DefinitionKind::TypeVar(type_var) => Some(type_var),
712-
_ => None,
713-
}
714-
}
715-
716709
/// Returns the [`TextRange`] of the definition target.
717710
///
718711
/// A definition target would mainly be the node representing the place being defined i.e.,

0 commit comments

Comments
 (0)