Added support for type guard forms x is ..., x is not ..., x == ... and x != .... Support for these were recently added to mypy. This addresses https://github.com/microsoft/pyright/issues/4397.

This commit is contained in:
Eric Traut 2023-01-04 06:13:44 -07:00
parent 7be1d77360
commit c7626944f1
4 changed files with 104 additions and 0 deletions

View File

@ -168,6 +168,8 @@ In addition to assignment-based type narrowing, Pyright supports the following t
* `x is None` and `x is not None`
* `x == None` and `x != None`
* `x is ...` and `x is not ...`
* `x == ...` and `x != ...`
* `type(x) is T` and `type(x) is not T`
* `x is E` and `x is not E` (where E is a literal enum or bool)
* `x == L` and `x != L` (where L is an expression that evaluates to a literal type)

View File

@ -164,6 +164,22 @@ export function getTypeNarrowingCallback(
}
}
// Look for "X is ...", "X is not ...", "X == ...", and "X != ...".
if (testExpression.rightExpression.nodeType === ParseNodeType.Ellipsis) {
// Allow the LHS to be either a simple expression or an assignment
// expression that assigns to a simple name.
let leftExpression = testExpression.leftExpression;
if (leftExpression.nodeType === ParseNodeType.AssignmentExpression) {
leftExpression = leftExpression.name;
}
if (ParseTreeUtils.isMatchingExpression(reference, leftExpression)) {
return (type: Type) => {
return narrowTypeForIsEllipsis(evaluator, type, adjIsPositiveTest);
};
}
}
// Look for "type(X) is Y" or "type(X) is not Y".
if (isOrIsNotOperator && testExpression.leftExpression.nodeType === ParseNodeType.Call) {
if (
@ -876,6 +892,49 @@ function narrowTypeForIsNone(evaluator: TypeEvaluator, type: Type, isPositiveTes
);
}
// Handle type narrowing for expressions of the form "x is ..." and "x is not ...".
function narrowTypeForIsEllipsis(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
? addConditionToType(NoneType.createInstance(), subtype.condition)
: adjustedSubtype;
}
const isEllipsis = isClassInstance(subtype) && ClassType.isBuiltIn(subtype, 'ellipsis');
// See if it's a match for "...".
if (isEllipsis === isPositiveTest) {
return subtype;
}
return undefined;
}
);
}
// 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
// of classes (including arbitrarily-nested tuples). This method determines

View File

@ -0,0 +1,37 @@
# This sample tests the type analyzer's type narrowing logic for
# conditions of the form "X is ...", "X is not ...",
# "X == .." and "X != ...".
import types
from typing import TypeVar
_T = TypeVar("_T", str, ellipsis)
def func1(val: int | ellipsis):
if val is not ...:
reveal_type(val, expected_text="int")
else:
reveal_type(val, expected_text="ellipsis")
def func2(val: _T):
if val is ...:
reveal_type(val, expected_text="ellipsis*")
else:
reveal_type(val, expected_text="str*")
def func3(val: int | types.EllipsisType):
if val != ...:
reveal_type(val, expected_text="int")
else:
reveal_type(val, expected_text="ellipsis")
def func4(val: int | ellipsis):
if not val == ...:
reveal_type(val, expected_text="int")
else:
reveal_type(val, expected_text="ellipsis")

View File

@ -321,6 +321,12 @@ test('TypeNarrowingIsNoneTuple2', () => {
TestUtils.validateResults(analysisResults, 0);
});
test('TypeNarrowingIsEllipsis1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingIsEllipsis1.py']);
TestUtils.validateResults(analysisResults, 0);
});
test('TypeNarrowingLiteral1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingLiteral1.py']);