Repository: wemake-services/wemake-python-styleguide

Issue:
Forbid kwarg unpacking in class definition
# Rule request

<!-- Thanks for coming up with a proposal! -->

## Thesis

<!-- What exactly do you propose? Please, provide code examples. -->

We need to forbid code like this:

```python
class Some(Base, **kwargs):
    ...
```

But, this code is valid:

```python
class Some(Base, arg=1):
    ...
```

## Reasoning

<!--
Why do you think this is a good idea?

Please, include some strong points and considerations.
-->

Dynamic class generation with unknown values is really bad! 

<!--

  Love wemake-python-styleguide? ❤️
  Do you want your features to be implemented faster?

  Please consider supporting our collective:
👉  https://opencollective.com/wemake-python-styleguide/donate

-->



Discussion:
Hello, @sobolevn Can you assign me this issue? I'd like to contribute. Thanks. 
Thanks a lot!
Thank you, @sobolevn ! Can you please elaborate the issue? It'd be really helpful. 
@StutiV what help do you need? I would love to answer speicific questions 🙂 
@sobolevn  Which file should I be making these changes in?
@StutiV it is probably `ast/classes.py`. Feel free to choose the best visitor for the job.
https://github.com/wemake-services/wemake-python-styleguide/blob/master/wemake_python_styleguide/visitors/ast/classes.py

Probably `WrongClassVisitor`
@sobolevn can you assign this issue for me?
@StakanAgan done! Thanks!
@sobolevn do you have an example full example where a class receives parameters, please? Does the class need to have a metaclass or something like?
For example, this is fine:

```python
from pydantic import BaseModel, Extra

class Model(BaseModel, extra=Extra.forbid):
    a: str
```

This is not (but will work the same way):

```python
from pydantic import BaseModel, Extra

kw = {'extra': Extra.forbid}
class Model(BaseModel, **kw):
    a: str
```

Source: https://pydantic-docs.helpmanual.io/usage/model_config/

Resolving patch:
diff --git a/wemake_python_styleguide/presets/types/tree.py b/wemake_python_styleguide/presets/types/tree.py
--- a/wemake_python_styleguide/presets/types/tree.py
+++ b/wemake_python_styleguide/presets/types/tree.py
@@ -89,7 +89,8 @@
 
     iterables.IterableUnpackingVisitor,
 
-    classes.WrongClassVisitor,
+    classes.WrongClassDefVisitor,
+    classes.WrongClassBodyVisitor,
     classes.WrongMethodVisitor,
     classes.WrongSlotsVisitor,
     classes.ClassAttributeVisitor,
diff --git a/wemake_python_styleguide/violations/best_practices.py b/wemake_python_styleguide/violations/best_practices.py
--- a/wemake_python_styleguide/violations/best_practices.py
+++ b/wemake_python_styleguide/violations/best_practices.py
@@ -86,6 +86,7 @@
    BareRaiseViolation
    RedundantEnumerateViolation
    RaiseFromItselfViolation
+   KwargsUnpackingInClassDefinitionViolation
 
 Best practices
 --------------
@@ -160,6 +161,7 @@
 .. autoclass:: BareRaiseViolation
 .. autoclass:: RedundantEnumerateViolation
 .. autoclass:: RaiseFromItselfViolation
+.. autoclass:: KwargsUnpackingInClassDefinitionViolation
 
 """
 
@@ -2672,3 +2674,35 @@ class RaiseFromItselfViolation(ASTViolation):
 
     error_template = 'Found error raising from itself'
     code = 469
+
+
+@final
+class KwargsUnpackingInClassDefinitionViolation(ASTViolation):
+    """
+    Forbid kwarg unpacking in class definition.
+
+    Reasoning:
+        Dynamic class generation with unknown arguments is bad because it
+        creates too much flexibility and possibilities for errors.
+        It also limits the typechecking capabilities.
+
+    Solution:
+        Use keyword arguments noramlly without unpacking them.
+
+    Example::
+
+        # Correct:
+        class MyClass(argument='argument'):
+            ...
+
+        # Wrong:
+        arguments = {'argument': 'argument'}
+        class MyClass(**arguments):
+            ...
+
+    .. versionadded:: 0.16.0
+
+    """
+
+    error_template = 'Found kwarg unpacking in class definition'
+    code = 470
diff --git a/wemake_python_styleguide/visitors/ast/classes.py b/wemake_python_styleguide/visitors/ast/classes.py
--- a/wemake_python_styleguide/visitors/ast/classes.py
+++ b/wemake_python_styleguide/visitors/ast/classes.py
@@ -22,25 +22,18 @@
 
 
 @final
-class WrongClassVisitor(base.BaseNodeVisitor):
+class WrongClassDefVisitor(base.BaseNodeVisitor):
     """
