Skip to content

Commit e09ad12

Browse files
committed
Add b024: abstract class with no abstract methods
1 parent 13e2882 commit e09ad12

File tree

4 files changed

+159
-0
lines changed

4 files changed

+159
-0
lines changed

README.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ positives due to similarly named user-defined functions.
154154
the loop, because `late-binding closures are a classic gotcha
155155
<https://docs.python-guide.org/writing/gotchas/#late-binding-closures>`__.
156156

157+
**B024**: Abstract base class with no abstract method. Remember to use @abstractmethod, @abstractclassmethod, and/or @abstractproperty decorators.
158+
157159
Opinionated warnings
158160
~~~~~~~~~~~~~~~~~~~~
159161

@@ -282,6 +284,11 @@ MIT
282284
Change Log
283285
----------
284286

287+
FUTURE
288+
~~~~~~~~~~
289+
* Add B024: abstract base class with no abstract methods (#273)
290+
291+
285292
22.7.1
286293
~~~~~~~~~~
287294

bugbear.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ def visit_ClassDef(self, node):
416416
self.check_for_b903(node)
417417
self.check_for_b018(node)
418418
self.check_for_b021(node)
419+
self.check_for_b024(node)
419420
self.generic_visit(node)
420421

421422
def visit_Try(self, node):
@@ -608,6 +609,39 @@ def check_for_b023(self, loop_node):
608609
if reassigned_in_loop.issuperset(err.vars):
609610
self.errors.append(err)
610611

612+
def check_for_b024(self, node: ast.ClassDef):
613+
"""Check for inheritance from abstract classes in abc and lack of
614+
any methods decorated with abstract*"""
615+
616+
def is_abc_class(value):
617+
abc_names = ("ABC", "ABCMeta")
618+
return (isinstance(value, ast.Name) and value.id in abc_names) or (
619+
isinstance(value, ast.Attribute) and value.attr in abc_names
620+
)
621+
622+
def is_abstract_decorator(expr):
623+
return (isinstance(expr, ast.Name) and expr.id[:8] == "abstract") or (
624+
isinstance(expr, ast.Attribute) and expr.attr[:8] == "abstract"
625+
)
626+
627+
for base in node.bases:
628+
if is_abc_class(base):
629+
break
630+
else:
631+
for keyword in node.keywords:
632+
if keyword.arg == "metaclass" and is_abc_class(keyword.value):
633+
break
634+
else:
635+
return
636+
637+
for stmt in node.body:
638+
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
639+
for expr in stmt.decorator_list:
640+
if is_abstract_decorator(expr):
641+
return
642+
643+
self.errors.append(B024(node.lineno, node.col_offset, vars=(node.name,)))
644+
611645
def _get_assigned_names(self, loop_node):
612646
loop_targets = (ast.For, ast.AsyncFor, ast.comprehension)
613647
for node in children_in_scope(loop_node):
@@ -1139,6 +1173,12 @@ def visit_Lambda(self, node):
11391173
)
11401174

11411175
B023 = Error(message="B023 Function definition does not bind loop variable {!r}.")
1176+
B024 = Error(
1177+
message=(
1178+
"{} is an abstract base class, but it has no abstract methods. Remember to use"
1179+
" @abstractmethod, @abstractclassmethod and/or @abstractproperty decorators."
1180+
)
1181+
)
11421182

11431183
# Warnings disabled by default.
11441184
B901 = Error(

tests/b024.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import abc
2+
import abc as notabc
3+
from abc import ABC, ABCMeta
4+
from abc import abstractmethod
5+
from abc import abstractmethod as abstract
6+
from abc import abstractmethod as abstractaoeuaoeuaoeu
7+
from abc import abstractmethod as notabstract
8+
9+
"""
10+
Should emit:
11+
B024 - on lines 13, 42, 47, 56, 60, 72, 76
12+
"""
13+
14+
15+
class Base_1(ABC): # error
16+
def method(self):
17+
...
18+
19+
20+
class Base_2(ABC):
21+
@abstractmethod
22+
def method(self):
23+
...
24+
25+
26+
class Base_3(ABC):
27+
@abc.abstractmethod
28+
def method(self):
29+
...
30+
31+
32+
class Base_4(ABC):
33+
@notabc.abstractmethod
34+
def method(self):
35+
...
36+
37+
38+
class Base_5(ABC):
39+
@abstract
40+
def method(self):
41+
...
42+
43+
44+
class Base_6(ABC):
45+
@abstractaoeuaoeuaoeu
46+
def method(self):
47+
...
48+
49+
50+
class Base_7(ABC): # error
51+
@notabstract
52+
def method(self):
53+
...
54+
55+
56+
class MetaBase_1(metaclass=ABCMeta): # error
57+
def method(self):
58+
...
59+
60+
61+
class MetaBase_2(metaclass=ABCMeta):
62+
@abstractmethod
63+
def method(self):
64+
...
65+
66+
67+
class abc_Base_1(abc.ABC): # error
68+
def method(self):
69+
...
70+
71+
72+
class abc_Base_2(metaclass=abc.ABCMeta): # error
73+
def method(self):
74+
...
75+
76+
77+
class notabc_Base_1(notabc.ABC): # error
78+
def method(self):
79+
...
80+
81+
82+
class notabc_Base_2(metaclass=notabc.ABCMeta): # error
83+
def method(self):
84+
...
85+
86+
87+
class multi_super_1(notabc.ABC, abc.ABCMeta): # error
88+
def method(self):
89+
...
90+
91+
92+
class multi_super_2(notabc.ABC, metaclass=abc.ABCMeta): # error
93+
def method(self):
94+
...

tests/test_bugbear.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
B021,
3535
B022,
3636
B023,
37+
B024,
3738
B901,
3839
B902,
3940
B903,
@@ -350,6 +351,23 @@ def test_b023(self):
350351
)
351352
self.assertEqual(errors, expected)
352353

354+
def test_b024(self):
355+
filename = Path(__file__).absolute().parent / "b024.py"
356+
bbc = BugBearChecker(filename=str(filename))
357+
errors = list(bbc.run())
358+
expected = self.errors(
359+
B024(13, 0, vars=("Base_1",)),
360+
B024(42, 0, vars=("Base_7",)),
361+
B024(47, 0, vars=("MetaBase_1",)),
362+
B024(56, 0, vars=("abc_Base_1",)),
363+
B024(60, 0, vars=("abc_Base_2",)),
364+
B024(64, 0, vars=("notabc_Base_1",)),
365+
B024(68, 0, vars=("notabc_Base_2",)),
366+
B024(72, 0, vars=("multi_super_1",)),
367+
B024(76, 0, vars=("multi_super_2",)),
368+
)
369+
self.assertEqual(errors, expected)
370+
353371
def test_b901(self):
354372
filename = Path(__file__).absolute().parent / "b901.py"
355373
bbc = BugBearChecker(filename=str(filename))

0 commit comments

Comments
 (0)