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_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]
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -174,6 +174,39 @@ def _(x: Foo[int], y: Bar[str], z: 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
|
||||
|
||||
`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], 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[Any]], type[Foo[Any]]))
|
||||
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]])) # error: [static-assert-error]
|
||||
static_assert(not is_assignable_to(TypeOf[Foo[bool]], type[Bar[int]])) # error: [static-assert-error]
|
||||
static_assert(not is_assignable_to(TypeOf[Bar[int]], type[Foo[bool]]))
|
||||
static_assert(not is_assignable_to(TypeOf[Foo[bool]], type[Bar[int]]))
|
||||
```
|
||||
|
||||
## `type[]` is not assignable to types disjoint from `builtins.type`
|
||||
|
|
|
|||
|
|
@ -1566,7 +1566,7 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
}
|
||||
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)),
|
||||
|
|
@ -2406,7 +2406,7 @@ impl<'db> Type<'db> {
|
|||
.subclass_of()
|
||||
.into_class()
|
||||
.map(|subclass_of_class| {
|
||||
ClassType::NonGeneric(class).has_relation_to_impl(
|
||||
class.default_specialization(db).has_relation_to_impl(
|
||||
db,
|
||||
subclass_of_class,
|
||||
inferable,
|
||||
|
|
@ -6707,7 +6707,9 @@ impl<'db> Type<'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)),
|
||||
};
|
||||
Ok(ty)
|
||||
|
|
|
|||
|
|
@ -362,6 +362,11 @@ impl<'db> VarianceInferable<'db> for GenericAlias<'db> {
|
|||
get_size2::GetSize,
|
||||
)]
|
||||
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>),
|
||||
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]
|
||||
impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
|
||||
#[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()
|
||||
}
|
||||
ast::Expr::Subscript(ast::ExprSubscript {
|
||||
value,
|
||||
slice: parameters,
|
||||
..
|
||||
}) => {
|
||||
ast::Expr::Subscript(
|
||||
subscript @ ast::ExprSubscript {
|
||||
value,
|
||||
slice: parameters,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
let parameters_ty = match self.infer_expression(value, TypeContext::default()) {
|
||||
Type::SpecialForm(SpecialFormType::Union) => match &**parameters {
|
||||
ast::Expr::Tuple(tuple) => {
|
||||
|
|
@ -698,6 +700,40 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
|||
}
|
||||
_ => 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);
|
||||
todo_type!("unsupported nested subscript in type[X]")
|
||||
|
|
|
|||
Loading…
Reference in New Issue