Iterate a TypeVar through its upper bound when unpacking#3864
Open
mikeleppane wants to merge 1 commit into
Open
Iterate a TypeVar through its upper bound when unpacking#3864mikeleppane wants to merge 1 commit into
mikeleppane wants to merge 1 commit into
Conversation
Unpacking `u, v = x` where `x: Z` and `Z` is a TypeVar bounded by `tuple[str, int]` revealed `int | str` for both targets instead of the positional `str` and `int`. The direct `x: tuple[str, int]` case was already correct, so the two were not equivalent, even though pyright, mypy, and ty all agree on the positional types. `AnswersSolver::iterate` had no arm for `Type::Quantified`, so a bounded TypeVar fell through to the iterable-protocol fallback, which views the value as `Iterable[T]` and collapses a fixed-shape tuple to the join of its element types, losing each position. Iterate a TypeVar through its upper bound instead, so the bound's tuple shape is preserved. Reusing `upper_bound` lets the existing union and tuple arms handle the rest: constrained TypeVars iterate per constraint, and an unbounded TypeVar resolves to `object` and is correctly reported as not iterable. This is the same resolve-through-the-bound rule already used for attribute access on a bounded TypeVar. Fixes facebook#3841
|
According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3841
What
Unpacking a value whose type is a TypeVar bounded by a tuple now preserves the
positional element types, matching the behavior of the equivalent
direct-tuple parameter.
pyright (
str*/int*), mypy (str/int), and ty (str/int) all produce thepositional types; pyrefly was the outlier.
Why
A bounded TypeVar
Z <: tuple[str, int]is, within the body, a fixed-shape2-tuple: position 0 is a
str, position 1 anint. Unpacking should hand backthose labelled positions.
AnswersSolver::iteratehad explicit arms for concrete tuples,Type::Var, andType::Union, but no arm forType::Quantified. A bounded TypeVar thereforefell through to the generic iterable-protocol fallback, which views the value as
Iterable[T]for a singleTand collapses the tuple to the join of itselement types (
str | int). Because that fallback yields one element type forevery index, both
uandvcame outint | str.How
pyrefly/lib/alt/solve.rs— add one arm toiterate:A TypeVar iterates like its upper bound. The same resolve-through-the-bound rule
already used for attribute access on a bounded TypeVar (
attr.rs). ReusingQuantified::upper_boundmeans the existing arms handle every restriction shapewith no extra branching:
tuple[str, int]) → recurses into the concrete-tuple arm → positions preserved.list[int]) → recurses into the fallback →int— identical to today.Type::Unionarm → per-position union.object→ not iterable → correct error.Guarded on
q.is_type_var(), so ParamSpec and TypeVarTuple are untouched (a*Tsinside a tuple is still handled by the existingTuple::Unpackedarm).Deliberately not touched:
binding_to_type_unpacked_valueand the iterableprotocol were already correct; the only gap was the missing arm. The
not-iterable error for a non-iterable bound now names the bound (e.g.
object,int) rather than the TypeVar. This matches what pyright and mypy alreadyreport.
Test plan
New
testcase!s inpyrefly/lib/test/tuple.rs:test_unpack_typevar_bound_to_tupleZ: tuple[str, int]→str,inttest_unpack_typevar_bound_to_tuple_three_elementstest_unpack_typevar_bound_to_unbounded_tupletuple[int, ...]bound →intper targettest_unpack_typevar_bound_to_tuple_starreda, *bthrough the boundtest_unpack_constrained_typevar_tupletest_unpack_typevar_unbounded_not_iterableZstill errors (no over-broadening)test_unpack_typevar_bound_not_iterableZ: int) still errorscargo test -p pyrefly --lib: 5723 passed, 0 failedtest.py --no-test --no-conformance --no-jsonschema): cleanmypy_primer(~52 projects incl. pandas, pandas-stubs, pydantic, sympy, scipy-stubs, xarray, attrs, pandera): 0 diagnostic diffs — no regressions