From c7cf8812c6053c47051bf64840b560ad568f08bd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 4 Dec 2025 19:06:11 -0800 Subject: [PATCH 1/2] Allow tuple[Any, ...] to assign to tuple[int, *tuple[int, ...]] --- .../type_properties/is_assignable_to.md | 6 ++++ crates/ty_python_semantic/src/types/tuple.rs | 36 +++++++++++++++---- 2 files changed, 36 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 885d3c2e4f1944..34317e51677219 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. @@ -580,6 +583,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 6405ae3ae7dce4..c852fdcbdc7e66 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -994,10 +994,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 @@ -1033,10 +1045,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 From bc4cc801e38adce189f17b5b4ebdba95d8804e54 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Dec 2025 10:59:03 -0800 Subject: [PATCH 2/2] Add test --- .../resources/mdtest/type_properties/is_assignable_to.md | 4 ++++ 1 file changed, 4 insertions(+) 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 34317e51677219..895c835bd522e4 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 @@ -563,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]))