@@ -418,7 +418,7 @@ def visit_ClassDef(self, node):
418418 self .check_for_b903 (node )
419419 self .check_for_b018 (node )
420420 self .check_for_b021 (node )
421- self .check_for_b024 (node )
421+ self .check_for_b024_and_b027 (node )
422422 self .generic_visit (node )
423423
424424 def visit_Try (self , node ):
@@ -612,17 +612,19 @@ def check_for_b023(self, loop_node):
612612 if reassigned_in_loop .issuperset (err .vars ):
613613 self .errors .append (err )
614614
615- def check_for_b024 (self , node : ast .ClassDef ):
615+ def check_for_b024_and_b027 (self , node : ast .ClassDef ): # noqa: C901
616616 """Check for inheritance from abstract classes in abc and lack of
617617 any methods decorated with abstract*"""
618618
619- def is_abc_class (value ):
619+ def is_abc_class (value , name = "ABC" ):
620+ # class foo(metaclass = [abc.]ABCMeta)
620621 if isinstance (value , ast .keyword ):
621- return value .arg == "metaclass" and is_abc_class (value .value )
622- abc_names = ("ABC" , "ABCMeta" )
623- return (isinstance (value , ast .Name ) and value .id in abc_names ) or (
622+ return value .arg == "metaclass" and is_abc_class (value .value , "ABCMeta" )
623+ # class foo(ABC)
624+ # class foo(abc.ABC)
625+ return (isinstance (value , ast .Name ) and value .id == name ) or (
624626 isinstance (value , ast .Attribute )
625- and value .attr in abc_names
627+ and value .attr == name
626628 and isinstance (value .value , ast .Name )
627629 and value .value .id == "abc"
628630 )
@@ -632,16 +634,56 @@ def is_abstract_decorator(expr):
632634 isinstance (expr , ast .Attribute ) and expr .attr [:8 ] == "abstract"
633635 )
634636
637+ def empty_body (body ) -> bool :
638+ def is_str_or_ellipsis (node ):
639+ # ast.Ellipsis and ast.Str used in python<3.8
640+ return isinstance (node , (ast .Ellipsis , ast .Str )) or (
641+ isinstance (node , ast .Constant )
642+ and (node .value is Ellipsis or isinstance (node .value , str ))
643+ )
644+
645+ # Function body consist solely of `pass`, `...`, and/or (doc)string literals
646+ return all (
647+ isinstance (stmt , ast .Pass )
648+ or (isinstance (stmt , ast .Expr ) and is_str_or_ellipsis (stmt .value ))
649+ for stmt in body
650+ )
651+
652+ # don't check multiple inheritance
653+ # https://github.com/PyCQA/flake8-bugbear/issues/277
654+ if len (node .bases ) + len (node .keywords ) > 1 :
655+ return
656+
657+ # only check abstract classes
635658 if not any (map (is_abc_class , (* node .bases , * node .keywords ))):
636659 return
637660
661+ has_abstract_method = False
662+
638663 for stmt in node .body :
639- if isinstance (stmt , (ast .FunctionDef , ast .AsyncFunctionDef )) and any (
664+ # https://github.com/PyCQA/flake8-bugbear/issues/293
665+ # Ignore abc's that declares a class attribute that must be set
666+ if isinstance (stmt , (ast .AnnAssign , ast .Assign )):
667+ has_abstract_method = True
668+ continue
669+
670+ # only check function defs
671+ if not isinstance (stmt , (ast .FunctionDef , ast .AsyncFunctionDef )):
672+ continue
673+
674+ has_abstract_decorator = any (
640675 map (is_abstract_decorator , stmt .decorator_list )
641- ):
642- return
676+ )
677+
678+ has_abstract_method |= has_abstract_decorator
679+
680+ if not has_abstract_decorator and empty_body (stmt .body ):
681+ self .errors .append (
682+ B027 (stmt .lineno , stmt .col_offset , vars = (stmt .name ,))
683+ )
643684
644- self .errors .append (B024 (node .lineno , node .col_offset , vars = (node .name ,)))
685+ if not has_abstract_method :
686+ self .errors .append (B024 (node .lineno , node .col_offset , vars = (node .name ,)))
645687
646688 def check_for_b026 (self , call : ast .Call ):
647689 if not call .keywords :
@@ -1211,8 +1253,8 @@ def visit_Lambda(self, node):
12111253B024 = Error (
12121254 message = (
12131255 "B024 {} is an abstract base class, but it has no abstract methods. Remember to"
1214- " use @abstractmethod, @abstractclassmethod and/or @abstractproperty "
1215- " decorators ."
1256+ " use the @abstractmethod decorator, potentially in conjunction with "
1257+ " @classmethod, @property and/or @staticmethod ."
12161258 )
12171259)
12181260B025 = Error (
@@ -1229,6 +1271,12 @@ def visit_Lambda(self, node):
12291271 "surprise and mislead readers."
12301272 )
12311273)
1274+ B027 = Error (
1275+ message = (
1276+ "B027 {} is an empty method in an abstract base class, but has no abstract"
1277+ " decorator. Consider adding @abstractmethod."
1278+ )
1279+ )
12321280
12331281# Warnings disabled by default.
12341282B901 = Error (
0 commit comments