[ty] support generic aliases in `type[...]`, like `type[C[int]]` (#21552)

Closes https://github.com/astral-sh/ty/issues/1101.
This commit is contained in:
Jack O'Connor 2025-11-24 13:56:42 -08:00 committed by GitHub
parent bab688b76c
commit 0631e72187
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 88 additions and 18 deletions

View File

@ -171,7 +171,7 @@ class Config:
import generic_a import generic_a
import generic_b import generic_b
# TODO should be error: [invalid-assignment] "Object of type `<class 'generic_b.Container[int]'>` is not assignable to `type[generic_a.Container[int]]`" # error: [invalid-assignment] "Object of type `<class 'generic_b.Container[int]'>` is not assignable to `type[generic_a.Container[int]]`"
container: type[generic_a.Container[int]] = generic_b.Container[int] container: type[generic_a.Container[int]] = generic_b.Container[int]
``` ```

View File

@ -174,6 +174,39 @@ def _(x: Foo[int], y: Bar[str], z: list[bytes]):
reveal_type(type(z)) # revealed: type[list[bytes]] reveal_type(type(z)) # revealed: type[list[bytes]]
``` ```
## Checking generic `type[]` types
```toml
[environment]
python-version = "3.12"
```
```py
class C[T]:
pass
class D[T]:
pass
var: type[C[int]] = C[int]
var: type[C[int]] = D[int] # error: [invalid-assignment] "Object of type `<class 'D[int]'>` is not assignable to `type[C[int]]`"
```
However, generic `Protocol` classes are still TODO:
```py
from typing import Protocol
class Proto[U](Protocol):
def some_method(self): ...
# TODO: should be error: [invalid-assignment]
var: type[Proto[int]] = C[int]
def _(p: type[Proto[int]]):
reveal_type(p) # revealed: type[@Todo(type[T] for protocols)]
```
## `@final` classes ## `@final` classes
`type[]` types are eagerly converted to class-literal types if a class decorated with `@final` is `type[]` types are eagerly converted to class-literal types if a class decorated with `@final` is

View File

@ -243,13 +243,13 @@ static_assert(is_assignable_to(TypeOf[Bar[int]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar[bool]], type[Foo[int]])) static_assert(is_assignable_to(TypeOf[Bar[bool]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]])) static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]])) static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar[Unknown]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar], type[Foo])) static_assert(is_assignable_to(TypeOf[Bar], type[Foo]))
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[Any]])) static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[Any]]))
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]])) static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]]))
# TODO: these should pass (all subscripts inside `type[]` type expressions are currently TODO types) static_assert(not is_assignable_to(TypeOf[Bar[int]], type[Foo[bool]]))
static_assert(not is_assignable_to(TypeOf[Bar[int]], type[Foo[bool]])) # error: [static-assert-error] static_assert(not is_assignable_to(TypeOf[Foo[bool]], type[Bar[int]]))
static_assert(not is_assignable_to(TypeOf[Foo[bool]], type[Bar[int]])) # error: [static-assert-error]
``` ```
## `type[]` is not assignable to types disjoint from `builtins.type` ## `type[]` is not assignable to types disjoint from `builtins.type`

View File

