From ef45c97dab2e1ab8268308a53e6dd452db40b0db Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Dec 2025 11:04:23 -0800 Subject: [PATCH] [ty] Allow `tuple[Any, ...]` to assign to `tuple[int, *tuple[int, ...]]` (#21803) ## Summary Closes https://github.com/astral-sh/ty/issues/1750. --- .../type_properties/is_assignable_to.md | 10 ++++++ crates/ty_python_semantic/src/types/tuple.rs | 36 +++++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 3bd3cd1f09..f9f85641f7 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -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, 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. @@ -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, 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[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(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, *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[Any, ...])) diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 787bbe0688..9b046cd4cf 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -998,10 +998,22 @@ impl<'db> VariableLengthTuple> { relation_visitor, disjointness_visitor, ), - EitherOrBoth::Right(_) => { + EitherOrBoth::Right(other_ty) => { // The rhs has a required element that the lhs is not guaranteed to - // provide. - return ConstraintSet::from(false); + // 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); + } + self.variable.has_relation_to_impl( + db, + other_ty, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) } }; if result @@ -1037,10 +1049,22 @@ impl<'db> VariableLengthTuple> { relation_visitor, disjointness_visitor, ), - EitherOrBoth::Right(_) => { + EitherOrBoth::Right(other_ty) => { // The rhs has a required element that the lhs is not guaranteed to - // provide. - return ConstraintSet::from(false); + // 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); + } + self.variable.has_relation_to_impl( + db, + *other_ty, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) } }; if result