Skip to content

Commit 881f054

Browse files
authored
Merge pull request #274 from jakkdl/abstract_class_no_abstract_methods
Add b024: abstract class with no abstract methods
2 parents 13e2882 + 4036ee1 commit 881f054

File tree

4 files changed

+152
-0
lines changed

4 files changed

+152
-0
lines changed

README.rst

+7
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

+38
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,37 @@ 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+
if isinstance(value, ast.keyword):
618+
return value.arg == "metaclass" and is_abc_class(value.value)
619+
abc_names = ("ABC", "ABCMeta")
620+
return (isinstance(value, ast.Name) and value.id in abc_names) or (
621+
isinstance(value, ast.Attribute)
622+
and value.attr in abc_names
623+
and isinstance(value.value, ast.Name)
624+
and value.value.id == "abc"
625+
)
626+
627+
def is_abstract_decorator(expr):
628+
return (isinstance(expr, ast.Name) and expr.id[:8] == "abstract") or (
629+
isinstance(expr, ast.Attribute) and expr.attr[:8] == "abstract"
630+
)
631+
632+
if not any(map(is_abc_class, (*node.bases, *node.keywords))):
633+
return
634+
635+
for stmt in node.body:
636+
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)) and any(
637+
map(is_abstract_decorator, stmt.decorator_list)
638+
):
639+
return
640+
641+
self.errors.append(B024(node.lineno, node.col_offset, vars=(node.name,)))
642+
611643
def _get_assigned_names(self, loop_node):
612644
loop_targets = (ast.For, ast.AsyncFor, ast.comprehension)
613645
for node in children_in_scope(loop_node):
@@ -1139,6 +1171,12 @@ def visit_Lambda(self, node):
11391171
)
11401172

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

11431181
# Warnings disabled by default.
11441182
B901 = Error(

tests/b024.py

+91
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, 34, 52, 58, 69, 74, 84, 89
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):
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

+16
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,21 @@ 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(52, 0, vars=("Base_7",)),
361+
B024(58, 0, vars=("MetaBase_1",)),
362+
B024(69, 0, vars=("abc_Base_1",)),
363+
B024(74, 0, vars=("abc_Base_2",)),
364+
B024(84, 0, vars=("multi_super_1",)),
365+
B024(89, 0, vars=("multi_super_2",)),
366+
)
367+
self.assertEqual(errors, expected)
368+
353369
def test_b901(self):
354370
filename = Path(__file__).absolute().parent / "b901.py"
355371
bbc = BugBearChecker(filename=str(filename))

0 commit comments

Comments
 (0)