Repository: wemake-services/wemake-python-styleguide

Issue:
Forbid multiple assignment targets for context managers
# Rule request

<!-- Thanks for coming up with a proposal! -->

## Thesis

Code example:

```python
with open('') as first, second:
    ...
```

Quote from `pylint`:

```
confusing-with-statement (W0124):
--
  | Following "as" with another context manager looks like a tuple. Emitted when a with statement component returns multiple values and uses name binding with as only for a part of those values, as in with ctx() as a, b. This can be misleading, since it's not clear if the context manager returns a tuple or if the node without a name binding is another context manager.
```



Discussion:

Resolving patch:
diff --git a/wemake_python_styleguide/violations/consistency.py b/wemake_python_styleguide/violations/consistency.py
--- a/wemake_python_styleguide/violations/consistency.py
+++ b/wemake_python_styleguide/violations/consistency.py
@@ -41,6 +41,7 @@
    MissingSpaceBetweenKeywordAndParenViolation
    WrongConditionalViolation
    ObjectInBaseClassesListViolation
+   TooManyContextManagerAssignmentsViolation
 
 Consistency checks
 ------------------
@@ -61,6 +62,7 @@
 .. autoclass:: MissingSpaceBetweenKeywordAndParenViolation
 .. autoclass:: WrongConditionalViolation
 .. autoclass:: ObjectInBaseClassesListViolation
+.. autoclass:: TooManyContextManagerAssignmentsViolation
 
 """
 
@@ -581,3 +583,31 @@ class SomeClassName(FirstParentClass, SecondParentClass, object): ...
     should_use_text = False
     error_template = 'Founded extra `object` in parent classes list'
     code = 315
+
+
+@final
+class TooManyContextManagerAssignmentsViolation(ASTViolation):
+    """
+    Forbid multiple assignment targets for context managers.
+
+    Reasoning:
+        Following "as" with another context manager looks like a tuple.
+        Emitted when a with statement component returns multiple values
+        and uses name binding with as only for a part of those values,
+        as in with ctx() as a, b.This can be misleading, since it's not
+        clear if the context manager returns a tupleor if the node
+        without a name binding is another context manager.
+
+    Example::
+
+        # Wrong:
+        with open('') as first, second:
+            ...
+
+    .. versionadded:: 0.6.0
+
+    """
+
+    should_use_text = False
+    error_template = 'Found context manager with too many assignments'
+    code = 316
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
@@ -14,6 +14,9 @@
     TooManyMethodsViolation,
     TooManyModuleMembersViolation,
 )
+from wemake_python_styleguide.violations.consistency import (
+    TooManyContextManagerAssignmentsViolation,
+)
 from wemake_python_styleguide.visitors.base import BaseNodeVisitor
 from wemake_python_styleguide.visitors.decorators import alias
 
@@ -227,3 +230,25 @@ def visit_If(self, node: ast.If) -> None:
         """
         self._check_elifs(node)
         self.generic_visit(node)
+
+
+@final
+class ContextManagerVisitor(BaseNodeVisitor):
+    """Checks context managers."""
+
+    def _check_target_assignment(self, node: ast.With):
+        if len(node.items) > 1:
+            self.add_violation(
+                TooManyContextManagerAssignmentsViolation(node),
+            )
+
+    def visit_With(self, node: ast.With) -> None:
+        """
+        Checks the number of assignments for context managers.
+
+        Raises:
+            TooManyContextManagerAssignmentsViolation
+
+        """
+        self._check_target_assignment(node)
+        self.generic_visit(node)
diff --git a/wemake_python_styleguide/visitors/presets/complexity.py b/wemake_python_styleguide/visitors/presets/complexity.py
--- a/wemake_python_styleguide/visitors/presets/complexity.py
+++ b/wemake_python_styleguide/visitors/presets/complexity.py
@@ -20,5 +20,6 @@
     counts.MethodMembersVisitor,
     counts.ConditionsVisitor,
     counts.ElifVisitor,
+    counts.ContextManagerVisitor,
     classes.ClassComplexityVisitor,
 )


Test patch:
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
@@ -78,6 +78,7 @@ def test_noqa_fixture_disabled(absolute_path, all_violations):
         'Z313': 1,
         'Z314': 1,
         'Z315': 1,
+        'Z316': 0,
 
         'Z400': 0,
         'Z401': 0,
diff --git a/tests/test_visitors/test_ast/test_complexity/test_counts/test_context_manager_targets.py b/tests/test_visitors/test_ast/test_complexity/test_counts/test_context_manager_targets.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_complexity/test_counts/test_context_manager_targets.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+
+import pytest
+
+from wemake_python_styleguide.violations.consistency import (
+    TooManyContextManagerAssignmentsViolation,
+)
+from wemake_python_styleguide.visitors.ast.complexity.counts import (
+    ContextManagerVisitor,
+)
+
+incorrect_count = """
+with open('') as first, second:
+    ...
+"""
+
+correct_count = """
+with open('') as first:
+    ...
+"""
+
+composite_assign = """
+with open('first') as first, open('second') as second:
+    ...
+"""
+
+
+@pytest.mark.parametrize('code', [
+    incorrect_count,
+    composite_assign,
+])
+def test_context_manager_multiple_targets(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    default_options,
+):
+    """Testing incorrect count context manager assignment."""
+    tree = parse_ast_tree(code)
+
+    visitor = ContextManagerVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [TooManyContextManagerAssignmentsViolation])
+
+
+def test_context_manager_alone_target(
+    assert_errors,
+    parse_ast_tree,
+    default_options,
+):
+    """Testing correct count context manager assignment."""
+    tree = parse_ast_tree(correct_count)
+
+    visitor = ContextManagerVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])