Added support for isinstance and issubclass type narrowing where the variable type and the test type have no apparent relationship. The type evaluator synthesizes a class that is a subclass of both types — effectively an "intersection" of the two.

This commit is contained in:
Eric Traut 2021-07-03 10:02:08 -06:00
parent 4c96bcfe58
commit c78f6db84b
4 changed files with 128 additions and 12 deletions

View File

@ -17891,7 +17891,27 @@ export function createTypeEvaluator(
const classTypeList = getIsInstanceClassTypes(arg1Type);
if (classTypeList) {
return (type: Type) => {
return narrowTypeForIsInstance(type, classTypeList, isInstanceCheck, isPositiveTest);
const narrowedType = narrowTypeForIsInstance(
type,
classTypeList,
isInstanceCheck,
isPositiveTest,
/* allowIntersections */ false,
testExpression
);
if (!isNever(narrowedType)) {
return narrowedType;
}
// Try again with intersection types allowed.
return narrowTypeForIsInstance(
type,
classTypeList,
isInstanceCheck,
isPositiveTest,
/* allowIntersections */ true,
testExpression
);
};
}
}
@ -18018,7 +18038,9 @@ export function createTypeEvaluator(
type: Type,
classTypeList: (ClassType | TypeVarType | NoneType)[],
isInstanceCheck: boolean,
isPositiveTest: boolean
isPositiveTest: boolean,
allowIntersections: boolean,
errorNode: ExpressionNode
): Type {
const expandedTypes = mapSubtypes(type, (subtype) => {
return transformPossibleRecursiveTypeAlias(subtype);
@ -18084,15 +18106,34 @@ export function createTypeEvaluator(
// If the variable type is a superclass of the isinstance
// filter, we can narrow the type to the subclass.
filteredTypes.push(addConditionToType(filterType, constraints));
} else if (
ClassType.isProtocolClass(concreteFilterType) &&
ClassType.isProtocolClass(varType)
) {
// If both the filter type and the variable type are protocol classes,
// the correct answer is an intersection of the two types, but we don't
// support intersections in the type system, so the best we can do
// is to choose one.
filteredTypes.push(filterType);
} else if (allowIntersections) {
// The two types appear to have no relation. It's possible that the
// two types are protocols or the program is expecting one type to
// be a mix-in class used with the other. In this case, we'll
// synthesize a new class type that represents an intersection of
// the two types.
const className = `<subclass of ${varType.details.name} and ${concreteFilterType.details.name}>`;
const fileInfo = getFileInfo(errorNode);
const newClassType = ClassType.createInstantiable(
className,
getClassFullName(errorNode, fileInfo.moduleName, className),
fileInfo.moduleName,
fileInfo.filePath,
ClassTypeFlags.None,
getTypeSourceId(errorNode),
/* declaredMetaclass */ undefined,
varType.details.effectiveMetaclass,
varType.details.docString
);
newClassType.details.baseClasses = [
ClassType.cloneAsInstantiable(varType),
concreteFilterType,
];
computeMroLinearization(newClassType);
filteredTypes.push(
isInstanceCheck ? ClassType.cloneAsInstance(newClassType) : newClassType
);
}
}
} else if (isTypeVar(filterType) && TypeBase.isInstantiable(filterType)) {

View File

@ -10,6 +10,6 @@ def f(v: Any) -> bool:
if isinstance(v, Iterable):
t_v1: Literal["Iterable[Unknown]"] = reveal_type(v)
if isinstance(v, Sized):
t_v2: Literal["Sized"] = reveal_type(v)
t_v2: Literal["<subclass of Iterable and Sized>"] = reveal_type(v)
return True
return False

View File

@ -0,0 +1,69 @@
# This sample tests the handling of isinstance and issubclass type
# narrowing in the case where there is no overlap between the
# value type and the test type.
from typing import Literal, Type
class A:
a_val: int
class B:
b_val: int
class C:
c_val: int
def func1(val: A):
if isinstance(val, B):
val.a_val
val.b_val
# This should generate an error
val.c_val
t1: Literal["<subclass of A and B>"] = reveal_type(val)
if isinstance(val, C):
val.a_val
val.b_val
val.c_val
t2: Literal["<subclass of <subclass of A and B> and C>"] = reveal_type(val)
else:
val.a_val
# This should generate an error
val.b_val
t3: Literal["A"] = reveal_type(val)
def func2(val: Type[A]):
if issubclass(val, B):
val.a_val
val.b_val
# This should generate an error
val.c_val
t1: Literal["Type[<subclass of A and B>]"] = reveal_type(val)
if issubclass(val, C):
val.a_val
val.b_val
val.c_val
t2: Literal[
"Type[<subclass of <subclass of A and B> and C>]"
] = reveal_type(val)
else:
val.a_val
# This should generate an error
val.b_val
t3: Literal["Type[A]"] = reveal_type(val)

View File

@ -352,6 +352,12 @@ test('TypeNarrowing22', () => {
TestUtils.validateResults(analysisResults, 0);
});
test('TypeNarrowing23', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowing23.py']);
TestUtils.validateResults(analysisResults, 4);
});
test('ReturnTypes1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['returnTypes1.py']);