-    This class is responsible for restricting some ``class`` anti-patterns.
+    This class is responsible for restricting some ``class`` def anti-patterns.
 
     Here we check for stylistic issues and design patterns.
     """
 
-    _allowed_body_nodes: ClassVar[types.AnyNodes] = (
-        *FunctionNodes,
-        ast.ClassDef,  # we allow some nested classes
-        *AssignNodes,  # fields and annotations
-    )
-
     def visit_ClassDef(self, node: ast.ClassDef) -> None:
         """Checking class definitions."""
         self._check_base_classes_count(node)
         self._check_base_classes(node)
-        self._check_wrong_body_nodes(node)
-        self._check_getters_setters_methods(node)
+        self._check_kwargs_unpacking(node)
         self.generic_visit(node)
 
     def _check_base_classes_count(self, node: ast.ClassDef) -> None:
@@ -57,6 +50,27 @@ def _check_base_classes(self, node: ast.ClassDef) -> None:
 
             self._check_base_classes_rules(node, base_name)
 
+    def _is_correct_base_class(self, base_class: ast.AST) -> bool:
+        if isinstance(base_class, ast.Name):
+            return True
+        elif isinstance(base_class, ast.Attribute):
+            return all(
+                isinstance(sub_node, (ast.Name, ast.Attribute))
+                for sub_node in attributes.parts(base_class)
+            )
+        elif isinstance(base_class, ast.Subscript):
+            parts = list(attributes.parts(base_class))
+            subscripts = list(filter(
+                lambda part: isinstance(part, ast.Subscript), parts,
+            ))
+            correct_items = all(
+                isinstance(sub_node, (ast.Name, ast.Attribute, ast.Subscript))
+                for sub_node in parts
+            )
+
+            return len(subscripts) == 1 and correct_items
+        return False
+
     def _check_base_classes_rules(
         self,
         node: ast.ClassDef,
@@ -77,6 +91,34 @@ def _check_base_classes_rules(
                 oop.BuiltinSubclassViolation(node, text=id_attr),
             )
 
+    def _check_kwargs_unpacking(self, node: ast.ClassDef) -> None:
+        for keyword in node.keywords:
+            if keyword.arg is None:
+                self.add_violation(
+                    bp.KwargsUnpackingInClassDefinitionViolation(node),
+                )
+
+
+@final
+class WrongClassBodyVisitor(base.BaseNodeVisitor):
+    """
+    This class is responsible for restricting some ``class`` body anti-patterns.
+
+    Here we check for stylistic issues and design patterns.
+    """
+
+    _allowed_body_nodes: ClassVar[types.AnyNodes] = (
+        *FunctionNodes,
+        ast.ClassDef,  # we allow some nested classes
+        *AssignNodes,  # fields and annotations
+    )
+
+    def visit_ClassDef(self, node: ast.ClassDef) -> None:
+        """Checking class definitions."""
+        self._check_wrong_body_nodes(node)
+        self._check_getters_setters_methods(node)
+        self.generic_visit(node)
+
     def _check_wrong_body_nodes(self, node: ast.ClassDef) -> None:
         for sub_node in node.body:
             if isinstance(sub_node, self._allowed_body_nodes):
@@ -85,27 +127,6 @@ def _check_wrong_body_nodes(self, node: ast.ClassDef) -> None:
                 continue
             self.add_violation(oop.WrongClassBodyContentViolation(sub_node))
 
-    def _is_correct_base_class(self, base_class: ast.AST) -> bool:
-        if isinstance(base_class, ast.Name):
-            return True
-        elif isinstance(base_class, ast.Attribute):
-            return all(
-                isinstance(sub_node, (ast.Name, ast.Attribute))
-                for sub_node in attributes.parts(base_class)
-            )
-        elif isinstance(base_class, ast.Subscript):
-            parts = list(attributes.parts(base_class))
-            subscripts = list(filter(
-                lambda part: isinstance(part, ast.Subscript), parts,
-            ))
-            correct_items = all(
-                isinstance(sub_node, (ast.Name, ast.Attribute, ast.Subscript))
-                for sub_node in parts
-            )
-
-            return len(subscripts) == 1 and correct_items
-        return False
-
     def _check_getters_setters_methods(self, node: ast.ClassDef) -> None:
         class_attributes, instance_attributes = classes.get_attributes(
             node,


Test patch:
diff --git a/tests/fixtures/noqa/noqa.py b/tests/fixtures/noqa/noqa.py
--- a/tests/fixtures/noqa/noqa.py
+++ b/tests/fixtures/noqa/noqa.py
@@ -806,3 +806,7 @@ def bare_raise_function():
 except TypeError as raise_from_itself:
     my_print(raise_from_itself)
     raise raise_from_itself from raise_from_itself  # noqa: WPS469
+
+
+class TestClass(object, **{}):  # noqa: WPS470
+    """Docs."""
diff --git a/tests/test_checker/test_noqa.py b/tests/test_checker/test_noqa.py
--- a/tests/test_checker/test_noqa.py
+++ b/tests/test_checker/test_noqa.py
@@ -249,6 +249,7 @@
     'WPS467': 1,
     'WPS468': 2,
     'WPS469': 1,
+    'WPS470': 1,
 
     'WPS500': 1,
     'WPS501': 1,
diff --git a/tests/test_visitors/test_ast/test_classes/test_base_classes/test_base_builtin_classes.py b/tests/test_visitors/test_ast/test_classes/test_base_classes/test_base_builtin_classes.py
--- a/tests/test_visitors/test_ast/test_classes/test_base_classes/test_base_builtin_classes.py
+++ b/tests/test_visitors/test_ast/test_classes/test_base_classes/test_base_builtin_classes.py
@@ -1,7 +1,7 @@
 import pytest
 
 from wemake_python_styleguide.violations.oop import BuiltinSubclassViolation
-from wemake_python_styleguide.visitors.ast.classes import WrongClassVisitor
+from wemake_python_styleguide.visitors.ast.classes import WrongClassDefVisitor
 
 class_with_base = """
 class TestClass({0}):
