mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 18:28:24 +00:00
[ty] Add property test generators for variable-length tuples (#18901)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Add property test generators for the new variable-length tuples. This covers homogeneous tuples as well. The property tests did their job! This identified several fixes we needed to make to various type property methods. cf https://github.com/astral-sh/ruff/pull/18600#issuecomment-2993764471 --------- Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
919af9628d
commit
66f50fb04b
7 changed files with 549 additions and 191 deletions
|
@ -99,13 +99,138 @@ static_assert(is_singleton(None))
|
|||
static_assert(not is_singleton(tuple[None]))
|
||||
```
|
||||
|
||||
## Tuples containing `Never`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
The `Never` type contains no inhabitants, so a tuple type that contains `Never` as a mandatory
|
||||
element also contains no inhabitants.
|
||||
|
||||
```py
|
||||
from typing import Never
|
||||
from ty_extensions import static_assert, is_equivalent_to
|
||||
|
||||
static_assert(is_equivalent_to(tuple[Never], Never))
|
||||
static_assert(is_equivalent_to(tuple[int, Never], Never))
|
||||
static_assert(is_equivalent_to(tuple[Never, *tuple[int, ...]], Never))
|
||||
```
|
||||
|
||||
If the variable-length portion of a tuple is `Never`, then that portion of the tuple must always be
|
||||
empty. This means that the tuple is not actually variable-length!
|
||||
|
||||
```py
|
||||
from typing import Never
|
||||
from ty_extensions import static_assert, is_equivalent_to
|
||||
|
||||
static_assert(is_equivalent_to(tuple[Never, ...], tuple[()]))
|
||||
static_assert(is_equivalent_to(tuple[int, *tuple[Never, ...]], tuple[int]))
|
||||
static_assert(is_equivalent_to(tuple[int, *tuple[Never, ...], int], tuple[int, int]))
|
||||
static_assert(is_equivalent_to(tuple[*tuple[Never, ...], int], tuple[int]))
|
||||
```
|
||||
|
||||
## Homogeneous non-empty tuples
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
A homogeneous tuple can contain zero or more elements of a particular type. You can represent a
|
||||
tuple that can contain _one_ or more elements of that type (or any other number of minimum elements)
|
||||
using a mixed tuple.
|
||||
|
||||
```py
|
||||
def takes_zero_or_more(t: tuple[int, ...]) -> None: ...
|
||||
def takes_one_or_more(t: tuple[int, *tuple[int, ...]]) -> None: ...
|
||||
def takes_two_or_more(t: tuple[int, int, *tuple[int, ...]]) -> None: ...
|
||||
|
||||
takes_zero_or_more(())
|
||||
takes_zero_or_more((1,))
|
||||
takes_zero_or_more((1, 2))
|
||||
|
||||
takes_one_or_more(()) # error: [invalid-argument-type]
|
||||
takes_one_or_more((1,))
|
||||
takes_one_or_more((1, 2))
|
||||
|
||||
takes_two_or_more(()) # error: [invalid-argument-type]
|
||||
takes_two_or_more((1,)) # error: [invalid-argument-type]
|
||||
takes_two_or_more((1, 2))
|
||||
```
|
||||
|
||||
The required elements can also appear in the suffix of the mixed tuple type.
|
||||
|
||||
```py
|
||||
def takes_one_or_more_suffix(t: tuple[*tuple[int, ...], int]) -> None: ...
|
||||
def takes_two_or_more_suffix(t: tuple[*tuple[int, ...], int, int]) -> None: ...
|
||||
def takes_two_or_more_mixed(t: tuple[int, *tuple[int, ...], int]) -> None: ...
|
||||
|
||||
takes_one_or_more_suffix(()) # error: [invalid-argument-type]
|
||||
takes_one_or_more_suffix((1,))
|
||||
takes_one_or_more_suffix((1, 2))
|
||||
|
||||
takes_two_or_more_suffix(()) # error: [invalid-argument-type]
|
||||
takes_two_or_more_suffix((1,)) # error: [invalid-argument-type]
|
||||
takes_two_or_more_suffix((1, 2))
|
||||
|
||||
takes_two_or_more_mixed(()) # error: [invalid-argument-type]
|
||||
takes_two_or_more_mixed((1,)) # error: [invalid-argument-type]
|
||||
takes_two_or_more_mixed((1, 2))
|
||||
```
|
||||
|
||||
The tuple types are equivalent regardless of whether the required elements appear in the prefix or
|
||||
suffix.
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_subtype_of, is_equivalent_to
|
||||
|
||||
static_assert(is_equivalent_to(tuple[int, *tuple[int, ...]], tuple[*tuple[int, ...], int]))
|
||||
|
||||
static_assert(is_equivalent_to(tuple[int, int, *tuple[int, ...]], tuple[*tuple[int, ...], int, int]))
|
||||
static_assert(is_equivalent_to(tuple[int, int, *tuple[int, ...]], tuple[int, *tuple[int, ...], int]))
|
||||
```
|
||||
|
||||
This is true when the prefix/suffix and variable-length types are equivalent, not just identical.
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_subtype_of, is_equivalent_to
|
||||
|
||||
static_assert(is_equivalent_to(tuple[int | str, *tuple[str | int, ...]], tuple[*tuple[str | int, ...], int | str]))
|
||||
|
||||
static_assert(
|
||||
is_equivalent_to(tuple[int | str, str | int, *tuple[str | int, ...]], tuple[*tuple[int | str, ...], str | int, int | str])
|
||||
)
|
||||
static_assert(
|
||||
is_equivalent_to(tuple[int | str, str | int, *tuple[str | int, ...]], tuple[str | int, *tuple[int | str, ...], int | str])
|
||||
)
|
||||
```
|
||||
|
||||
## Disjointness
|
||||
|
||||
A tuple `tuple[P1, P2]` is disjoint from a tuple `tuple[Q1, Q2]` if either `P1` is disjoint from
|
||||
`Q1` or if `P2` is disjoint from `Q2`:
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
Two tuples with incompatible minimum lengths are always disjoint, regardless of their element types.
|
||||
(The lengths are incompatible if the minimum length of one tuple is larger than the maximum length
|
||||
of the other.)
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_disjoint_from
|
||||
|
||||
static_assert(is_disjoint_from(tuple[()], tuple[int]))
|
||||
static_assert(not is_disjoint_from(tuple[()], tuple[int, ...]))
|
||||
static_assert(not is_disjoint_from(tuple[int], tuple[int, ...]))
|
||||
static_assert(not is_disjoint_from(tuple[str, ...], tuple[int, ...]))
|
||||
```
|
||||
|
||||
A tuple that is required to contain elements `P1, P2` is disjoint from a tuple that is required to
|
||||
contain elements `Q1, Q2` if either `P1` is disjoint from `Q1` or if `P2` is disjoint from `Q2`.
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
@final
|
||||
|
@ -124,9 +249,28 @@ static_assert(is_disjoint_from(tuple[F1, F2], tuple[F2, F1]))
|
|||
static_assert(is_disjoint_from(tuple[F1, N1], tuple[F2, N2]))
|
||||
static_assert(is_disjoint_from(tuple[N1, F1], tuple[N2, F2]))
|
||||
static_assert(not is_disjoint_from(tuple[N1, N2], tuple[N2, N1]))
|
||||
|
||||
static_assert(is_disjoint_from(tuple[F1, *tuple[int, ...], F2], tuple[F2, *tuple[int, ...], F1]))
|
||||
static_assert(is_disjoint_from(tuple[F1, *tuple[int, ...], N1], tuple[F2, *tuple[int, ...], N2]))
|
||||
static_assert(is_disjoint_from(tuple[N1, *tuple[int, ...], F1], tuple[N2, *tuple[int, ...], F2]))
|
||||
static_assert(not is_disjoint_from(tuple[N1, *tuple[int, ...], N2], tuple[N2, *tuple[int, ...], N1]))
|
||||
|
||||
static_assert(not is_disjoint_from(tuple[F1, F2, *tuple[object, ...]], tuple[*tuple[object, ...], F2, F1]))
|
||||
static_assert(not is_disjoint_from(tuple[F1, N1, *tuple[object, ...]], tuple[*tuple[object, ...], F2, N2]))
|
||||
static_assert(not is_disjoint_from(tuple[N1, F1, *tuple[object, ...]], tuple[*tuple[object, ...], N2, F2]))
|
||||
static_assert(not is_disjoint_from(tuple[N1, N2, *tuple[object, ...]], tuple[*tuple[object, ...], N2, N1]))
|
||||
```
|
||||
|
||||
We currently model tuple types to *not* be disjoint from arbitrary instance types, because we allow
|
||||
The variable-length portion of a tuple can never cause the tuples to be disjoint, since all
|
||||
variable-length tuple types contain the empty tuple. (Note that per above, the variable-length
|
||||
portion of a tuple cannot be `Never`; internally we simplify this to a fixed-length tuple.)
|
||||
|
||||
```py
|
||||
static_assert(not is_disjoint_from(tuple[F1, ...], tuple[F2, ...]))
|
||||
static_assert(not is_disjoint_from(tuple[N1, ...], tuple[N2, ...]))
|
||||
```
|
||||
|
||||
We currently model tuple types to _not_ be disjoint from arbitrary instance types, because we allow
|
||||
for the possibility of `tuple` to be subclassed
|
||||
|
||||
```py
|
||||
|
@ -152,21 +296,71 @@ class CommonSubtypeOfTuples(I1, I2): ...
|
|||
|
||||
## Truthiness
|
||||
|
||||
The truthiness of the empty tuple is `False`:
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type, Literal
|
||||
|
||||
assert_type(bool(()), Literal[False])
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
The truthiness of non-empty tuples is always `True`, even if all elements are falsy:
|
||||
The truthiness of the empty tuple is `False`.
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type, Literal
|
||||
from ty_extensions import static_assert, is_assignable_to, AlwaysFalsy
|
||||
|
||||
assert_type(bool(()), Literal[False])
|
||||
|
||||
static_assert(is_assignable_to(tuple[()], AlwaysFalsy))
|
||||
```
|
||||
|
||||
The truthiness of non-empty tuples is always `True`. This is true even if all elements are falsy,
|
||||
and even if any element is gradual, since the truthiness of a tuple depends only on its length, not
|
||||
its content.
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type, Any, Literal
|
||||
from ty_extensions import static_assert, is_assignable_to, AlwaysTruthy
|
||||
|
||||
assert_type(bool((False,)), Literal[True])
|
||||
assert_type(bool((False, False)), Literal[True])
|
||||
|
||||
static_assert(is_assignable_to(tuple[Any], AlwaysTruthy))
|
||||
static_assert(is_assignable_to(tuple[Any, Any], AlwaysTruthy))
|
||||
static_assert(is_assignable_to(tuple[bool], AlwaysTruthy))
|
||||
static_assert(is_assignable_to(tuple[bool, bool], AlwaysTruthy))
|
||||
static_assert(is_assignable_to(tuple[Literal[False]], AlwaysTruthy))
|
||||
static_assert(is_assignable_to(tuple[Literal[False], Literal[False]], AlwaysTruthy))
|
||||
```
|
||||
|
||||
The truthiness of variable-length tuples is ambiguous, since that type contains both empty and
|
||||
non-empty tuples.
|
||||
|
||||
```py
|
||||
from typing_extensions import Any, Literal
|
||||
from ty_extensions import static_assert, is_assignable_to, AlwaysFalsy, AlwaysTruthy
|
||||
|
||||
static_assert(not is_assignable_to(tuple[Any, ...], AlwaysFalsy))
|
||||
static_assert(not is_assignable_to(tuple[Any, ...], AlwaysTruthy))
|
||||
static_assert(not is_assignable_to(tuple[bool, ...], AlwaysFalsy))
|
||||
static_assert(not is_assignable_to(tuple[bool, ...], AlwaysTruthy))
|
||||
static_assert(not is_assignable_to(tuple[Literal[False], ...], AlwaysFalsy))
|
||||
static_assert(not is_assignable_to(tuple[Literal[False], ...], AlwaysTruthy))
|
||||
static_assert(not is_assignable_to(tuple[Literal[True], ...], AlwaysFalsy))
|
||||
static_assert(not is_assignable_to(tuple[Literal[True], ...], AlwaysTruthy))
|
||||
|
||||
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], AlwaysTruthy))
|
||||
static_assert(is_assignable_to(tuple[int, *tuple[bool, ...]], AlwaysTruthy))
|
||||
static_assert(is_assignable_to(tuple[int, *tuple[Literal[False], ...]], AlwaysTruthy))
|
||||
static_assert(is_assignable_to(tuple[int, *tuple[Literal[True], ...]], AlwaysTruthy))
|
||||
|
||||
static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], AlwaysTruthy))
|
||||
static_assert(is_assignable_to(tuple[*tuple[bool, ...], int], AlwaysTruthy))
|
||||
static_assert(is_assignable_to(tuple[*tuple[Literal[False], ...], int], AlwaysTruthy))
|
||||
static_assert(is_assignable_to(tuple[*tuple[Literal[True], ...], int], AlwaysTruthy))
|
||||
|
||||
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], AlwaysTruthy))
|
||||
static_assert(is_assignable_to(tuple[int, *tuple[bool, ...], int], AlwaysTruthy))
|
||||
static_assert(is_assignable_to(tuple[int, *tuple[Literal[False], ...], int], AlwaysTruthy))
|
||||
static_assert(is_assignable_to(tuple[int, *tuple[Literal[True], ...], int], AlwaysTruthy))
|
||||
```
|
||||
|
||||
Both of these results are conflicting with the fact that tuples can be subclassed, and that we
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue