Repository: wemake-services/wemake-python-styleguide

Issue:
Feature: count async methods as methods inside classes
Here we count how many methods the class has: https://github.com/wemake-services/wemake-python-styleguide/blob/master/wemake_python_styleguide/visitors/ast/complexity/counts.py#L89

After that we can detect its complexity.
But, currently we only count regular `def`s. We need to count `async def`s as well.

What need to be done?
1. We need to create a new node handler is the visitor: `visit_any_function`
2. We need to alias `visit_AsyncFunctionDef`, `visit_FunctionDef` to `visit_any_function`
3. We need to add new unit tests for `async` methods: https://github.com/wemake-services/wemake-python-styleguide/blob/master/tests/test_visitors/test_ast/test_complexity/test_counts/test_method_counts.py




Discussion:
Hi, I will take this issue.

Resolving patch:
diff --git a/wemake_python_styleguide/visitors/ast/complexity/counts.py b/wemake_python_styleguide/visitors/ast/complexity/counts.py
--- a/wemake_python_styleguide/visitors/ast/complexity/counts.py
+++ b/wemake_python_styleguide/visitors/ast/complexity/counts.py
@@ -17,6 +17,7 @@
 
 ConditionNodes = Union[ast.If, ast.While, ast.IfExp]
 ModuleMembers = Union[ast.AsyncFunctionDef, ast.FunctionDef, ast.ClassDef]
+MethodMembers = Union[ast.FunctionDef, ast.AsyncFunctionDef]
 
 
 @alias('visit_module_members', (
@@ -86,6 +87,10 @@ def visit_any_import(self, node: AnyImport) -> None:
         self.generic_visit(node)
 
 
+@alias('visit_any_function', (
+    'visit_FunctionDef',
+    'visit_AsyncFunctionDef',
+))
 class MethodMembersVisitor(BaseNodeVisitor):
     """Counts methods in a single class."""
 
@@ -94,7 +99,7 @@ def __init__(self, *args, **kwargs) -> None:
         super().__init__(*args, **kwargs)
         self._methods: DefaultDict[ast.ClassDef, int] = defaultdict(int)
 
-    def _check_method(self, node: ast.FunctionDef) -> None:
+    def _check_method(self, node: MethodMembers) -> None:
         parent = getattr(node, 'parent', None)
         if isinstance(parent, ast.ClassDef):
             self._methods[parent] += 1
@@ -104,7 +109,7 @@ def _post_visit(self) -> None:
             if count > self.options.max_methods:
                 self.add_violation(TooManyMethodsViolation(text=node.name))
 
-    def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
+    def visit_any_function(self, node: MethodMembers) -> None:
         """
         Counts the number of methods in a single class.
 


Test patch:
diff --git a/tests/test_visitors/test_ast/test_complexity/test_counts/test_method_counts.py b/tests/test_visitors/test_ast/test_complexity/test_counts/test_method_counts.py
--- a/tests/test_visitors/test_ast/test_complexity/test_counts/test_method_counts.py
+++ b/tests/test_visitors/test_ast/test_complexity/test_counts/test_method_counts.py
@@ -13,6 +13,18 @@ def first(): ...
 def second(): ...
 """
 
+module_without_methods_with_async_functions = """
+async def first(): ...
+
+async def second(): ...
+"""
+
+module_without_methods_with_async_and_usual_functions = """
+def first(): ...
+
+async def second(): ...
+"""
+
 class_with_methods = """
 class First(object):
     def method(self): ...
@@ -20,6 +32,20 @@ def method(self): ...
     def method2(self): ...
 """
 
+class_with_async_methods = """
+class First(object):
+    async def method(self): ...
+
+    async def method2(self): ...
+"""
+
+class_with_async_and_usual_methods = """
+class First(object):
+    def method(self): ...
+
+    async def method2(self): ...
+"""
+
 class_with_class_methods = """
 class First(object):
     @classmethod
@@ -29,11 +55,35 @@ def method(cls): ...
     def method2(cls): ...
 """
 
+class_with_async_class_methods = """
+class First(object):
+    @classmethod
+    async def method(cls): ...
+
+    @classmethod
+    async def method2(cls): ...
+"""
+
+class_with_async_and_usual_class_methods = """
+class First(object):
+    @classmethod
+    def method(cls): ...
+
+    @classmethod
+    async def method2(cls): ...
+"""
+
 
 @pytest.mark.parametrize('code', [
     module_without_methods,
+    module_without_methods_with_async_functions,
+    module_without_methods_with_async_and_usual_functions,
     class_with_methods,
+    class_with_async_methods,
+    class_with_async_and_usual_methods,
     class_with_class_methods,
+    class_with_async_class_methods,
+    class_with_async_and_usual_class_methods,
 ])
 def test_method_counts_normal(
     assert_errors, parse_ast_tree, code, default_options,
@@ -49,7 +99,11 @@ def test_method_counts_normal(
 
 @pytest.mark.parametrize('code', [
     class_with_methods,
+    class_with_async_methods,
+    class_with_async_and_usual_methods,
     class_with_class_methods,
+    class_with_async_class_methods,
+    class_with_async_and_usual_class_methods,
 ])
 def test_method_counts_violation(
     assert_errors, parse_ast_tree, code, options,