Skip to content

Commit 0e28577

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

File tree

4 files changed

+161
-0
lines changed

4 files changed

+161
-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: 46 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,45 @@ 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)
620+
and value.attr in abc_names
621+
and isinstance(value.value, ast.Name)
622+
and value.value.id == "abc"
623+
)
624+
625+
def is_abstract_decorator(expr):
626+
return (isinstance(expr, ast.Name) and expr.id[:8] == "abstract") or (
627+
isinstance(expr, ast.Attribute)
628+
and expr.attr[:8] == "abstract"
629+
and isinstance(expr.value, ast.Name)
630+
and expr.value.id == "abc"
631+
)
632+
633+
for base in node.bases:
634+
if is_abc_class(base):
635+
break
636+
else:
637+
for keyword in node.keywords:
638+
if keyword.arg == "metaclass" and is_abc_class(keyword.value):
639+
break
640+
else:
641+
return
642+
643+
for stmt in node.body:
644+
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
645+
for expr in stmt.decorator_list:
646+
if is_abstract_decorator(expr):
647+
return
648+
649+
self.errors.append(B024(node.lineno, node.col_offset, vars=(node.name,)))
650+
611651
def _get_assigned_names(self, loop_node):
612652
loop_targets = (ast.For, ast.AsyncFor, ast.comprehension)
613653
for node in children_in_scope(loop_node):
@@ -1139,6 +1179,12 @@ def visit_Lambda(self, node):
11391179
)
11401180

11411181
B023 = Error(message="B023 Function definition does not bind loop variable {!r}.")
1182+
B024 = Error(
1183+
message=(
1184+
"{} is an abstract base class, but it has no abstract methods. Remember to use"
1185+
" @abstractmethod, @abstractclassmethod and/or @abstractproperty decorators."
1186+
)
1187+
)
11421188

11431189
# Warnings disabled by default.
11441190
B901 = Error(

tests/b024.py

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

tests/test_bugbear.py

Lines changed: 17 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,22 @@ 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(17, 0, vars=("Base_1",)),
360+
B024(34, 0, vars=("Base_4",)),
361+
B024(52, 0, vars=("Base_7",)),
362+
B024(58, 0, vars=("MetaBase_1",)),
363+
B024(69, 0, vars=("abc_Base_1",)),
364+
B024(74, 0, vars=("abc_Base_2",)),
365+
B024(84, 0, vars=("multi_super_1",)),
366+
B024(89, 0, vars=("multi_super_2",)),
367+
)
368+
self.assertEqual(errors, expected)
369+
353370
def test_b901(self):
354371
filename = Path(__file__).absolute().parent / "b901.py"
355372
bbc = BugBearChecker(filename=str(filename))

0 commit comments

Comments
 (0)