From 5dd56264fb4e394c9919cf897cab39f52ccba124 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 19 Nov 2025 15:06:18 +0100 Subject: [PATCH] [ty] Add tests for generic implicit type aliases (#21522) ## Summary Add a set of comprehensive tests for generic implicit type aliases to illustrate the current behavior with many flavors of `@Todo` types and false positive diagnostics. The tests are partially based on the typing conformance suite, and the expected behavior has been checked against other type checkers. --- .../resources/mdtest/implicit_type_aliases.md | 182 +++++++++++++++++- 1 file changed, 174 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 12cde54595..f5a6292a9b 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -368,25 +368,191 @@ def g(obj: Y): ## Generic types -Implicit type aliases can also refer to generic types: +Implicit type aliases can also be generic: ```py -from typing_extensions import TypeVar +from typing_extensions import TypeVar, ParamSpec, Callable, Union, Annotated T = TypeVar("T") +U = TypeVar("U") + +P = ParamSpec("P") MyList = list[T] - -def _(my_list: MyList[int]): - # TODO: This should be `list[int]` - reveal_type(my_list) # revealed: @Todo(specialized generic alias in type expression) - +MyDict = dict[T, U] +MyType = type[T] +IntAndType = tuple[int, T] +Pair = tuple[T, T] +Sum = tuple[T, U] ListOrTuple = list[T] | tuple[T, ...] +ListOrTupleLegacy = Union[list[T], tuple[T, ...]] +MyCallable = Callable[P, T] +AnnotatedType = Annotated[T, "tag"] +# TODO: Consider displaying this as ``, … instead? (and similar for some others below) +reveal_type(MyList) # revealed: +reveal_type(MyDict) # revealed: +reveal_type(MyType) # revealed: GenericAlias +reveal_type(IntAndType) # revealed: +reveal_type(Pair) # revealed: +reveal_type(Sum) # revealed: reveal_type(ListOrTuple) # revealed: types.UnionType +reveal_type(ListOrTupleLegacy) # revealed: types.UnionType +reveal_type(MyCallable) # revealed: GenericAlias +reveal_type(AnnotatedType) # revealed: -def _(list_or_tuple: ListOrTuple[int]): +def _( + list_of_ints: MyList[int], + dict_str_to_int: MyDict[str, int], + # TODO: no error here + # error: [invalid-type-form] "`typing.TypeVar` is not a generic class" + subclass_of_int: MyType[int], + int_and_str: IntAndType[str], + pair_of_ints: Pair[int], + int_and_bytes: Sum[int, bytes], + list_or_tuple: ListOrTuple[int], + list_or_tuple_legacy: ListOrTupleLegacy[int], + # TODO: no error here + # error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `tuple[str, bytes]`?" + my_callable: MyCallable[[str, bytes], int], + annotated_int: AnnotatedType[int], +): + # TODO: This should be `list[int]` + reveal_type(list_of_ints) # revealed: @Todo(specialized generic alias in type expression) + # TODO: This should be `dict[str, int]` + reveal_type(dict_str_to_int) # revealed: @Todo(specialized generic alias in type expression) + # TODO: This should be `type[int]` + reveal_type(subclass_of_int) # revealed: Unknown + # TODO: This should be `tuple[int, str]` + reveal_type(int_and_str) # revealed: @Todo(specialized generic alias in type expression) + # TODO: This should be `tuple[int, int]` + reveal_type(pair_of_ints) # revealed: @Todo(specialized generic alias in type expression) + # TODO: This should be `tuple[int, bytes]` + reveal_type(int_and_bytes) # revealed: @Todo(specialized generic alias in type expression) + # TODO: This should be `list[int] | tuple[int, ...]` reveal_type(list_or_tuple) # revealed: @Todo(Generic specialization of types.UnionType) + # TODO: This should be `list[int] | tuple[int, ...]` + reveal_type(list_or_tuple_legacy) # revealed: @Todo(Generic specialization of types.UnionType) + # TODO: This should be `(str, bytes) -> int` + reveal_type(my_callable) # revealed: @Todo(Generic specialization of typing.Callable) + # TODO: This should be `int` + reveal_type(annotated_int) # revealed: @Todo(Generic specialization of typing.Annotated) +``` + +Generic implicit type aliases can be partially specialized: + +```py +U = TypeVar("U") + +DictStrTo = MyDict[str, U] + +reveal_type(DictStrTo) # revealed: GenericAlias + +def _( + # TODO: No error here + # error: [invalid-type-form] "Invalid subscript of object of type `GenericAlias` in type expression" + dict_str_to_int: DictStrTo[int], +): + # TODO: This should be `dict[str, int]` + reveal_type(dict_str_to_int) # revealed: Unknown +``` + +Using specializations of generic implicit type aliases in other implicit type aliases works as +expected: + +```py +IntsOrNone = MyList[int] | None +IntsOrStrs = Pair[int] | Pair[str] +ListOfPairs = MyList[Pair[str]] + +reveal_type(IntsOrNone) # revealed: UnionType +reveal_type(IntsOrStrs) # revealed: UnionType +reveal_type(ListOfPairs) # revealed: GenericAlias + +def _( + # TODO: This should not be an error + # error: [invalid-type-form] "Variable of type `UnionType` is not allowed in a type expression" + ints_or_none: IntsOrNone, + # TODO: This should not be an error + # error: [invalid-type-form] "Variable of type `UnionType` is not allowed in a type expression" + ints_or_strs: IntsOrStrs, + list_of_pairs: ListOfPairs, +): + # TODO: This should be `list[int] | None` + reveal_type(ints_or_none) # revealed: Unknown + # TODO: This should be `tuple[int, int] | tuple[str, str]` + reveal_type(ints_or_strs) # revealed: Unknown + # TODO: This should be `list[tuple[str, str]]` + reveal_type(list_of_pairs) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions) +``` + +If a generic implicit type alias is used unspecialized in a type expression, we treat it as an +`Unknown` specialization: + +```py +def _( + my_list: MyList, + my_dict: MyDict, + my_callable: MyCallable, +): + # TODO: Should be `list[Unknown]` + reveal_type(my_list) # revealed: list[typing.TypeVar] + # TODO: Should be `dict[Unknown, Unknown]` + reveal_type(my_dict) # revealed: dict[typing.TypeVar, typing.TypeVar] + # TODO: Should be `(...) -> Unknown` + reveal_type(my_callable) # revealed: (...) -> typing.TypeVar +``` + +(Generic) implicit type aliases can be used as base classes: + +```py +from typing_extensions import Generic +from ty_extensions import reveal_mro + +class GenericBase(Generic[T]): + pass + +ConcreteBase = GenericBase[int] + +class Derived1(ConcreteBase): + pass + +# revealed: (, , typing.Generic, ) +reveal_mro(Derived1) + +GenericBaseAlias = GenericBase[T] + +# TODO: No error here +# error: [non-subscriptable] "Cannot subscript object of type `` with no `__class_getitem__` method" +class Derived2(GenericBaseAlias[int]): + pass +``` + +A generic alias that is already fully specialized cannot be specialized again: + +```py +ListOfInts = list[int] + +# TODO: this should be an error +def _(doubly_specialized: ListOfInts[int]): + # TODO: this should be `Unknown` + reveal_type(doubly_specialized) # revealed: @Todo(specialized generic alias in type expression) +``` + +Specializing a generic implicit type alias with an incorrect number of type arguments also results +in an error: + +```py +def _( + # TODO: this should be an error + list_too_many_args: MyList[int, str], + # TODO: this should be an error + dict_too_few_args: MyDict[int], +): + # TODO: this should be `Unknown` + reveal_type(list_too_many_args) # revealed: @Todo(specialized generic alias in type expression) + # TODO: this should be `Unknown` + reveal_type(dict_too_few_args) # revealed: @Todo(specialized generic alias in type expression) ``` ## `Literal`s