Repository: python/mypy

Issue:
Catch accidentally abstract classes
We've had a series of typeshed issues where classes accidentally became abstract because an abstract method in a parent class wasn't overridden (see python/typeshed#1476). It would be nice if mypy could tell us about this sort of issue automatically, since for now we have to rely on bug reports from end users.

I'm not sure if there's a good programmatic way to distinguish between accidentally and intentionally abstract classes. Here are a few ideas:
- Warn if a class is abstract, but doesn't itself have any abstractmethods (all abstractmethods are inherited from a parent class).
- Require all intentionally abstract classes to specify ABCMeta as an explicit metaclass. This would be fairly disruptive.
- Don't do anything. This problem has repeatedly affected typeshed, but it's probably much easier to discover outside stubs, since the type checker will also see code using the class, not just its definition like in stubs.


Discussion:

Resolving patch:
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -858,6 +858,7 @@ def calculate_abstract_status(self, typ: TypeInfo) -> None:
         """
         concrete = set()  # type: Set[str]
         abstract = []  # type: List[str]
+        abstract_in_this_class = []  # type: List[str]
         for base in typ.mro:
             for name, symnode in base.names.items():
                 node = symnode.node
@@ -874,12 +875,30 @@ def calculate_abstract_status(self, typ: TypeInfo) -> None:
                     if fdef.is_abstract and name not in concrete:
                         typ.is_abstract = True
                         abstract.append(name)
+                        if base is typ:
+                            abstract_in_this_class.append(name)
                 elif isinstance(node, Var):
                     if node.is_abstract_var and name not in concrete:
                         typ.is_abstract = True
                         abstract.append(name)
+                        if base is typ:
+                            abstract_in_this_class.append(name)
                 concrete.add(name)
+        # In stubs, abstract classes need to be explicitly marked because it is too
+        # easy to accidentally leave a concrete class abstract by forgetting to
+        # implement some methods.
         typ.abstract_attributes = sorted(abstract)
+        if not self.is_stub_file:
+            return
+        if (typ.declared_metaclass and typ.declared_metaclass.type.fullname() == 'abc.ABCMeta'):
+            return
+        if typ.is_protocol:
+            return
+        if abstract and not abstract_in_this_class:
+            attrs = ", ".join('"{}"'.format(attr) for attr in sorted(abstract))
+            self.fail("Class {} has abstract attributes {}".format(typ.fullname(), attrs), typ)
+            self.note("If it is meant to be abstract, add 'abc.ABCMeta' as an explicit metaclass",
+                      typ)
 
     def setup_type_promotion(self, defn: ClassDef) -> None:
         """Setup extra, ad-hoc subtyping relationships between classes (promotion).


Test patch:
diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test
--- a/test-data/unit/check-classes.test
+++ b/test-data/unit/check-classes.test
@@ -4377,6 +4377,65 @@ main:4: error: Revealed local types are:
 main:4: error: t: builtins.str
 main:4: error: y: builtins.float
 
+[case testAbstractClasses]
+import a
+import b
+
+[file a.pyi]
+from abc import ABCMeta, abstractmethod
+from typing import Protocol
+
+class A:  # OK, has @abstractmethod
+    @abstractmethod
+    def f(self) -> None:
+        pass
+
+class B(A):  # E: Class a.B has abstract attributes "f"  # N: If it is meant to be abstract, add 'abc.ABCMeta' as an explicit metaclass
+    pass
+
+class C(A, metaclass=ABCMeta):  # OK, has ABCMeta as a metaclass
+    pass
+
+class D(A):  # OK, implements the abstract method
+    def f(self) -> None:
+        pass
+
+class E(Protocol):  # OK, is a protocol
+    @abstractmethod
+    def f(self) -> None:
+        pass
+
+class F(E, Protocol):  # OK, is a protocol
+    pass
+
+[file b.py]
+# All of these are OK because this is not a stub file.
+from abc import ABCMeta, abstractmethod
+from typing import Protocol
+
+class A:
+    @abstractmethod
+    def f(self) -> None:
+        pass
+
+class B(A):
+    pass
+
+class C(A, metaclass=ABCMeta):
+    pass
+
+class D(A):
+    def f(self) -> None:
+        pass
+
+class E(Protocol):
+    @abstractmethod
+    def f(self) -> None:
+        pass
+
+class F(E, Protocol):
+    pass
+
 [case testClassMethodOverride]
 from typing import Callable, Any