Enhanced the isinstance type narrowing logic so it filters types based on the number of entries in a tuple if the type derives from a tuple. This addresses https://github.com/microsoft/pyright/issues/5346. (#5348)

Co-authored-by: Eric Traut <erictr@microsoft.com>
This commit is contained in:
Eric Traut 2023-06-20 18:17:06 -07:00 committed by GitHub
parent 37535a3171
commit ed73c0e965
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 105 additions and 37 deletions

View File

@ -1258,45 +1258,47 @@ function narrowTypeForIsInstance(
// we haven't learned anything new about the variable type.
filteredTypes.push(addConditionToType(varType, constraints));
} else if (filterIsSubclass) {
// If the variable type is a superclass of the isinstance
// filter, we can narrow the type to the subclass.
let specializedFilterType = filterType;
// Try to retain the type arguments for the filter type. This is
// important because a specialized version of the filter cannot
// be passed to isinstance or issubclass.
if (isClass(filterType)) {
if (
ClassType.isSpecialBuiltIn(filterType) ||
filterType.details.typeParameters.length > 0
) {
const typeVarContext = new TypeVarContext(getTypeVarScopeId(filterType));
const unspecializedFilterType = ClassType.cloneForSpecialization(
filterType,
/* typeArguments */ undefined,
/* isTypeArgumentExplicit */ false
);
if (evaluator.assignType(varType, filterType)) {
// If the variable type is a superclass of the isinstance
// filter, we can narrow the type to the subclass.
let specializedFilterType = filterType;
// Try to retain the type arguments for the filter type. This is
// important because a specialized version of the filter cannot
// be passed to isinstance or issubclass.
if (isClass(filterType)) {
if (
populateTypeVarContextBasedOnExpectedType(
evaluator,
unspecializedFilterType,
varType,
typeVarContext,
/* liveTypeVarScopes */ undefined,
errorNode.start
)
ClassType.isSpecialBuiltIn(filterType) ||
filterType.details.typeParameters.length > 0
) {
specializedFilterType = applySolvedTypeVars(
unspecializedFilterType,
typeVarContext,
{ unknownIfNotFound: true }
) as ClassType;
const typeVarContext = new TypeVarContext(getTypeVarScopeId(filterType));
const unspecializedFilterType = ClassType.cloneForSpecialization(
filterType,
/* typeArguments */ undefined,
/* isTypeArgumentExplicit */ false
);
if (
populateTypeVarContextBasedOnExpectedType(
evaluator,
unspecializedFilterType,
varType,
typeVarContext,
/* liveTypeVarScopes */ undefined,
errorNode.start
)
) {
specializedFilterType = applySolvedTypeVars(
unspecializedFilterType,
typeVarContext,
{ unknownIfNotFound: true }
) as ClassType;
}
}
}
}
filteredTypes.push(addConditionToType(specializedFilterType, constraints));
filteredTypes.push(addConditionToType(specializedFilterType, constraints));
}
} else if (
allowIntersections &&
!ClassType.isFinal(varType) &&

View File

@ -0,0 +1,63 @@
# This sample tests the case where a filter (guard) type has a subtype
# relationship to the type of the variable being filtered but the
# type arguments sometimes mean that it cannot be a subtype.
from typing import Generic, NamedTuple, TypeVar
T = TypeVar("T")
class NT1(NamedTuple, Generic[T]):
pass
def func1(val: NT1[str] | tuple[int, int]):
if isinstance(val, NT1):
reveal_type(val, expected_text="NT1[str]")
else:
reveal_type(val, expected_text="tuple[int, int]")
class NT2(NamedTuple, Generic[T]):
a: T
b: str
def func2(val: NT2[str] | tuple[int, int]):
if isinstance(val, NT2):
reveal_type(val, expected_text="NT2[str]")
else:
reveal_type(val, expected_text="tuple[int, int]")
def func3(val: NT2[str] | tuple[int, str]):
if isinstance(val, NT2):
reveal_type(val, expected_text="NT2[str] | NT2[Unknown]")
else:
reveal_type(val, expected_text="tuple[int, str]")
class NT3(NamedTuple, Generic[T]):
a: T
b: T
def func4(val: NT3[str] | tuple[int, int]):
if isinstance(val, NT3):
reveal_type(val, expected_text="NT3[str] | NT3[Unknown]")
else:
reveal_type(val, expected_text="tuple[int, int]")
def func5(val: NT3[str] | tuple[str, str, str]):
if isinstance(val, NT3):
reveal_type(val, expected_text="NT3[str]")
else:
reveal_type(val, expected_text="tuple[str, str, str]")
def func6(val: NT3[str] | tuple[str, ...]):
if isinstance(val, NT3):
reveal_type(val, expected_text="NT3[str] | NT3[Unknown]")
else:
reveal_type(val, expected_text="tuple[str, ...]")

View File

@ -442,10 +442,13 @@ test('TypeNarrowingIsinstance16', () => {
});
test('TypeNarrowingIsinstance17', () => {
// This test requires Python 3.10 because it uses PEP 604 notation for unions.
const configOptions = new ConfigOptions('.');
configOptions.defaultPythonVersion = PythonVersion.V3_10;
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingIsinstance17.py'], configOptions);
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingIsinstance17.py']);
TestUtils.validateResults(analysisResults, 0);
});
test('TypeNarrowingIsinstance18', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingIsinstance18.py']);
TestUtils.validateResults(analysisResults, 0);
});