mirror of https://github.com/astral-sh/ruff
[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:
parent
bab688b76c
commit
0631e72187
|
|
@ -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]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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]")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue