Fixed recent regression that incorrectly narrowed the type of kwargs when used in a type guard of the form if "a" in kwargs. This addresses #7731. (#7747)

This commit is contained in:
Eric Traut 2024-04-22 18:19:10 -07:00 committed by GitHub
parent 419a8f41c1
commit 73d894c1be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 64 additions and 39 deletions

View File

@ -45,6 +45,7 @@ import {
isModule,
isNever,
isOverloadedFunction,
isParamSpec,
isSameWithoutLiteralValue,
isTypeSame,
isTypeVar,
@ -2197,46 +2198,56 @@ function narrowTypeForTypedDictKey(
literalKey: ClassType,
isPositiveTest: boolean
): Type {
const narrowedType = evaluator.mapSubtypesExpandTypeVars(referenceType, /* options */ undefined, (subtype) => {
if (isClassInstance(subtype) && ClassType.isTypedDictClass(subtype)) {
const entries = getTypedDictMembersForClass(evaluator, subtype, /* allowNarrowed */ true);
const tdEntry = entries.knownItems.get(literalKey.literalValue as string) ?? entries.extraItems;
if (isPositiveTest) {
if (!tdEntry) {
return undefined;
}
// If the entry is currently not required and not marked provided, we can mark
// it as provided after this guard expression confirms it is.
if (tdEntry.isRequired || tdEntry.isProvided) {
return subtype;
}
const newNarrowedEntriesMap = new Map<string, TypedDictEntry>(subtype.typedDictNarrowedEntries ?? []);
// Add the new entry.
newNarrowedEntriesMap.set(literalKey.literalValue as string, {
valueType: tdEntry.valueType,
isReadOnly: tdEntry.isReadOnly,
isRequired: false,
isProvided: true,
});
// Clone the TypedDict object with the new entries.
return ClassType.cloneAsInstance(
ClassType.cloneForNarrowedTypedDictEntries(
ClassType.cloneAsInstantiable(subtype),
newNarrowedEntriesMap
)
);
} else {
return tdEntry !== undefined && (tdEntry.isRequired || tdEntry.isProvided) ? undefined : subtype;
const narrowedType = evaluator.mapSubtypesExpandTypeVars(
referenceType,
/* options */ undefined,
(subtype, unexpandedSubtype) => {
if (isParamSpec(unexpandedSubtype)) {
return unexpandedSubtype;
}
}
return subtype;
});
if (isClassInstance(subtype) && ClassType.isTypedDictClass(subtype)) {
const entries = getTypedDictMembersForClass(evaluator, subtype, /* allowNarrowed */ true);
const tdEntry = entries.knownItems.get(literalKey.literalValue as string) ?? entries.extraItems;
if (isPositiveTest) {
if (!tdEntry) {
return undefined;
}
// If the entry is currently not required and not marked provided, we can mark
// it as provided after this guard expression confirms it is.
if (tdEntry.isRequired || tdEntry.isProvided) {
return subtype;
}
const newNarrowedEntriesMap = new Map<string, TypedDictEntry>(
subtype.typedDictNarrowedEntries ?? []
);
// Add the new entry.
newNarrowedEntriesMap.set(literalKey.literalValue as string, {
valueType: tdEntry.valueType,
isReadOnly: tdEntry.isReadOnly,
isRequired: false,
isProvided: true,
});
// Clone the TypedDict object with the new entries.
return ClassType.cloneAsInstance(
ClassType.cloneForNarrowedTypedDictEntries(
ClassType.cloneAsInstantiable(subtype),
newNarrowedEntriesMap
)
);
} else {
return tdEntry !== undefined && (tdEntry.isRequired || tdEntry.isProvided) ? undefined : subtype;
}
}
return subtype;
}
);
return narrowedType;
}

View File

@ -1,6 +1,6 @@
# This sample tests type narrowing for the "in" operator.
from typing import Literal, TypeVar, TypedDict
from typing import Callable, Generic, Literal, ParamSpec, TypeVar, TypedDict
import random
@ -160,3 +160,17 @@ def func12(v: T1):
reveal_type(v, expected_text="TD1*")
else:
reveal_type(v, expected_text="TD2*")
P = ParamSpec("P")
class Container(Generic[P]):
def __init__(self, func: Callable[P, str]) -> None:
self.func = func
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> str:
if "data" in kwargs:
raise ValueError("data is not allowed in kwargs")
return self.func(*args, **kwargs)