[ty] default-specialize class-literal types in assignment to generic-alias types

This commit is contained in:
Carl Meyer 2025-12-09 17:58:34 -08:00 committed by David Peter
parent 7bf50e70a7
commit 0d9429cc64
3 changed files with 132 additions and 55 deletions

View File

@ -152,61 +152,6 @@ class Foo(type[int]): ...
reveal_mro(Foo) # revealed: (<class 'Foo'>, <class 'type'>, <class 'object'>)
```
## Display of generic `type[]` types
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Generic, TypeVar
class Foo[T]: ...
S = TypeVar("S")
class Bar(Generic[S]): ...
def _(x: Foo[int], y: Bar[str], z: list[bytes]):
reveal_type(type(x)) # revealed: type[Foo[int]]
reveal_type(type(y)) # revealed: type[Bar[str]]
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

View File

@ -274,3 +274,119 @@ class Foo[T]: ...
# error: [invalid-parameter-default] "Default value of type `<class 'Foo'>` is not assignable to annotated parameter type `type[T@f]`"
def f[T: Foo[Any]](x: type[T] = Foo): ...
```
## Display of generic `type[]` types
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Generic, TypeVar
class Foo[T]: ...
S = TypeVar("S")
class Bar(Generic[S]): ...
def _(x: Foo[int], y: Bar[str], z: list[bytes]):
reveal_type(type(x)) # revealed: type[Foo[int]]
reveal_type(type(y)) # revealed: type[Bar[str]]
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)]
```
## Generic `@final` classes
```toml
[environment]
python-version = "3.13"
```
An unspecialized generic final class object is assignable to its default-specialized `type[]` type
(which is actually internally simplified to a GenericAlias type, since there cannot be subclasses.)
```py
from typing import final
@final
class P[T]:
x: T
def expects_type_p(x: type[P]):
pass
def expects_type_p_of_int(x: type[P[int]]):
pass
# OK, the default specialization of `P` is assignable to `type[P[Unknown]]`
expects_type_p(P)
# also OK, because the default specialization is `P[Unknown]` which is assignable to `P[int]`
expects_type_p_of_int(P)
```
The same principles apply when typevar defaults are used, but the results are a bit different
because the default-specialization is no longer a forgiving `Unknown` type:
```py
@final
class P[T = str]:
x: T
def expects_type_p(x: type[P]):
pass
def expects_type_p_of_int(x: type[P[int]]):
pass
def expects_type_p_of_str(x: type[P[str]]):
pass
# OK, the default specialization is now `P[str]`, but we have the default specialization on both
# sides, so it is assignable.
expects_type_p(P)
# Also OK if the explicit specialization lines up with the default, in either direction:
expects_type_p(P[str])
expects_type_p_of_str(P)
# Not OK if the specializations don't line up:
expects_type_p(P[int]) # error: [invalid-argument-type]
expects_type_p_of_int(P[str]) # error: [invalid-argument-type]
expects_type_p_of_str(P[int]) # error: [invalid-argument-type]
```

View File

@ -2709,6 +2709,22 @@ impl<'db> Type<'db> {
)
})
.unwrap_or_else(|| ConstraintSet::from(relation.is_assignability())),
// Similarly, `Literal[<class 'C'>]` is assignable to `C[...]` (a generic-alias type)
// if the default specialization of `C` is assignable to `C[...]`. This scenario
// occurs with final generic types, where `type[C[...]]` is simplified to the
// generic-alias type `C[...]`, due to the fact that `C[...]` has no subclasses.
(Type::ClassLiteral(class), Type::GenericAlias(target_alias)) => {
class.default_specialization(db).has_relation_to_impl(
db,
ClassType::from(target_alias),
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
}
(Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
.subclass_of()
.into_class(db)