Skip to content

Commit

Permalink
Add b024: abstract class with no abstract methods
Browse files Browse the repository at this point in the history
  • Loading branch information
jakkdl committed Aug 18, 2022
1 parent 13e2882 commit 4036ee1
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 0 deletions.
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ positives due to similarly named user-defined functions.
the loop, because `late-binding closures are a classic gotcha
<https://docs.python-guide.org/writing/gotchas/#late-binding-closures>`__.

**B024**: Abstract base class with no abstract method. Remember to use @abstractmethod, @abstractclassmethod, and/or @abstractproperty decorators.

Opinionated warnings
~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -282,6 +284,11 @@ MIT
Change Log
----------

FUTURE
~~~~~~~~~~
* Add B024: abstract base class with no abstract methods (#273)


22.7.1
~~~~~~~~~~

Expand Down
38 changes: 38 additions & 0 deletions bugbear.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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(
Expand Down
91 changes: 91 additions & 0 deletions tests/b024.py
Original file line number Diff line number Diff line change
@@ -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):
...
16 changes: 16 additions & 0 deletions tests/test_bugbear.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
B021,
B022,
B023,
B024,
B901,
B902,
B903,
Expand Down Expand Up @@ -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))
Expand Down

0 comments on commit 4036ee1

Please sign in to comment.