PY-12132 Support ABC classes (pep-3119)

Report error for abstract method in non-abstract class.

GitOrigin-RevId: 824d45a310b95ada628d6e607ae1ced6e351d849
This commit is contained in:
Petr
2025-04-09 14:53:21 +00:00
committed by intellij-monorepo-bot
parent 22454fca8f
commit 3060358b39
10 changed files with 160 additions and 29 deletions
@@ -38,6 +38,20 @@ class Triangle(Figure):
def do_triangle(self):
pass
Triangle()
</code></pre>
It also warns you if <code>abc.abstractmethod</code> is used in a class whose metaclass is not <code>abc.ABCMeta</code>:
<code><pre>
from abc import abstractmethod
class MyClass:
@abstractmethod # 'MyClass' is not abstract
def foo(self):
...
</code></pre>
</body>
</html>
@@ -885,6 +885,7 @@ INSP.method.may.be.static=Method <code>#ref</code> may be 'static'
INSP.NAME.abstract.class=Invalid abstract class definition and usages
INSP.abstract.class.class.must.implement.all.abstract.methods=Class {0} must implement all abstract methods
INSP.abstract.class.cannot.instantiate.abstract.class=Cannot instantiate abstract class ''{0}''
INSP.abstract.class.abstract.methods.are.allowed.in.classes.whose.metaclass.is.abcmeta=Abstract methods are allowed in classes whose metaclass is 'ABCMeta'
#PyAssignmentToLoopOrWithParameterInspection
INSP.NAME.assignment.to.loop.or.with.parameter=Assignments to 'for' loop or 'with' statement parameter
@@ -24,9 +24,9 @@ import com.jetbrains.python.psi.types.*;
import com.jetbrains.python.refactoring.PyPsiRefactoringUtil;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Objects;
import java.util.*;
public final class PyAbstractClassInspection extends PyInspection {
@@ -54,7 +54,9 @@ public final class PyAbstractClassInspection extends PyInspection {
QualifiedResolveResult resolveResult = calleeReferenceExpression.followAssignmentsChain(getResolveContext());
if (resolveResult.getElement() instanceof PyClass pyClass) {
if (canHaveAbstractMethods(pyClass)) {
if (hasAbstractMethod(pyClass) || !getAllSuperAbstractMethods(pyClass).isEmpty()) {
boolean hasAbstractMethod =
ContainerUtil.exists(pyClass.getMethods(), method -> PyKnownDecoratorUtil.hasAbstractDecorator(method, myTypeEvalContext));
if (hasAbstractMethod || !getAllSuperAbstractMethods(pyClass).isEmpty()) {
registerProblem(node, canNotInstantiateAbstractClassMessage(pyClass), ProblemHighlightType.WARNING);
}
else if (isAbstract(pyClass)) {
@@ -67,29 +69,51 @@ public final class PyAbstractClassInspection extends PyInspection {
@Override
public void visitPyClass(@NotNull PyClass pyClass) {
if (isAbstract(pyClass) || hasAbstractMethod(pyClass) || PyProtocolsKt.isProtocol(pyClass, myTypeEvalContext)) {
if (isAbstract(pyClass) || PyProtocolsKt.isProtocol(pyClass, myTypeEvalContext)) {
return;
}
final List<PyFunction> toImplement = getAllSuperAbstractMethods(pyClass);
List<PyDecorator> abstractDecorators = new ArrayList<>();
for (PyFunction method : pyClass.getMethods()) {
PyDecoratorList decoratorList = method.getDecoratorList();
if (decoratorList == null) continue;
final ASTNode nameNode = pyClass.getNameNode();
if (!toImplement.isEmpty() && nameNode != null) {
final SmartList<LocalQuickFix> quickFixes = new SmartList<>();
LocalQuickFix qf = PythonUiService.getInstance().createPyImplementMethodsQuickFix(pyClass, toImplement);
if (qf != null) {
quickFixes.add(qf);
for (PyDecorator decorator : decoratorList.getDecorators()) {
boolean isAbstract = ContainerUtil.exists(PyKnownDecoratorUtil.asKnownDecorators(decorator, myTypeEvalContext),
PyKnownDecorator::isAbstract);
if (isAbstract) {
abstractDecorators.add(decorator);
}
}
quickFixes.add(new SetABCMetaAsMetaclassQuickFix());
}
if (LanguageLevel.forElement(pyClass).isPy3K()) {
quickFixes.add(new AddABCToSuperclassesQuickFix());
if (!canHaveAbstractMethods(pyClass)) {
for (PyDecorator decorator : abstractDecorators) {
final SmartList<LocalQuickFix> quickFixes = new SmartList<>();
addMakeClassAbstractFixes(pyClass, quickFixes);
registerProblem(decorator,
PyPsiBundle.message("INSP.abstract.class.abstract.methods.are.allowed.in.classes.whose.metaclass.is.abcmeta"),
quickFixes.toArray(LocalQuickFix.EMPTY_ARRAY));
}
}
registerProblem(nameNode.getPsi(),
PyPsiBundle.message("INSP.abstract.class.class.must.implement.all.abstract.methods", pyClass.getName()),
quickFixes.toArray(LocalQuickFix.EMPTY_ARRAY));
if (abstractDecorators.isEmpty()) {
final List<PyFunction> toImplement = getAllSuperAbstractMethods(pyClass);
final ASTNode nameNode = pyClass.getNameNode();
if (!toImplement.isEmpty() && nameNode != null) {
final SmartList<LocalQuickFix> quickFixes = new SmartList<>();
LocalQuickFix qf = PythonUiService.getInstance().createPyImplementMethodsQuickFix(pyClass, toImplement);
if (qf != null) {
quickFixes.add(qf);
}
addMakeClassAbstractFixes(pyClass, quickFixes);
registerProblem(nameNode.getPsi(),
PyPsiBundle.message("INSP.abstract.class.class.must.implement.all.abstract.methods", pyClass.getName()),
quickFixes.toArray(LocalQuickFix.EMPTY_ARRAY));
}
}
}
@@ -117,15 +141,6 @@ public final class PyAbstractClassInspection extends PyInspection {
return false;
}
private boolean hasAbstractMethod(@NotNull PyClass pyClass) {
for (PyFunction method : pyClass.getMethods()) {
if (PyKnownDecoratorUtil.hasAbstractDecorator(method, myTypeEvalContext)) {
return true;
}
}
return false;
}
private @NotNull List<PyFunction> getAllSuperAbstractMethods(@NotNull PyClass pyClass) {
/* Do not report problem if class contains only methods that raise NotImplementedError without any abc.* decorators
but keep ability to implement them via quickfix (see PY-38680) */
@@ -133,6 +148,28 @@ public final class PyAbstractClassInspection extends PyInspection {
function -> PyKnownDecoratorUtil.hasAbstractDecorator(function, myTypeEvalContext));
}
private static void addMakeClassAbstractFixes(@NotNull PsiElement element, @NotNull Collection<LocalQuickFix> fixes) {
fixes.add(new SetABCMetaAsMetaclassQuickFix());
if (LanguageLevel.forElement(element).isPy3K()) {
fixes.add(new AddABCToSuperclassesQuickFix());
}
}
private static @Nullable PyClass getPyClass(@NotNull PsiElement element) {
// element is a class name node
if (element.getParent() instanceof PyClass pyClass) {
return pyClass;
}
// element is a method decorator
if (element instanceof PyDecorator decorator) {
return Optional.ofNullable(decorator.getTarget())
.map(PyFunction::getContainingClass)
.orElse(null);
}
return null;
}
private static class AddABCToSuperclassesQuickFix extends PsiUpdateModCommandQuickFix {
@Override
@@ -142,7 +179,7 @@ public final class PyAbstractClassInspection extends PyInspection {
@Override
public void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
final PyClass cls = PyUtil.as(element.getParent(), PyClass.class);
final PyClass cls = getPyClass(element);
if (cls == null) return;
final PyClass abcClass = PyPsiFacade.getInstance(project).createClassByQName(PyNames.ABC, cls);
@@ -161,7 +198,7 @@ public final class PyAbstractClassInspection extends PyInspection {
@Override
public void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
final PyClass cls = PyUtil.as(element.getParent(), PyClass.class);
final PyClass cls = getPyClass(element);
if (cls == null) return;
final PyClass abcMetaClass = PyPsiFacade.getInstance(project).createClassByQName(PyNames.ABC_META, cls);
@@ -0,0 +1,27 @@
from abc import abstractmethod, ABCMeta
class A:
<weak_warning descr="Abstract methods are allowed in classes whose metaclass is 'ABCMeta'">@abstractmethod</weak_warning>
def foo(self): ...
class AbstractClassA(metaclass=ABCMeta):
...
class AbstractClassB(AbstractClassA):
@abstractmethod
def foo(self): ...
class AbstractClassC(AbstractClassB):
@abstractmethod
def bar(self): ...
class AbstractClassD(AbstractClassC):
def foo(self): ...
def bar(self): ...
@abstractmethod
def buz(self): ...
@@ -0,0 +1,7 @@
import abc
class A:
@abc.ab<caret>stractmethod
def meth(self):
...
@@ -0,0 +1,8 @@
import abc
from abc import ABC
class A(ABC):
@abc.abstractmethod
def meth(self):
...
@@ -0,0 +1,7 @@
from abc import abstractmethod
class A:
@ab<caret>stractmethod
def meth(self):
...
@@ -0,0 +1,7 @@
from abc import abstractmethod, ABCMeta
class A(metaclass=ABCMeta):
@abstractmethod
def meth(self):
...
@@ -146,6 +146,15 @@ public class Py3QuickFixTest extends PyTestCase {
true);
}
// PY-12132
public void testAddABCToSuperclassesCaretAtAbstractMethod() {
doInspectionTest("PyAbstractClassInspection/quickFix/AddABCToSuperclassesCaretAtAbstractMethod/main.py",
PyAbstractClassInspection.class,
"Add '" + PyNames.ABC + "' to superclasses",
true,
true);
}
// PY-30789
public void testSetABCMetaAsMetaclassPy3() {
final String[] testFiles = {
@@ -169,6 +178,15 @@ public class Py3QuickFixTest extends PyTestCase {
true);
}
// PY-12132
public void testSetABCMetaAsMetaclassPy3CaretAtAbstractMethod() {
doInspectionTest("PyAbstractClassInspection/quickFix/SetABCMetaAsMetaclassPy3CaretAtAbstractMethod/main.py",
PyAbstractClassInspection.class,
"Set '" + PyNames.ABC_META + "' as metaclass",
true,
true);
}
@Override
@NonNls
protected String getTestDataPath() {
@@ -91,6 +91,11 @@ public class PyAbstractClassInspectionTest extends PyInspectionTestCase {
doTest();
}
// PY-12132
public void testAbstractMethodInNonAbstractClass() {
doTest();
}
@NotNull
@Override
protected Class<? extends PyInspection> getInspectionClass() {