[ty] Allow `tuple[Any, ...]` to assign to `tuple[int, *tuple[int, ...]]` (#21803)

## Summary

Closes https://github.com/astral-sh/ty/issues/1750.
This commit is contained in:
Charlie Marsh 2025-12-05 11:04:23 -08:00 committed by GitHub
parent 9714c589e1
commit ef45c97dab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 40 additions and 6 deletions

View File

@ -537,6 +537,9 @@ static_assert(is_assignable_to(tuple[Any, ...], tuple[Any, Any]))
static_assert(is_assignable_to(tuple[Any, ...], tuple[int, ...])) static_assert(is_assignable_to(tuple[Any, ...], tuple[int, ...]))
static_assert(is_assignable_to(tuple[Any, ...], tuple[int])) static_assert(is_assignable_to(tuple[Any, ...], tuple[int]))
static_assert(is_assignable_to(tuple[Any, ...], tuple[int, int])) static_assert(is_assignable_to(tuple[Any, ...], tuple[int, int]))
static_assert(is_assignable_to(tuple[Any, ...], tuple[int, *tuple[int, ...]]))
static_assert(is_assignable_to(tuple[Any, ...], tuple[*tuple[int, ...], int]))
static_assert(is_assignable_to(tuple[Any, ...], tuple[int, *tuple[int, ...], int]))
``` ```
This also applies when `tuple[Any, ...]` is unpacked into a mixed tuple. This also applies when `tuple[Any, ...]` is unpacked into a mixed tuple.
@ -560,6 +563,10 @@ static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[int, ...]))
static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[int])) static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[int]))
static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[int, int])) static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[int, int]))
# `*tuple[Any, ...]` can materialize to a tuple of any length as a special case,
# so this passes:
static_assert(is_assignable_to(tuple[*tuple[Any, ...], Any], tuple[*tuple[Any, ...], Any, Any]))
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int, *tuple[Any, ...], int])) static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int, *tuple[Any, ...], int]))
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[Any, ...])) static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[Any, ...]))
static_assert(not is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[Any])) static_assert(not is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[Any]))
@ -580,6 +587,9 @@ static_assert(not is_assignable_to(tuple[int, ...], tuple[Any, Any]))
static_assert(is_assignable_to(tuple[int, ...], tuple[int, ...])) static_assert(is_assignable_to(tuple[int, ...], tuple[int, ...]))
static_assert(not is_assignable_to(tuple[int, ...], tuple[int])) static_assert(not is_assignable_to(tuple[int, ...], tuple[int]))
static_assert(not is_assignable_to(tuple[int, ...], tuple[int, int])) static_assert(not is_assignable_to(tuple[int, ...], tuple[int, int]))
static_assert(not is_assignable_to(tuple[int, ...], tuple[int, *tuple[int, ...]]))
static_assert(not is_assignable_to(tuple[int, ...], tuple[*tuple[int, ...], int]))
static_assert(not is_assignable_to(tuple[int, ...], tuple[int, *tuple[int, ...], int]))
static_assert(is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int, *tuple[Any, ...]])) static_assert(is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int, *tuple[Any, ...]]))
static_assert(is_assignable_to(tuple[int, *tuple[int, ...]], tuple[Any, ...])) static_assert(is_assignable_to(tuple[int, *tuple[int, ...]], tuple[Any, ...]))

View File

@ -998,11 +998,23 @@ impl<'db> VariableLengthTuple<Type<'db>> {
relation_visitor, relation_visitor,
disjointness_visitor, disjointness_visitor,
), ),
EitherOrBoth::Right(_) => { EitherOrBoth::Right(other_ty) => {
// The rhs has a required element that the lhs is not guaranteed to // The rhs has a required element that the lhs is not guaranteed to
// provide. // provide, unless the lhs has a dynamic variable-length portion
// that can materialize to provide it (for assignability only),
// as in `tuple[Any, ...]` matching `tuple[int, int]`.
if !relation.is_assignability() || !self.variable.is_dynamic() {
return ConstraintSet::from(false); return ConstraintSet::from(false);
} }
self.variable.has_relation_to_impl(
db,
other_ty,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
}
}; };
if result if result
.intersect(db, pair_constraints) .intersect(db, pair_constraints)
@ -1037,11 +1049,23 @@ impl<'db> VariableLengthTuple<Type<'db>> {
relation_visitor, relation_visitor,
disjointness_visitor, disjointness_visitor,
), ),
EitherOrBoth::Right(_) => { EitherOrBoth::Right(other_ty) => {
// The rhs has a required element that the lhs is not guaranteed to // The rhs has a required element that the lhs is not guaranteed to
// provide. // provide, unless the lhs has a dynamic variable-length portion
// that can materialize to provide it (for assignability only),
// as in `tuple[Any, ...]` matching `tuple[int, int]`.
if !relation.is_assignability() || !self.variable.is_dynamic() {
return ConstraintSet::from(false); return ConstraintSet::from(false);
} }
self.variable.has_relation_to_impl(
db,
*other_ty,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
}
}; };
if result if result
.intersect(db, pair_constraints) .intersect(db, pair_constraints)