From 0d9429cc64f6880798e3bb137d64895cbfad9003 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 9 Dec 2025 17:58:34 -0800 Subject: [PATCH] [ty] default-specialize class-literal types in assignment to generic-alias types --- .../resources/mdtest/type_of/basic.md | 55 --------- .../resources/mdtest/type_of/generics.md | 116 ++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 16 +++ 3 files changed, 132 insertions(+), 55 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md index 4e3d9e9f07..da89cc4c16 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md @@ -152,61 +152,6 @@ class Foo(type[int]): ... reveal_mro(Foo) # revealed: (, , ) ``` -## 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 `` 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 diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md index 3c8f157dad..a4ce1048eb 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md @@ -274,3 +274,119 @@ class Foo[T]: ... # error: [invalid-parameter-default] "Default value of type `` 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 `` 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] +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index d0efb0db19..d430d11385 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2709,6 +2709,22 @@ impl<'db> Type<'db> { ) }) .unwrap_or_else(|| ConstraintSet::from(relation.is_assignability())), + + // Similarly, `Literal[]` 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)