[ty] Generic types aliases (implicit and PEP 613) (#21553)

## Summary

Add support for generic PEP 613 type aliases and generic implicit type
aliases:
```py
from typing import TypeVar

T = TypeVar("T")
ListOrSet = list[T] | set[T]

def _(xs: ListOrSet[int]):
    reveal_type(xs)  # list[int] | set[int]
```

closes https://github.com/astral-sh/ty/issues/1643
closes https://github.com/astral-sh/ty/issues/1629
closes https://github.com/astral-sh/ty/issues/1596
closes https://github.com/astral-sh/ty/issues/573
closes https://github.com/astral-sh/ty/issues/221

## Typing conformance

```diff
-aliases_explicit.py:52:5: error[type-assertion-failure] Type `list[int]` does not match asserted type `@Todo(specialized generic alias in type expression)`
-aliases_explicit.py:53:5: error[type-assertion-failure] Type `tuple[str, ...] | list[str]` does not match asserted type `@Todo(Generic specialization of types.UnionType)`
-aliases_explicit.py:54:5: error[type-assertion-failure] Type `tuple[int, int, int, str]` does not match asserted type `@Todo(specialized generic alias in type expression)`
-aliases_explicit.py:56:5: error[type-assertion-failure] Type `(int, str, /) -> str` does not match asserted type `@Todo(Generic specialization of typing.Callable)`
-aliases_explicit.py:59:5: error[type-assertion-failure] Type `int | str | None | list[list[int]]` does not match asserted type `int | str | None | list[@Todo(specialized generic alias in type expression)]`
```

New true negatives ✔️ 

```diff
+aliases_explicit.py:41:36: error[invalid-type-arguments] Too many type arguments: expected 1, got 2
-aliases_explicit.py:57:5: error[type-assertion-failure] Type `(int, str, str, /) -> None` does not match asserted type `@Todo(Generic specialization of typing.Callable)`
+aliases_explicit.py:57:5: error[type-assertion-failure] Type `(int, str, str, /) -> None` does not match asserted type `(...) -> Unknown`
```

These require `ParamSpec`

```diff
+aliases_explicit.py:67:24: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
+aliases_explicit.py:68:24: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
+aliases_explicit.py:69:29: error[invalid-type-arguments] Too many type arguments: expected 1, got 2
+aliases_explicit.py:70:29: error[invalid-type-arguments] Too many type arguments: expected 1, got 2
+aliases_explicit.py:71:29: error[invalid-type-arguments] Too many type arguments: expected 1, got 2
+aliases_explicit.py:102:20: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
```

New true positives ✔️ 

```diff
-aliases_implicit.py:63:5: error[type-assertion-failure] Type `list[int]` does not match asserted type `@Todo(specialized generic alias in type expression)`
-aliases_implicit.py:64:5: error[type-assertion-failure] Type `tuple[str, ...] | list[str]` does not match asserted type `@Todo(Generic specialization of types.UnionType)`
-aliases_implicit.py:65:5: error[type-assertion-failure] Type `tuple[int, int, int, str]` does not match asserted type `@Todo(specialized generic alias in type expression)`
-aliases_implicit.py:67:5: error[type-assertion-failure] Type `(int, str, /) -> str` does not match asserted type `@Todo(Generic specialization of typing.Callable)`
-aliases_implicit.py:70:5: error[type-assertion-failure] Type `int | str | None | list[list[int]]` does not match asserted type `int | str | None | list[@Todo(specialized generic alias in type expression)]`
-aliases_implicit.py:71:5: error[type-assertion-failure] Type `list[bool]` does not match asserted type `@Todo(specialized generic alias in type expression)`
```

New true negatives ✔️ 

```diff
+aliases_implicit.py:54:36: error[invalid-type-arguments] Too many type arguments: expected 1, got 2
-aliases_implicit.py:68:5: error[type-assertion-failure] Type `(int, str, str, /) -> None` does not match asserted type `@Todo(Generic specialization of typing.Callable)`
+aliases_implicit.py:68:5: error[type-assertion-failure] Type `(int, str, str, /) -> None` does not match asserted type `(...) -> Unknown`
```

These require `ParamSpec`

```diff
+aliases_implicit.py:76:24: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
+aliases_implicit.py:77:24: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
+aliases_implicit.py:78:29: error[invalid-type-arguments] Too many type arguments: expected 1, got 2
+aliases_implicit.py:79:29: error[invalid-type-arguments] Too many type arguments: expected 1, got 2
+aliases_implicit.py:80:29: error[invalid-type-arguments] Too many type arguments: expected 1, got 2
+aliases_implicit.py:81:25: error[invalid-type-arguments] Type `str` is not assignable to upper bound `int | float` of type variable `TFloat@GoodTypeAlias12`
+aliases_implicit.py:135:20: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
```

New true positives ✔️ 

```diff
+callables_annotation.py:172:19: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
+callables_annotation.py:175:19: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
+callables_annotation.py:188:25: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
+callables_annotation.py:189:25: error[invalid-type-arguments] Too many type arguments: expected 0, got 1
```

These require `ParamSpec` and `Concatenate`.

```diff
-generics_defaults_specialization.py:26:5: error[type-assertion-failure] Type `SomethingWithNoDefaults[int, str]` does not match asserted type `SomethingWithNoDefaults[int, typing.TypeVar]`
+generics_defaults_specialization.py:26:5: error[type-assertion-failure] Type `SomethingWithNoDefaults[int, str]` does not match asserted type `SomethingWithNoDefaults[int, DefaultStrT]`
```

Favorable diagnostic change ✔️ 

```diff
-generics_defaults_specialization.py:27:5: error[type-assertion-failure] Type `SomethingWithNoDefaults[int, bool]` does not match asserted type `@Todo(specialized generic alias in type expression)`
```

New true negative ✔️ 

```diff
-generics_defaults_specialization.py:30:1: error[non-subscriptable] Cannot subscript object of type `<class 'SomethingWithNoDefaults[int, typing.TypeVar]'>` with no `__class_getitem__` method
+generics_defaults_specialization.py:30:15: error[invalid-type-arguments] Too many type arguments: expected between 0 and 1, got 2
```

Correct new diagnostic ✔️ 


```diff
-generics_variance.py:175:25: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:175:35: error[non-subscriptable] Cannot subscript object of type `<class 'Co[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:179:29: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:179:39: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:183:21: error[non-subscriptable] Cannot subscript object of type `<class 'Co[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:183:27: error[non-subscriptable] Cannot subscript object of type `<class 'Co[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:187:25: error[non-subscriptable] Cannot subscript object of type `<class 'Co[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:187:31: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:191:33: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:191:43: error[non-subscriptable] Cannot subscript object of type `<class 'Co[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:191:49: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:196:5: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:196:15: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
-generics_variance.py:196:25: error[non-subscriptable] Cannot subscript object of type `<class 'Contra[typing.TypeVar]'>` with no `__class_getitem__` method
```

One of these should apparently be an error, but not of this kind, so
this is good ✔️

```diff
-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
```

Good, those were false positives. ✔️ 

I skipped the analysis for everything involving `TypeVarTuple`.

## Ecosystem impact

**[Full report with detailed
diff](https://david-generic-implicit-alias.ecosystem-663.pages.dev/diff)**

Previous iterations of this PR showed all kinds of problems. In it's
current state, I do not see any large systematic problems, but it is
hard to tell with 5k diagnostic changes.

## Performance

* There is a huge 4x regression in `colour-science/colour`, related to
[this large
file](https://github.com/colour-science/colour/blob/develop/colour/io/luts/tests/test_lut.py)
with [many assignments of hard-coded arrays (lists of lists) to
`np.NDArray`
types](83e754c8b6/colour/io/luts/tests/test_lut.py (L701-L781))
that we now understand. We now take ~2 seconds to check this file, so
definitely not great, but maybe acceptable for now.

## Test Plan

Updated and new Markdown tests
This commit is contained in:
David Peter 2025-11-28 20:38:24 +01:00 committed by GitHub
parent 594b7b04d3
commit 42f152108a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 652 additions and 140 deletions

View File

@ -120,7 +120,7 @@ static COLOUR_SCIENCE: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17", max_dep_date: "2025-06-17",
python_version: PythonVersion::PY310, python_version: PythonVersion::PY310,
}, },
600, 1070,
); );
static FREQTRADE: Benchmark = Benchmark::new( static FREQTRADE: Benchmark = Benchmark::new(

View File

@ -61,8 +61,7 @@ async def main():
result = await task result = await task
# TODO: this should be `int` reveal_type(result) # revealed: int
reveal_type(result) # revealed: Unknown
``` ```
### `asyncio.gather` ### `asyncio.gather`
@ -79,9 +78,8 @@ async def main():
task("B"), task("B"),
) )
# TODO: these should be `int` reveal_type(a) # revealed: int
reveal_type(a) # revealed: Unknown reveal_type(b) # revealed: int
reveal_type(b) # revealed: Unknown
``` ```
## Under the hood ## Under the hood

View File

@ -191,13 +191,13 @@ def _(
reveal_type(int_or_callable) # revealed: int | ((str, /) -> bytes) reveal_type(int_or_callable) # revealed: int | ((str, /) -> bytes)
reveal_type(callable_or_int) # revealed: ((str, /) -> bytes) | int reveal_type(callable_or_int) # revealed: ((str, /) -> bytes) | int
# TODO should be Unknown | int # TODO should be Unknown | int
reveal_type(type_var_or_int) # revealed: typing.TypeVar | int reveal_type(type_var_or_int) # revealed: T@TypeVarOrInt | int
# TODO should be int | Unknown # TODO should be int | Unknown
reveal_type(int_or_type_var) # revealed: int | typing.TypeVar reveal_type(int_or_type_var) # revealed: int | T@IntOrTypeVar
# TODO should be Unknown | None # TODO should be Unknown | None
reveal_type(type_var_or_none) # revealed: typing.TypeVar | None reveal_type(type_var_or_none) # revealed: T@TypeVarOrNone | None
# TODO should be None | Unknown # TODO should be None | Unknown
reveal_type(none_or_type_var) # revealed: None | typing.TypeVar reveal_type(none_or_type_var) # revealed: None | T@NoneOrTypeVar
``` ```
If a type is unioned with itself in a value expression, the result is just that type. No If a type is unioned with itself in a value expression, the result is just that type. No
@ -366,7 +366,9 @@ def g(obj: Y):
reveal_type(obj) # revealed: list[int | str] reveal_type(obj) # revealed: list[int | str]
``` ```
## Generic types ## Generic implicit type aliases
### Functionality
Implicit type aliases can also be generic: Implicit type aliases can also be generic:
@ -388,73 +390,62 @@ ListOrTuple = list[T] | tuple[T, ...]
ListOrTupleLegacy = Union[list[T], tuple[T, ...]] ListOrTupleLegacy = Union[list[T], tuple[T, ...]]
MyCallable = Callable[P, T] MyCallable = Callable[P, T]
AnnotatedType = Annotated[T, "tag"] AnnotatedType = Annotated[T, "tag"]
TransparentAlias = T
MyOptional = T | None
# TODO: Consider displaying this as `<class 'list[T]'>`, … instead? (and similar for some others below) reveal_type(MyList) # revealed: <class 'list[T@MyList]'>
reveal_type(MyList) # revealed: <class 'list[typing.TypeVar]'> reveal_type(MyDict) # revealed: <class 'dict[T@MyDict, U@MyDict]'>
reveal_type(MyDict) # revealed: <class 'dict[typing.TypeVar, typing.TypeVar]'>
reveal_type(MyType) # revealed: GenericAlias reveal_type(MyType) # revealed: GenericAlias
reveal_type(IntAndType) # revealed: <class 'tuple[int, typing.TypeVar]'> reveal_type(IntAndType) # revealed: <class 'tuple[int, T@IntAndType]'>
reveal_type(Pair) # revealed: <class 'tuple[typing.TypeVar, typing.TypeVar]'> reveal_type(Pair) # revealed: <class 'tuple[T@Pair, T@Pair]'>
reveal_type(Sum) # revealed: <class 'tuple[typing.TypeVar, typing.TypeVar]'> reveal_type(Sum) # revealed: <class 'tuple[T@Sum, U@Sum]'>
reveal_type(ListOrTuple) # revealed: types.UnionType reveal_type(ListOrTuple) # revealed: types.UnionType
reveal_type(ListOrTupleLegacy) # revealed: types.UnionType reveal_type(ListOrTupleLegacy) # revealed: types.UnionType
reveal_type(MyCallable) # revealed: GenericAlias reveal_type(MyCallable) # revealed: @Todo(Callable[..] specialized with ParamSpec)
reveal_type(AnnotatedType) # revealed: <typing.Annotated special form> reveal_type(AnnotatedType) # revealed: <typing.Annotated special form>
reveal_type(TransparentAlias) # revealed: typing.TypeVar
reveal_type(MyOptional) # revealed: types.UnionType
def _( def _(
list_of_ints: MyList[int], list_of_ints: MyList[int],
dict_str_to_int: MyDict[str, 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], subclass_of_int: MyType[int],
int_and_str: IntAndType[str], int_and_str: IntAndType[str],
pair_of_ints: Pair[int], pair_of_ints: Pair[int],
int_and_bytes: Sum[int, bytes], int_and_bytes: Sum[int, bytes],
list_or_tuple: ListOrTuple[int], list_or_tuple: ListOrTuple[int],
list_or_tuple_legacy: ListOrTupleLegacy[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], my_callable: MyCallable[[str, bytes], int],
annotated_int: AnnotatedType[int], annotated_int: AnnotatedType[int],
transparent_alias: TransparentAlias[int],
optional_int: MyOptional[int],
): ):
# TODO: This should be `list[int]` reveal_type(list_of_ints) # revealed: list[int]
reveal_type(list_of_ints) # revealed: @Todo(specialized generic alias in type expression) reveal_type(dict_str_to_int) # revealed: dict[str, int]
# TODO: This should be `dict[str, int]` reveal_type(subclass_of_int) # revealed: type[int]
reveal_type(dict_str_to_int) # revealed: @Todo(specialized generic alias in type expression) reveal_type(int_and_str) # revealed: tuple[int, str]
# TODO: This should be `type[int]` reveal_type(pair_of_ints) # revealed: tuple[int, int]
reveal_type(subclass_of_int) # revealed: Unknown reveal_type(int_and_bytes) # revealed: tuple[int, bytes]
# TODO: This should be `tuple[int, str]` reveal_type(list_or_tuple) # revealed: list[int] | tuple[int, ...]
reveal_type(int_and_str) # revealed: @Todo(specialized generic alias in type expression) reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...]
# 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` # TODO: This should be `(str, bytes) -> int`
reveal_type(my_callable) # revealed: @Todo(Generic specialization of typing.Callable) reveal_type(my_callable) # revealed: @Todo(Callable[..] specialized with ParamSpec)
# TODO: This should be `int` reveal_type(annotated_int) # revealed: int
reveal_type(annotated_int) # revealed: @Todo(Generic specialization of typing.Annotated) reveal_type(transparent_alias) # revealed: int
reveal_type(optional_int) # revealed: int | None
``` ```
Generic implicit type aliases can be partially specialized: Generic implicit type aliases can be partially specialized:
```py ```py
U = TypeVar("U")
DictStrTo = MyDict[str, U] DictStrTo = MyDict[str, U]
reveal_type(DictStrTo) # revealed: GenericAlias reveal_type(DictStrTo) # revealed: <class 'dict[str, U@DictStrTo]'>
def _( 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], dict_str_to_int: DictStrTo[int],
): ):
# TODO: This should be `dict[str, int]` reveal_type(dict_str_to_int) # revealed: 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 Using specializations of generic implicit type aliases in other implicit type aliases works as
@ -464,43 +455,118 @@ expected:
IntsOrNone = MyList[int] | None IntsOrNone = MyList[int] | None
IntsOrStrs = Pair[int] | Pair[str] IntsOrStrs = Pair[int] | Pair[str]
ListOfPairs = MyList[Pair[str]] ListOfPairs = MyList[Pair[str]]
ListOrTupleOfInts = ListOrTuple[int]
AnnotatedInt = AnnotatedType[int]
SubclassOfInt = MyType[int]
CallableIntToStr = MyCallable[[int], str]
reveal_type(IntsOrNone) # revealed: UnionType reveal_type(IntsOrNone) # revealed: types.UnionType
reveal_type(IntsOrStrs) # revealed: UnionType reveal_type(IntsOrStrs) # revealed: types.UnionType
reveal_type(ListOfPairs) # revealed: GenericAlias reveal_type(ListOfPairs) # revealed: <class 'list[tuple[str, str]]'>
reveal_type(ListOrTupleOfInts) # revealed: types.UnionType
reveal_type(AnnotatedInt) # revealed: <typing.Annotated special form>
reveal_type(SubclassOfInt) # revealed: GenericAlias
reveal_type(CallableIntToStr) # revealed: @Todo(Callable[..] specialized with ParamSpec)
def _( 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, 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, ints_or_strs: IntsOrStrs,
list_of_pairs: ListOfPairs, list_of_pairs: ListOfPairs,
list_or_tuple_of_ints: ListOrTupleOfInts,
annotated_int: AnnotatedInt,
subclass_of_int: SubclassOfInt,
callable_int_to_str: CallableIntToStr,
): ):
# TODO: This should be `list[int] | None` reveal_type(ints_or_none) # revealed: list[int] | None
reveal_type(ints_or_none) # revealed: Unknown reveal_type(ints_or_strs) # revealed: tuple[int, int] | tuple[str, str]
# TODO: This should be `tuple[int, int] | tuple[str, str]` reveal_type(list_of_pairs) # revealed: list[tuple[str, str]]
reveal_type(ints_or_strs) # revealed: Unknown reveal_type(list_or_tuple_of_ints) # revealed: list[int] | tuple[int, ...]
# TODO: This should be `list[tuple[str, str]]` reveal_type(annotated_int) # revealed: int
reveal_type(list_of_pairs) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions) reveal_type(subclass_of_int) # revealed: type[int]
# TODO: This should be `(int, /) -> str`
reveal_type(callable_int_to_str) # revealed: @Todo(Callable[..] specialized with ParamSpec)
``` ```
If a generic implicit type alias is used unspecialized in a type expression, we treat it as an A generic implicit type alias can also be used in another generic implicit type alias:
`Unknown` specialization:
```py
from typing_extensions import Any
B = TypeVar("B", bound=int)
MyOtherList = MyList[T]
MyOtherType = MyType[T]
TypeOrList = MyType[B] | MyList[B]
reveal_type(MyOtherList) # revealed: <class 'list[T@MyOtherList]'>
reveal_type(MyOtherType) # revealed: GenericAlias
reveal_type(TypeOrList) # revealed: types.UnionType
def _(
list_of_ints: MyOtherList[int],
subclass_of_int: MyOtherType[int],
type_or_list: TypeOrList[Any],
):
reveal_type(list_of_ints) # revealed: list[int]
reveal_type(subclass_of_int) # revealed: type[int]
reveal_type(type_or_list) # revealed: type[Any] | list[Any]
```
If a generic implicit type alias is used unspecialized in a type expression, we use the default
specialization. For type variables without defaults, this is `Unknown`:
```py ```py
def _( def _(
my_list: MyList, list_unknown: MyList,
my_dict: MyDict, dict_unknown: MyDict,
subclass_of_unknown: MyType,
int_and_unknown: IntAndType,
pair_of_unknown: Pair,
unknown_and_unknown: Sum,
list_or_tuple: ListOrTuple,
list_or_tuple_legacy: ListOrTupleLegacy,
my_callable: MyCallable, my_callable: MyCallable,
annotated_unknown: AnnotatedType,
optional_unknown: MyOptional,
): ):
# TODO: Should be `list[Unknown]` # TODO: This should be `list[Unknown]`
reveal_type(my_list) # revealed: list[typing.TypeVar] reveal_type(list_unknown) # revealed: list[T@MyList]
# TODO: Should be `dict[Unknown, Unknown]` # TODO: This should be `dict[Unknown, Unknown]`
reveal_type(my_dict) # revealed: dict[typing.TypeVar, typing.TypeVar] reveal_type(dict_unknown) # revealed: dict[T@MyDict, U@MyDict]
# TODO: Should be `type[Unknown]`
reveal_type(subclass_of_unknown) # revealed: type[T@MyType]
# TODO: Should be `tuple[int, Unknown]`
reveal_type(int_and_unknown) # revealed: tuple[int, T@IntAndType]
# TODO: Should be `tuple[Unknown, Unknown]`
reveal_type(pair_of_unknown) # revealed: tuple[T@Pair, T@Pair]
# TODO: Should be `tuple[Unknown, Unknown]`
reveal_type(unknown_and_unknown) # revealed: tuple[T@Sum, U@Sum]
# TODO: Should be `list[Unknown] | tuple[Unknown, ...]`
reveal_type(list_or_tuple) # revealed: list[T@ListOrTuple] | tuple[T@ListOrTuple, ...]
# TODO: Should be `list[Unknown] | tuple[Unknown, ...]`
reveal_type(list_or_tuple_legacy) # revealed: list[T@ListOrTupleLegacy] | tuple[T@ListOrTupleLegacy, ...]
# TODO: Should be `(...) -> Unknown` # TODO: Should be `(...) -> Unknown`
reveal_type(my_callable) # revealed: (...) -> typing.TypeVar reveal_type(my_callable) # revealed: @Todo(Callable[..] specialized with ParamSpec)
# TODO: Should be `Unknown`
reveal_type(annotated_unknown) # revealed: T@AnnotatedType
# TODO: Should be `Unknown | None`
reveal_type(optional_unknown) # revealed: T@MyOptional | None
```
For a type variable with a default, we use the default type:
```py
T_default = TypeVar("T_default", default=int)
MyListWithDefault = list[T_default]
def _(
list_of_str: MyListWithDefault[str],
list_of_int: MyListWithDefault,
):
reveal_type(list_of_str) # revealed: list[str]
# TODO: this should be `list[int]`
reveal_type(list_of_int) # revealed: list[T_default@MyListWithDefault]
``` ```
(Generic) implicit type aliases can be used as base classes: (Generic) implicit type aliases can be used as base classes:
@ -522,37 +588,209 @@ reveal_mro(Derived1)
GenericBaseAlias = GenericBase[T] 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]): class Derived2(GenericBaseAlias[int]):
pass pass
``` ```
### Imported aliases
Generic implicit type aliases can be imported from other modules and specialized:
`my_types.py`:
```py
from typing_extensions import TypeVar
T = TypeVar("T")
MyList = list[T]
```
`main.py`:
```py
from my_types import MyList
import my_types as mt
def _(
list_of_ints1: MyList[int],
list_of_ints2: mt.MyList[int],
):
reveal_type(list_of_ints1) # revealed: list[int]
reveal_type(list_of_ints2) # revealed: list[int]
```
### In stringified annotations
Generic implicit type aliases can be specialized in stringified annotations:
```py
from typing_extensions import TypeVar
T = TypeVar("T")
MyList = list[T]
def _(
list_of_ints: "MyList[int]",
):
reveal_type(list_of_ints) # revealed: list[int]
```
### Tuple unpacking
```toml
[environment]
python-version = "3.11"
```
```py
from typing import TypeVar
T = TypeVar("T")
U = TypeVar("U")
V = TypeVar("V")
X = tuple[T, *tuple[U, ...], V]
Y = X[T, tuple[int, str, U], bytes]
def g(obj: Y[bool, range]):
reveal_type(obj) # revealed: tuple[bool, *tuple[tuple[int, str, range], ...], bytes]
```
### Error cases
A generic alias that is already fully specialized cannot be specialized again: A generic alias that is already fully specialized cannot be specialized again:
```py ```py
ListOfInts = list[int] ListOfInts = list[int]
# TODO: this should be an error # error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1"
def _(doubly_specialized: ListOfInts[int]): def _(doubly_specialized: ListOfInts[int]):
# TODO: this should be `Unknown` # TODO: This should ideally be `list[Unknown]` or `Unknown`
reveal_type(doubly_specialized) # revealed: @Todo(specialized generic alias in type expression) reveal_type(doubly_specialized) # revealed: list[int]
``` ```
Specializing a generic implicit type alias with an incorrect number of type arguments also results Specializing a generic implicit type alias with an incorrect number of type arguments also results
in an error: in an error:
```py ```py
from typing_extensions import TypeVar
T = TypeVar("T")
U = TypeVar("U")
MyList = list[T]
MyDict = dict[T, U]
def _( def _(
# TODO: this should be an error # error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2"
list_too_many_args: MyList[int, str], list_too_many_args: MyList[int, str],
# TODO: this should be an error # error: [invalid-type-arguments] "No type argument provided for required type variable `U`"
dict_too_few_args: MyDict[int], dict_too_few_args: MyDict[int],
): ):
# TODO: this should be `Unknown` reveal_type(list_too_many_args) # revealed: list[Unknown]
reveal_type(list_too_many_args) # revealed: @Todo(specialized generic alias in type expression) reveal_type(dict_too_few_args) # revealed: dict[Unknown, Unknown]
# TODO: this should be `Unknown` ```
reveal_type(dict_too_few_args) # revealed: @Todo(specialized generic alias in type expression)
Trying to specialize a non-name node results in an error:
```py
from ty_extensions import TypeOf
IntOrStr = int | str
def this_does_not_work() -> TypeOf[IntOrStr]:
raise NotImplementedError()
def _(
# TODO: Better error message (of kind `invalid-type-form`)?
# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1"
specialized: this_does_not_work()[int],
):
reveal_type(specialized) # revealed: int | str
```
Similarly, if you try to specialize a union type without a binding context, we emit an error:
```py
# TODO: Better error message (of kind `invalid-type-form`)?
# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1"
x: (list[T] | set[T])[int]
def _():
# TODO: `list[Unknown] | set[Unknown]` might be better
reveal_type(x) # revealed: list[typing.TypeVar] | set[typing.TypeVar]
```
### Multiple definitions
#### Shadowed definitions
When a generic type alias shadows a definition from an outer scope, the inner definition is used:
```py
from typing_extensions import TypeVar
T = TypeVar("T")
MyAlias = list[T]
def outer():
MyAlias = set[T]
def _(x: MyAlias[int]):
reveal_type(x) # revealed: set[int]
```
#### Statically known conditions
```py
from typing_extensions import TypeVar
T = TypeVar("T")
if True:
MyAlias1 = list[T]
else:
MyAlias1 = set[T]
if False:
MyAlias2 = list[T]
else:
MyAlias2 = set[T]
def _(
x1: MyAlias1[int],
x2: MyAlias2[int],
):
reveal_type(x1) # revealed: list[int]
reveal_type(x2) # revealed: set[int]
```
#### Statically unknown conditions
If several definitions are visible, we emit an error:
```py
from typing_extensions import TypeVar
T = TypeVar("T")
def flag() -> bool:
return True
if flag():
MyAlias = list[T]
else:
MyAlias = set[T]
# It is questionable whether this should be supported or not. It might also be reasonable to
# emit an error here (e.g. "Invalid subscript of object of type `<class 'list[T@MyAlias]'> |
# <class 'set[T@MyAlias]'>` in type expression"). If we ever choose to do so, the revealed
# type should probably be `Unknown`.
def _(x: MyAlias[int]):
reveal_type(x) # revealed: list[int] | set[int]
``` ```
## `Literal`s ## `Literal`s
@ -642,8 +880,7 @@ Deprecated = Annotated[T, "deprecated attribute"]
class C: class C:
old: Deprecated[int] old: Deprecated[int]
# TODO: Should be `int` reveal_type(C().old) # revealed: int
reveal_type(C().old) # revealed: @Todo(Generic specialization of typing.Annotated)
``` ```
If the metadata argument is missing, we emit an error (because this code fails at runtime), but If the metadata argument is missing, we emit an error (because this code fails at runtime), but
@ -1298,3 +1535,21 @@ def _(
reveal_type(recursive_dict3) # revealed: dict[Divergent, int] reveal_type(recursive_dict3) # revealed: dict[Divergent, int]
reveal_type(recursive_dict4) # revealed: dict[Divergent, int] reveal_type(recursive_dict4) # revealed: dict[Divergent, int]
``` ```
### Self-referential generic implicit type aliases
```py
from typing import TypeVar
T = TypeVar("T")
NestedDict = dict[str, "NestedDict[T] | T"]
NestedList = list["NestedList[T] | None"]
def _(
nested_dict_int: NestedDict[int],
nested_list_str: NestedList[str],
):
reveal_type(nested_dict_int) # revealed: dict[str, Divergent]
reveal_type(nested_list_str) # revealed: list[Divergent]
```

View File

@ -44,9 +44,6 @@ class _SupportsDType(Protocol[_DTypeT_co]):
@property @property
def dtype(self) -> _DTypeT_co: ... def dtype(self) -> _DTypeT_co: ...
# TODO: no errors here
# error: [invalid-type-arguments] "Type `typing.TypeVar` is not assignable to upper bound `generic[Any]` of type variable `_ScalarT_co@dtype`"
# error: [invalid-type-arguments] "Type `typing.TypeVar` is not assignable to upper bound `generic[Any]` of type variable `_ScalarT_co@dtype`"
_DTypeLike: TypeAlias = type[_ScalarT] | dtype[_ScalarT] | _SupportsDType[dtype[_ScalarT]] _DTypeLike: TypeAlias = type[_ScalarT] | dtype[_ScalarT] | _SupportsDType[dtype[_ScalarT]]
DTypeLike: TypeAlias = _DTypeLike[Any] | str | None DTypeLike: TypeAlias = _DTypeLike[Any] | str | None

View File

@ -96,6 +96,45 @@ def _(x: MyAlias):
reveal_type(x) # revealed: int | ((str, /) -> int) reveal_type(x) # revealed: int | ((str, /) -> int)
``` ```
## Generic aliases
A more comprehensive set of tests can be found in
[`implicit_type_aliases.md`](./implicit_type_aliases.md). If the implementations ever diverge, we
may need to duplicate more tests here.
### Basic
```py
from typing import TypeAlias, TypeVar
T = TypeVar("T")
MyList: TypeAlias = list[T]
ListOrSet: TypeAlias = list[T] | set[T]
reveal_type(MyList) # revealed: <class 'list[T]'>
reveal_type(ListOrSet) # revealed: types.UnionType
def _(list_of_int: MyList[int], list_or_set_of_str: ListOrSet[str]):
reveal_type(list_of_int) # revealed: list[int]
reveal_type(list_or_set_of_str) # revealed: list[str] | set[str]
```
### Stringified generic alias
```py
from typing import TypeAlias, TypeVar
T = TypeVar("T")
U = TypeVar("U")
TotallyStringifiedPEP613: TypeAlias = "dict[T, U]"
TotallyStringifiedPartiallySpecialized: TypeAlias = "TotallyStringifiedPEP613[U, int]"
def f(x: "TotallyStringifiedPartiallySpecialized[str]"):
reveal_type(x) # revealed: @Todo(Generic stringified PEP-613 type alias)
```
## Subscripted generic alias in union ## Subscripted generic alias in union
```py ```py
@ -107,8 +146,7 @@ Alias1: TypeAlias = list[T] | set[T]
MyAlias: TypeAlias = int | Alias1[str] MyAlias: TypeAlias = int | Alias1[str]
def _(x: MyAlias): def _(x: MyAlias):
# TODO: int | list[str] | set[str] reveal_type(x) # revealed: int | list[str] | set[str]
reveal_type(x) # revealed: int | @Todo(Specialization of union type alias)
``` ```
## Imported ## Imported
@ -173,7 +211,7 @@ NestedDict: TypeAlias = dict[K, Union[V, "NestedDict[K, V]"]]
def _(nested: NestedDict[str, int]): def _(nested: NestedDict[str, int]):
# TODO should be `dict[str, int | NestedDict[str, int]]` # TODO should be `dict[str, int | NestedDict[str, int]]`
reveal_type(nested) # revealed: @Todo(specialized generic alias in type expression) reveal_type(nested) # revealed: dict[@Todo(specialized recursive generic type alias), Divergent]
my_isinstance(1, int) my_isinstance(1, int)
my_isinstance(1, int | str) my_isinstance(1, int | str)

View File

@ -90,6 +90,12 @@ impl<'db> Definition<'db> {
.to_string(), .to_string(),
) )
} }
DefinitionKind::Assignment(assignment) => {
let target_node = assignment.target.node(&module);
target_node
.as_name_expr()
.map(|name_expr| name_expr.id.as_str().to_string())
}
_ => None, _ => None,
} }
} }

