From 9e80e5a3a6b4980b792b2ac175a540e1f4d034f4 Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 13 Nov 2025 19:02:24 +0100 Subject: [PATCH] =?UTF-8?q?[ty]=20Support=20`type[=E2=80=A6]`=20and=20`Typ?= =?UTF-8?q?e[=E2=80=A6]`=20in=20implicit=20type=20aliases=20(#21421)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Support `type[…]` in implicit type aliases, for example: ```py SubclassOfInt = type[int] reveal_type(SubclassOfInt) # GenericAlias def _(subclass_of_int: SubclassOfInt): reveal_type(subclass_of_int) # type[int] ``` part of https://github.com/astral-sh/ty/issues/221 ## Typing conformance ```diff -specialtypes_type.py:138:5: error[type-assertion-failure] Argument does not have asserted type `type[Any]` -specialtypes_type.py:140:5: error[type-assertion-failure] Argument does not have asserted type `type[Any]` ``` Two new tests passing :heavy_check_mark: ```diff -specialtypes_type.py:146:1: error[unresolved-attribute] Object of type `GenericAlias` has no attribute `unknown` ``` An `TA4.unknown` attribute on a PEP 613 alias (`TA4: TypeAlias = type[Any]`) is being accessed, and the conformance suite expects this to be an error. Since we currently use the inferred type for these type aliases (and possibly in the future as well), we treat this as a direct access of the attribute on `type[Any]`, which falls back to an access on `Any` itself, which succeeds. :red_circle: ``` +specialtypes_type.py:152:16: error[invalid-type-form] `typing.TypeVar` is not a generic class +specialtypes_type.py:156:16: error[invalid-type-form] `typing.TypeVar` is not a generic class ``` New errors because we don't handle `T = TypeVar("T"); MyType = type[T]; MyType[T]` yet. Support for this is being tracked in https://github.com/astral-sh/ty/issues/221 :red_circle: ## Ecosystem impact Looks mostly good, a few known problems. ## Test Plan New Markdown tests --- .../resources/mdtest/implicit_type_aliases.md | 163 ++++++++++++++++++ .../resources/mdtest/type_of/basic.md | 3 +- crates/ty_python_semantic/src/types.rs | 25 ++- .../src/types/class_base.rs | 3 + .../src/types/infer/builder.rs | 19 +- .../types/infer/builder/type_expression.rs | 2 +- 6 files changed, 207 insertions(+), 8 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index 0a45e9e3c4..fd0610e1aa 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -66,6 +66,8 @@ IntOrAnnotated = int | Annotated[str, "meta"] AnnotatedOrInt = Annotated[str, "meta"] | int IntOrOptional = int | Optional[str] OptionalOrInt = Optional[str] | int +IntOrTypeOfStr = int | type[str] +TypeOfStrOrInt = type[str] | int reveal_type(IntOrStr) # revealed: types.UnionType reveal_type(IntOrStrOrBytes1) # revealed: types.UnionType @@ -97,6 +99,8 @@ reveal_type(IntOrAnnotated) # revealed: types.UnionType reveal_type(AnnotatedOrInt) # revealed: types.UnionType reveal_type(IntOrOptional) # revealed: types.UnionType reveal_type(OptionalOrInt) # revealed: types.UnionType +reveal_type(IntOrTypeOfStr) # revealed: types.UnionType +reveal_type(TypeOfStrOrInt) # revealed: types.UnionType def _( int_or_str: IntOrStr, @@ -129,6 +133,8 @@ def _( annotated_or_int: AnnotatedOrInt, int_or_optional: IntOrOptional, optional_or_int: OptionalOrInt, + int_or_type_of_str: IntOrTypeOfStr, + type_of_str_or_int: TypeOfStrOrInt, ): reveal_type(int_or_str) # revealed: int | str reveal_type(int_or_str_or_bytes1) # revealed: int | str | bytes @@ -160,6 +166,8 @@ def _( reveal_type(annotated_or_int) # revealed: str | int reveal_type(int_or_optional) # revealed: int | str | None reveal_type(optional_or_int) # revealed: str | None | int + reveal_type(int_or_type_of_str) # revealed: int | type[str] + reveal_type(type_of_str_or_int) # revealed: type[str] | int ``` If a type is unioned with itself in a value expression, the result is just that type. No @@ -599,6 +607,158 @@ def _( reveal_type(invalid) # revealed: str | Unknown ``` +## `type[…]` and `Type[…]` + +### `type[…]` + +We support implicit type aliases using `type[…]`: + +```py +from typing import Any, Union, Protocol, TypeVar, Generic + +T = TypeVar("T") + +class A: ... +class B: ... +class G(Generic[T]): ... + +class P(Protocol): + def method(self) -> None: ... + +SubclassOfA = type[A] +SubclassOfAny = type[Any] +SubclassOfAOrB1 = type[A | B] +SubclassOfAOrB2 = type[A] | type[B] +SubclassOfAOrB3 = Union[type[A], type[B]] +SubclassOfG = type[G] +SubclassOfGInt = type[G[int]] +SubclassOfP = type[P] + +reveal_type(SubclassOfA) # revealed: GenericAlias +reveal_type(SubclassOfAny) # revealed: GenericAlias +reveal_type(SubclassOfAOrB1) # revealed: GenericAlias +reveal_type(SubclassOfAOrB2) # revealed: types.UnionType +reveal_type(SubclassOfAOrB3) # revealed: types.UnionType +reveal_type(SubclassOfG) # revealed: GenericAlias +reveal_type(SubclassOfGInt) # revealed: GenericAlias +reveal_type(SubclassOfP) # revealed: GenericAlias + +def _( + subclass_of_a: SubclassOfA, + subclass_of_any: SubclassOfAny, + subclass_of_a_or_b1: SubclassOfAOrB1, + subclass_of_a_or_b2: SubclassOfAOrB2, + subclass_of_a_or_b3: SubclassOfAOrB3, + subclass_of_g: SubclassOfG, + subclass_of_g_int: SubclassOfGInt, + subclass_of_p: SubclassOfP, +): + reveal_type(subclass_of_a) # revealed: type[A] + reveal_type(subclass_of_a()) # revealed: A + + reveal_type(subclass_of_any) # revealed: type[Any] + reveal_type(subclass_of_any()) # revealed: Any + + reveal_type(subclass_of_a_or_b1) # revealed: type[A] | type[B] + reveal_type(subclass_of_a_or_b1()) # revealed: A | B + + reveal_type(subclass_of_a_or_b2) # revealed: type[A] | type[B] + reveal_type(subclass_of_a_or_b2()) # revealed: A | B + + reveal_type(subclass_of_a_or_b3) # revealed: type[A] | type[B] + reveal_type(subclass_of_a_or_b3()) # revealed: A | B + + reveal_type(subclass_of_g) # revealed: type[G[Unknown]] + reveal_type(subclass_of_g()) # revealed: G[Unknown] + + reveal_type(subclass_of_g_int) # revealed: type[G[int]] + reveal_type(subclass_of_g_int()) # revealed: G[int] + + reveal_type(subclass_of_p) # revealed: type[P] +``` + +Invalid uses result in diagnostics: + +```py +# error: [invalid-type-form] +InvalidSubclass = type[1] +``` + +### `Type[…]` + +The same also works for `typing.Type[…]`: + +```py +from typing import Any, Union, Protocol, TypeVar, Generic, Type + +T = TypeVar("T") + +class A: ... +class B: ... +class G(Generic[T]): ... + +class P(Protocol): + def method(self) -> None: ... + +SubclassOfA = Type[A] +SubclassOfAny = Type[Any] +SubclassOfAOrB1 = Type[A | B] +SubclassOfAOrB2 = Type[A] | Type[B] +SubclassOfAOrB3 = Union[Type[A], Type[B]] +SubclassOfG = Type[G] +SubclassOfGInt = Type[G[int]] +SubclassOfP = Type[P] + +reveal_type(SubclassOfA) # revealed: GenericAlias +reveal_type(SubclassOfAny) # revealed: GenericAlias +reveal_type(SubclassOfAOrB1) # revealed: GenericAlias +reveal_type(SubclassOfAOrB2) # revealed: types.UnionType +reveal_type(SubclassOfAOrB3) # revealed: types.UnionType +reveal_type(SubclassOfG) # revealed: GenericAlias +reveal_type(SubclassOfGInt) # revealed: GenericAlias +reveal_type(SubclassOfP) # revealed: GenericAlias + +def _( + subclass_of_a: SubclassOfA, + subclass_of_any: SubclassOfAny, + subclass_of_a_or_b1: SubclassOfAOrB1, + subclass_of_a_or_b2: SubclassOfAOrB2, + subclass_of_a_or_b3: SubclassOfAOrB3, + subclass_of_g: SubclassOfG, + subclass_of_g_int: SubclassOfGInt, + subclass_of_p: SubclassOfP, +): + reveal_type(subclass_of_a) # revealed: type[A] + reveal_type(subclass_of_a()) # revealed: A + + reveal_type(subclass_of_any) # revealed: type[Any] + reveal_type(subclass_of_any()) # revealed: Any + + reveal_type(subclass_of_a_or_b1) # revealed: type[A] | type[B] + reveal_type(subclass_of_a_or_b1()) # revealed: A | B + + reveal_type(subclass_of_a_or_b2) # revealed: type[A] | type[B] + reveal_type(subclass_of_a_or_b2()) # revealed: A | B + + reveal_type(subclass_of_a_or_b3) # revealed: type[A] | type[B] + reveal_type(subclass_of_a_or_b3()) # revealed: A | B + + reveal_type(subclass_of_g) # revealed: type[G[Unknown]] + reveal_type(subclass_of_g()) # revealed: G[Unknown] + + reveal_type(subclass_of_g_int) # revealed: type[G[int]] + reveal_type(subclass_of_g_int()) # revealed: G[int] + + reveal_type(subclass_of_p) # revealed: type[P] +``` + +Invalid uses result in diagnostics: + +```py +# error: [invalid-type-form] +InvalidSubclass = Type[1] +``` + ## Stringified annotations? From the [typing spec on type aliases](https://typing.python.org/en/latest/spec/aliases.html): @@ -633,15 +793,18 @@ from typing import Union ListOfInts = list["int"] StrOrStyle = Union[str, "Style"] +SubclassOfStyle = type["Style"] class Style: ... def _( list_of_ints: ListOfInts, str_or_style: StrOrStyle, + subclass_of_style: SubclassOfStyle, ): reveal_type(list_of_ints) # revealed: list[int] reveal_type(str_or_style) # revealed: str | Style + reveal_type(subclass_of_style) # revealed: type[Style] ``` ## Recursive 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 80223c37a4..3a167d528b 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md @@ -149,8 +149,7 @@ from ty_extensions import reveal_mro class Foo(type[int]): ... -# TODO: should be `tuple[, , ] -reveal_mro(Foo) # revealed: (, @Todo(GenericAlias instance), ) +reveal_mro(Foo) # revealed: (, , ) ``` ## Display of generic `type[]` types diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 194bc6db90..b7f4327ad6 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6607,6 +6607,17 @@ impl<'db> Type<'db> { .inner(db) .in_type_expression(db, scope_id, typevar_binding_context)?) } + KnownInstanceType::TypeGenericAlias(ty) => { + // When `type[…]` appears in a value position (e.g. in an implicit type alias), + // we infer its argument as a type expression. This ensures that we can emit + // diagnostics for invalid type expressions, and more importantly, that we can + // make use of stringified annotations. The drawback is that we need to turn + // instances back into the corresponding subclass-of types here. This process + // (`int` -> instance of `int` -> subclass of `int`) can be lossy, but it is + // okay for all valid arguments to `type[…]`. + + Ok(ty.inner(db).to_meta_type(db)) + } }, Type::SpecialForm(special_form) => match special_form { @@ -7847,6 +7858,9 @@ pub enum KnownInstanceType<'db> { /// A single instance of `typing.Annotated` Annotated(InternedType<'db>), + /// An instance of `typing.GenericAlias` representing a `type[...]` expression. + TypeGenericAlias(InternedType<'db>), + /// An identity callable created with `typing.NewType(name, base)`, which behaves like a /// subtype of `base` in type expressions. See the `struct NewType` payload for an example. NewType(NewType<'db>), @@ -7881,7 +7895,9 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( visitor.visit_type(db, *element); } } - KnownInstanceType::Literal(ty) | KnownInstanceType::Annotated(ty) => { + KnownInstanceType::Literal(ty) + | KnownInstanceType::Annotated(ty) + | KnownInstanceType::TypeGenericAlias(ty) => { visitor.visit_type(db, ty.inner(db)); } KnownInstanceType::NewType(newtype) => { @@ -7928,6 +7944,7 @@ impl<'db> KnownInstanceType<'db> { Self::UnionType(list) => Self::UnionType(list.normalized_impl(db, visitor)), Self::Literal(ty) => Self::Literal(ty.normalized_impl(db, visitor)), Self::Annotated(ty) => Self::Annotated(ty.normalized_impl(db, visitor)), + Self::TypeGenericAlias(ty) => Self::TypeGenericAlias(ty.normalized_impl(db, visitor)), Self::NewType(newtype) => Self::NewType( newtype .map_base_class_type(db, |class_type| class_type.normalized_impl(db, visitor)), @@ -7950,8 +7967,9 @@ impl<'db> KnownInstanceType<'db> { Self::Field(_) => KnownClass::Field, Self::ConstraintSet(_) => KnownClass::ConstraintSet, Self::UnionType(_) => KnownClass::UnionType, - Self::Literal(_) => KnownClass::GenericAlias, - Self::Annotated(_) => KnownClass::GenericAlias, + Self::Literal(_) | Self::Annotated(_) | Self::TypeGenericAlias(_) => { + KnownClass::GenericAlias + } Self::NewType(_) => KnownClass::NewType, } } @@ -8037,6 +8055,7 @@ impl<'db> KnownInstanceType<'db> { KnownInstanceType::Annotated(_) => { f.write_str("") } + KnownInstanceType::TypeGenericAlias(_) => f.write_str("GenericAlias"), KnownInstanceType::NewType(declaration) => { write!(f, "", declaration.name(self.db)) } diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 9cc09acc0f..83f7cf423c 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -180,6 +180,9 @@ impl<'db> ClassBase<'db> { // wrappers are just identity callables at runtime, so this sort of inheritance // doesn't work and isn't allowed. | KnownInstanceType::NewType(_) => None, + KnownInstanceType::TypeGenericAlias(_) => { + Self::try_from_type(db, KnownClass::Type.to_class_literal(db), subclass) + } KnownInstanceType::Annotated(ty) => Self::try_from_type(db, ty.inner(db), subclass), }, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 0f473b3e81..34c333df0e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -9457,7 +9457,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::KnownInstance( KnownInstanceType::UnionType(_) | KnownInstanceType::Literal(_) - | KnownInstanceType::Annotated(_), + | KnownInstanceType::Annotated(_) + | KnownInstanceType::TypeGenericAlias(_), ), Type::ClassLiteral(..) | Type::SubclassOf(..) @@ -9466,7 +9467,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::KnownInstance( KnownInstanceType::UnionType(_) | KnownInstanceType::Literal(_) - | KnownInstanceType::Annotated(_), + | KnownInstanceType::Annotated(_) + | KnownInstanceType::TypeGenericAlias(_), ), ast::Operator::BitOr, ) if pep_604_unions_allowed() => { @@ -10627,7 +10629,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // special cases, too. if class.is_tuple(self.db()) { return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice)); + } else if class.is_known(self.db(), KnownClass::Type) { + let argument_ty = self.infer_type_expression(slice); + return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( + InternedType::new(self.db(), argument_ty), + )); } + if let Some(generic_context) = class.generic_context(self.db()) { return self.infer_explicit_class_specialization( subscript, @@ -10764,6 +10772,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } } + Type::SpecialForm(SpecialFormType::Type) => { + // Similar to the branch above that handles `type[…]`, handle `typing.Type[…]` + let argument_ty = self.infer_type_expression(slice); + return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( + InternedType::new(self.db(), argument_ty), + )); + } _ => {} } diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 40469fdc9c..20c5362a0b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -826,7 +826,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.infer_type_expression(slice); todo_type!("Generic specialization of types.UnionType") } - KnownInstanceType::Literal(ty) => { + KnownInstanceType::Literal(ty) | KnownInstanceType::TypeGenericAlias(ty) => { self.infer_type_expression(slice); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!(