[ty] Make tuple subclass constructors sound (#19469)

This commit is contained in:
Alex Waygood 2025-07-21 22:25:11 +01:00 committed by GitHub
parent fcdffe4ac9
commit cb5a9ff8dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 201 additions and 66 deletions

View file

@ -93,14 +93,14 @@ class SingleElementTupleSubclass(tuple[int]): ...
reveal_type(bool(SingleElementTupleSubclass((0,)))) # revealed: Literal[True]
reveal_type(SingleElementTupleSubclass.__bool__) # revealed: (self: tuple[int], /) -> Literal[True]
reveal_type(SingleElementTupleSubclass().__bool__) # revealed: () -> Literal[True]
reveal_type(SingleElementTupleSubclass((1,)).__bool__) # revealed: () -> Literal[True]
# Unknown length, but we know the length is guaranteed to be >=2
class MixedTupleSubclass(tuple[int, *tuple[str, ...], bytes]): ...
reveal_type(bool(MixedTupleSubclass((1, b"foo")))) # revealed: Literal[True]
reveal_type(MixedTupleSubclass.__bool__) # revealed: (self: tuple[int, *tuple[str, ...], bytes], /) -> Literal[True]
reveal_type(MixedTupleSubclass().__bool__) # revealed: () -> Literal[True]
reveal_type(MixedTupleSubclass((1, b"foo")).__bool__) # revealed: () -> Literal[True]
# Unknown length with an overridden `__bool__`:
class VariadicTupleSubclassWithDunderBoolOverride(tuple[int, ...]):

View file

@ -19,14 +19,20 @@ def _(p: P, q: Q):
## Instantiating tuples
Like all classes, tuples can be instantiated by invoking the `tuple` class. When instantiating a
specialization of `tuple` we (TODO: should) check that the values passed in match the element types
defined in the specialization.
specialization of `tuple` we check that the values passed in match the element types defined in the
specialization.
```toml
[environment]
python-version = "3.11"
```
```py
from typing_extensions import Iterable, Never
reveal_type(tuple()) # revealed: tuple[()]
reveal_type(tuple[int]((1,))) # revealed: tuple[int]
reveal_type(tuple[int, *tuple[str, ...]]((1,))) # revealed: tuple[int, *tuple[str, ...]]
reveal_type(().__class__()) # revealed: tuple[()]
reveal_type((1, 2).__class__((1, 2))) # revealed: tuple[Literal[1], Literal[2]]
@ -56,6 +62,63 @@ reveal_type((1,).__class__()) # revealed: tuple[Literal[1]]
reveal_type((1, 2).__class__()) # revealed: tuple[Literal[1], Literal[2]]
```
## Instantiating tuple subclasses
Tuple subclasses inherit the special-cased constructors from their tuple superclasses:
```toml
[environment]
python-version = "3.11"
```
```py
from typing_extensions import Iterable, Never
class UnspecializedTupleSubclass(tuple): ...
class EmptyTupleSubclass(tuple[()]): ...
class SingleElementTupleSubclass(tuple[int]): ...
class VariadicTupleSubclass(tuple[int, ...]): ...
class MixedTupleSubclass(tuple[int, *tuple[str, ...]]): ...
reveal_type(UnspecializedTupleSubclass()) # revealed: UnspecializedTupleSubclass
reveal_type(UnspecializedTupleSubclass(())) # revealed: UnspecializedTupleSubclass
reveal_type(UnspecializedTupleSubclass((1, 2, "foo"))) # revealed: UnspecializedTupleSubclass
reveal_type(UnspecializedTupleSubclass([1, 2, "foo", b"bar"])) # revealed: UnspecializedTupleSubclass
reveal_type(EmptyTupleSubclass()) # revealed: EmptyTupleSubclass
reveal_type(EmptyTupleSubclass(())) # revealed: EmptyTupleSubclass
# error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[()]`, found `tuple[Literal[1], Literal[2]]`"
reveal_type(EmptyTupleSubclass((1, 2))) # revealed: EmptyTupleSubclass
reveal_type(SingleElementTupleSubclass((1,))) # revealed: SingleElementTupleSubclass
# error: [missing-argument] "No argument provided for required parameter `iterable`"
reveal_type(SingleElementTupleSubclass()) # revealed: SingleElementTupleSubclass
reveal_type(VariadicTupleSubclass()) # revealed: VariadicTupleSubclass
reveal_type(VariadicTupleSubclass(())) # revealed: VariadicTupleSubclass
reveal_type(VariadicTupleSubclass([1, 2, 3])) # revealed: VariadicTupleSubclass
reveal_type(VariadicTupleSubclass((1, 2, 3, 4))) # revealed: VariadicTupleSubclass
reveal_type(MixedTupleSubclass((1,))) # revealed: MixedTupleSubclass
reveal_type(MixedTupleSubclass((1, "foo"))) # revealed: MixedTupleSubclass
# error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[int, *tuple[str, ...]]`, found `tuple[Literal[1], Literal[b"foo"]]`"
reveal_type(MixedTupleSubclass((1, b"foo"))) # revealed: MixedTupleSubclass
# error: [missing-argument] "No argument provided for required parameter `iterable`"
reveal_type(MixedTupleSubclass()) # revealed: MixedTupleSubclass
def _(empty: EmptyTupleSubclass, single_element: SingleElementTupleSubclass, mixed: MixedTupleSubclass, x: tuple[int, int]):
# error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[()]`, found `tuple[Literal[1], Literal[2]]`"
empty.__class__((1, 2))
# error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[int]`, found `tuple[Literal[1], Literal[2]]`"
single_element.__class__((1, 2))
# error: [missing-argument] "No argument provided for required parameter `iterable`"
mixed.__class__()
```
## Subtyping relationships
The type `tuple[S1, S2]` is a subtype of `tuple[T1, T2]` if and only if `S1` is a subtype of `T1`

View file

@ -916,6 +916,7 @@ c: Callable[[Any], str] = A().g
```py
from typing import Any, Callable
from ty_extensions import static_assert, is_assignable_to
c: Callable[[object], type] = type
c: Callable[[str], Any] = str
@ -936,6 +937,15 @@ class C:
def __init__(self, x: int) -> None: ...
c: Callable[[int], C] = C
def f(a: Callable[..., Any], b: Callable[[Any], Any]): ...
f(tuple, tuple)
def g(a: Callable[[Any, Any], Any]): ...
# error: [invalid-argument-type] "Argument to function `g` is incorrect: Expected `(Any, Any, /) -> Any`, found `<class 'tuple'>`"
g(tuple)
```
### Generic class literal types