Skip to content

Commit edc61da

Browse files
committed
[ty] Reduce false positives when subscripting classes generic over TypeVarTuples
1 parent f02ee29 commit edc61da

6 files changed

Lines changed: 55 additions & 7 deletions

File tree

crates/ty_python_semantic/resources/mdtest/annotations/starred.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ def append_int(*args: *Ts) -> tuple[*Ts, int]:
1818
return (*args, 1)
1919

2020
# TODO should be tuple[Literal[True], Literal["a"], int]
21-
reveal_type(append_int(True, "a")) # revealed: tuple[@Todo(PEP 646), ...]
21+
reveal_type(append_int(True, "a")) # revealed: tuple[@Todo(TypeVarTuple), ...]
2222
```

crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Unsupported special types
22

3+
## Functional classes
4+
35
We do not understand the functional syntax for creating `TypedDict`s or `Enum`s yet. But we also do
46
not emit false positives when these are used in type expressions.
57

@@ -14,3 +16,32 @@ MyTypedDict = typing.TypedDict("MyTypedDict", {"foo": int})
1416

1517
def f(a: MyEnum, b: MyTypedDict): ...
1618
```
19+
20+
## No false positives for subscripting a class generic over a `TypeVarTuple`
21+
22+
We don't support `TypeVarTuple` yet, but we also try to avoid emitting false positives when you
23+
subscript classes generic over a `TypeVarTuple`:
24+
25+
```toml
26+
[environment]
27+
python-version = "3.12"
28+
```
29+
30+
```py
31+
from typing import Generic, TypeVarTuple, Unpack
32+
33+
Ts = TypeVarTuple("Ts")
34+
35+
class Foo(Generic[Unpack[Ts]]): ...
36+
37+
x: Foo[int, str, bytes] # fine
38+
39+
class Bar(Generic[*Ts]): ...
40+
41+
y: Bar[int, str, bytes] # fine
42+
43+
class Baz[*Ts]: ...
44+
45+
# TODO: false positive
46+
z: Baz[int, str, bytes] # error: [not-subscriptable]
47+
```

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
6969
reveal_type(e) # revealed: tuple[str, ...]
7070

7171
reveal_type(f) # revealed: tuple[str, *tuple[int, ...], bytes]
72-
reveal_type(g) # revealed: tuple[@Todo(PEP 646), ...]
72+
reveal_type(g) # revealed: tuple[@Todo(TypeVarTuple), ...]
7373

7474
reveal_type(h) # revealed: tuple[list[int], list[int]]
7575
reveal_type(i) # revealed: tuple[str | int, str | int]

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15098,7 +15098,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
1509815098
}
1509915099

1510015100
if let Some(first_excess_type_argument_index) = first_excess_type_argument_index {
15101-
if typevars_len == 0 {
15101+
if let Type::GenericAlias(alias) = value_ty
15102+
&& let spec = alias.specialization(self.db())
15103+
&& spec
15104+
.types(self.db())
15105+
.contains(&Type::Dynamic(DynamicType::TodoTypeVarTuple))
15106+
{
15107+
// Avoid false-positive errors when specializing a class
15108+
// that's generic over a legacy TypeVarTuple
15109+
} else if typevars_len == 0 {
1510215110
// Type parameter list cannot be empty, so if we reach here, `value_ty` is not a generic type.
1510315111
if let Some(builder) = self
1510415112
.context

crates/ty_python_semantic/src/types/infer/builder/type_expression.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
713713
}
714714

715715
let ty = if return_todo {
716-
Some(TupleType::homogeneous(self.db(), todo_type!("PEP 646")))
716+
Some(TupleType::homogeneous(
717+
self.db(),
718+
Type::Dynamic(DynamicType::TodoTypeVarTuple),
719+
))
717720
} else {
718721
TupleType::new(self.db(), &element_types.build())
719722
};
@@ -738,7 +741,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
738741
let single_element_ty = self.infer_type_expression(single_element);
739742
if element_could_alter_type_of_whole_tuple(single_element, single_element_ty, self)
740743
{
741-
Some(TupleType::homogeneous(self.db(), todo_type!("PEP 646")))
744+
Some(TupleType::homogeneous(
745+
self.db(),
746+
Type::Dynamic(DynamicType::TodoTypeVarTuple),
747+
))
742748
} else {
743749
TupleType::heterogeneous(self.db(), std::iter::once(single_element_ty))
744750
}

crates/ty_test/src/matcher.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,11 @@ fn discard_todo_metadata(ty: &str) -> Cow<'_, str> {
208208
{
209209
/// `@Todo` variants that are hardcoded and always display their message,
210210
/// even in release mode.
211-
const PRESERVED_TODO_VARIANTS: &[&str] =
212-
&["@Todo(StarredExpression)", "@Todo(typing.Unpack)"];
211+
const PRESERVED_TODO_VARIANTS: &[&str] = &[
212+
"@Todo(StarredExpression)",
213+
"@Todo(typing.Unpack)",
214+
"@Todo(TypeVarTuple)",
215+
];
213216

214217
static TODO_METADATA_REGEX: LazyLock<regex::Regex> =
215218
LazyLock::new(|| regex::Regex::new(r"@Todo\([^)]*\)").unwrap());

0 commit comments

Comments
 (0)