@ -1566,7 +1566,7 @@ impl<'db> Type<'db> {
} }
} }
Type::ClassLiteral(class_literal) => { Type::ClassLiteral(class_literal) => {
Some(ClassType::NonGeneric(class_literal).into_callable(db)) Some(class_literal.default_specialization(db).into_callable(db))
} }
Type::GenericAlias(alias) => Some(ClassType::Generic(alias).into_callable(db)), Type::GenericAlias(alias) => Some(ClassType::Generic(alias).into_callable(db)),
@ -2406,7 +2406,7 @@ impl<'db> Type<'db> {
.subclass_of() .subclass_of()
.into_class() .into_class()
.map(|subclass_of_class| { .map(|subclass_of_class| {
ClassType::NonGeneric(class).has_relation_to_impl( class.default_specialization(db).has_relation_to_impl(
db, db,
subclass_of_class, subclass_of_class,
inferable, inferable,
@ -6707,7 +6707,9 @@ impl<'db> Type<'db> {
KnownClass::Float.to_instance(db), KnownClass::Float.to_instance(db),
], ],
), ),
_ if class.is_typed_dict(db) => Type::typed_dict(*class), _ if class.is_typed_dict(db) => {
Type::typed_dict(class.default_specialization(db))
}
_ => Type::instance(db, class.default_specialization(db)), _ => Type::instance(db, class.default_specialization(db)),
}; };
Ok(ty) Ok(ty)

View File

@ -362,6 +362,11 @@ impl<'db> VarianceInferable<'db> for GenericAlias<'db> {
get_size2::GetSize, get_size2::GetSize,
)] )]
pub enum ClassType<'db> { pub enum ClassType<'db> {
// `NonGeneric` is intended to mean that the `ClassLiteral` has no type parameters. There are
// places where we currently violate this rule (e.g. so that we print `Foo` instead of
// `Foo[Unknown]`), but most callers who need to make a `ClassType` from a `ClassLiteral`
// should use `ClassLiteral::default_specialization` instead of assuming
// `ClassType::NonGeneric`.
NonGeneric(ClassLiteral<'db>), NonGeneric(ClassLiteral<'db>),
Generic(GenericAlias<'db>), Generic(GenericAlias<'db>),
} }
@ -3664,12 +3669,6 @@ impl<'db> From<ClassLiteral<'db>> for Type<'db> {
} }
} }
impl<'db> From<ClassLiteral<'db>> for ClassType<'db> {
fn from(class: ClassLiteral<'db>) -> ClassType<'db> {
ClassType::NonGeneric(class)
}
}
#[salsa::tracked] #[salsa::tracked]
impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
#[salsa::tracked(cycle_initial=crate::types::variance_cycle_initial, heap_size=ruff_memory_usage::heap_size)] #[salsa::tracked(cycle_initial=crate::types::variance_cycle_initial, heap_size=ruff_memory_usage::heap_size)]

View File

@ -679,11 +679,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
} }
Type::unknown() Type::unknown()
} }
ast::Expr::Subscript(ast::ExprSubscript { ast::Expr::Subscript(
value, subscript @ ast::ExprSubscript {
slice: parameters, value,
.. slice: parameters,
}) => { ..
},
) => {
let parameters_ty = match self.infer_expression(value, TypeContext::default()) { let parameters_ty = match self.infer_expression(value, TypeContext::default()) {
Type::SpecialForm(SpecialFormType::Union) => match &**parameters { Type::SpecialForm(SpecialFormType::Union) => match &**parameters {
ast::Expr::Tuple(tuple) => { ast::Expr::Tuple(tuple) => {
@ -698,6 +700,40 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
} }
_ => self.infer_subclass_of_type_expression(parameters), _ => self.infer_subclass_of_type_expression(parameters),
}, },
value_ty @ Type::ClassLiteral(class_literal) => {
if class_literal.is_protocol(self.db()) {
SubclassOfType::from(
self.db(),
todo_type!("type[T] for protocols").expect_dynamic(),
)
} else {
match class_literal.generic_context(self.db()) {
Some(generic_context) => {
let db = self.db();
let specialize = |types: &[Option<Type<'db>>]| {
SubclassOfType::from(
db,
class_literal.apply_specialization(db, |_| {
generic_context
.specialize_partial(db, types.iter().copied())
}),
)
};
self.infer_explicit_callable_specialization(
subscript,
value_ty,
generic_context,
specialize,
)
}
None => {
// TODO: emit a diagnostic if you try to specialize a non-generic class.
self.infer_type_expression(parameters);
todo_type!("specialized non-generic class")
}
}
}
}
_ => { _ => {
self.infer_type_expression(parameters); self.infer_type_expression(parameters);
todo_type!("unsupported nested subscript in type[X]") todo_type!("unsupported nested subscript in type[X]")