View File

@ -3726,7 +3726,7 @@ impl<'db> Type<'db> {
dunder_call dunder_call
.has_relation_to_impl( .has_relation_to_impl(
db, db,
CallableType::unknown(db), Type::Callable(CallableType::unknown(db)),
inferable, inferable,
TypeRelation::Assignability, TypeRelation::Assignability,
relation_visitor, relation_visitor,
@ -7219,7 +7219,7 @@ impl<'db> Type<'db> {
} }
KnownInstanceType::Literal(ty) => Ok(ty.inner(db)), KnownInstanceType::Literal(ty) => Ok(ty.inner(db)),
KnownInstanceType::Annotated(ty) => Ok(ty.inner(db)), KnownInstanceType::Annotated(ty) => Ok(ty.inner(db)),
KnownInstanceType::TypeGenericAlias(ty) => { KnownInstanceType::TypeGenericAlias(instance) => {
// When `type[…]` appears in a value position (e.g. in an implicit type alias), // 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 // 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 // diagnostics for invalid type expressions, and more importantly, that we can
@ -7228,7 +7228,7 @@ impl<'db> Type<'db> {
// (`int` -> instance of `int` -> subclass of `int`) can be lossy, but it is // (`int` -> instance of `int` -> subclass of `int`) can be lossy, but it is
// okay for all valid arguments to `type[…]`. // okay for all valid arguments to `type[…]`.
Ok(ty.inner(db).to_meta_type(db)) Ok(instance.inner(db).to_meta_type(db))
} }
KnownInstanceType::Callable(callable) => Ok(Type::Callable(*callable)), KnownInstanceType::Callable(callable) => Ok(Type::Callable(*callable)),
KnownInstanceType::LiteralStringAlias(ty) => Ok(ty.inner(db)), KnownInstanceType::LiteralStringAlias(ty) => Ok(ty.inner(db)),
@ -7258,7 +7258,7 @@ impl<'db> Type<'db> {
SpecialFormType::OrderedDict => Ok(KnownClass::OrderedDict.to_instance(db)), SpecialFormType::OrderedDict => Ok(KnownClass::OrderedDict.to_instance(db)),
// TODO: Use an opt-in rule for a bare `Callable` // TODO: Use an opt-in rule for a bare `Callable`
SpecialFormType::Callable => Ok(CallableType::unknown(db)), SpecialFormType::Callable => Ok(Type::Callable(CallableType::unknown(db))),
// Special case: `NamedTuple` in a type expression is understood to describe the type // Special case: `NamedTuple` in a type expression is understood to describe the type
// `tuple[object, ...] & <a protocol that any `NamedTuple` class would satisfy>`. // `tuple[object, ...] & <a protocol that any `NamedTuple` class would satisfy>`.
@ -7578,7 +7578,9 @@ impl<'db> Type<'db> {
match self { match self {
Type::TypeVar(bound_typevar) => bound_typevar.apply_type_mapping_impl(db, type_mapping, visitor), Type::TypeVar(bound_typevar) => bound_typevar.apply_type_mapping_impl(db, type_mapping, visitor),
Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => match type_mapping { Type::KnownInstance(known_instance) => match known_instance {
KnownInstanceType::TypeVar(typevar) => {
match type_mapping {
TypeMapping::BindLegacyTypevars(binding_context) => { TypeMapping::BindLegacyTypevars(binding_context) => {
Type::TypeVar(BoundTypeVarInstance::new(db, typevar, *binding_context)) Type::TypeVar(BoundTypeVarInstance::new(db, typevar, *binding_context))
} }
@ -7591,6 +7593,57 @@ impl<'db> Type<'db> {
TypeMapping::ReplaceParameterDefaults | TypeMapping::ReplaceParameterDefaults |
TypeMapping::EagerExpansion => self, TypeMapping::EagerExpansion => self,
} }
}
KnownInstanceType::UnionType(instance) => {
if let Ok(union_type) = instance.union_type(db) {
Type::KnownInstance(KnownInstanceType::UnionType(
UnionTypeInstance::new(
db,
instance._value_expr_types(db),
Ok(union_type.apply_type_mapping_impl(db, type_mapping, tcx, visitor)
)
)))
} else {
self
}
},
KnownInstanceType::Annotated(ty) => {
Type::KnownInstance(KnownInstanceType::Annotated(
InternedType::new(
db,
ty.inner(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor),
)
))
},
KnownInstanceType::Callable(callable_type) => {
Type::KnownInstance(KnownInstanceType::Callable(
callable_type.apply_type_mapping_impl(db, type_mapping, tcx, visitor),
))
},
KnownInstanceType::TypeGenericAlias(ty) => {
Type::KnownInstance(KnownInstanceType::TypeGenericAlias(
InternedType::new(
db,
ty.inner(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor),
)
))
},
KnownInstanceType::SubscriptedProtocol(_) |
KnownInstanceType::SubscriptedGeneric(_) |
KnownInstanceType::TypeAliasType(_) |
KnownInstanceType::Deprecated(_) |
KnownInstanceType::Field(_) |
KnownInstanceType::ConstraintSet(_) |
KnownInstanceType::GenericContext(_) |
KnownInstanceType::Specialization(_) |
KnownInstanceType::Literal(_) |
KnownInstanceType::LiteralStringAlias(_) |
KnownInstanceType::NewType(_) => {
// TODO: For some of these, we may need to apply the type mapping to inner types.
self
},
}
Type::FunctionLiteral(function) => { Type::FunctionLiteral(function) => {
let function = Type::FunctionLiteral(function.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); let function = Type::FunctionLiteral(function.apply_type_mapping_impl(db, type_mapping, tcx, visitor));
@ -7755,8 +7808,7 @@ impl<'db> Type<'db> {
// some other generic context's specialization is applied to it. // some other generic context's specialization is applied to it.
| Type::ClassLiteral(_) | Type::ClassLiteral(_)
| Type::BoundSuper(_) | Type::BoundSuper(_)
| Type::SpecialForm(_) | Type::SpecialForm(_) => self,
| Type::KnownInstance(_) => self,
} }
} }
@ -7893,6 +7945,44 @@ impl<'db> Type<'db> {
}); });
} }
Type::KnownInstance(known_instance) => match known_instance {
KnownInstanceType::UnionType(instance) => {
if let Ok(union_type) = instance.union_type(db) {
union_type.find_legacy_typevars_impl(
db,
binding_context,
typevars,
visitor,
);
}
}
KnownInstanceType::Annotated(ty) => {
ty.inner(db)
.find_legacy_typevars_impl(db, binding_context, typevars, visitor);
}
KnownInstanceType::Callable(callable_type) => {
callable_type.find_legacy_typevars_impl(db, binding_context, typevars, visitor);
}
KnownInstanceType::TypeGenericAlias(ty) => {
ty.inner(db)
.find_legacy_typevars_impl(db, binding_context, typevars, visitor);
}
KnownInstanceType::SubscriptedProtocol(_)
| KnownInstanceType::SubscriptedGeneric(_)
| KnownInstanceType::TypeVar(_)
| KnownInstanceType::TypeAliasType(_)
| KnownInstanceType::Deprecated(_)
| KnownInstanceType::Field(_)
| KnownInstanceType::ConstraintSet(_)
| KnownInstanceType::GenericContext(_)
| KnownInstanceType::Specialization(_)
| KnownInstanceType::Literal(_)
| KnownInstanceType::LiteralStringAlias(_)
| KnownInstanceType::NewType(_) => {
// TODO: For some of these, we may need to try to find legacy typevars in inner types.
}
},
Type::Dynamic(_) Type::Dynamic(_)
| Type::Never | Type::Never
| Type::AlwaysTruthy | Type::AlwaysTruthy
@ -7920,7 +8010,6 @@ impl<'db> Type<'db> {
| Type::EnumLiteral(_) | Type::EnumLiteral(_)
| Type::BoundSuper(_) | Type::BoundSuper(_)
| Type::SpecialForm(_) | Type::SpecialForm(_)
| Type::KnownInstance(_)
| Type::TypedDict(_) => {} | Type::TypedDict(_) => {}
} }
} }
@ -11595,8 +11684,8 @@ impl<'db> CallableType<'db> {
} }
/// Create a callable type which accepts any parameters and returns an `Unknown` type. /// Create a callable type which accepts any parameters and returns an `Unknown` type.
pub(crate) fn unknown(db: &'db dyn Db) -> Type<'db> { pub(crate) fn unknown(db: &'db dyn Db) -> CallableType<'db> {
Type::Callable(Self::single(db, Signature::unknown())) Self::single(db, Signature::unknown())
} }
pub(crate) fn bind_self( pub(crate) fn bind_self(

View File

@ -4796,6 +4796,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
unpacked.expression_type(target) unpacked.expression_type(target)
} }
TargetKind::Single => { TargetKind::Single => {
// This could be an implicit type alias (OptionalList = list[T] | None). Use the definition
// of `OptionalList` as the binding context while inferring the RHS (`list[T] | None`), in
// order to bind `T` to `OptionalList`.
let previous_typevar_binding_context =
self.typevar_binding_context.replace(definition);
let value_ty = if let Some(standalone_expression) = self.index.try_expression(value) let value_ty = if let Some(standalone_expression) = self.index.try_expression(value)
{ {
self.infer_standalone_expression_impl(value, standalone_expression, tcx) self.infer_standalone_expression_impl(value, standalone_expression, tcx)
@ -4834,6 +4840,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_expression(value, tcx) self.infer_expression(value, tcx)
}; };
self.typevar_binding_context = previous_typevar_binding_context;
// `TYPE_CHECKING` is a special variable that should only be assigned `False` // `TYPE_CHECKING` is a special variable that should only be assigned `False`
// at runtime, but is always considered `True` in type checking. // at runtime, but is always considered `True` in type checking.
// See mdtest/known_constants.md#user-defined-type_checking for details. // See mdtest/known_constants.md#user-defined-type_checking for details.
@ -5580,11 +5588,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.deferred_state = DeferredExpressionState::Deferred; self.deferred_state = DeferredExpressionState::Deferred;
} }
// This might be a PEP-613 type alias (`OptionalList: TypeAlias = list[T] | None`). Use
// the definition of `OptionalList` as the binding context while inferring the
// RHS (`list[T] | None`), in order to bind `T` to `OptionalList`.
let previous_typevar_binding_context = self.typevar_binding_context.replace(definition);
let inferred_ty = self.infer_maybe_standalone_expression( let inferred_ty = self.infer_maybe_standalone_expression(
value, value,
TypeContext::new(Some(declared.inner_type())), TypeContext::new(Some(declared.inner_type())),
); );
self.typevar_binding_context = previous_typevar_binding_context;
self.deferred_state = previous_deferred_state; self.deferred_state = previous_deferred_state;
self.dataclass_field_specifiers.clear(); self.dataclass_field_specifiers.clear();
@ -10860,6 +10875,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
fn infer_subscript_load(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { fn infer_subscript_load(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> {
let value_ty = self.infer_expression(&subscript.value, TypeContext::default()); let value_ty = self.infer_expression(&subscript.value, TypeContext::default());
// If we have an implicit type alias like `MyList = list[T]`, and if `MyList` is being
// used in another implicit type alias like `Numbers = MyList[int]`, then we infer the
// right hand side as a value expression, and need to handle the specialization here.
if value_ty.is_generic_alias() {
return self.infer_explicit_type_alias_specialization(subscript, value_ty, false);
}
self.infer_subscript_load_impl(value_ty, subscript) self.infer_subscript_load_impl(value_ty, subscript)
} }
@ -10929,7 +10952,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
} }
Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => { Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => {
if let Some(generic_context) = type_alias.generic_context(self.db()) { if let Some(generic_context) = type_alias.generic_context(self.db()) {
return self.infer_explicit_type_alias_specialization( return self.infer_explicit_type_alias_type_specialization(
subscript, subscript,
value_ty, value_ty,
type_alias, type_alias,
@ -11065,6 +11088,35 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
)); ));
} }
Type::SpecialForm(SpecialFormType::Callable) => { Type::SpecialForm(SpecialFormType::Callable) => {
// TODO: Remove this once we support ParamSpec properly. This is necessary to avoid
// a lot of false positives downstream, because we can't represent the specialized
// `Callable[P, _]` type yet.
if let Some(first_arg) = subscript
.slice
.as_ref()
.as_tuple_expr()
.and_then(|args| args.elts.first())
&& first_arg.is_name_expr()
{
let first_arg_ty = self.infer_expression(first_arg, TypeContext::default());
if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = first_arg_ty
&& typevar.kind(self.db()).is_paramspec()
{
return todo_type!("Callable[..] specialized with ParamSpec");
}
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic(format_args!(
"The first argument to `Callable` must be either a list of types, \
ParamSpec, Concatenate, or `...`",
));
}
return Type::KnownInstance(KnownInstanceType::Callable(
CallableType::unknown(self.db()),
));
}
let callable = self let callable = self
.infer_callable_type(subscript) .infer_callable_type(subscript)
.as_callable() .as_callable()
@ -11161,8 +11213,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.map(Type::from) .map(Type::from)
.unwrap_or_else(Type::unknown); .unwrap_or_else(Type::unknown);
} }
Type::KnownInstance(KnownInstanceType::UnionType(_)) => { Type::KnownInstance(
return todo_type!("Specialization of union type alias"); KnownInstanceType::UnionType(_)
| KnownInstanceType::Annotated(_)
| KnownInstanceType::Callable(_)
| KnownInstanceType::TypeGenericAlias(_),
) => {
return self.infer_explicit_type_alias_specialization(subscript, value_ty, false);
} }
_ => {} _ => {}
} }
@ -11194,7 +11251,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
) )
} }
fn infer_explicit_type_alias_specialization( fn infer_explicit_type_alias_type_specialization(
&mut self, &mut self,
subscript: &ast::ExprSubscript, subscript: &ast::ExprSubscript,
value_ty: Type<'db>, value_ty: Type<'db>,

View File

@ -2,6 +2,7 @@ use itertools::Either;
use ruff_python_ast as ast; use ruff_python_ast as ast;
use super::{DeferredExpressionState, TypeInferenceBuilder}; use super::{DeferredExpressionState, TypeInferenceBuilder};
use crate::FxOrderSet;
use crate::types::diagnostic::{ use crate::types::diagnostic::{
self, INVALID_TYPE_FORM, NON_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form, self, INVALID_TYPE_FORM, NON_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form,
report_invalid_arguments_to_callable, report_invalid_arguments_to_callable,
@ -12,9 +13,9 @@ use crate::types::string_annotation::parse_string_annotation;
use crate::types::tuple::{TupleSpecBuilder, TupleType}; use crate::types::tuple::{TupleSpecBuilder, TupleType};
use crate::types::visitor::any_over_type; use crate::types::visitor::any_over_type;
use crate::types::{ use crate::types::{
CallableType, DynamicType, IntersectionBuilder, KnownClass, KnownInstanceType, BindingContext, CallableType, DynamicType, GenericContext, IntersectionBuilder, KnownClass,
LintDiagnosticGuard, SpecialFormType, SubclassOfType, Type, TypeAliasType, TypeContext, KnownInstanceType, LintDiagnosticGuard, SpecialFormType, SubclassOfType, Type, TypeAliasType,
TypeIsType, UnionBuilder, UnionType, todo_type, TypeContext, TypeIsType, TypeMapping, UnionBuilder, UnionType, todo_type,
}; };
/// Type expressions /// Type expressions
@ -734,6 +735,84 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
} }
} }
/// Infer the type of an explicitly specialized generic type alias (implicit or PEP 613).
pub(crate) fn infer_explicit_type_alias_specialization(
&mut self,
subscript: &ast::ExprSubscript,
mut value_ty: Type<'db>,
in_type_expression: bool,
) -> Type<'db> {
let db = self.db();
if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = value_ty
&& let Some(definition) = typevar.definition(db)
{
value_ty = value_ty.apply_type_mapping(
db,
&TypeMapping::BindLegacyTypevars(BindingContext::Definition(definition)),
TypeContext::default(),
);
}
let mut variables = FxOrderSet::default();
value_ty.find_legacy_typevars(db, None, &mut variables);
let generic_context = GenericContext::from_typevar_instances(db, variables);
let scope_id = self.scope();
let current_typevar_binding_context = self.typevar_binding_context;
// TODO
// If we explicitly specialize a recursive generic (PEP-613 or implicit) type alias,
// we currently miscount the number of type variables. For example, for a nested
// dictionary type alias `NestedDict = dict[K, "V | NestedDict[K, V]"]]`, we might
// infer `<class 'dict[K, Divergent]'>`, and therefore count just one type variable
// instead of two. So until we properly support these, specialize all remaining type
// variables with a `@Todo` type (since we don't know which of the type arguments
// belongs to the remaining type variables).
if any_over_type(self.db(), value_ty, &|ty| ty.is_divergent(), true) {
let value_ty = value_ty.apply_specialization(
db,
generic_context.specialize(
db,
std::iter::repeat_n(
todo_type!("specialized recursive generic type alias"),
generic_context.len(db),
)
.collect(),
),
);
return if in_type_expression {
value_ty
.in_type_expression(db, scope_id, current_typevar_binding_context)
.unwrap_or_else(|_| Type::unknown())
} else {
value_ty
};
}
let specialize = |types: &[Option<Type<'db>>]| {
let specialized = value_ty.apply_specialization(
db,
generic_context.specialize_partial(db, types.iter().copied()),
);
if in_type_expression {
specialized
.in_type_expression(db, scope_id, current_typevar_binding_context)
.unwrap_or_else(|_| Type::unknown())
} else {
specialized
}
};
self.infer_explicit_callable_specialization(
subscript,
value_ty,
generic_context,
specialize,
)
}
fn infer_subscript_type_expression( fn infer_subscript_type_expression(
&mut self, &mut self,
subscript: &ast::ExprSubscript, subscript: &ast::ExprSubscript,
@ -824,15 +903,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
} }
Type::unknown() Type::unknown()
} }
KnownInstanceType::TypeVar(_) => {
self.infer_type_expression(slice);
todo_type!("TypeVar annotations")
}
KnownInstanceType::TypeAliasType(type_alias @ TypeAliasType::PEP695(_)) => { KnownInstanceType::TypeAliasType(type_alias @ TypeAliasType::PEP695(_)) => {
match type_alias.generic_context(self.db()) { match type_alias.generic_context(self.db()) {
Some(generic_context) => { Some(generic_context) => {
let specialized_type_alias = self let specialized_type_alias = self
.infer_explicit_type_alias_specialization( .infer_explicit_type_alias_type_specialization(
subscript, subscript,
value_ty, value_ty,
type_alias, type_alias,
@ -870,11 +945,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
self.infer_type_expression(slice); self.infer_type_expression(slice);
todo_type!("Generic stringified PEP-613 type alias") todo_type!("Generic stringified PEP-613 type alias")
} }
KnownInstanceType::UnionType(_) => { KnownInstanceType::Literal(ty) => {
self.infer_type_expression(slice);
todo_type!("Generic specialization of types.UnionType")
}
KnownInstanceType::Literal(ty) | KnownInstanceType::TypeGenericAlias(ty) => {
self.infer_type_expression(slice); self.infer_type_expression(slice);
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic(format_args!( builder.into_diagnostic(format_args!(
@ -884,13 +955,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
} }
Type::unknown() Type::unknown()
} }
KnownInstanceType::Callable(_) => { KnownInstanceType::TypeVar(_) => {
self.infer_type_expression(slice); self.infer_explicit_type_alias_specialization(subscript, value_ty, false)
todo_type!("Generic specialization of typing.Callable")
} }
KnownInstanceType::Annotated(_) => {
self.infer_type_expression(slice); KnownInstanceType::UnionType(_)
todo_type!("Generic specialization of typing.Annotated") | KnownInstanceType::Callable(_)
| KnownInstanceType::Annotated(_)
| KnownInstanceType::TypeGenericAlias(_) => {
self.infer_explicit_type_alias_specialization(subscript, value_ty, true)
} }
KnownInstanceType::NewType(newtype) => { KnownInstanceType::NewType(newtype) => {
self.infer_type_expression(&subscript.slice); self.infer_type_expression(&subscript.slice);
@ -904,7 +977,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
} }
}, },
Type::Dynamic(_) => { Type::Dynamic(_) => {
self.infer_type_expression(slice); // Infer slice as a value expression to avoid false-positive
// `invalid-type-form` diagnostics, when we have e.g.
// `MyCallable[[int, str], None]` but `MyCallable` is dynamic.
self.infer_expression(slice, TypeContext::default());
value_ty value_ty
} }
Type::ClassLiteral(class) => { Type::ClassLiteral(class) => {
@ -933,11 +1009,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
} }
} }
Type::GenericAlias(_) => { Type::GenericAlias(_) => {
self.infer_type_expression(slice); self.infer_explicit_type_alias_specialization(subscript, value_ty, true)
// If the generic alias is already fully specialized, this is an error. But it
// could have been specialized with another typevar (e.g. a type alias like `MyList
// = list[T]`), in which case it's later valid to do `MyList[int]`.
todo_type!("specialized generic alias in type expression")
} }
Type::StringLiteral(_) => { Type::StringLiteral(_) => {
self.infer_type_expression(slice); self.infer_type_expression(slice);
@ -1049,7 +1121,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
{ {
Type::single_callable(db, Signature::new(parameters, Some(return_type))) Type::single_callable(db, Signature::new(parameters, Some(return_type)))
} else { } else {
CallableType::unknown(db) Type::Callable(CallableType::unknown(db))
}; };
// `Signature` / `Parameters` are not a `Type` variant, so we're storing // `Signature` / `Parameters` are not a `Type` variant, so we're storing

View File

@ -238,7 +238,7 @@ impl ClassInfoConstraintFunction {
Type::SpecialForm(SpecialFormType::Callable) Type::SpecialForm(SpecialFormType::Callable)
if self == ClassInfoConstraintFunction::IsInstance => if self == ClassInfoConstraintFunction::IsInstance =>
{ {
Some(CallableType::unknown(db).top_materialization(db)) Some(Type::Callable(CallableType::unknown(db)).top_materialization(db))
} }
Type::SpecialForm(special_form) => special_form Type::SpecialForm(special_form) => special_form