Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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]
```

Expand Down
33 changes: 33 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/type_of/basic.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,15 +241,18 @@ class Bar(Foo[T_co], Generic[T_co]): ...

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]]))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this new Unknown case redundant with the Any case right above it, or are is their behavior different enough that it makes sense to test both?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's redundant, we very rarely handle Unknown differently from Any. But there's also no harm in including it, don't really care either way.

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]]))

# `TypeOf` does not implicitly default-specialize. The unspecialized class literal object `Bar` does
# not inhabit the type `type[Foo[int]]`.
static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]]))
```

## `type[]` is not assignable to types disjoint from `builtins.type`
Expand Down
8 changes: 5 additions & 3 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1561,7 +1561,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)),
Expand Down Expand Up @@ -2386,7 +2386,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,
Expand Down Expand Up @@ -6687,7 +6687,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)
Expand Down
11 changes: 5 additions & 6 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>),
}
Expand Down Expand Up @@ -3662,12 +3667,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)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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]")
Expand Down
Loading