[ty] support PEP 613 type aliases (#21394)
Refs https://github.com/astral-sh/ty/issues/544 ## Summary Takes a more incremental approach to PEP 613 type alias support (vs https://github.com/astral-sh/ruff/pull/20107). Instead of eagerly inferring the RHS of a PEP 613 type alias as a type expression, infer it as a value expression, just like we do for implicit type aliases, taking advantage of the same support for e.g. unions and other type special forms. The main reason I'm following this path instead of the one in https://github.com/astral-sh/ruff/pull/20107 is that we've realized that people do sometimes use PEP 613 type aliases as values, not just as types (because they are just a normal runtime assignment, unlike PEP 695 type aliases which create an opaque `TypeAliasType`). This PR doesn't yet provide full support for recursive type aliases (they don't panic, but they just fall back to `Unknown` at the recursion point). This is future work. ## Test Plan Added mdtests. Many new ecosystem diagnostics, mostly because we understand new types in lots of places. Conformance suite changes are correct. Performance regression is due to understanding lots of new types; nothing we do in this PR is inherently expensive.
This commit is contained in:
@@ -12,11 +12,8 @@ P = ParamSpec("P")
|
||||
Ts = TypeVarTuple("Ts")
|
||||
R_co = TypeVar("R_co", covariant=True)
|
||||
|
||||
Alias: TypeAlias = int
|
||||
|
||||
def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
|
||||
reveal_type(args) # revealed: tuple[@Todo(`Unpack[]` special form), ...]
|
||||
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
|
||||
return args
|
||||
|
||||
def g() -> TypeGuard[int]: ...
|
||||
|
||||
@@ -2208,9 +2208,9 @@ reveal_type(False.real) # revealed: Literal[0]
|
||||
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
|
||||
|
||||
```py
|
||||
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[@Todo(Support for `typing.TypeAlias`)], /) -> bytes
|
||||
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[Buffer], /) -> bytes
|
||||
reveal_type(b"foo".join)
|
||||
# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`) | tuple[@Todo(Support for `typing.TypeAlias`), ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> bool
|
||||
# revealed: bound method Literal[b"foo"].endswith(suffix: Buffer | tuple[Buffer, ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> bool
|
||||
reveal_type(b"foo".endswith)
|
||||
```
|
||||
|
||||
|
||||
@@ -313,8 +313,7 @@ reveal_type(A() + "foo") # revealed: A
|
||||
reveal_type("foo" + A()) # revealed: A
|
||||
|
||||
reveal_type(A() + b"foo") # revealed: A
|
||||
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
|
||||
reveal_type(b"foo" + A()) # revealed: bytes
|
||||
reveal_type(b"foo" + A()) # revealed: A
|
||||
|
||||
reveal_type(A() + ()) # revealed: A
|
||||
reveal_type(() + A()) # revealed: A
|
||||
|
||||
@@ -54,10 +54,8 @@ reveal_type(2**largest_u32) # revealed: int
|
||||
|
||||
def variable(x: int):
|
||||
reveal_type(x**2) # revealed: int
|
||||
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
|
||||
reveal_type(2**x) # revealed: int
|
||||
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
|
||||
reveal_type(x**x) # revealed: int
|
||||
reveal_type(2**x) # revealed: Any
|
||||
reveal_type(x**x) # revealed: Any
|
||||
```
|
||||
|
||||
If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but
|
||||
|
||||
@@ -598,9 +598,9 @@ from typing_extensions import Self
|
||||
|
||||
reveal_type(object.__new__) # revealed: def __new__(cls) -> Self@__new__
|
||||
reveal_type(object().__new__) # revealed: def __new__(cls) -> Self@__new__
|
||||
# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||
reveal_type(int.__new__)
|
||||
# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||
reveal_type((42).__new__)
|
||||
|
||||
class X:
|
||||
|
||||
@@ -10,13 +10,13 @@ import pickle
|
||||
|
||||
reveal_type(open("")) # revealed: TextIOWrapper[_WrappedBuffer]
|
||||
reveal_type(open("", "r")) # revealed: TextIOWrapper[_WrappedBuffer]
|
||||
reveal_type(open("", "rb")) # revealed: @Todo(`builtins.open` return type)
|
||||
reveal_type(open("", "rb")) # revealed: BufferedReader[_BufferedReaderStream]
|
||||
|
||||
with open("foo.pickle", "rb") as f:
|
||||
x = pickle.load(f) # fine
|
||||
|
||||
def _(mode: str):
|
||||
reveal_type(open("", mode)) # revealed: @Todo(`builtins.open` return type)
|
||||
reveal_type(open("", mode)) # revealed: IO[Any]
|
||||
```
|
||||
|
||||
## `os.fdopen`
|
||||
@@ -29,7 +29,7 @@ import os
|
||||
|
||||
reveal_type(os.fdopen(0)) # revealed: TextIOWrapper[_WrappedBuffer]
|
||||
reveal_type(os.fdopen(0, "r")) # revealed: TextIOWrapper[_WrappedBuffer]
|
||||
reveal_type(os.fdopen(0, "rb")) # revealed: @Todo(`os.fdopen` return type)
|
||||
reveal_type(os.fdopen(0, "rb")) # revealed: BufferedReader[_BufferedReaderStream]
|
||||
|
||||
with os.fdopen(0, "rb") as f:
|
||||
x = pickle.load(f) # fine
|
||||
@@ -43,9 +43,9 @@ And similarly for `Path.open()`:
|
||||
from pathlib import Path
|
||||
import pickle
|
||||
|
||||
reveal_type(Path("").open()) # revealed: @Todo(`Path.open` return type)
|
||||
reveal_type(Path("").open("r")) # revealed: @Todo(`Path.open` return type)
|
||||
reveal_type(Path("").open("rb")) # revealed: @Todo(`Path.open` return type)
|
||||
reveal_type(Path("").open()) # revealed: TextIOWrapper[_WrappedBuffer]
|
||||
reveal_type(Path("").open("r")) # revealed: TextIOWrapper[_WrappedBuffer]
|
||||
reveal_type(Path("").open("rb")) # revealed: BufferedReader[_BufferedReaderStream]
|
||||
|
||||
with Path("foo.pickle").open("rb") as f:
|
||||
x = pickle.load(f) # fine
|
||||
@@ -61,7 +61,7 @@ import pickle
|
||||
|
||||
reveal_type(NamedTemporaryFile()) # revealed: _TemporaryFileWrapper[bytes]
|
||||
reveal_type(NamedTemporaryFile("r")) # revealed: _TemporaryFileWrapper[str]
|
||||
reveal_type(NamedTemporaryFile("rb")) # revealed: @Todo(`tempfile.NamedTemporaryFile` return type)
|
||||
reveal_type(NamedTemporaryFile("rb")) # revealed: _TemporaryFileWrapper[bytes]
|
||||
|
||||
with NamedTemporaryFile("rb") as f:
|
||||
x = pickle.load(f) # fine
|
||||
|
||||
@@ -127,7 +127,7 @@ x = lambda y: y
|
||||
reveal_type(x.__code__) # revealed: CodeType
|
||||
reveal_type(x.__name__) # revealed: str
|
||||
reveal_type(x.__defaults__) # revealed: tuple[Any, ...] | None
|
||||
reveal_type(x.__annotations__) # revealed: dict[str, @Todo(Support for `typing.TypeAlias`)]
|
||||
reveal_type(x.__annotations__) # revealed: dict[str, Any]
|
||||
reveal_type(x.__dict__) # revealed: dict[str, Any]
|
||||
reveal_type(x.__doc__) # revealed: str | None
|
||||
reveal_type(x.__kwdefaults__) # revealed: dict[str, Any] | None
|
||||
|
||||
@@ -1,8 +1,147 @@
|
||||
# PEP 613 type aliases
|
||||
|
||||
## No panics
|
||||
PEP 613 type aliases are simple assignment statements, annotated with `typing.TypeAlias` to mark
|
||||
them as a type alias. At runtime, they behave the same as implicit type aliases. Our support for
|
||||
them is currently the same as for implicit type aliases, but we don't reproduce the full
|
||||
implicit-type-alias test suite here, just some particularly interesting cases.
|
||||
|
||||
We do not fully support PEP 613 type aliases yet. For now, just make sure that we don't panic:
|
||||
## Basic
|
||||
|
||||
### as `TypeAlias`
|
||||
|
||||
```py
|
||||
from typing import TypeAlias
|
||||
|
||||
IntOrStr: TypeAlias = int | str
|
||||
|
||||
def _(x: IntOrStr):
|
||||
reveal_type(x) # revealed: int | str
|
||||
```
|
||||
|
||||
### as `typing.TypeAlias`
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
IntOrStr: typing.TypeAlias = int | str
|
||||
|
||||
def _(x: IntOrStr):
|
||||
reveal_type(x) # revealed: int | str
|
||||
```
|
||||
|
||||
## Can be used as value
|
||||
|
||||
Because PEP 613 type aliases are just annotated assignments, they can be used as values, like a
|
||||
legacy type expression (and unlike a PEP 695 type alias). We might prefer this wasn't allowed, but
|
||||
people do use it.
|
||||
|
||||
```py
|
||||
from typing import TypeAlias
|
||||
|
||||
MyExc: TypeAlias = Exception
|
||||
|
||||
try:
|
||||
raise MyExc("error")
|
||||
except MyExc as e:
|
||||
reveal_type(e) # revealed: Exception
|
||||
```
|
||||
|
||||
## Can inherit from an alias
|
||||
|
||||
```py
|
||||
from typing import TypeAlias
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
MyList: TypeAlias = list["int"]
|
||||
|
||||
class Foo(MyList): ...
|
||||
|
||||
static_assert(is_subtype_of(Foo, list[int]))
|
||||
```
|
||||
|
||||
## Cannot inherit from a stringified alias
|
||||
|
||||
```py
|
||||
from typing import TypeAlias
|
||||
|
||||
MyList: TypeAlias = "list[int]"
|
||||
|
||||
# error: [invalid-base] "Invalid class base with type `str`"
|
||||
class Foo(MyList): ...
|
||||
```
|
||||
|
||||
## Unknown type in PEP 604 union
|
||||
|
||||
If we run into an unknown type in a PEP 604 union in the right-hand side of a PEP 613 type alias, we
|
||||
still understand it as a union type, just with an unknown element.
|
||||
|
||||
```py
|
||||
from typing import TypeAlias
|
||||
from nonexistent import unknown_type # error: [unresolved-import]
|
||||
|
||||
MyAlias: TypeAlias = int | unknown_type | str
|
||||
|
||||
def _(x: MyAlias):
|
||||
reveal_type(x) # revealed: int | Unknown | str
|
||||
```
|
||||
|
||||
## Callable type in union
|
||||
|
||||
```py
|
||||
from typing import TypeAlias, Callable
|
||||
|
||||
MyAlias: TypeAlias = int | Callable[[str], int]
|
||||
|
||||
def _(x: MyAlias):
|
||||
reveal_type(x) # revealed: int | ((str, /) -> int)
|
||||
```
|
||||
|
||||
## Subscripted generic alias in union
|
||||
|
||||
```py
|
||||
from typing import TypeAlias, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
Alias1: TypeAlias = list[T] | set[T]
|
||||
MyAlias: TypeAlias = int | Alias1[str]
|
||||
|
||||
def _(x: MyAlias):
|
||||
# TODO: int | list[str] | set[str]
|
||||
reveal_type(x) # revealed: int | @Todo(Specialization of union type alias)
|
||||
```
|
||||
|
||||
## Imported
|
||||
|
||||
`alias.py`:
|
||||
|
||||
```py
|
||||
from typing import TypeAlias
|
||||
|
||||
MyAlias: TypeAlias = int | str
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from alias import MyAlias
|
||||
|
||||
def _(x: MyAlias):
|
||||
reveal_type(x) # revealed: int | str
|
||||
```
|
||||
|
||||
## String literal in right-hand side
|
||||
|
||||
```py
|
||||
from typing import TypeAlias
|
||||
|
||||
IntOrStr: TypeAlias = "int | str"
|
||||
|
||||
def _(x: IntOrStr):
|
||||
reveal_type(x) # revealed: int | str
|
||||
```
|
||||
|
||||
## Cyclic
|
||||
|
||||
```py
|
||||
from typing import TypeAlias
|
||||
@@ -18,6 +157,26 @@ def _(rec: RecursiveHomogeneousTuple):
|
||||
reveal_type(rec) # revealed: tuple[Divergent, ...]
|
||||
```
|
||||
|
||||
## Conditionally imported on Python < 3.10
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.9"
|
||||
```
|
||||
|
||||
```py
|
||||
try:
|
||||
# error: [unresolved-import]
|
||||
from typing import TypeAlias
|
||||
except ImportError:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
MyAlias: TypeAlias = int
|
||||
|
||||
def _(x: MyAlias):
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## PEP-613 aliases in stubs are deferred
|
||||
|
||||
Although the right-hand side of a PEP-613 alias is a value expression, inference of this value is
|
||||
@@ -46,7 +205,31 @@ f(stub.B())
|
||||
|
||||
class Unrelated: ...
|
||||
|
||||
# TODO: we should emit `[invalid-argument-type]` here
|
||||
# (the alias is a `@Todo` because it's imported from another file)
|
||||
# error: [invalid-argument-type]
|
||||
f(Unrelated())
|
||||
```
|
||||
|
||||
## Invalid position
|
||||
|
||||
`typing.TypeAlias` must be used as the sole annotation in an annotated assignment. Use in any other
|
||||
context is an error.
|
||||
|
||||
```py
|
||||
from typing import TypeAlias
|
||||
|
||||
# error: [invalid-type-form]
|
||||
def _(x: TypeAlias):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
# error: [invalid-type-form]
|
||||
y: list[TypeAlias] = []
|
||||
```
|
||||
|
||||
## Right-hand side is required
|
||||
|
||||
```py
|
||||
from typing import TypeAlias
|
||||
|
||||
# error: [invalid-type-form]
|
||||
Empty: TypeAlias
|
||||
```
|
||||
|
||||
@@ -28,7 +28,7 @@ def f() -> None:
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
|
||||
reveal_type(IntOrStr.__value__) # revealed: @Todo(Support for `typing.TypeAlias`)
|
||||
reveal_type(IntOrStr.__value__) # revealed: Any
|
||||
```
|
||||
|
||||
## Invalid assignment
|
||||
|
||||
@@ -122,7 +122,7 @@ properties on instance types:
|
||||
|
||||
```py
|
||||
reveal_type(sys.version_info.micro) # revealed: int
|
||||
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(Support for `typing.TypeAlias`)
|
||||
reveal_type(sys.version_info.releaselevel) # revealed: Literal["alpha", "beta", "candidate", "final"]
|
||||
reveal_type(sys.version_info.serial) # revealed: int
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user