Fixed bug in protocol matching logic to reject a protocol match if a variable is a ClassVar in the protocol but not in the concrete class (or vice versa). This addresses #6869. (#6920)

This commit is contained in:
Eric Traut 2024-01-05 16:14:48 -07:00 committed by GitHub
parent 818b423eb0
commit 4b6d636206
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 65 additions and 5 deletions

View File

@ -586,6 +586,26 @@ function assignClassToProtocolInternal(
typesAreConsistent = false;
}
const isDestClassVar = destSymbol.isClassVar();
const isSrcClassVar = srcSymbol.isClassVar();
// If the source is marked as a ClassVar but the dest (the protocol) is not,
// or vice versa, the types are not consistent.
if (isDestClassVar !== isSrcClassVar) {
if (isDestClassVar) {
if (subDiag) {
subDiag.addMessage(Localizer.DiagnosticAddendum.memberIsClassVarInProtocol().format({ name }));
}
} else {
if (subDiag) {
subDiag.addMessage(
Localizer.DiagnosticAddendum.memberIsNotClassVarInProtocol().format({ name })
);
}
}
typesAreConsistent = false;
}
const destPrimaryDecl = getLastTypedDeclaredForSymbol(destSymbol);
const srcPrimaryDecl = getLastTypedDeclaredForSymbol(srcSymbol);

View File

@ -1268,12 +1268,16 @@ export namespace Localizer {
);
export const memberIsAbstractMore = () =>
new ParameterizedString<{ count: number }>(getRawString('DiagnosticAddendum.memberIsAbstractMore'));
export const memberIsClassVarInProtocol = () =>
new ParameterizedString<{ name: string }>(getRawString('DiagnosticAddendum.memberIsClassVarInProtocol'));
export const memberIsFinalInProtocol = () =>
new ParameterizedString<{ name: string }>(getRawString('DiagnosticAddendum.memberIsFinalInProtocol'));
export const memberIsInitVar = () =>
new ParameterizedString<{ name: string }>(getRawString('DiagnosticAddendum.memberIsInitVar'));
export const memberIsInvariant = () =>
new ParameterizedString<{ name: string }>(getRawString('DiagnosticAddendum.memberIsInvariant'));
export const memberIsNotClassVarInProtocol = () =>
new ParameterizedString<{ name: string }>(getRawString('DiagnosticAddendum.memberIsNotClassVarInProtocol'));
export const memberIsNotFinalInProtocol = () =>
new ParameterizedString<{ name: string }>(getRawString('DiagnosticAddendum.memberIsNotFinalInProtocol'));
export const memberIsWritableInProtocol = () =>

View File

@ -650,9 +650,11 @@
"memberAssignment": "Expression of type \"{type}\" cannot be assigned to member \"{name}\" of class \"{classType}\"",
"memberIsAbstract": "\"{type}.{name}\" is abstract",
"memberIsAbstractMore": "and {count} more...",
"memberIsClassVarInProtocol": "\"{name}\" is defined as a ClassVar in protocol",
"memberIsFinalInProtocol": "\"{name}\" is marked Final in protocol",
"memberIsInitVar": "Member \"{name}\" is an init-only field",
"memberIsInvariant": "\"{name}\" is invariant because it is mutable",
"memberIsNotClassVarInProtocol": "\"{name}\" is not defined as a ClassVar in protocol",
"memberIsNotFinalInProtocol": "\"{name}\" is not marked Final in protocol",
"memberIsWritableInProtocol": "\"{name}\" is writable in protocol",
"memberTypeMismatch": "\"{name}\" is an incompatible type",

View File

@ -3,13 +3,13 @@
# synthesized data classes.
from dataclasses import dataclass
from typing import Any, Protocol
from typing import Any, ClassVar, Protocol
class IsDataclass(Protocol):
# Checking for this attribute seems to currently be
# the most reliable way to ascertain that something is a dataclass
__dataclass_fields__: dict[str, Any]
__dataclass_fields__: ClassVar[dict[str, Any]]
def dataclass_only(x: IsDataclass):

View File

@ -1,7 +1,7 @@
# This sample tests the assignment of protocols that
# include property declarations.
from typing import ContextManager, NamedTuple, Protocol, TypeVar
from typing import ClassVar, ContextManager, NamedTuple, Protocol, Sequence, TypeVar
from dataclasses import dataclass
@ -208,3 +208,35 @@ p10_1: Proto10 = NT9()
p10_2: Proto10 = DCFrozen9()
p10_3: Proto10 = DC9()
class Proto11(Protocol):
val1: ClassVar[Sequence[int]]
class Concrete11:
val1: Sequence[int]
# This should generate an error because of a ClassVar mismatch.
p11_1: Protoll = Concrete11()
class Proto12(Protocol):
val1: Sequence[int]
class Concrete12:
val1: ClassVar[Sequence[int]]
# This should generate an error because of a ClassVar mismatch.
p12_1: Protol2 = Concrete12()
def func12(p11: Proto11, p12: Proto12):
# This should generate an error because of a ClassVar mismatch.
v1: Proto12 = p11
# This should generate an error because of a ClassVar mismatch.
v2: Proto11 = p12

View File

@ -30,6 +30,8 @@ class B:
x: int
# This should generate an error because x is not a ClassVar in B
# but is a ClassVar in the protocol.
b: ProtoB = B()

View File

@ -1116,13 +1116,13 @@ test('Protocol2', () => {
test('Protocol3', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['protocol3.py']);
TestUtils.validateResults(analysisResults, 8);
TestUtils.validateResults(analysisResults, 12);
});
test('Protocol4', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['protocol4.py']);
TestUtils.validateResults(analysisResults, 3);
TestUtils.validateResults(analysisResults, 4);
});
test('Protocol5', () => {