diff --git a/pyrefly/lib/alt/solve.rs b/pyrefly/lib/alt/solve.rs index bb519fd536..72a5e96217 100644 --- a/pyrefly/lib/alt/solve.rs +++ b/pyrefly/lib/alt/solve.rs @@ -867,6 +867,17 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { Type::Var(v) if let Some(_guard) = self.recurse(*v) => { self.iterate(&self.solver().force_var(*v), range, errors, orig_context) } + Type::Quantified(q) if q.is_type_var() => { + // A TypeVar iterates like its upper bound: `Z: tuple[str, int]` must + // unpack positionally rather than collapse to the element join via the + // iterable protocol. Mirrors attribute access on a bounded TypeVar. + self.iterate( + &q.upper_bound(self.stdlib, self.heap), + range, + errors, + orig_context, + ) + } Type::Union(f) => f .members .iter() diff --git a/pyrefly/lib/test/tuple.rs b/pyrefly/lib/test/tuple.rs index d5f87e41ac..ef3aefdc2a 100644 --- a/pyrefly/lib/test/tuple.rs +++ b/pyrefly/lib/test/tuple.rs @@ -370,6 +370,79 @@ def test[*Ts](x1: tuple[int, *tuple[str, ...]], x2: tuple[*Ts]) -> None: "#, ); +testcase!( + test_unpack_typevar_bound_to_tuple, + r#" +from typing import reveal_type +def f[Z: tuple[str, int]](x: Z): + u, v = x + reveal_type(u) # E: revealed type: str + reveal_type(v) # E: revealed type: int +"#, +); + +testcase!( + test_unpack_typevar_bound_to_tuple_three_elements, + r#" +from typing import reveal_type +def f[Z: tuple[str, int, bytes]](x: Z): + a, b, c = x + reveal_type(a) # E: revealed type: str + reveal_type(b) # E: revealed type: int + reveal_type(c) # E: revealed type: bytes +"#, +); + +testcase!( + test_unpack_typevar_bound_to_unbounded_tuple, + r#" +from typing import reveal_type +def f[Z: tuple[int, ...]](x: Z): + a, b = x + reveal_type(a) # E: revealed type: int + reveal_type(b) # E: revealed type: int +"#, +); + +testcase!( + test_unpack_typevar_bound_to_tuple_starred, + r#" +from typing import reveal_type +def f[Z: tuple[str, int, bytes]](x: Z): + a, *b = x + reveal_type(a) # E: revealed type: str + reveal_type(b) # E: revealed type: list[bytes | int] +"#, +); + +testcase!( + test_unpack_constrained_typevar_tuple, + r#" +from typing import TypeVar, reveal_type +Z = TypeVar("Z", tuple[str, int], tuple[bool, bytes]) +def f(x: Z): + a, b = x + reveal_type(a) # E: revealed type: bool | str + reveal_type(b) # E: revealed type: bytes | int +"#, +); + +testcase!( + test_unpack_typevar_unbounded_not_iterable, + r#" +def f[Z](x: Z): + a, b = x # E: Type `object` is not iterable +"#, +); + +testcase!( + test_unpack_typevar_bound_not_iterable, + r#" +def f[Z: int](x: Z): + a, b = x # E: Type `int` is not iterable +"#, +); + testcase!( test_tuple_slice_non_literal, r#"