From 4036ee109c2db9520172150e4005b47a7a742727 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Wed, 17 Aug 2022 14:25:06 +0200
Subject: [PATCH] Add b024: abstract class with no abstract methods
---
README.rst | 7 ++++
bugbear.py | 38 ++++++++++++++++++
tests/b024.py | 91 +++++++++++++++++++++++++++++++++++++++++++
tests/test_bugbear.py | 16 ++++++++
4 files changed, 152 insertions(+)
create mode 100644 tests/b024.py
diff --git a/README.rst b/README.rst
index 6005c41..b5873ce 100644
--- a/README.rst
+++ b/README.rst
@@ -154,6 +154,8 @@ positives due to similarly named user-defined functions.
the loop, because `late-binding closures are a classic gotcha
`__.
+**B024**: Abstract base class with no abstract method. Remember to use @abstractmethod, @abstractclassmethod, and/or @abstractproperty decorators.
+
Opinionated warnings
~~~~~~~~~~~~~~~~~~~~
@@ -282,6 +284,11 @@ MIT
Change Log
----------
+FUTURE
+~~~~~~~~~~
+* Add B024: abstract base class with no abstract methods (#273)
+
+
22.7.1
~~~~~~~~~~
diff --git a/bugbear.py b/bugbear.py
index dfa54a2..8fcf171 100644
--- a/bugbear.py
+++ b/bugbear.py
@@ -416,6 +416,7 @@ def visit_ClassDef(self, node):
self.check_for_b903(node)
self.check_for_b018(node)
self.check_for_b021(node)
+ self.check_for_b024(node)
self.generic_visit(node)
def visit_Try(self, node):
@@ -608,6 +609,37 @@ def check_for_b023(self, loop_node):
if reassigned_in_loop.issuperset(err.vars):
self.errors.append(err)
+ def check_for_b024(self, node: ast.ClassDef):
+ """Check for inheritance from abstract classes in abc and lack of
+ any methods decorated with abstract*"""
+
+ def is_abc_class(value):
+ if isinstance(value, ast.keyword):
+ return value.arg == "metaclass" and is_abc_class(value.value)
+ abc_names = ("ABC", "ABCMeta")
+ return (isinstance(value, ast.Name) and value.id in abc_names) or (
+ isinstance(value, ast.Attribute)
+ and value.attr in abc_names
+ and isinstance(value.value, ast.Name)
+ and value.value.id == "abc"
+ )
+
+ def is_abstract_decorator(expr):
+ return (isinstance(expr, ast.Name) and expr.id[:8] == "abstract") or (
+ isinstance(expr, ast.Attribute) and expr.attr[:8] == "abstract"
+ )
+
+ if not any(map(is_abc_class, (*node.bases, *node.keywords))):
+ return
+
+ for stmt in node.body:
+ if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)) and any(
+ map(is_abstract_decorator, stmt.decorator_list)
+ ):
+ return
+
+ self.errors.append(B024(node.lineno, node.col_offset, vars=(node.name,)))
+
def _get_assigned_names(self, loop_node):
loop_targets = (ast.For, ast.AsyncFor, ast.comprehension)
for node in children_in_scope(loop_node):
@@ -1139,6 +1171,12 @@ def visit_Lambda(self, node):
)
B023 = Error(message="B023 Function definition does not bind loop variable {!r}.")
+B024 = Error(
+ message=(
+ "{} is an abstract base class, but it has no abstract methods. Remember to use"
+ " @abstractmethod, @abstractclassmethod and/or @abstractproperty decorators."
+ )
+)
# Warnings disabled by default.
B901 = Error(
diff --git a/tests/b024.py b/tests/b024.py
new file mode 100644
index 0000000..5b23bb2
--- /dev/null
+++ b/tests/b024.py
@@ -0,0 +1,91 @@
+import abc
+import abc as notabc
+from abc import ABC, ABCMeta
+from abc import abstractmethod
+from abc import abstractmethod as abstract
+from abc import abstractmethod as abstractaoeuaoeuaoeu
+from abc import abstractmethod as notabstract
+
+import foo
+
+"""
+Should emit:
+B024 - on lines 17, 34, 52, 58, 69, 74, 84, 89
+"""
+
+
+class Base_1(ABC): # error
+ def method(self):
+ ...
+
+
+class Base_2(ABC):
+ @abstractmethod
+ def method(self):
+ ...
+
+
+class Base_3(ABC):
+ @abc.abstractmethod
+ def method(self):
+ ...
+
+
+class Base_4(ABC):
+ @notabc.abstractmethod
+ def method(self):
+ ...
+
+
+class Base_5(ABC):
+ @abstract
+ def method(self):
+ ...
+
+
+class Base_6(ABC):
+ @abstractaoeuaoeuaoeu
+ def method(self):
+ ...
+
+
+class Base_7(ABC): # error
+ @notabstract
+ def method(self):
+ ...
+
+
+class MetaBase_1(metaclass=ABCMeta): # error
+ def method(self):
+ ...
+
+
+class MetaBase_2(metaclass=ABCMeta):
+ @abstractmethod
+ def method(self):
+ ...
+
+
+class abc_Base_1(abc.ABC): # error
+ def method(self):
+ ...
+
+
+class abc_Base_2(metaclass=abc.ABCMeta): # error
+ def method(self):
+ ...
+
+
+class notabc_Base_1(notabc.ABC): # safe
+ def method(self):
+ ...
+
+
+class multi_super_1(notabc.ABC, abc.ABCMeta): # error
+ def method(self):
+ ...
+
+
+class multi_super_2(notabc.ABC, metaclass=abc.ABCMeta): # error
+ def method(self):
+ ...
diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py
index 750e96f..5db013b 100644
--- a/tests/test_bugbear.py
+++ b/tests/test_bugbear.py
@@ -34,6 +34,7 @@
B021,
B022,
B023,
+ B024,
B901,
B902,
B903,
@@ -350,6 +351,21 @@ def test_b023(self):
)
self.assertEqual(errors, expected)
+ def test_b024(self):
+ filename = Path(__file__).absolute().parent / "b024.py"
+ bbc = BugBearChecker(filename=str(filename))
+ errors = list(bbc.run())
+ expected = self.errors(
+ B024(17, 0, vars=("Base_1",)),
+ B024(52, 0, vars=("Base_7",)),
+ B024(58, 0, vars=("MetaBase_1",)),
+ B024(69, 0, vars=("abc_Base_1",)),
+ B024(74, 0, vars=("abc_Base_2",)),
+ B024(84, 0, vars=("multi_super_1",)),
+ B024(89, 0, vars=("multi_super_2",)),
+ )
+ self.assertEqual(errors, expected)
+
def test_b901(self):
filename = Path(__file__).absolute().parent / "b901.py"
bbc = BugBearChecker(filename=str(filename))