[ty] Fix more false positives related to Generic or Protocol being subscripted with a ParamSpec or TypeVarTuple (#19764)

This commit is contained in:
Alex Waygood 2025-08-05 15:45:56 +01:00 committed by GitHub
parent 934fd37d2b
commit 4090297a11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 47 additions and 4 deletions

View file

@ -7,18 +7,30 @@ At its simplest, to define a generic class using the legacy syntax, you inherit
```py ```py
from ty_extensions import generic_context from ty_extensions import generic_context
from typing import Generic, TypeVar from typing_extensions import Generic, TypeVar, TypeVarTuple, ParamSpec, Unpack
T = TypeVar("T") T = TypeVar("T")
S = TypeVar("S") S = TypeVar("S")
P = ParamSpec("P")
Ts = TypeVarTuple("Ts")
class SingleTypevar(Generic[T]): ... class SingleTypevar(Generic[T]): ...
class MultipleTypevars(Generic[T, S]): ... class MultipleTypevars(Generic[T, S]): ...
class SingleParamSpec(Generic[P]): ...
class TypeVarAndParamSpec(Generic[P, T]): ...
class SingleTypeVarTuple(Generic[Unpack[Ts]]): ...
class TypeVarAndTypeVarTuple(Generic[T, Unpack[Ts]]): ...
# revealed: tuple[T@SingleTypevar] # revealed: tuple[T@SingleTypevar]
reveal_type(generic_context(SingleTypevar)) reveal_type(generic_context(SingleTypevar))
# revealed: tuple[T@MultipleTypevars, S@MultipleTypevars] # revealed: tuple[T@MultipleTypevars, S@MultipleTypevars]
reveal_type(generic_context(MultipleTypevars)) reveal_type(generic_context(MultipleTypevars))
# TODO: support `ParamSpec`/`TypeVarTuple` properly (these should not reveal `None`)
reveal_type(generic_context(SingleParamSpec)) # revealed: None
reveal_type(generic_context(TypeVarAndParamSpec)) # revealed: None
reveal_type(generic_context(SingleTypeVarTuple)) # revealed: None
reveal_type(generic_context(TypeVarAndTypeVarTuple)) # revealed: None
``` ```
Inheriting from `Generic` multiple times yields a `duplicate-base` diagnostic, just like any other Inheriting from `Generic` multiple times yields a `duplicate-base` diagnostic, just like any other

View file

@ -7,19 +7,30 @@ python-version = "3.13"
## Defining a generic class ## Defining a generic class
At its simplest, to define a generic class using PEP 695 syntax, you add a list of typevars after At its simplest, to define a generic class using PEP 695 syntax, you add a list of `TypeVar`s,
the class name. `ParamSpec`s or `TypeVarTuple`s after the class name.
```py ```py
from ty_extensions import generic_context from ty_extensions import generic_context
class SingleTypevar[T]: ... class SingleTypevar[T]: ...
class MultipleTypevars[T, S]: ... class MultipleTypevars[T, S]: ...
class SingleParamSpec[**P]: ...
class TypeVarAndParamSpec[T, **P]: ...
class SingleTypeVarTuple[*Ts]: ...
class TypeVarAndTypeVarTuple[T, *Ts]: ...
# revealed: tuple[T@SingleTypevar] # revealed: tuple[T@SingleTypevar]
reveal_type(generic_context(SingleTypevar)) reveal_type(generic_context(SingleTypevar))
# revealed: tuple[T@MultipleTypevars, S@MultipleTypevars] # revealed: tuple[T@MultipleTypevars, S@MultipleTypevars]
reveal_type(generic_context(MultipleTypevars)) reveal_type(generic_context(MultipleTypevars))
# TODO: support `ParamSpec`/`TypeVarTuple` properly
# (these should include the `ParamSpec`s and `TypeVarTuple`s in their generic contexts)
reveal_type(generic_context(SingleParamSpec)) # revealed: tuple[()]
reveal_type(generic_context(TypeVarAndParamSpec)) # revealed: tuple[T@TypeVarAndParamSpec]
reveal_type(generic_context(SingleTypeVarTuple)) # revealed: tuple[()]
reveal_type(generic_context(TypeVarAndTypeVarTuple)) # revealed: tuple[T@TypeVarAndTypeVarTuple]
``` ```
You cannot use the same typevar more than once. You cannot use the same typevar more than once.

View file

@ -6374,6 +6374,8 @@ pub enum DynamicType {
/// A special Todo-variant for type aliases declared using `typing.TypeAlias`. /// A special Todo-variant for type aliases declared using `typing.TypeAlias`.
/// A temporary variant to detect and special-case the handling of these aliases in autocomplete suggestions. /// A temporary variant to detect and special-case the handling of these aliases in autocomplete suggestions.
TodoTypeAlias, TodoTypeAlias,
/// A special Todo-variant for `Unpack[Ts]`, so that we can treat it specially in `Generic[Unpack[Ts]]`
TodoUnpack,
} }
impl DynamicType { impl DynamicType {
@ -6398,6 +6400,13 @@ impl std::fmt::Display for DynamicType {
f.write_str("@Todo") f.write_str("@Todo")
} }
} }
DynamicType::TodoUnpack => {
if cfg!(debug_assertions) {
f.write_str("@Todo(typing.Unpack)")
} else {
f.write_str("@Todo")
}
}
DynamicType::TodoTypeAlias => { DynamicType::TodoTypeAlias => {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
f.write_str("@Todo(Support for `typing.TypeAlias`)") f.write_str("@Todo(Support for `typing.TypeAlias`)")

View file

@ -52,7 +52,8 @@ impl<'db> ClassBase<'db> {
ClassBase::Dynamic( ClassBase::Dynamic(
DynamicType::Todo(_) DynamicType::Todo(_)
| DynamicType::TodoPEP695ParamSpec | DynamicType::TodoPEP695ParamSpec
| DynamicType::TodoTypeAlias, | DynamicType::TodoTypeAlias
| DynamicType::TodoUnpack,
) => "@Todo", ) => "@Todo",
ClassBase::Protocol => "Protocol", ClassBase::Protocol => "Protocol",
ClassBase::Generic => "Generic", ClassBase::Generic => "Generic",

View file

@ -7286,6 +7286,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
todo @ Type::Dynamic( todo @ Type::Dynamic(
DynamicType::Todo(_) DynamicType::Todo(_)
| DynamicType::TodoPEP695ParamSpec | DynamicType::TodoPEP695ParamSpec
| DynamicType::TodoUnpack
| DynamicType::TodoTypeAlias, | DynamicType::TodoTypeAlias,
), ),
_, _,
@ -7296,6 +7297,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
todo @ Type::Dynamic( todo @ Type::Dynamic(
DynamicType::Todo(_) DynamicType::Todo(_)
| DynamicType::TodoPEP695ParamSpec | DynamicType::TodoPEP695ParamSpec
| DynamicType::TodoUnpack
| DynamicType::TodoTypeAlias, | DynamicType::TodoTypeAlias,
), ),
_, _,
@ -8803,6 +8805,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Some(todo_type!("doubly-specialized typing.Generic")) Some(todo_type!("doubly-specialized typing.Generic"))
} }
(Type::SpecialForm(SpecialFormType::Unpack), _) => {
Some(Type::Dynamic(DynamicType::TodoUnpack))
}
(Type::SpecialForm(special_form), _) if special_form.class().is_special_form() => { (Type::SpecialForm(special_form), _) if special_form.class().is_special_form() => {
Some(todo_type!("Inference of subscript on special form")) Some(todo_type!("Inference of subscript on special form"))
} }
@ -8969,6 +8975,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.iter() .iter()
.map(|typevar| match typevar { .map(|typevar| match typevar {
Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => Ok(*typevar), Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => Ok(*typevar),
Type::Dynamic(DynamicType::TodoUnpack) => Err(GenericContextError::NotYetSupported),
Type::NominalInstance(NominalInstanceType { class, .. }) Type::NominalInstance(NominalInstanceType { class, .. })
if matches!( if matches!(
class.known(self.db()), class.known(self.db()),

View file

@ -262,6 +262,9 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
(DynamicType::TodoPEP695ParamSpec, _) => Ordering::Less, (DynamicType::TodoPEP695ParamSpec, _) => Ordering::Less,
(_, DynamicType::TodoPEP695ParamSpec) => Ordering::Greater, (_, DynamicType::TodoPEP695ParamSpec) => Ordering::Greater,
(DynamicType::TodoUnpack, _) => Ordering::Less,
(_, DynamicType::TodoUnpack) => Ordering::Greater,
(DynamicType::TodoTypeAlias, _) => Ordering::Less, (DynamicType::TodoTypeAlias, _) => Ordering::Less,
(_, DynamicType::TodoTypeAlias) => Ordering::Greater, (_, DynamicType::TodoTypeAlias) => Ordering::Greater,
} }