mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
committed by
intellij-monorepo-bot
parent
c4c2096159
commit
44d07d2450
@@ -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
|
||||
+56
-38
@@ -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 }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from mod import C
|
||||
|
||||
class D(C):
|
||||
x = 4 # type: ClassVar[int]
|
||||
+7
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user