[ty] Support variable-length tuples in unpacking assignments (#18948)

This PR updates our unpacking assignment logic to use the new tuple
machinery. As a result, we can now unpack variable-length tuples
correctly.

As part of this, the `TupleSpec` classes have been renamed to `Tuple`,
and can now contain any element (Rust) type, not just `Type<'db>`. The
unpacker uses a tuple of `UnionBuilder`s to maintain the types that will
be assigned to each target, as we iterate through potentially many union
elements on the rhs. We also add a new consuming iterator for tuples,
and update the `all_elements` methods to wrap the result in an enum
(similar to `itertools::Position`) letting you know which part of the
tuple each element appears in. I also added a new
`UnionBuilder::try_build`, which lets you specify a different fallback
type if the union contains no elements.
This commit is contained in:
Douglas Creager 2025-06-27 15:29:04 -04:00 committed by GitHub
parent a50a993b9c
commit c60e590b4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 779 additions and 423 deletions

View file

@ -24,7 +24,7 @@ error[invalid-assignment]: Not enough values to unpack
1 | [a, *b, c, d] = (1, 2) # error: [invalid-assignment]
| ^^^^^^^^^^^^^ ------ Got 2
| |
| Expected 3 or more
| Expected at least 3
|
info: rule `invalid-assignment` is enabled by default

View file

@ -106,7 +106,7 @@ reveal_type(d) # revealed: Literal[5]
### Starred expression (1)
```py
# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more"
# error: [invalid-assignment] "Not enough values to unpack: Expected at least 3"
[a, *b, c, d] = (1, 2)
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: list[Unknown]
@ -119,7 +119,7 @@ reveal_type(d) # revealed: Unknown
```py
[a, *b, c] = (1, 2)
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: list[Unknown]
reveal_type(b) # revealed: list[Never]
reveal_type(c) # revealed: Literal[2]
```
@ -154,7 +154,7 @@ reveal_type(c) # revealed: list[Literal[3, 4]]
### Starred expression (6)
```py
# error: [invalid-assignment] "Not enough values to unpack: Expected 5 or more"
# error: [invalid-assignment] "Not enough values to unpack: Expected at least 5"
(a, b, c, *d, e, f) = (1,)
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@ -258,6 +258,155 @@ def _(value: list[int]):
reveal_type(c) # revealed: int
```
## Homogeneous tuples
### Simple unpacking
```py
def _(value: tuple[int, ...]):
a, b = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int
```
### Nested unpacking
```py
def _(value: tuple[tuple[int, ...], ...]):
a, (b, c) = value
reveal_type(a) # revealed: tuple[int, ...]
reveal_type(b) # revealed: int
reveal_type(c) # revealed: int
```
### Invalid nested unpacking
```py
def _(value: tuple[int, ...]):
# error: [not-iterable] "Object of type `int` is not iterable"
a, (b, c) = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
```
### Starred expression
```py
def _(value: tuple[int, ...]):
a, *b, c = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: list[int]
reveal_type(c) # revealed: int
```
## Mixed tuples
```toml
[environment]
python-version = "3.11"
```
### Simple unpacking (1)
```py
def _(value: tuple[int, *tuple[str, ...]]):
a, b = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: str
```
### Simple unpacking (2)
```py
def _(value: tuple[int, int, *tuple[str, ...]]):
a, b = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int
```
### Simple unpacking (3)
```py
def _(value: tuple[int, *tuple[str, ...], int]):
a, b, c = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: str
reveal_type(c) # revealed: int
```
### Invalid unpacked
```py
def _(value: tuple[int, int, int, *tuple[str, ...]]):
# error: [invalid-assignment] "Too many values to unpack: Expected 2"
a, b = value
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```
### Nested unpacking
```py
def _(value: tuple[str, *tuple[tuple[int, ...], ...]]):
a, (b, c) = value
reveal_type(a) # revealed: str
reveal_type(b) # revealed: int
reveal_type(c) # revealed: int
```
### Invalid nested unpacking
```py
def _(value: tuple[str, *tuple[int, ...]]):
# error: [not-iterable] "Object of type `int` is not iterable"
a, (b, c) = value
reveal_type(a) # revealed: str
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
```
### Starred expression (1)
```py
def _(value: tuple[int, *tuple[str, ...]]):
a, *b, c = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: list[str]
reveal_type(c) # revealed: str
```
### Starred expression (2)
```py
def _(value: tuple[int, *tuple[str, ...], int]):
a, *b, c = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: list[str]
reveal_type(c) # revealed: int
```
### Starred expression (3)
```py
def _(value: tuple[int, *tuple[str, ...], int]):
a, *b, c, d = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: list[str]
reveal_type(c) # revealed: str
reveal_type(d) # revealed: int
```
### Starred expression (4)
```py
def _(value: tuple[int, int, *tuple[str, ...], int]):
a, *b, c = value
reveal_type(a) # revealed: int
reveal_type(b) # revealed: list[int | str]
reveal_type(c) # revealed: int
```
## String
### Simple unpacking
@ -290,7 +439,7 @@ reveal_type(b) # revealed: Unknown
### Starred expression (1)
```py
# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more"
# error: [invalid-assignment] "Not enough values to unpack: Expected at least 3"
(a, *b, c, d) = "ab"
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: list[Unknown]
@ -299,7 +448,7 @@ reveal_type(d) # revealed: Unknown
```
```py
# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more"
# error: [invalid-assignment] "Not enough values to unpack: Expected at least 3"
(a, b, *c, d) = "a"
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@ -312,7 +461,7 @@ reveal_type(d) # revealed: Unknown
```py
(a, *b, c) = "ab"
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: list[Unknown]
reveal_type(b) # revealed: list[Never]
reveal_type(c) # revealed: LiteralString
```