Fixed bug that resulted in a false negative when a TypedDict value was assigned to a protocol that included attributes that matched the TypedDict keys.

This commit is contained in:
Eric Traut 2021-10-11 19:24:04 -07:00
parent b0c32438e7
commit 047c4d0601
3 changed files with 70 additions and 0 deletions

View File

@ -532,6 +532,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
let boolClassType: Type | undefined;
let strClassType: Type | undefined;
let dictClassType: Type | undefined;
let typedDictClassType: Type | undefined;
let incompleteTypeCache: TypeCache | undefined;
const returnTypeInferenceContextStack: ReturnTypeInferenceContext[] = [];
@ -745,6 +746,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
boolClassType = getBuiltInType(node, 'bool');
strClassType = getBuiltInType(node, 'str');
dictClassType = getBuiltInType(node, 'dict');
typedDictClassType = getTypingType(node, '_TypedDict');
}
}
@ -17379,6 +17381,15 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
);
const genericDestTypeVarMap = new TypeVarMap(getTypeVarScopeId(destType));
// If the source is a TypedDict, use the _TypedDict placeholder class
// instead. We don't want to use the TypedDict members for protocol
// comparison.
if (ClassType.isTypedDictClass(srcType)) {
if (typedDictClassType && isInstantiableClass(typedDictClassType)) {
srcType = typedDictClassType;
}
}
let typesAreConsistent = true;
const srcClassTypeVarMap = buildTypeVarMapFromSpecializedClass(srcType);

View File

@ -0,0 +1,53 @@
# This sample tests the type compatibility checks when the source
# is a TypedDict and the dest is a protocol.
from typing import Protocol, TypeVar, TypedDict
class HasName(Protocol):
name: str
class SupportsClear(Protocol):
def clear(self) -> None:
...
_T = TypeVar("_T")
class SupportsUpdate(Protocol):
def update(self: _T, __m: _T) -> None:
...
class B(TypedDict):
name: str
def print_name(x: HasName):
print(x.name)
my_typed_dict: B = {"name": "my name"}
# This should generate an error. The "name"
# attribute of a TypedDict can't be accessed
# through a member access expression.
print_name(my_typed_dict)
def do_clear(x: SupportsClear):
x.clear()
# This should generate an error. Although a "dict"
# class supports clear, a TypedDict does not.
do_clear(my_typed_dict)
def do_update(x: SupportsUpdate):
x.update(x)
do_update(my_typed_dict)

View File

@ -932,3 +932,9 @@ test('TypedDict14', () => {
TestUtils.validateResults(analysisResults, 1);
});
test('TypedDict15', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typedDict15.py']);
TestUtils.validateResults(analysisResults, 2);
});