[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.
This commit is contained in:
David Peter 2025-11-19 15:06:18 +01:00 committed by GitHub
parent 18a14bfaf1
commit 5dd56264fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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 `<class 'list[T]'>`, … instead? (and similar for some others below)
reveal_type(MyList) # revealed: <class 'list[typing.TypeVar]'>
reveal_type(MyDict) # revealed: <class 'dict[typing.TypeVar, typing.TypeVar]'>
reveal_type(MyType) # revealed: GenericAlias
reveal_type(IntAndType) # revealed: <class 'tuple[int, typing.TypeVar]'>
reveal_type(Pair) # revealed: <class 'tuple[typing.TypeVar, typing.TypeVar]'>
reveal_type(Sum) # revealed: <class 'tuple[typing.TypeVar, typing.TypeVar]'>
reveal_type(ListOrTuple) # revealed: types.UnionType
reveal_type(ListOrTupleLegacy) # revealed: types.UnionType
reveal_type(MyCallable) # revealed: GenericAlias
reveal_type(AnnotatedType) # revealed: <typing.Annotated special form>
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: (<class 'Derived1'>, <class 'GenericBase[int]'>, typing.Generic, <class 'object'>)
reveal_mro(Derived1)
GenericBaseAlias = GenericBase[T]
# TODO: No error here
# error: [non-subscriptable] "Cannot subscript object of type `<class 'GenericBase[typing.TypeVar]'>` 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