-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] Add property test generators for variable-length tuples #18901
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d190495
148f0e4
93dc5d5
724ea5b
9da796b
e7ea01e
89f4b71
7701f68
fd9d992
fea7112
5e92755
e75a0cc
c0dddb9
e22a8ea
a9ace8f
35a675f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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])) | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| 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 | ||||||||||
| 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`: | ||||||||||
| ```toml | ||||||||||
| [environment] | ||||||||||
| python-version = "3.11" | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| The truthiness of the empty tuple is `False`. | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be worth a note here like this:
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a note at the bottom of the section mentioning this (along with a test that shows we currently allow a subclass that overrides
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (I'm also planning on tackling this this week or next as part of my tuple subclasses/NamedTuples work)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh that's probably sufficient if we have it mentioned somewhere |
||||||||||
|
|
||||||||||
| ```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`, even if all elements are falsy: | ||||||||||
| 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, Literal | ||||||||||
| 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)) | ||||||||||
dcreager marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
|
||||||||||
| 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 | ||||||||||
|
|
||||||||||
Uh oh!
There was an error while loading. Please reload this page.