From cd732906e6626ca80992ef2bd5d31a940cadefbc Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 3 Dec 2024 13:34:03 -0800 Subject: [PATCH] Added check for invalid use of `ClassVar` qualifier within a `NamedTuple` or `TypedDict` attribute annotation. This addresses #9526. --- .../src/analyzer/parseTreeUtils.ts | 9 -------- .../src/analyzer/typeEvaluator.ts | 23 +++++++++++++++---- .../src/tests/samples/classVar6.py | 18 +++++++++++++++ .../src/tests/samples/dataclassNamedTuple1.py | 3 --- .../src/tests/typeEvaluator7.test.ts | 6 +++++ 5 files changed, 43 insertions(+), 16 deletions(-) create mode 100644 packages/pyright-internal/src/tests/samples/classVar6.py diff --git a/packages/pyright-internal/src/analyzer/parseTreeUtils.ts b/packages/pyright-internal/src/analyzer/parseTreeUtils.ts index 333f3f72fd2f..c2f72b510fd0 100644 --- a/packages/pyright-internal/src/analyzer/parseTreeUtils.ts +++ b/packages/pyright-internal/src/analyzer/parseTreeUtils.ts @@ -1178,15 +1178,6 @@ export function isFinalAllowedForAssignmentTarget(targetNode: ExpressionNode): b return false; } -export function isClassVarAllowedForAssignmentTarget(targetNode: ExpressionNode): boolean { - const classNode = getEnclosingClass(targetNode, /* stopAtFunction */ true); - if (!classNode) { - return false; - } - - return true; -} - export function isRequiredAllowedForAssignmentTarget(targetNode: ExpressionNode): boolean { const classNode = getEnclosingClass(targetNode, /* stopAtFunction */ true); if (!classNode) { diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index 6638872b3674..9252603b8c5b 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -4360,7 +4360,7 @@ export function createTypeEvaluator( let annotationType: Type | undefined = getTypeOfAnnotation(target.d.annotation, { varTypeAnnotation: true, allowFinal: ParseTreeUtils.isFinalAllowedForAssignmentTarget(target.d.valueExpr), - allowClassVar: ParseTreeUtils.isClassVarAllowedForAssignmentTarget(target.d.valueExpr), + allowClassVar: isClassVarAllowedForAssignmentTarget(target.d.valueExpr), }); if (annotationType) { @@ -4433,6 +4433,21 @@ export function createTypeEvaluator( } } + function isClassVarAllowedForAssignmentTarget(targetNode: ExpressionNode): boolean { + const classNode = ParseTreeUtils.getEnclosingClass(targetNode, /* stopAtFunction */ true); + if (!classNode) { + return false; + } + + // ClassVar is not allowed in a TypedDict or a NamedTuple class. + const classType = getTypeOfClass(classNode)?.classType; + if (!classType) { + return false; + } + + return !ClassType.isTypedDictClass(classType) && !classType.shared.namedTupleEntries; + } + function verifyRaiseExceptionType(node: ExpressionNode, allowNone: boolean) { const baseExceptionType = getBuiltInType(node, 'BaseException'); const exceptionType = getTypeOfExpression(node).type; @@ -19924,7 +19939,7 @@ export function createTypeEvaluator( const annotationType = getTypeOfAnnotation(node.d.annotation, { varTypeAnnotation: true, allowFinal: ParseTreeUtils.isFinalAllowedForAssignmentTarget(node.d.valueExpr), - allowClassVar: ParseTreeUtils.isClassVarAllowedForAssignmentTarget(node.d.valueExpr), + allowClassVar: isClassVarAllowedForAssignmentTarget(node.d.valueExpr), }); writeTypeCache(node.d.valueExpr, { type: annotationType }, EvalFlags.None); @@ -20073,7 +20088,7 @@ export function createTypeEvaluator( getTypeOfAnnotation(annotationNode, { varTypeAnnotation: true, allowFinal: ParseTreeUtils.isFinalAllowedForAssignmentTarget(annotationParent.d.leftExpr), - allowClassVar: ParseTreeUtils.isClassVarAllowedForAssignmentTarget(annotationParent.d.leftExpr), + allowClassVar: isClassVarAllowedForAssignmentTarget(annotationParent.d.leftExpr), }); } else { evaluateTypesForAssignmentStatement(annotationParent); @@ -22048,7 +22063,7 @@ export function createTypeEvaluator( declaration.node.parent?.nodeType === ParseNodeType.MemberAccess ? declaration.node.parent : declaration.node; - const allowClassVar = ParseTreeUtils.isClassVarAllowedForAssignmentTarget(declNode); + const allowClassVar = isClassVarAllowedForAssignmentTarget(declNode); const allowFinal = ParseTreeUtils.isFinalAllowedForAssignmentTarget(declNode); const allowRequired = ParseTreeUtils.isRequiredAllowedForAssignmentTarget(declNode) || diff --git a/packages/pyright-internal/src/tests/samples/classVar6.py b/packages/pyright-internal/src/tests/samples/classVar6.py new file mode 100644 index 000000000000..186c0c6d381b --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/classVar6.py @@ -0,0 +1,18 @@ +# This sample tests that a ClassVar is disallowed when used in a +# NamedTuple or TypedDict class as reflected in the runtime. + +from typing import ClassVar, NamedTuple, TypedDict + +class NT1(NamedTuple): + # This should generate an error. + x: ClassVar + + # This should generate an error. + y: ClassVar[int] + +class TD1(TypedDict): + # This should generate an error. + x: ClassVar + + # This should generate an error. + y: ClassVar[int] diff --git a/packages/pyright-internal/src/tests/samples/dataclassNamedTuple1.py b/packages/pyright-internal/src/tests/samples/dataclassNamedTuple1.py index 0806b5a05c20..0cb0f7e2266f 100644 --- a/packages/pyright-internal/src/tests/samples/dataclassNamedTuple1.py +++ b/packages/pyright-internal/src/tests/samples/dataclassNamedTuple1.py @@ -15,9 +15,6 @@ class DataTuple(NamedTuple): def _m(self): pass - # ClassVar variables should not be included. - class_var: ClassVar[int] = 4 - id: int aid: Other value: str = "" diff --git a/packages/pyright-internal/src/tests/typeEvaluator7.test.ts b/packages/pyright-internal/src/tests/typeEvaluator7.test.ts index ea9bc1cf380c..4c47830a6ca0 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator7.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator7.test.ts @@ -805,6 +805,12 @@ test('ClassVar5', () => { TestUtils.validateResults(analysisResults, 0); }); +test('ClassVar6', () => { + const analysisResults = TestUtils.typeAnalyzeSampleFiles(['classVar6.py']); + + TestUtils.validateResults(analysisResults, 4); +}); + test('TypeVar1', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVar1.py']);