@@ -26,7 +26,7 @@ def test_builtin_subclass(
     """Testing that it is not possible to subclass builtins."""
     tree = parse_ast_tree(class_with_base.format(super_class))
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassDefVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [BuiltinSubclassViolation])
@@ -47,7 +47,7 @@ def test_regular_subclass(
     """Testing that it is possible to subclass regulars."""
     tree = parse_ast_tree(class_with_base.format(super_class))
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassDefVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [])
diff --git a/tests/test_visitors/test_ast/test_classes/test_base_classes/test_base_exception_base_class.py b/tests/test_visitors/test_ast/test_classes/test_base_classes/test_base_exception_base_class.py
--- a/tests/test_visitors/test_ast/test_classes/test_base_classes/test_base_exception_base_class.py
+++ b/tests/test_visitors/test_ast/test_classes/test_base_classes/test_base_exception_base_class.py
@@ -1,7 +1,7 @@
 from wemake_python_styleguide.violations.best_practices import (
     BaseExceptionSubclassViolation,
 )
-from wemake_python_styleguide.visitors.ast.classes import WrongClassVisitor
+from wemake_python_styleguide.visitors.ast.classes import WrongClassDefVisitor
 
 class_with_base = """
 class Meta({0}):
@@ -17,7 +17,7 @@ def test_base_exception_subclass(
     """Testing that it is not possible to subclass `BaseException`."""
     tree = parse_ast_tree(class_with_base.format('BaseException'))
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassDefVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [BaseExceptionSubclassViolation])
diff --git a/tests/test_visitors/test_ast/test_classes/test_base_classes/test_class_definition_arguments.py b/tests/test_visitors/test_ast/test_classes/test_base_classes/test_class_definition_arguments.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_classes/test_base_classes/test_class_definition_arguments.py
@@ -0,0 +1,67 @@
+import pytest
+
+from wemake_python_styleguide.violations.best_practices import (
+    KwargsUnpackingInClassDefinitionViolation,
+)
+from wemake_python_styleguide.visitors.ast.classes import WrongClassDefVisitor
+
+class_definition_with_unpacking_one = """
+kwargs = {'some_arg': 'some_arg'}
+class TestClass(object, **kwargs):
+    '''Docs.'''
+"""
+
+class_definition_with_unpacking_two = """
+class TestClass(object, **{}):
+    '''Docs.'''
+"""  # noqa: P103
+
+class_definition_with_unpacking_and_arguments = """
+kwargs = {'another_arg': 'another_arg'}
+class TestClass(object, some_arg='some_arg', **kwargs):
+    '''Docs.'''
+"""
+
+
+@pytest.mark.parametrize('code', [
+    class_definition_with_unpacking_one,
+    class_definition_with_unpacking_two,
+    class_definition_with_unpacking_and_arguments,
+])
+def test_kwargs_unpacking_violation(
+    assert_errors,
+    parse_ast_tree,
+    default_options,
+    code,
+):
+    """Testing that it is not possible to unpack values in class definition."""
+    tree = parse_ast_tree(code)
+
+    visitor = WrongClassDefVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [KwargsUnpackingInClassDefinitionViolation])
+
+
+class_definition_with_keyword_arg = """
+class TestClass(object, some_arg='some_arg'):
+    '''Docs.'''
+"""
+
+
+@pytest.mark.parametrize('code', [
+    class_definition_with_keyword_arg,
+])
+def test_kwargs_unpacking_violation_except(
+    assert_errors,
+    parse_ast_tree,
+    default_options,
+    code,
+):
+    """Testing that is possible to pass kw arguments in class definition."""
+    tree = parse_ast_tree(code)
+
+    visitor = WrongClassDefVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])
diff --git a/tests/test_visitors/test_ast/test_classes/test_base_classes/test_expression_base_class.py b/tests/test_visitors/test_ast/test_classes/test_base_classes/test_expression_base_class.py
--- a/tests/test_visitors/test_ast/test_classes/test_base_classes/test_expression_base_class.py
+++ b/tests/test_visitors/test_ast/test_classes/test_base_classes/test_expression_base_class.py
@@ -1,7 +1,7 @@
 import pytest
 
 from wemake_python_styleguide.violations.oop import WrongBaseClassViolation
-from wemake_python_styleguide.visitors.ast.classes import WrongClassVisitor
+from wemake_python_styleguide.visitors.ast.classes import WrongClassDefVisitor
 
 class_with_base = """
 class Meta({0}):
