PY-20811 Support for typing.ClassVar type annotations (PEP 526)

* Dedicated inspections for `ClassVar` variables in variable declarations, variable reassignments, function parameters, local and return variables
* Types of `ClassVar` variables now resolves correctly
* Tests for `ClassVar` inspections

GitOrigin-RevId: 0fd0ef0126ba2c2801ef82bcbeca4ea9b0c48c73
This commit is contained in:
Daniil Kalinin
2022-05-24 20:12:55 +00:00
committed by intellij-monorepo-bot
parent c4c2096159
commit 44d07d2450
10 changed files with 409 additions and 40 deletions
@@ -157,6 +157,7 @@
<localInspection language="Python" shortName="PyDunderSlotsInspection" suppressId="PyDunderSlots" bundle="messages.PyPsiBundle" key="INSP.NAME.dunder.slots" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyDunderSlotsInspection"/>
<localInspection language="Python" shortName="PyExceptClausesOrderInspection" suppressId="PyExceptClausesOrder" bundle="messages.PyPsiBundle" key="INSP.NAME.bad.except.clauses.order" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyExceptClausesOrderInspection"/>
<localInspection language="Python" shortName="PyFinalInspection" suppressId="PyFinal" bundle="messages.PyPsiBundle" key="INSP.NAME.final.classes.methods.and.variables" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyFinalInspection"/>
<localInspection language="Python" shortName="PyClassVarInspection" suppressId="PyClassVar" bundle="messages.PyPsiBundle" key="INSP.NAME.class.var.variables" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyClassVarInspection"/>
<localInspection language="Python" shortName="PyFromFutureImportInspection" suppressId="PyFromFutureImport" bundle="messages.PyPsiBundle" key="INSP.NAME.from.future.import" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyFromFutureImportInspection"/>
<localInspection language="Python" shortName="PyGlobalUndefinedInspection" suppressId="PyGlobalUndefined" bundle="messages.PyPsiBundle" key="INSP.NAME.global.undefined" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyGlobalUndefinedInspection"/>
<localInspection language="Python" shortName="PyInconsistentIndentationInspection" suppressId="PyInconsistentIndentation" bundle="messages.PyPsiBundle" key="INSP.NAME.inconsistent.indentation" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyInconsistentIndentationInspection"/>
@@ -0,0 +1,22 @@
<html>
<body>
<p>Reports invalid usages of <a href="https://docs.python.org/3/library/typing.html#typing.ClassVar">ClassVar</a> annotations.</p>
<p><b>Example:</b></p>
<pre style="font-family: monospace">
from typing import ClassVar
class Cat:
color: ClassVar[str] = "white"
weight: int
def __init__(self, weight: int):
self.weight = weight
Cat.color = "black" # OK
my_cat = Cat(5)
my_cat.color = "gray" # Error, setting class variable on instance
</pre>
</body>
</html>
@@ -1205,4 +1205,14 @@ ANN.patterns.repeated.star.pattern=Repeated star pattern
element.presentation.location.string.in.class=({0} in {1})
element.presentation.location.string.in.class.stub=({0} in {1} stub)
element.presentation.location.string.module=({0})
element.presentation.location.string.module.stub=({0} stub)
element.presentation.location.string.module.stub=({0} stub)
# PyClassVarInspection
INSP.NAME.class.var.variables=Invalid usage of ClassVar variables
INSP.class.var.can.not.be.assigned.to.instance=Cannot assign to class variable ''{0}'' via instance
INSP.class.var.can.be.used.only.in.class.body='ClassVar' can only be used for assignments in class body
INSP.class.var.can.not.be.used.in.function.body='ClassVar' cannot be used in annotations for local variables
INSP.class.var.can.not.override.class.variable=Cannot override class variable ''{0}'' (previously declared on base class ''{1}'') with instance variable
INSP.class.var.can.not.override.instance.variable=Cannot override instance variable ''{0}'' (previously declared on base class ''{1}'') with class variable
INSP.class.var.can.not.be.used.in.annotations.for.function.parameters='ClassVar' cannot be used in annotations for function parameters
INSP.class.var.can.not.be.used.in.annotation.for.function.return.value='ClassVar' cannot be used in annotation for a function return value
@@ -43,6 +43,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -844,6 +845,10 @@ public class PyTypingTypeProvider extends PyTypeProviderBase {
if (callableType != null) {
return Ref.create(callableType);
}
final Ref<PyType> classVarType = getClassVarType(resolved, context);
if (classVarType != null) {
return classVarType;
}
final Ref<PyType> classObjType = getClassObjectType(resolved, context);
if (classObjType != null) {
return Ref.create(addGenericAlias(classObjType.get(), alias));
@@ -946,6 +951,20 @@ public class PyTypingTypeProvider extends PyTypeProviderBase {
return null;
}
@Nullable
private static Ref<PyType> getClassVarType(@NotNull PsiElement resolved, @NotNull Context context) {
if (resolved instanceof PySubscriptionExpression) {
final PySubscriptionExpression subscriptionExpr = (PySubscriptionExpression)resolved;
if (resolvesToClassVar(subscriptionExpr.getOperand(), context.getTypeContext())) {
final PyExpression indexExpr = subscriptionExpr.getIndexExpression();
if (indexExpr != null) {
return getType(indexExpr, context);
}
}
}
return null;
}
@Nullable
private static Ref<PyType> getAliasedType(@NotNull PsiElement resolved, @NotNull Context context) {
if (resolved instanceof PyReferenceExpression && ((PyReferenceExpression)resolved).asQualifiedName() != null) {
@@ -1096,7 +1115,6 @@ public class PyTypingTypeProvider extends PyTypeProviderBase {
private static Ref<PyType> getFinalType(@NotNull PsiElement resolved, @NotNull Context context) {
if (resolved instanceof PySubscriptionExpression) {
final PySubscriptionExpression subscriptionExpr = (PySubscriptionExpression)resolved;
if (resolvesToFinal(subscriptionExpr.getOperand(), context.getTypeContext())) {
final PyExpression indexExpr = subscriptionExpr.getIndexExpression();
if (indexExpr != null) {
@@ -1108,35 +1126,38 @@ public class PyTypingTypeProvider extends PyTypeProviderBase {
return null;
}
private static <T extends PyTypeCommentOwner & PyAnnotationOwner> boolean isSpecialModifierImpl(@NotNull T owner,
@NotNull TypeEvalContext context,
@NotNull BiFunction<PyExpression, TypeEvalContext, Boolean> resolver) {
final PyExpression annotation = getAnnotationValue(owner, context);
if (annotation instanceof PySubscriptionExpression) {
return resolver.apply(((PySubscriptionExpression)annotation).getOperand(), context);
}
else if (annotation instanceof PyReferenceExpression) {
return resolver.apply(annotation, context);
}
final String typeCommentValue = owner.getTypeCommentAnnotation();
final PyExpression typeComment = typeCommentValue == null ? null : toExpression(typeCommentValue, owner);
if (typeComment instanceof PySubscriptionExpression) {
return resolver.apply(((PySubscriptionExpression)typeComment).getOperand(), context);
}
else if (typeComment instanceof PyReferenceExpression) {
return resolver.apply(typeComment, context);
}
return false;
}
public static boolean isFinal(@NotNull PyDecoratable decoratable, @NotNull TypeEvalContext context) {
return ContainerUtil.exists(PyKnownDecoratorUtil.getKnownDecorators(decoratable, context),
d -> d == TYPING_FINAL || d == TYPING_FINAL_EXT);
}
public static <T extends PyTypeCommentOwner & PyAnnotationOwner> boolean isFinal(@NotNull T owner, @NotNull TypeEvalContext context) {
return PyUtil.getParameterizedCachedValue(owner, context, p -> isFinalImpl(owner, p));
}
private static <T extends PyTypeCommentOwner & PyAnnotationOwner> boolean isFinalImpl(@NotNull T owner,
@NotNull TypeEvalContext context) {
final PyExpression annotation = getAnnotationValue(owner, context);
if (annotation instanceof PySubscriptionExpression) {
return resolvesToFinal(((PySubscriptionExpression)annotation).getOperand(), context);
}
else if (annotation instanceof PyReferenceExpression) {
return resolvesToFinal(annotation, context);
}
final String typeCommentValue = owner.getTypeCommentAnnotation();
final PyExpression typeComment = typeCommentValue == null ? null : toExpression(typeCommentValue, owner);
if (typeComment instanceof PySubscriptionExpression) {
return resolvesToFinal(((PySubscriptionExpression)typeComment).getOperand(), context);
}
else if (typeComment instanceof PyReferenceExpression) {
return resolvesToFinal(typeComment, context);
}
return false;
return PyUtil.getParameterizedCachedValue(owner, context, p -> isSpecialModifierImpl(owner, p, (e, c) -> {
return resolvesToFinal(e, c);
}));
}
private static boolean resolvesToFinal(@NotNull PyExpression expression, @NotNull TypeEvalContext context) {
@@ -1144,6 +1165,17 @@ public class PyTypingTypeProvider extends PyTypeProviderBase {
return qualifiedNames.contains(FINAL) || qualifiedNames.contains(FINAL_EXT);
}
public static <T extends PyAnnotationOwner & PyTypeCommentOwner> boolean isClassVar(@NotNull T owner, @NotNull TypeEvalContext context) {
return PyUtil.getParameterizedCachedValue(owner, context, p -> isSpecialModifierImpl(owner, p, (e, c) -> {
return resolvesToClassVar(e, c);
}));
}
private static boolean resolvesToClassVar(@NotNull PyExpression expression, @NotNull TypeEvalContext context) {
final var qualifiedNames = resolveToQualifiedNames(expression, context);
return qualifiedNames.contains(CLASS_VAR);
}
@Nullable
private static PyExpression getAnnotationValue(@NotNull PyAnnotationOwner owner, @NotNull TypeEvalContext context) {
if (context.maySwitchToAST(owner)) {
@@ -1687,20 +1719,6 @@ public class PyTypingTypeProvider extends PyTypeProviderBase {
return Ref.create(PyTypeParser.getTypeByName(call, type, context));
}
public static boolean isClassVar(@NotNull PyAnnotationOwner annotationOwner, @NotNull TypeEvalContext context) {
final PyExpression annotationValue = getAnnotationValue(annotationOwner, context);
if (annotationValue instanceof PySubscriptionExpression) {
final PyExpression operand = ((PySubscriptionExpression)annotationValue).getOperand();
return operand instanceof PyReferenceExpression && resolveToQualifiedNames(operand, context).contains(CLASS_VAR);
}
else if (annotationValue instanceof PyReferenceExpression) {
return resolveToQualifiedNames(annotationValue, context).contains(CLASS_VAR);
}
return false;
}
/**
* Checks whether the given assignment is type hinted with {@code typing.TypeAlias}.
* <p>
@@ -0,0 +1,120 @@
package com.jetbrains.python.inspections
import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.types.PyClassType
import com.jetbrains.python.psi.types.TypeEvalContext
class PyClassVarInspection : PyInspection() {
override fun buildVisitor(holder: ProblemsHolder,
isOnTheFly: Boolean,
session: LocalInspectionToolSession): PsiElementVisitor = Visitor(holder,
PyInspectionVisitor.getContext(session))
private class Visitor(holder: ProblemsHolder, context: TypeEvalContext) : PyInspectionVisitor(holder, context) {
override fun visitPyTargetExpression(node: PyTargetExpression) {
super.visitPyTargetExpression(node)
if (node.hasAssignedValue()) {
if (node.isQualified) {
checkClassVarReassignment(node)
}
else {
checkClassVarDeclaration(node)
}
}
}
override fun visitPyNamedParameter(node: PyNamedParameter) {
super.visitPyNamedParameter(node)
if (node.isClassVar()) {
registerProblem(node.annotation?.value ?: node.typeComment,
PyPsiBundle.message("INSP.class.var.can.not.be.used.in.annotations.for.function.parameters"))
}
}
override fun visitPyFunction(node: PyFunction) {
super.visitPyFunction(node)
PyTypingTypeProvider.getReturnTypeAnnotation(node, myTypeEvalContext)?.let {
if (resolvesToClassVar(if (it is PySubscriptionExpression) it.operand else it)) {
registerProblem(node.typeComment ?: node.annotation?.value,
PyPsiBundle.message("INSP.class.var.can.not.be.used.in.annotation.for.function.return.value"))
}
}
}
private fun checkClassVarDeclaration(target: PyTargetExpression) {
when (val scopeOwner = ScopeUtil.getScopeOwner(target)) {
is PyFile -> {
if (PyUtil.isTopLevel(target) && target.isClassVar()) {
registerProblem(target.typeComment ?: target.annotation?.value,
PyPsiBundle.message("INSP.class.var.can.be.used.only.in.class.body"))
}
}
is PyFunction -> {
if (target.isClassVar()) {
registerProblem(target.typeComment ?: target.annotation?.value,
PyPsiBundle.message("INSP.class.var.can.not.be.used.in.function.body"))
}
}
is PyClass -> {
checkInheritedClassClassVarReassignmentOnClassLevel(target, scopeOwner)
}
}
}
private fun checkClassVarReassignment(target: PyTargetExpression) {
val qualifierType = target.qualifier?.let { myTypeEvalContext.getType(it) }
if (qualifierType is PyClassType && !qualifierType.isDefinition) {
checkInstanceClassVarReassignment(target, qualifierType.pyClass)
}
}
private fun checkInheritedClassClassVarReassignmentOnClassLevel(target: PyTargetExpression, cls: PyClass) {
val name = target.name ?: return
for (ancestor in cls.getAncestorClasses(myTypeEvalContext)) {
val ancestorClassAttribute = ancestor.findClassAttribute(name, false, myTypeEvalContext)
if (ancestorClassAttribute != null && ancestorClassAttribute.hasExplicitType() && target.hasExplicitType()) {
if (ancestorClassAttribute.isClassVar() && !target.isClassVar()) {
registerProblem(target, PyPsiBundle.message("INSP.class.var.can.not.override.class.variable", name, ancestor.name))
break
}
if (!ancestorClassAttribute.isClassVar() && target.isClassVar()) {
registerProblem(target, PyPsiBundle.message("INSP.class.var.can.not.override.instance.variable", name, ancestor.name))
break
}
}
}
}
private fun checkInstanceClassVarReassignment(target: PyQualifiedExpression, cls: PyClass) {
val name = target.name ?: return
for (ancestor in listOf(cls) + cls.getAncestorClasses(myTypeEvalContext)) {
val inheritedClassAttribute = ancestor.findClassAttribute(name, false, myTypeEvalContext)
if (inheritedClassAttribute != null && inheritedClassAttribute.isClassVar()) {
registerProblem(target, PyPsiBundle.message("INSP.class.var.can.not.be.assigned.to.instance", name))
return
}
}
}
private fun PyTargetExpression.hasExplicitType(): Boolean =
annotationValue != null || typeCommentAnnotation != null
private fun <T> T.isClassVar(): Boolean where T : PyAnnotationOwner, T : PyTypeCommentOwner =
PyTypingTypeProvider.isClassVar(this, myTypeEvalContext)
private fun resolvesToClassVar(expression: PyExpression): Boolean {
return expression is PyReferenceExpression &&
PyTypingTypeProvider.resolveToQualifiedNames(expression, myTypeEvalContext)
.any { it == PyTypingTypeProvider.CLASS_VAR }
}
}
}
@@ -0,0 +1,6 @@
from typing import ClassVar
from mod import C
class D(C):
x = 4 # type: ClassVar[int]
@@ -0,0 +1,7 @@
from typing import ClassVar
class A:
x = 1 # type: ClassVar[int]
class B(A):
x = 2
class C(B):
x = 3
@@ -16,6 +16,6 @@ class C:
self.z = <warning descr="Expected type 'int', got 'str' instead">'bar'</warning>
self.class_var = 1
self.class_var = 'bar'
self.class_var = <warning descr="Expected type 'int', got 'str' instead">'bar'</warning>
C.class_var = 1
C.class_var = 'bar'
@@ -1769,6 +1769,22 @@ public class PyTypingTest extends PyTestCase {
"expr = A() | A()");
}
public void testClassVarTypeResolvedFromAnnotation() {
doTest("int",
"from typing import ClassVar\n" +
"class A:\n" +
" x: ClassVar[int] = 1\n" +
"expr = A.x");
}
public void testClassVarTypeResolvedFromTypeComment() {
doTest("int",
"from typing import ClassVar\n" +
"class A:\n" +
" x = 1 # type: ClassVar[int]\n" +
"expr = A.x");
}
private void doTestNoInjectedText(@NotNull String text) {
myFixture.configureByText(PythonFileType.INSTANCE, text);
final InjectedLanguageManager languageManager = InjectedLanguageManager.getInstance(myFixture.getProject());
@@ -0,0 +1,169 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.inspections;
import com.jetbrains.python.fixtures.PyInspectionTestCase;
import com.jetbrains.python.psi.LanguageLevel;
import org.jetbrains.annotations.NotNull;
public class PyClassVarInspectionTest extends PyInspectionTestCase {
public void testCanAssignOnClassAttribute() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar\n" +
"class A:\n" +
" x = 1 # type: ClassVar[int]\n" +
"A.x = 2"));
}
public void testCanNotAssignOnInstance() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar\n" +
"class A:\n" +
" x = 1 # type: ClassVar[int]\n" +
"<warning descr=\"Cannot assign to class variable 'x' via instance\">A().x</warning> = 2"));
}
public void testCanNotAssignOutsideOfClassWithTypeComment() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar\n" +
"x = 1 <warning descr=\"'ClassVar' can only be used for assignments in class body\"># type: ClassVar[int]</warning>\n"));
}
public void testCanNotAssignOutsideOfClassWithAnnotation() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar\n" +
"x: <warning descr=\"'ClassVar' can only be used for assignments in class body\">ClassVar[int]</warning> = 1\n"));
}
public void testCannotAssignOnSubclassInstance() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar\n" +
"class A:\n" +
" x = 1 # type: ClassVar[int]\n" +
"class B(A):\n" +
" pass\n" +
"<warning descr=\"Cannot assign to class variable 'x' via instance\">B().x</warning> = 2"));
}
public void testCanNotOverrideOnSelf() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar\n" +
"class A:\n" +
" x = None # type: ClassVar[int]\n" +
" def __init__(self) -> None:\n" +
" <warning descr=\"Cannot assign to class variable 'x' via instance\">self.x</warning> = 1"));
}
public void testCanNotOverrideOnSelfInSubclass() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar\n" +
"class A:\n" +
" x = None # type: ClassVar[int]\n" +
"class B(A):\n" +
" def __init__(self) -> None:\n" +
" <warning descr=\"Cannot assign to class variable 'x' via instance\">self.x</warning> = 0"));
}
public void testCanNotAssignOnClassInstanceFromType() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar, Type\n" +
"class A:\n" +
" x = None # type: ClassVar[int]\n" +
"def f(a: Type[A]) -> None:\n" +
" <warning descr=\"Cannot assign to class variable 'x' via instance\">a().x</warning> = 0"));
}
public void testCanAssignOnClassObjectFromType() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar, Type\n" +
"class A:\n" +
" x = None # type: ClassVar[int]\n" +
"def f(a: Type[A]) -> None:\n" +
" a.x = 0"));
}
public void testCanNotOverrideClassVarWithNormalAttribute() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar\n" +
"class A:\n" +
" x = 1 # type: ClassVar[int]\n" +
"class B(A):\n" +
" <warning descr=\"Cannot override class variable 'x' (previously declared on base class 'A') with instance variable\">x</warning> = 2 # type: int"));
}
public void testCanNotOverrideNormalAttributeWithClassVar() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar\n" +
"class A:\n" +
" x = 1 # type: int\n" +
"class B(A):\n" +
" <warning descr=\"Cannot override instance variable 'x' (previously declared on base class 'A') with class variable\">x</warning> = 2 # type: ClassVar[int]"));
}
public void testOverrideClassVarWithImplicitThenExplicitMultiFile() {
runWithLanguageLevel(LanguageLevel.getLatest(), this::doMultiFileTest);
}
public void testCanNotOverrideMultiBaseClassVar() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar\n" +
"class A:\n" +
" x = 1 # type: ClassVar[int]\n" +
"class B:\n" +
" x = 2 # type: int\n" +
"class C(A, B):\n" +
" <warning descr=\"Cannot override instance variable 'x' (previously declared on base class 'B') with class variable\">x</warning> = 3 # type: ClassVar[int]"));
}
public void testCanOverrideClassVarWithImplicitClassVar() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar\n" +
"class A:\n" +
" x = 1 # type: ClassVar[int]\n" +
"class B(A):\n" +
" x = 2"));
}
public void testOverrideClassVarWithImplicitThenExplicit() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar\n" +
"class A:\n" +
" x = 1 # type: ClassVar[int]\n" +
"class B(A):\n" +
" x = 2\n" +
"class C(B):\n" +
" x = 3\n" +
"class D(C):\n" +
" x = 4 # type: ClassVar[int]"));
}
public void testClassVarCanNotBeUsedAsFunctionParameterAnnotation() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar\n" +
"\n" +
"def foo(a: <warning descr=\"'ClassVar' cannot be used in annotations for function parameters\">ClassVar</warning>):\n" +
" pass"));
}
public void testClassVarCanNotBeUsedAsFunctionReturnParameter() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar\n" +
"\n" +
"def foo() -> <warning descr=\"'ClassVar' cannot be used in annotation for a function return value\">ClassVar</warning>:\n" +
" pass"));
}
public void testClassVarCanNotBeDeclaredInFunctionBody() {
runWithLanguageLevel(LanguageLevel.getLatest(),
() -> doTestByText("from typing import ClassVar\n" +
"class Cls:\n" +
" def foo(self):\n" +
" x: <warning descr=\"'ClassVar' cannot be used in annotations for local variables\">ClassVar</warning> = \"str\""));
}
@Override
protected @NotNull Class<? extends PyInspection> getInspectionClass() {
return PyClassVarInspection.class;
}
}