diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/starred.md b/crates/ty_python_semantic/resources/mdtest/annotations/starred.md index e7eb565cca7b6..d59b61a7c5c01 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/starred.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/starred.md @@ -18,5 +18,5 @@ def append_int(*args: *Ts) -> tuple[*Ts, int]: return (*args, 1) # TODO should be tuple[Literal[True], Literal["a"], int] -reveal_type(append_int(True, "a")) # revealed: tuple[@Todo(PEP 646), ...] +reveal_type(append_int(True, "a")) # revealed: tuple[@Todo(TypeVarTuple), ...] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md index cad4f50a7f9a3..120ff9e25f896 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md @@ -1,5 +1,7 @@ # Unsupported special types +## Functional classes + We do not understand the functional syntax for creating `TypedDict`s or `Enum`s yet. But we also do not emit false positives when these are used in type expressions. @@ -14,3 +16,32 @@ MyTypedDict = typing.TypedDict("MyTypedDict", {"foo": int}) def f(a: MyEnum, b: MyTypedDict): ... ``` + +## No false positives for subscripting a class generic over a `TypeVarTuple` + +We don't support `TypeVarTuple` yet, but we also try to avoid emitting false positives when you +subscript classes generic over a `TypeVarTuple`: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Generic, TypeVarTuple, Unpack + +Ts = TypeVarTuple("Ts") + +class Foo(Generic[Unpack[Ts]]): ... + +x: Foo[int, str, bytes] # fine + +class Bar(Generic[*Ts]): ... + +y: Bar[int, str, bytes] # fine + +class Baz[*Ts]: ... + +# TODO: false positive +z: Baz[int, str, bytes] # error: [not-subscriptable] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md index 383fa0fffd5fd..e4facfcf4be51 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md @@ -69,7 +69,7 @@ reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]] reveal_type(e) # revealed: tuple[str, ...] reveal_type(f) # revealed: tuple[str, *tuple[int, ...], bytes] -reveal_type(g) # revealed: tuple[@Todo(PEP 646), ...] +reveal_type(g) # revealed: tuple[@Todo(TypeVarTuple), ...] reveal_type(h) # revealed: tuple[list[int], list[int]] reveal_type(i) # revealed: tuple[str | int, str | int] diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 3f884db297bd4..a0ca05616bf71 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -15098,7 +15098,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } if let Some(first_excess_type_argument_index) = first_excess_type_argument_index { - if typevars_len == 0 { + if let Type::GenericAlias(alias) = value_ty + && let spec = alias.specialization(self.db()) + && spec + .types(self.db()) + .contains(&Type::Dynamic(DynamicType::TodoTypeVarTuple)) + { + // Avoid false-positive errors when specializing a class + // that's generic over a legacy TypeVarTuple + } else if typevars_len == 0 { // Type parameter list cannot be empty, so if we reach here, `value_ty` is not a generic type. if let Some(builder) = self .context diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 0159c04af06ae..61d34e4ec6799 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -713,7 +713,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } let ty = if return_todo { - Some(TupleType::homogeneous(self.db(), todo_type!("PEP 646"))) + Some(TupleType::homogeneous( + self.db(), + Type::Dynamic(DynamicType::TodoTypeVarTuple), + )) } else { TupleType::new(self.db(), &element_types.build()) }; @@ -738,7 +741,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let single_element_ty = self.infer_type_expression(single_element); if element_could_alter_type_of_whole_tuple(single_element, single_element_ty, self) { - Some(TupleType::homogeneous(self.db(), todo_type!("PEP 646"))) + Some(TupleType::homogeneous( + self.db(), + Type::Dynamic(DynamicType::TodoTypeVarTuple), + )) } else { TupleType::heterogeneous(self.db(), std::iter::once(single_element_ty)) } diff --git a/crates/ty_test/src/matcher.rs b/crates/ty_test/src/matcher.rs index ade91912f5cf6..cabbd32305deb 100644 --- a/crates/ty_test/src/matcher.rs +++ b/crates/ty_test/src/matcher.rs @@ -208,8 +208,11 @@ fn discard_todo_metadata(ty: &str) -> Cow<'_, str> { { /// `@Todo` variants that are hardcoded and always display their message, /// even in release mode. - const PRESERVED_TODO_VARIANTS: &[&str] = - &["@Todo(StarredExpression)", "@Todo(typing.Unpack)"]; + const PRESERVED_TODO_VARIANTS: &[&str] = &[ + "@Todo(StarredExpression)", + "@Todo(typing.Unpack)", + "@Todo(TypeVarTuple)", + ]; static TODO_METADATA_REGEX: LazyLock = LazyLock::new(|| regex::Regex::new(r"@Todo\([^)]*\)").unwrap());