Fixed bug in protocol matching that results in a false positive when the subject object is a dataclass that contains a callable. It should be considered an instance member in this case, so it should not be bound to the class. This addresses #7735. (#7749)

This commit is contained in:
Eric Traut 2024-04-22 20:47:48 -07:00 committed by GitHub
parent d1461cee9f
commit cd483dcb90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 60 additions and 16 deletions

View File

@ -441,23 +441,40 @@ function assignClassToProtocolInternal(
// If the source is a method, bind it.
if (isFunction(srcMemberType) || isOverloadedFunction(srcMemberType)) {
if (isMemberFromMetaclass || isInstantiableClass(srcMemberInfo.classType)) {
const boundSrcFunction = evaluator.bindFunctionToClassOrObject(
sourceIsClassObject && !isMemberFromMetaclass
? srcType
: ClassType.cloneAsInstance(srcType),
srcMemberType,
isMemberFromMetaclass ? undefined : (srcMemberInfo.classType as ClassType),
/* treatConstructorAsClassMethod */ undefined,
isMemberFromMetaclass ? srcType : selfType,
diag?.createAddendum(),
recursionCount
);
let isInstanceMember = !srcMemberInfo.symbol.isClassMember();
if (boundSrcFunction) {
srcMemberType = removeParamSpecVariadicsFromSignature(boundSrcFunction);
} else {
typesAreConsistent = false;
return;
// Special-case dataclasses whose entries act like instance members.
if (ClassType.isDataClass(srcType)) {
const dataClassFields = ClassType.getDataClassEntries(srcType);
if (dataClassFields.some((entry) => entry.name === name)) {
isInstanceMember = true;
}
}
if (isMemberFromMetaclass) {
isInstanceMember = false;
}
// If this is a callable stored in an instance member, skip binding.
if (!isInstanceMember) {
const boundSrcFunction = evaluator.bindFunctionToClassOrObject(
sourceIsClassObject && !isMemberFromMetaclass
? srcType
: ClassType.cloneAsInstance(srcType),
srcMemberType,
isMemberFromMetaclass ? undefined : (srcMemberInfo.classType as ClassType),
/* treatConstructorAsClassMethod */ undefined,
isMemberFromMetaclass ? srcType : selfType,
diag?.createAddendum(),
recursionCount
);
if (boundSrcFunction) {
srcMemberType = removeParamSpecVariadicsFromSignature(boundSrcFunction);
} else {
typesAreConsistent = false;
return;
}
}
}
}

View File

@ -0,0 +1,21 @@
# This sample tests the case where a protocol is matched against a
# dataclass. Dataclass fields need to act as if they are instance
# members rather than class members, which means a callable stored
# in a dataclass member should not be bound to the dataclass itself.
from dataclasses import dataclass
from typing import Callable, Protocol
class HasA(Protocol):
@property
def a(self) -> Callable[[int], int]: ...
@dataclass
class A:
a: Callable[[int], int]
def func1(a: A):
has_a: HasA = a

View File

@ -1417,6 +1417,12 @@ test('Protocol48', () => {
TestUtils.validateResults(analysisResults, 0);
});
test('Protocol49', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['protocol49.py']);
TestUtils.validateResults(analysisResults, 0);
});
test('ProtocolExplicit1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['protocolExplicit1.py']);