Fixed a bug that results in incorrect type narrowing when using `isinstance` with an instance of a generic class as the first argument and a concrete subclass as the filter type. This addresses #8672. (#8675)

This commit is contained in:
Eric Traut 2024-08-06 10:30:12 -06:00 коммит произвёл GitHub
Родитель 35ab773671
Коммит c1df6595a1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
3 изменённых файлов: 52 добавлений и 12 удалений

Просмотреть файл

@ -89,6 +89,7 @@ import {
isUnboundedTupleClass,
lookUpClassMember,
lookUpObjectMember,
makeTypeVarsFree,
mapSubtypes,
MemberAccessFlags,
specializeTupleClass,
@ -1355,6 +1356,17 @@ function narrowTypeForIsInstanceInternal(
expandedTypes = evaluator.expandPromotionTypes(errorNode, expandedTypes);
const convertVarTypeToFree = (varType: Type): Type => {
// If this is a TypeIs check, type variables should remain bound.
if (isTypeIsCheck) {
return varType;
}
// If this is an isinstance or issubclass check, the type variables
// should be converted to "free" type variables.
return makeTypeVarsFree(varType, ParseTreeUtils.getTypeVarScopesForNode(errorNode));
};
// Filters the varType by the parameters of the isinstance
// and returns the list of types the varType could be after
// applying the filter.
@ -1443,7 +1455,7 @@ function narrowTypeForIsInstanceInternal(
} else if (filterIsSubclass) {
if (
evaluator.assignType(
convertToInstance(concreteVarType),
convertToInstance(convertVarTypeToFree(concreteVarType)),
convertToInstance(concreteFilterType),
/* diag */ undefined,
/* destConstraints */ undefined,
@ -1597,7 +1609,7 @@ function narrowTypeForIsInstanceInternal(
}
} else if (
evaluator.assignType(
concreteVarType,
convertVarTypeToFree(concreteVarType),
filterType,
/* diag */ undefined,
/* destConstraints */ undefined,
@ -1653,7 +1665,7 @@ function narrowTypeForIsInstanceInternal(
if (filterMetaclass && isInstantiableClass(filterMetaclass)) {
let isMetaclassOverlap = evaluator.assignType(
metaclassType,
convertVarTypeToFree(metaclassType),
ClassType.cloneAsInstance(filterMetaclass)
);
@ -1711,7 +1723,7 @@ function narrowTypeForIsInstanceInternal(
for (const filterType of filterTypes) {
const concreteFilterType = evaluator.makeTopLevelTypeVarsConcrete(filterType);
if (evaluator.assignType(varType, convertToInstance(concreteFilterType))) {
if (evaluator.assignType(convertVarTypeToFree(varType), convertToInstance(concreteFilterType))) {
// If the filter type is a Callable, use the original type. If the
// filter type is a callback protocol, use the filter type.
if (isFunction(filterType)) {
@ -1730,7 +1742,7 @@ function narrowTypeForIsInstanceInternal(
return false;
}
return evaluator.assignType(varType, convertToInstance(concreteFilterType));
return evaluator.assignType(convertVarTypeToFree(varType), convertToInstance(concreteFilterType));
})
) {
filteredTypes.push(unexpandedType);

Просмотреть файл

@ -1,7 +1,17 @@
# This sample exercises the type analyzer's isinstance type narrowing logic.
from types import NoneType
from typing import Any, Generic, Iterable, Iterator, Protocol, Sized, TypeVar, Union, runtime_checkable
from typing import (
Any,
Generic,
Iterable,
Iterator,
Protocol,
Sized,
TypeVar,
Union,
runtime_checkable,
)
S = TypeVar("S")
T = TypeVar("T")
@ -233,3 +243,21 @@ def func13(x: object | type[object]) -> None:
def func14(x: Iterable[T]):
if isinstance(x, Iterator):
reveal_type(x, expected_text="Iterator[T@func14]")
class Base15(Generic[T]):
value: T
class Child15(Base15[int]):
value: int
def func15(x: Base15[T]):
if isinstance(x, Child15):
# This should generate an error. It's here just to ensure that
# this code branch isn't marked unreachable.
reveal_type(x, expected_text="Never")
reveal_type(x, expected_text="Child15")
reveal_type(x.value, expected_text="int")

Просмотреть файл

@ -288,12 +288,6 @@ test('TypeNarrowing7', () => {
TestUtils.validateResults(analysisResults, 0);
});
test('TypeNarrowingIsinstance1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingIsinstance1.py']);
TestUtils.validateResults(analysisResults, 8);
});
test('TypeNarrowingAssert1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingAssert1.py']);
@ -372,6 +366,12 @@ test('TypeNarrowingEnum2', () => {
TestUtils.validateResults(analysisResults, 2);
});
test('TypeNarrowingIsinstance1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingIsinstance1.py']);
TestUtils.validateResults(analysisResults, 9);
});
test('TypeNarrowingIsinstance2', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingIsinstance2.py']);