mirror of
https://github.com/microsoft/pyright.git
synced 2024-09-11 07:55:56 +03:00
Added support for new type guard pattern: x[I] is None
and x[I] is not None
where x
is a tuple or union of tuples with known lengths and entry types and I
is an integer.
This commit is contained in:
parent
ff55b895f9
commit
1cbb690b11
@ -155,6 +155,7 @@ In addition to assignment-based type narrowing, Pyright supports the following t
|
|||||||
* `x.y == L` and `x.y != L` (where L is a literal expression and x is a type that is distinguished by a field with a literal type)
|
* `x.y == L` and `x.y != L` (where L is a literal expression and x is a type that is distinguished by a field with a literal type)
|
||||||
* `x[K] == V` and `x[K] != V` (where K and V are literal expressions and x is a type that is distinguished by a TypedDict field with a literal type)
|
* `x[K] == V` and `x[K] != V` (where K and V are literal expressions and x is a type that is distinguished by a TypedDict field with a literal type)
|
||||||
* `x[I] == V` and `x[I] != V` (where I and V are literal expressions and x is a known-length tuple that is distinguished by the index indicated by I)
|
* `x[I] == V` and `x[I] != V` (where I and V are literal expressions and x is a known-length tuple that is distinguished by the index indicated by I)
|
||||||
|
* `x[I] is None` and `x[I] is not None` (where I is a literal expression and x is a known-length tuple that is distinguished by the index indicated by I)
|
||||||
* `x in y` (where y is instance of list, set, frozenset, or deque)
|
* `x in y` (where y is instance of list, set, frozenset, or deque)
|
||||||
* `S in D` and `S not in D` (where S is a string literal and D is a TypedDict)
|
* `S in D` and `S not in D` (where S is a string literal and D is a TypedDict)
|
||||||
* `isinstance(x, T)` (where T is a type or a tuple of types)
|
* `isinstance(x, T)` (where T is a type or a tuple of types)
|
||||||
|
@ -117,42 +117,25 @@ export function getTypeNarrowingCallback(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ParseTreeUtils.isMatchingExpression(reference, leftExpression)) {
|
if (ParseTreeUtils.isMatchingExpression(reference, leftExpression)) {
|
||||||
// Narrow the type by filtering on "None".
|
|
||||||
return (type: Type) => {
|
return (type: Type) => {
|
||||||
const expandedType = mapSubtypes(type, (subtype) => {
|
return narrowTypeForIsNone(evaluator, type, adjIsPositiveTest);
|
||||||
return transformPossibleRecursiveTypeAlias(subtype);
|
};
|
||||||
});
|
}
|
||||||
return evaluator.mapSubtypesExpandTypeVars(
|
|
||||||
expandedType,
|
|
||||||
/* conditionFilter */ undefined,
|
|
||||||
(subtype, unexpandedSubtype) => {
|
|
||||||
if (isAnyOrUnknown(subtype)) {
|
|
||||||
// We need to assume that "Any" is always both None and not None,
|
|
||||||
// so it matches regardless of whether the test is positive or negative.
|
|
||||||
return subtype;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is a TypeVar that isn't constrained, use the unexpanded
|
if (
|
||||||
// TypeVar. For all other cases (including constrained TypeVars),
|
leftExpression.nodeType === ParseNodeType.Index &&
|
||||||
// use the expanded subtype.
|
ParseTreeUtils.isMatchingExpression(reference, leftExpression.baseExpression) &&
|
||||||
const adjustedSubtype =
|
leftExpression.items.length === 1 &&
|
||||||
isTypeVar(unexpandedSubtype) && unexpandedSubtype.details.constraints.length === 0
|
!leftExpression.trailingComma &&
|
||||||
? unexpandedSubtype
|
leftExpression.items[0].argumentCategory === ArgumentCategory.Simple &&
|
||||||
: subtype;
|
!leftExpression.items[0].name &&
|
||||||
|
leftExpression.items[0].valueExpression.nodeType === ParseNodeType.Number &&
|
||||||
// See if it's a match for object.
|
leftExpression.items[0].valueExpression.isInteger &&
|
||||||
if (isClassInstance(subtype) && ClassType.isBuiltIn(subtype, 'object')) {
|
!leftExpression.items[0].valueExpression.isImaginary
|
||||||
return adjIsPositiveTest ? NoneType.createInstance() : adjustedSubtype;
|
) {
|
||||||
}
|
const indexValue = leftExpression.items[0].valueExpression.value;
|
||||||
|
return (type: Type) => {
|
||||||
// See if it's a match for None.
|
return narrowTupleTypeForIsNone(evaluator, type, adjIsPositiveTest, indexValue);
|
||||||
if (isNone(subtype) === adjIsPositiveTest) {
|
|
||||||
return adjustedSubtype;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -460,6 +443,79 @@ export function getTypeNarrowingCallback(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle type narrowing for expressions of the form "a[I] is None" and "a[I] is not None" where
|
||||||
|
// I is an integer and a is a union of Tuples with known lengths and entry types.
|
||||||
|
function narrowTupleTypeForIsNone(evaluator: TypeEvaluator, type: Type, isPositiveTest: boolean, indexValue: number) {
|
||||||
|
return evaluator.mapSubtypesExpandTypeVars(type, /* conditionFilter */ undefined, (subtype) => {
|
||||||
|
if (
|
||||||
|
!isClassInstance(subtype) ||
|
||||||
|
!isTupleClass(subtype) ||
|
||||||
|
isOpenEndedTupleClass(subtype) ||
|
||||||
|
!subtype.tupleTypeArguments
|
||||||
|
) {
|
||||||
|
return subtype;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tupleLength = subtype.tupleTypeArguments.length;
|
||||||
|
if (indexValue < 0 || indexValue >= tupleLength) {
|
||||||
|
return subtype;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeOfEntry = evaluator.makeTopLevelTypeVarsConcrete(subtype.tupleTypeArguments[indexValue]);
|
||||||
|
|
||||||
|
if (isPositiveTest) {
|
||||||
|
if (!evaluator.canAssignType(typeOfEntry, NoneType.createInstance(), new DiagnosticAddendum())) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isNone(typeOfEntry)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return subtype;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle type narrowing for expressions of the form "x is None" and "x is not None".
|
||||||
|
function narrowTypeForIsNone(evaluator: TypeEvaluator, type: Type, isPositiveTest: boolean) {
|
||||||
|
const expandedType = mapSubtypes(type, (subtype) => {
|
||||||
|
return transformPossibleRecursiveTypeAlias(subtype);
|
||||||
|
});
|
||||||
|
|
||||||
|
return evaluator.mapSubtypesExpandTypeVars(
|
||||||
|
expandedType,
|
||||||
|
/* conditionFilter */ undefined,
|
||||||
|
(subtype, unexpandedSubtype) => {
|
||||||
|
if (isAnyOrUnknown(subtype)) {
|
||||||
|
// We need to assume that "Any" is always both None and not None,
|
||||||
|
// so it matches regardless of whether the test is positive or negative.
|
||||||
|
return subtype;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a TypeVar that isn't constrained, use the unexpanded
|
||||||
|
// TypeVar. For all other cases (including constrained TypeVars),
|
||||||
|
// use the expanded subtype.
|
||||||
|
const adjustedSubtype =
|
||||||
|
isTypeVar(unexpandedSubtype) && unexpandedSubtype.details.constraints.length === 0
|
||||||
|
? unexpandedSubtype
|
||||||
|
: subtype;
|
||||||
|
|
||||||
|
// See if it's a match for object.
|
||||||
|
if (isClassInstance(subtype) && ClassType.isBuiltIn(subtype, 'object')) {
|
||||||
|
return isPositiveTest ? NoneType.createInstance() : adjustedSubtype;
|
||||||
|
}
|
||||||
|
|
||||||
|
// See if it's a match for None.
|
||||||
|
if (isNone(subtype) === isPositiveTest) {
|
||||||
|
return adjustedSubtype;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// The "isinstance" and "issubclass" calls support two forms - a simple form
|
// The "isinstance" and "issubclass" calls support two forms - a simple form
|
||||||
// that accepts a single class, and a more complex form that accepts a tuple
|
// that accepts a single class, and a more complex form that accepts a tuple
|
||||||
// of classes. This method determines which form and returns a list of classes
|
// of classes. This method determines which form and returns a list of classes
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
# This sample tests the type narrowing case for unions of tuples
|
||||||
|
# where one or more of the entries is tested against type None.
|
||||||
|
|
||||||
|
from typing import Literal, TypeVar, Tuple, Union
|
||||||
|
|
||||||
|
_T1 = TypeVar("_T1")
|
||||||
|
|
||||||
|
|
||||||
|
def func1(a: Union[Tuple[_T1, None], Tuple[None, str]]) -> Tuple[_T1, None]:
|
||||||
|
if a[1] is None:
|
||||||
|
t1: Literal["Tuple[_T1@func1, None]"] = reveal_type(a)
|
||||||
|
return a
|
||||||
|
else:
|
||||||
|
t2: Literal["Tuple[None, str]"] = reveal_type(a)
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
|
||||||
|
_T2 = TypeVar("_T2", bound=Union[None, int])
|
||||||
|
|
||||||
|
|
||||||
|
def func2(a: Union[Tuple[_T2, None], Tuple[None, str]]):
|
||||||
|
if a[0] is None:
|
||||||
|
t1: Literal["Tuple[_T2@func2, None] | Tuple[None, str]"] = reveal_type(a)
|
||||||
|
else:
|
||||||
|
t2: Literal["Tuple[_T2@func2, None]"] = reveal_type(a)
|
||||||
|
|
||||||
|
|
||||||
|
_T3 = TypeVar("_T3", None, int)
|
||||||
|
|
||||||
|
|
||||||
|
def func3(a: Union[Tuple[_T3, None], Tuple[None, str]]):
|
||||||
|
if a[0] is None:
|
||||||
|
t1: Literal["Tuple[_T3@func3, None] | Tuple[None, str]"] = reveal_type(a)
|
||||||
|
else:
|
||||||
|
t2: Literal["Tuple[_T3@func3, None]"] = reveal_type(a)
|
||||||
|
|
||||||
|
|
||||||
|
def func4(a: Union[Tuple[Union[int, None]], Tuple[None, str]]):
|
||||||
|
if a[0] is None:
|
||||||
|
t1: Literal["Tuple[int | None] | Tuple[None, str]"] = reveal_type(a)
|
||||||
|
else:
|
||||||
|
t2: Literal["Tuple[int | None]"] = reveal_type(a)
|
@ -295,6 +295,12 @@ test('TypeNarrowingIsNone2', () => {
|
|||||||
TestUtils.validateResults(analysisResults, 0);
|
TestUtils.validateResults(analysisResults, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('TypeNarrowingIsNoneTuple1', () => {
|
||||||
|
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingIsNoneTuple1.py']);
|
||||||
|
|
||||||
|
TestUtils.validateResults(analysisResults, 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('TypeNarrowingLiteral1', () => {
|
test('TypeNarrowingLiteral1', () => {
|
||||||
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingLiteral1.py']);
|
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingLiteral1.py']);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user