Added support for recent addendum to typing spec that provides a special case for dataclasses so ClassVar and Final can be used together to distinguish between a regular Final instance variable and a ClassVar that is also Final. This addresses #7959. (#7962)

This commit is contained in:
Eric Traut 2024-05-20 15:52:34 -07:00 committed by GitHub
parent 24787dad22
commit 7fb7b4158d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 73 additions and 2 deletions

View File

@ -3922,6 +3922,13 @@ export class Binder extends ParseTreeWalker {
let finalTypeNode: ExpressionNode | undefined;
if (typeAnnotation) {
// Allow Final to be enclosed in ClassVar. Normally, Final implies
// ClassVar, but this combination is required in the case of dataclasses.
const classVarInfo = this._isAnnotationClassVar(typeAnnotation);
if (classVarInfo?.classVarTypeNode) {
typeAnnotation = classVarInfo.classVarTypeNode;
}
if (this._isTypingAnnotation(typeAnnotation, 'Final')) {
isFinal = true;
} else if (typeAnnotation.nodeType === ParseNodeType.Index && typeAnnotation.items.length === 1) {

View File

@ -7596,8 +7596,26 @@ export function createTypeEvaluator(
const typeArgs: TypeResultWithNode[] = [];
let adjFlags = flags;
if (options?.isFinalAnnotation || options?.isClassVarAnnotation) {
if (options?.isFinalAnnotation) {
adjFlags |= EvaluatorFlags.DisallowClassVar | EvaluatorFlags.DisallowFinal;
} else if (options?.isClassVarAnnotation) {
adjFlags |= EvaluatorFlags.DisallowClassVar;
// If the annotation is a variable within the body of a dataclass, a
// Final is allowed within the ClassVar annotation. In all other cases,
// it's disallowed.
let disallowFinal = true;
const enclosingClassNode = ParseTreeUtils.getEnclosingClass(node, /* stopeAtFunction */ true);
if (enclosingClassNode) {
const classTypeInfo = getTypeOfClass(enclosingClassNode);
if (classTypeInfo && ClassType.isDataClass(classTypeInfo.classType)) {
disallowFinal = false;
}
}
if (disallowFinal) {
adjFlags |= EvaluatorFlags.DisallowFinal;
}
} else {
adjFlags &= ~(
EvaluatorFlags.DoNotSpecialize |

View File

@ -30,6 +30,15 @@ class Foo(Generic[T]):
# be used in a ClassVar.
illegal2: ClassVar[T]
# This should generate an error because Final cannot be
# used with a ClassVar.
illegal3: ClassVar[Final] = 0
# This should generate an error because Final cannot be
# used with a ClassVar. A second error is generated because
# Final[int] is not interpreted as a valid type.
illegal4: ClassVar[Final[int]] = 0
ok1: ClassVar[list]
ok2: ClassVar[list[Any]]
ok3: Annotated[ClassVar[list[Self]], ""]

View File

@ -0,0 +1,31 @@
# This sample tests the case where a dataclass uses a ClassVar that
# is also Final.
from dataclasses import dataclass
from typing import ClassVar, Final
@dataclass
class A:
a: Final[int]
b: Final[str] = ""
c: ClassVar[Final[int]] = 0
d: ClassVar[Final] = 0
a = A(1)
# This should generate an error.
a.a = 0
# This should generate an error.
a.b = ""
# This should generate an error.
a.c = 0
# This should generate an error.
A.c = 0
# This should generate an error.
A.d = 0

View File

@ -344,6 +344,12 @@ test('DataClass16', () => {
TestUtils.validateResults(analysisResults, 1);
});
test('DataClass17', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['dataclass17.py']);
TestUtils.validateResults(analysisResults, 5);
});
test('DataClassFrozen1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['dataclassFrozen1.py']);

View File

@ -754,7 +754,7 @@ test('ClassVar2', () => {
test('ClassVar3', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['classVar3.py']);
TestUtils.validateResults(analysisResults, 10);
TestUtils.validateResults(analysisResults, 13);
});
test('ClassVar4', () => {