[ty] Understand homogeneous tuple annotations (#17998)

This commit is contained in:
Alex Waygood 2025-05-12 22:02:25 -04:00 committed by GitHub
parent f301931159
commit 55df9271ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 196 additions and 104 deletions

View file

@ -13,11 +13,10 @@ from typing_extensions import TypeVarTuple
Ts = TypeVarTuple("Ts")
def append_int(*args: *Ts) -> tuple[*Ts, int]:
# TODO: tuple[*Ts]
reveal_type(args) # revealed: tuple[Unknown, ...]
reveal_type(args) # revealed: @Todo(PEP 646)
return (*args, 1)
# TODO should be tuple[Literal[True], Literal["a"], int]
reveal_type(append_int(True, "a")) # revealed: @Todo(full tuple[...] support)
reveal_type(append_int(True, "a")) # revealed: @Todo(PEP 646)
```

View file

@ -15,16 +15,13 @@ R_co = TypeVar("R_co", covariant=True)
Alias: TypeAlias = int
def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
# TODO: should understand the annotation
reveal_type(args) # revealed: tuple[Unknown, ...]
reveal_type(args) # revealed: tuple[@Todo(`Unpack[]` special form), ...]
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
def g() -> TypeGuard[int]: ...
def h() -> TypeIs[int]: ...
def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co:
# TODO: should understand the annotation
reveal_type(args) # revealed: tuple[Unknown, ...]
reveal_type(args) # revealed: tuple[@Todo(Support for `typing.ParamSpec`), ...]
reveal_type(kwargs) # revealed: dict[str, @Todo(Support for `typing.ParamSpec`)]
return callback(42, *args, **kwargs)

View file

@ -56,13 +56,12 @@ reveal_type(a) # revealed: tuple[()]
reveal_type(b) # revealed: tuple[int]
reveal_type(c) # revealed: tuple[str, int]
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
reveal_type(e) # revealed: tuple[str, ...]
reveal_type(f) # revealed: @Todo(PEP 646)
reveal_type(g) # revealed: @Todo(PEP 646)
# TODO: homogeneous tuples, PEP-646 tuples, generics
reveal_type(e) # revealed: @Todo(full tuple[...] support)
reveal_type(f) # revealed: @Todo(full tuple[...] support)
reveal_type(g) # revealed: @Todo(full tuple[...] support)
reveal_type(h) # revealed: tuple[list[int], list[int]]
reveal_type(i) # revealed: tuple[str | int, str | int]
reveal_type(j) # revealed: tuple[str | int]
```

View file

@ -1676,7 +1676,7 @@ functions are instances of that class:
```py
def f(): ...
reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None
reveal_type(f.__defaults__) # revealed: tuple[Any, ...] | None
reveal_type(f.__kwdefaults__) # revealed: dict[str, Any] | None
```
@ -1730,7 +1730,7 @@ All attribute access on literal `bytes` types is currently delegated to `builtin
```py
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[@Todo(Support for `typing.TypeAlias`)], /) -> bytes
reveal_type(b"foo".join)
# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`), start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool
# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`) | tuple[@Todo(Support for `typing.TypeAlias`), ...], start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool
reveal_type(b"foo".endswith)
```

View file

@ -317,8 +317,7 @@ reveal_type(A() + b"foo") # revealed: A
reveal_type(b"foo" + A()) # revealed: bytes
reveal_type(A() + ()) # revealed: A
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
reveal_type(() + A()) # revealed: @Todo(full tuple[...] support)
reveal_type(() + A()) # revealed: A
literal_string_instance = "foo" * 1_000_000_000
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:

View file

@ -17,6 +17,7 @@ def _(x: tuple[int, str], y: tuple[None, tuple[int]]):
```py
def _(x: tuple[int, ...], y: tuple[str, ...]):
reveal_type(x + y) # revealed: @Todo(full tuple[...] support)
reveal_type(x + (1, 2)) # revealed: @Todo(full tuple[...] support)
# TODO: should be `tuple[int | str, ...]`
reveal_type(x + y) # revealed: tuple[int | Unknown, ...]
reveal_type(x + (1, 2)) # revealed: tuple[int, ...]
```

View file

@ -54,7 +54,7 @@ type(b"Foo", (), {})
# error: [no-matching-overload] "No overload of class `type` matches arguments"
type("Foo", Base, {})
# TODO: this should be an error
# error: [no-matching-overload] "No overload of class `type` matches arguments"
type("Foo", (1, 2), {})
# TODO: this should be an error

View file

@ -45,6 +45,8 @@ def foo(
x: type[AttributeError],
y: tuple[type[OSError], type[RuntimeError]],
z: tuple[type[BaseException], ...],
zz: tuple[type[TypeError | RuntimeError], ...],
zzz: type[BaseException] | tuple[type[BaseException], ...],
):
try:
help()
@ -53,8 +55,11 @@ def foo(
except y as f:
reveal_type(f) # revealed: OSError | RuntimeError
except z as g:
# TODO: should be `BaseException`
reveal_type(g) # revealed: @Todo(full tuple[...] support)
reveal_type(g) # revealed: BaseException
except zz as h:
reveal_type(h) # revealed: TypeError | RuntimeError
except zzz as i:
reveal_type(i) # revealed: BaseException
```
## Invalid exception handlers
@ -86,9 +91,9 @@ def foo(
# error: [invalid-exception-caught]
except y as f:
reveal_type(f) # revealed: OSError | RuntimeError | Unknown
# error: [invalid-exception-caught]
except z as g:
# TODO: should emit a diagnostic here:
reveal_type(g) # revealed: @Todo(full tuple[...] support)
reveal_type(g) # revealed: Unknown
```
## Object raised is not an exception

View file

@ -25,8 +25,7 @@ def f(a, b: int, c=1, d: int = 2, /, e=3, f: Literal[4] = 4, *args: object, g=5,
reveal_type(f) # revealed: Literal[4]
reveal_type(g) # revealed: Unknown | Literal[5]
reveal_type(h) # revealed: Literal[6]
# TODO: should be `tuple[object, ...]`
reveal_type(args) # revealed: tuple[Unknown, ...]
reveal_type(args) # revealed: tuple[object, ...]
reveal_type(kwargs) # revealed: dict[str, str]
```

View file

@ -68,10 +68,7 @@ y: MyIntOrStr = None
```py
type ListOrSet[T] = list[T] | set[T]
# TODO: Should be `tuple[typing.TypeVar | typing.ParamSpec | typing.TypeVarTuple, ...]`,
# as specified in the `typeshed` stubs.
reveal_type(ListOrSet.__type_params__) # revealed: @Todo(full tuple[...] support)
reveal_type(ListOrSet.__type_params__) # revealed: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
```
## `TypeAliasType` properties

View file

@ -70,7 +70,7 @@ def _(m: int, n: int):
tuple_slice = t[m:n]
# TODO: Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
reveal_type(tuple_slice) # revealed: @Todo(full tuple[...] support)
reveal_type(tuple_slice) # revealed: tuple[Unknown, ...]
```
## Inheritance
@ -101,7 +101,7 @@ class A: ...
def _(c: Tuple, d: Tuple[int, A], e: Tuple[Any, ...]):
reveal_type(c) # revealed: tuple[Unknown, ...]
reveal_type(d) # revealed: tuple[int, A]
reveal_type(e) # revealed: @Todo(full tuple[...] support)
reveal_type(e) # revealed: tuple[Any, ...]
```
### Inheritance

View file

@ -198,7 +198,7 @@ static_assert(is_assignable_to(Meta, type[Any]))
static_assert(is_assignable_to(Meta, type[Unknown]))
```
## Tuple types
## Heterogeneous tuple types
```py
from ty_extensions import static_assert, is_assignable_to, AlwaysTruthy, AlwaysFalsy
@ -232,6 +232,35 @@ static_assert(not is_assignable_to(tuple[int, int], tuple[Literal[1], int]))
static_assert(not is_assignable_to(tuple[Any, Literal[2]], tuple[int, str]))
```
## Assignability of heterogeneous tuple types to homogeneous tuple types
While a homogeneous tuple type is not assignable to any heterogeneous tuple types, a heterogeneous
tuple type can be assignable to a homogeneous tuple type, and homogeneous tuple types can be
assignable to `Sequence`:
```py
from typing import Literal, Any, Sequence
from ty_extensions import static_assert, is_assignable_to, Not, AlwaysFalsy
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Literal[1, 2], ...]))
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int, ...]))
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int | str, ...]))
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Any, ...]))
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Not[AlwaysFalsy], ...]))
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], Sequence[int]))
static_assert(is_assignable_to(tuple[int, ...], Sequence[int]))
static_assert(is_assignable_to(tuple[int, ...], Sequence[Any]))
static_assert(is_assignable_to(tuple[Any, ...], Sequence[int]))
static_assert(is_assignable_to(tuple[()], tuple[Literal[1, 2], ...]))
static_assert(is_assignable_to(tuple[()], tuple[int, ...]))
static_assert(is_assignable_to(tuple[()], tuple[int | str, ...]))
static_assert(is_assignable_to(tuple[()], tuple[Not[AlwaysFalsy], ...]))
static_assert(is_assignable_to(tuple[()], Sequence[int]))
static_assert(not is_assignable_to(tuple[int, int], tuple[str, ...]))
```
## Union types
```py

