Fixed bug in logic that validates subtyping relationships between TypeIs[T], TypeGuard[T] and bool when used in a return type of a callable. This addresses #8521. (#8523)

This commit is contained in:
Eric Traut 2024-07-24 10:26:58 -07:00 committed by GitHub
parent 258e2754bd
commit 21bfd0f5bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 120 additions and 6 deletions

View File

@ -8349,7 +8349,11 @@ export function createTypeEvaluator(
);
if (
!isTypeSame(assertedType, arg0TypeResult.type, { treatAnySameAsUnknown: true, ignorePseudoGeneric: true })
!isTypeSame(assertedType, arg0TypeResult.type, {
treatAnySameAsUnknown: true,
ignorePseudoGeneric: true,
ignoreTypeGuard: true,
})
) {
const srcDestTypes = printSrcDestTypes(arg0TypeResult.type, assertedType, { expandTypeAlias: true });
@ -23389,6 +23393,29 @@ export function createTypeEvaluator(
}
}
// If the type is a bool created with a `TypeGuard` or `TypeIs`, it is
// considered a subtype of `bool`.
if (destType.priv.typeGuardType) {
if (!srcType.priv.typeGuardType) {
return false;
}
// TypeGuard and TypeIs are not subtypes of each other.
if (!destType.priv.isStrictTypeGuard !== !srcType.priv.isStrictTypeGuard) {
return false;
}
return assignType(
destType.priv.typeGuardType,
srcType.priv.typeGuardType,
diag?.createAddendum(),
/* destTypeVarContext */ undefined,
/* srcTypeVarContext */ undefined,
flags,
recursionCount
);
}
for (let ancestorIndex = inheritanceChain.length - 1; ancestorIndex >= 0; ancestorIndex--) {
const ancestorType = inheritanceChain[ancestorIndex];

View File

@ -117,6 +117,7 @@ export interface TypeSameOptions {
ignoreTypeFlags?: boolean;
ignoreConditions?: boolean;
ignoreTypedDictNarrowEntries?: boolean;
ignoreTypeGuard?: boolean;
treatAnySameAsUnknown?: boolean;
}
@ -3211,6 +3212,24 @@ export function isTypeSame(type1: Type, type2: Type, options: TypeSameOptions =
return false;
}
if (!options.ignoreTypeGuard) {
// If one is a type guard and the other is not, they are not the same.
if (!type1.priv.typeGuardType !== !classType2.priv.typeGuardType) {
return false;
}
if (type1.priv.typeGuardType && classType2.priv.typeGuardType) {
// TypeIs and TypeGuard are not the equivalent.
if (!type1.priv.isStrictTypeGuard !== !classType2.priv.isStrictTypeGuard) {
return false;
}
if (!isTypeSame(type1.priv.typeGuardType, classType2.priv.typeGuardType, options, recursionCount)) {
return false;
}
}
}
if (!options.ignorePseudoGeneric || !ClassType.isPseudoGenericClass(type1)) {
// Make sure the type args match.
if (type1.priv.tupleTypeArguments && classType2.priv.tupleTypeArguments) {
@ -3340,7 +3359,12 @@ export function isTypeSame(type1: Type, type2: Type, options: TypeSameOptions =
if (
!return1Type ||
!return2Type ||
!isTypeSame(return1Type, return2Type, { ...options, ignoreTypeFlags: false }, recursionCount)
!isTypeSame(
return1Type,
return2Type,
{ ...options, ignoreTypeFlags: false, ignoreTypeGuard: false },
recursionCount
)
) {
return false;
}

View File

@ -1,10 +1,10 @@
# This sample tests overload matching in cases where the match
# is ambiguous due to an Any or Unknown argument.
# pyright: reportMissingModuleSource=false
from typing import Any, Generic, Literal, TypeVar, overload
from typing_extensions import ( # pyright: ignore[reportMissingModuleSource]
LiteralString,
)
from typing_extensions import LiteralString, TypeIs
_T = TypeVar("_T")
@ -338,3 +338,22 @@ def func19(a: ClassC, b: list, c: Any):
my_list2: list[int] = []
v1 = a.method1("hi", my_list2)
reveal_type(v1, expected_text="float")
@overload
def overload11(x: str) -> TypeIs[str]:
...
@overload
def overload11(x: int) -> TypeIs[int]:
...
def overload11(x: Any) -> Any:
return True
def func20(val: Any):
if overload11(val):
reveal_type(val, expected_text="Any")

View File

@ -1,7 +1,10 @@
# This sample tests the TypeIs form.
# pyright: reportMissingModuleSource=false
from typing import Any, Callable, Collection, Literal, Mapping, Sequence, TypeVar, Union
from typing_extensions import TypeIs # pyright: ignore[reportMissingModuleSource]
from typing_extensions import TypeIs
def is_str1(val: Union[str, int]) -> TypeIs[str]:

View File

@ -0,0 +1,36 @@
# This sample tests the subtyping relationships between TypeIs, TypeGuard,
# and bool.
# pyright: reportMissingModuleSource=false
from typing import Callable
from typing_extensions import TypeGuard, TypeIs
TypeIsInt = Callable[..., TypeIs[int]]
TypeIsFloat = Callable[..., TypeIs[float]]
BoolReturn = Callable[..., bool]
TypeGuardInt = Callable[..., TypeGuard[int]]
def func1(v1: TypeIsInt, v2: TypeIsFloat, v3: BoolReturn, v4: TypeGuardInt):
a1: TypeIsInt = v1
a2: TypeIsInt = v2 # Should generate an error
a3: TypeIsInt = v3 # Should generate an error
a4: TypeIsInt = v4 # Should generate an error
b1: TypeIsFloat = v1 # Should generate an error
b2: TypeIsFloat = v2
b3: TypeIsFloat = v3 # Should generate an error
b4: TypeIsFloat = v4 # Should generate an error
c1: BoolReturn = v1
c2: BoolReturn = v2
c3: BoolReturn = v3
c4: BoolReturn = v4
d1: TypeGuardInt = v1 # Should generate an error
d2: TypeGuardInt = v2 # Should generate an error
d3: TypeGuardInt = v3 # Should generate an error
d4: TypeGuardInt = v4

View File

@ -128,6 +128,11 @@ test('TypeIs1', () => {
TestUtils.validateResults(analysisResults, 2);
});
test('TypeIs2', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIs2.py']);
TestUtils.validateResults(analysisResults, 9);
});
test('Never1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['never1.py']);