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