Skip to content

Commit 42f1521

Browse files
authored
[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 astral-sh/ty#1643 closes astral-sh/ty#1629 closes astral-sh/ty#1596 closes astral-sh/ty#573 closes astral-sh/ty#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 :heavy_check_mark: ```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 :heavy_check_mark: ```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 :heavy_check_mark: ```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 :heavy_check_mark: ```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 :heavy_check_mark: ```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 :heavy_check_mark: ```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. :heavy_check_mark: 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](https://github.com/colour-science/colour/blob/83e754c8b6932532f246b9fe1727d14f3600cd83/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
1 parent 594b7b0 commit 42f1521

File tree

10 files changed

+653
-141
lines changed

10 files changed

+653
-141
lines changed

crates/ruff_benchmark/benches/ty_walltime.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ static COLOUR_SCIENCE: Benchmark = Benchmark::new(
120120
max_dep_date: "2025-06-17",
121121
python_version: PythonVersion::PY310,
122122
},
123-
600,
123+
1070,
124124
);
125125

126126
static FREQTRADE: Benchmark = Benchmark::new(

crates/ty_python_semantic/resources/mdtest/async.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ async def main():
6161

6262
result = await task
6363

64-
# TODO: this should be `int`
65-
reveal_type(result) # revealed: Unknown
64+
reveal_type(result) # revealed: int
6665
```
6766

6867
### `asyncio.gather`
@@ -79,9 +78,8 @@ async def main():
7978
task("B"),
8079
)
8180

82-
# TODO: these should be `int`
83-
reveal_type(a) # revealed: Unknown
84-
reveal_type(b) # revealed: Unknown
81+
reveal_type(a) # revealed: int
82+
reveal_type(b) # revealed: int
8583
```
8684

8785
## Under the hood

0 commit comments

Comments
 (0)