View file

@ -91,6 +91,10 @@ static_assert(is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[1]])
static_assert(is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[1], Literal[3]]))
static_assert(not is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[1], int]))
static_assert(not is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[int, ...]))
# TODO: should pass
static_assert(is_disjoint_from(tuple[int, int], tuple[None, ...])) # error: [static-assert-error]
```
## Unions

View file

@ -48,7 +48,9 @@ static_assert(not is_fully_static(Any | str))
static_assert(not is_fully_static(str | Unknown))
static_assert(not is_fully_static(Intersection[Any, Not[LiteralString]]))
static_assert(not is_fully_static(tuple[Any, ...]))
# TODO: should pass
static_assert(not is_fully_static(tuple[Any, ...])) # error: [static-assert-error]
static_assert(not is_fully_static(tuple[int, Any]))
static_assert(not is_fully_static(type[Any]))
```

View file

@ -118,7 +118,7 @@ static_assert(is_subtype_of(Literal[b"foo"], bytes))
static_assert(is_subtype_of(Literal[b"foo"], object))
```
## Tuple types
## Heterogeneous tuple types
```py
from ty_extensions import is_subtype_of, static_assert
@ -150,9 +150,36 @@ static_assert(not is_subtype_of(tuple[B1, B2], tuple[Unrelated, Unrelated]))
static_assert(not is_subtype_of(tuple[B1, B2], tuple[()]))
static_assert(not is_subtype_of(tuple[B1, B2], tuple[A1]))
static_assert(not is_subtype_of(tuple[B1, B2], tuple[A1, A2, Unrelated]))
static_assert(is_subtype_of(tuple[int], tuple[object, ...]))
```
# TODO: should pass
static_assert(is_subtype_of(tuple[int], tuple[object, ...])) # error: [static-assert-error]
## Subtyping of heterogeneous tuple types and homogeneous tuple types
While a homogeneous tuple type is not a subtype of any heterogeneous tuple types, a heterogeneous
tuple type can be a subtype of a homogeneous tuple type, and homogeneous tuple types can be subtypes
of `Sequence`:
```py
from typing import Literal, Any, Sequence
from ty_extensions import static_assert, is_subtype_of, Not, AlwaysFalsy
static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Literal[1, 2], ...]))
static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[int, ...]))
static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[int | str, ...]))
static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Not[AlwaysFalsy], ...]))
static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], Sequence[int]))
static_assert(is_subtype_of(tuple[int, ...], Sequence[int]))
static_assert(is_subtype_of(tuple[()], tuple[Literal[1, 2], ...]))
static_assert(is_subtype_of(tuple[()], tuple[int, ...]))
static_assert(is_subtype_of(tuple[()], tuple[int | str, ...]))
static_assert(is_subtype_of(tuple[()], tuple[Not[AlwaysFalsy], ...]))
static_assert(is_subtype_of(tuple[()], Sequence[int]))
static_assert(not is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Any, ...]))
static_assert(not is_subtype_of(tuple[int, int], tuple[str, ...]))
static_assert(not is_subtype_of(tuple[int, ...], Sequence[Any]))
static_assert(not is_subtype_of(tuple[Any, ...], Sequence[int]))
```
## Union types