@@ -34,7 +34,7 @@ def test_base_class_expression(
     """Testing that it is not possible to use any incorrect nodes as bases."""
     tree = parse_ast_tree(class_with_base.format(base))
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassDefVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [WrongBaseClassViolation])
@@ -70,7 +70,7 @@ def test_correct_base_classes(
     """Testing that it is possible to use correct nodes as bases."""
     tree = parse_ast_tree(class_with_base.format(base))
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassDefVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [])
diff --git a/tests/test_visitors/test_ast/test_classes/test_base_classes/test_object_as_parent.py b/tests/test_visitors/test_ast/test_classes/test_base_classes/test_object_as_parent.py
--- a/tests/test_visitors/test_ast/test_classes/test_base_classes/test_object_as_parent.py
+++ b/tests/test_visitors/test_ast/test_classes/test_base_classes/test_object_as_parent.py
@@ -3,7 +3,7 @@
 from wemake_python_styleguide.violations.consistency import (
     ObjectInBaseClassesListViolation,
 )
-from wemake_python_styleguide.visitors.ast.classes import WrongClassVisitor
+from wemake_python_styleguide.visitors.ast.classes import WrongClassDefVisitor
 
 extra_object = """
 class TestClassName(FirstName, SecondName, object):
@@ -33,7 +33,7 @@ def test_wrong_class_definition_multiple_parent(
     """Testing wrong class definition."""
     tree = parse_ast_tree(code)
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassDefVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [ObjectInBaseClassesListViolation])
@@ -52,7 +52,7 @@ def test_correct_class_definitions(
     """Testing correct class definition with single parent."""
     tree = parse_ast_tree(code)
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassDefVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [])
diff --git a/tests/test_visitors/test_ast/test_classes/test_base_classes/test_object_base_class.py b/tests/test_visitors/test_ast/test_classes/test_base_classes/test_object_base_class.py
--- a/tests/test_visitors/test_ast/test_classes/test_base_classes/test_object_base_class.py
+++ b/tests/test_visitors/test_ast/test_classes/test_base_classes/test_object_base_class.py
@@ -3,7 +3,7 @@
 from wemake_python_styleguide.violations.consistency import (
     RequiredBaseClassViolation,
 )
-from wemake_python_styleguide.visitors.ast.classes import WrongClassVisitor
+from wemake_python_styleguide.visitors.ast.classes import WrongClassDefVisitor
 
 class_without_base = """
 class Meta:
