Skip to content

Commit 676cd11

Browse files
authored
Fixed several bugs related to subscripts with unpack operators, notably when the unpack targets a tuple with a known length. This addresses #10723. (#10971)
1 parent 8996f91 commit 676cd11

3 files changed

Lines changed: 121 additions & 13 deletions

File tree

packages/pyright-internal/src/analyzer/typeEvaluator.ts

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8135,24 +8135,91 @@ export function createTypeEvaluator(
81358135
} else {
81368136
// Package up all of the positionals into a tuple.
81378137
const tupleTypeArgs: TupleTypeArg[] = [];
8138-
positionalArgs.forEach((arg) => {
8139-
const typeResult = getTypeOfExpression(arg.d.valueExpr);
8140-
tupleTypeArgs.push({ type: typeResult.type, isUnbounded: false });
8141-
if (typeResult.isIncomplete) {
8142-
isPositionalIndexTypeIncomplete = true;
8138+
8139+
const getDeterministicTupleEntries = (type: Type): TupleTypeArg[] | undefined => {
8140+
let aggregatedArgs: TupleTypeArg[] | undefined;
8141+
let isDeterministic = true;
8142+
8143+
doForEachSubtype(type, (subtype) => {
8144+
if (!isDeterministic) {
8145+
return;
8146+
}
8147+
8148+
const tupleType = getSpecializedTupleType(subtype);
8149+
const tupleTypeArgs = tupleType?.priv.tupleTypeArgs;
8150+
8151+
if (
8152+
!tupleTypeArgs ||
8153+
tupleTypeArgs.some((entry) => entry.isUnbounded || isTypeVarTuple(entry.type))
8154+
) {
8155+
isDeterministic = false;
8156+
return;
8157+
}
8158+
8159+
if (!aggregatedArgs) {
8160+
aggregatedArgs = tupleTypeArgs.map((entry) => ({ type: entry.type, isUnbounded: false }));
8161+
return;
8162+
}
8163+
8164+
if (aggregatedArgs.length !== tupleTypeArgs.length) {
8165+
isDeterministic = false;
8166+
return;
8167+
}
8168+
8169+
for (let i = 0; i < aggregatedArgs.length; i++) {
8170+
aggregatedArgs[i] = {
8171+
type: combineTypes([aggregatedArgs[i].type, tupleTypeArgs[i].type]),
8172+
isUnbounded: false,
8173+
};
8174+
}
8175+
});
8176+
8177+
if (!isDeterministic || !aggregatedArgs) {
8178+
return undefined;
81438179
}
8144-
});
81458180

8146-
unpackedListArgs.forEach((arg) => {
8147-
const typeResult = getTypeOfExpression(arg.d.valueExpr);
8148-
if (typeResult.isIncomplete) {
8149-
isPositionalIndexTypeIncomplete = true;
8181+
return aggregatedArgs;
8182+
};
8183+
8184+
node.d.items.forEach((arg) => {
8185+
if (arg.d.argCategory === ArgCategory.Simple) {
8186+
const typeResult = getTypeOfExpression(arg.d.valueExpr);
8187+
tupleTypeArgs.push({ type: typeResult.type, isUnbounded: false });
8188+
if (typeResult.isIncomplete) {
8189+
isPositionalIndexTypeIncomplete = true;
8190+
}
8191+
return;
8192+
}
8193+
8194+
if (arg.d.argCategory === ArgCategory.UnpackedList) {
8195+
const typeResult = getTypeOfExpression(arg.d.valueExpr);
8196+
if (typeResult.isIncomplete) {
8197+
isPositionalIndexTypeIncomplete = true;
8198+
}
8199+
8200+
const deterministicEntries = getDeterministicTupleEntries(typeResult.type);
8201+
if (deterministicEntries) {
8202+
appendArray(tupleTypeArgs, deterministicEntries);
8203+
return;
8204+
}
8205+
8206+
const iterableType =
8207+
getTypeOfIterator(typeResult, /* isAsync */ false, arg.d.valueExpr)?.type ??
8208+
UnknownType.create();
8209+
tupleTypeArgs.push({ type: iterableType, isUnbounded: true });
81508210
}
8151-
const iterableType =
8152-
getTypeOfIterator(typeResult, /* isAsync */ false, arg.d.valueExpr)?.type ?? UnknownType.create();
8153-
tupleTypeArgs.push({ type: iterableType, isUnbounded: true });
81548211
});
81558212

8213+
const unboundedCount = tupleTypeArgs.filter((typeArg) => typeArg.isUnbounded).length;
8214+
if (unboundedCount > 1) {
8215+
const firstUnboundedIndex = tupleTypeArgs.findIndex((typeArg) => typeArg.isUnbounded);
8216+
const removedEntries = tupleTypeArgs.splice(firstUnboundedIndex);
8217+
tupleTypeArgs.push({
8218+
type: combineTypes(removedEntries.map((entry) => entry.type)),
8219+
isUnbounded: true,
8220+
});
8221+
}
8222+
81568223
positionalIndexType = makeTupleObject(evaluatorInterface, tupleTypeArgs);
81578224
}
81588225

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# This sample tests the handling of unpack operators within
2+
# a subscript.
3+
4+
from typing import NamedTuple
5+
6+
7+
class Recorder[T]:
8+
def __getitem__(self, item: T) -> T:
9+
return item
10+
11+
12+
class OneInt(NamedTuple):
13+
value: int
14+
15+
16+
class IntStrPair(NamedTuple):
17+
first: int
18+
second: str
19+
20+
21+
recorder_pair: Recorder[tuple[int, str]] = Recorder()
22+
pair = IntStrPair(1, "value")
23+
result1 = recorder_pair[*pair]
24+
reveal_type(result1, expected_text="tuple[int, str]")
25+
26+
recorder_order: Recorder[tuple[int, str]] = Recorder()
27+
tail_value: str = "tail"
28+
result2 = recorder_order[*OneInt(2), tail_value]
29+
reveal_type(result2, expected_text="tuple[int, str]")
30+
31+
recorder_multi: Recorder[tuple[int, *tuple[int | str, ...]]] = Recorder()
32+
values1: list[int] = []
33+
values2: list[str] = []
34+
first_value: int = 0
35+
result3 = recorder_multi[first_value, *values1, *values2]
36+
reveal_type(result3, expected_text="tuple[int, *tuple[int | str, ...]]")

packages/pyright-internal/src/tests/typeEvaluator6.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,11 @@ test('Subscript3', () => {
964964
TestUtils.validateResults(analysisResults, 0);
965965
});
966966

967+
test('Subscript4', () => {
968+
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['subscript4.py']);
969+
TestUtils.validateResults(analysisResults, 0);
970+
});
971+
967972
test('Decorator1', () => {
968973
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['decorator1.py']);
969974

0 commit comments

Comments
 (0)