@@ -57,7 +57,7 @@ def test_wrong_base_class(
     """Testing that not using explicit base class is forbidden."""
     tree = parse_ast_tree(code)
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassDefVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [RequiredBaseClassViolation])
@@ -85,7 +85,7 @@ def test_regular_base_classes(
     """Testing that regular base classes work."""
     tree = parse_ast_tree(code.format(base))
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassDefVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [])
diff --git a/tests/test_visitors/test_ast/test_classes/test_getter_setter.py b/tests/test_visitors/test_ast/test_classes/test_getter_setter.py
--- a/tests/test_visitors/test_ast/test_classes/test_getter_setter.py
+++ b/tests/test_visitors/test_ast/test_classes/test_getter_setter.py
@@ -3,7 +3,7 @@
 from wemake_python_styleguide.violations.oop import (
     UnpythonicGetterSetterViolation,
 )
-from wemake_python_styleguide.visitors.ast.classes import WrongClassVisitor
+from wemake_python_styleguide.visitors.ast.classes import WrongClassBodyVisitor
 
 module_getter_and_setter = """
 attribute = 1
@@ -189,7 +189,7 @@ def test_valid_getter_and_setter(
     """Testing that correct usage of getter/setter is allowed."""
     tree = parse_ast_tree(mode(code))
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassBodyVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [])
@@ -209,7 +209,7 @@ def test_invalid_getter_and_setter(
     """Testing that wrong use of getter/setter is prohibited."""
     tree = parse_ast_tree(mode(code))
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassBodyVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [
@@ -245,7 +245,7 @@ def test_nonmatching_instance(
     )
     tree = parse_ast_tree(mode(test_instance))
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassBodyVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [])
@@ -287,7 +287,7 @@ def test_instance_getter_setter(
     )
     tree = parse_ast_tree(mode(test_instance))
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassBodyVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [UnpythonicGetterSetterViolation])
@@ -320,7 +320,7 @@ def test_nonmatching_class(
     )
     tree = parse_ast_tree(mode(test_instance))
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassBodyVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [])
@@ -361,7 +361,7 @@ def test_class_attributes_getter_setter(
     )
     tree = parse_ast_tree(mode(test_instance))
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassBodyVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [UnpythonicGetterSetterViolation])
@@ -386,7 +386,7 @@ def test_class_mixed(
     test_instance = class_mixed.format(access, first, second, third)
     tree = parse_ast_tree(mode(test_instance))
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassBodyVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [])
diff --git a/tests/test_visitors/test_ast/test_classes/test_wrong_class_body.py b/tests/test_visitors/test_ast/test_classes/test_wrong_class_body.py
--- a/tests/test_visitors/test_ast/test_classes/test_wrong_class_body.py
+++ b/tests/test_visitors/test_ast/test_classes/test_wrong_class_body.py
@@ -3,7 +3,7 @@
 from wemake_python_styleguide.violations.oop import (
     WrongClassBodyContentViolation,
 )
-from wemake_python_styleguide.visitors.ast.classes import WrongClassVisitor
+from wemake_python_styleguide.visitors.ast.classes import WrongClassBodyVisitor
 
 class_body_template = """
 class ClassWithBody(object):
@@ -30,7 +30,7 @@ def test_incorrect_body_items(
     """Testing that incorrect body nodes are prohibited."""
     tree = parse_ast_tree(class_body_template.format(code))
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassBodyVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [WrongClassBodyContentViolation])
@@ -56,7 +56,7 @@ def test_body_correct_items(
     """Testing correct body items are allowed."""
     tree = parse_ast_tree(mode(class_body_template.format(code)))
 
-    visitor = WrongClassVisitor(default_options, tree=tree)
+    visitor = WrongClassBodyVisitor(default_options, tree=tree)
     visitor.run()
 
     assert_errors(visitor, [])