Repository: wemake-services/wemake-python-styleguide

Issue:
609: Allow __enter__() inside an __enter__()
### What's wrong

One design pattern is to wrap a context manager. It would be nice to avoid WPS609 errors with this code, which seems to require accessing the direct magic methods.

### How it should be

Allow code like:
```
class Foo:
    ...

    def __enter__(self):
        self._conn.__enter__()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self._conn.__exit__(exc_type, exc_value, traceback)
```

I guess the same for aenter/aexit as well.


Discussion:
Yeah, I guess that calling the same magic method inside a magic method is fine.
If it's not taken, then I could work on this.
Just note, that we allow calling all same magic methods. Not just `__enter__`

Resolving patch:
diff --git a/wemake_python_styleguide/visitors/ast/attributes.py b/wemake_python_styleguide/visitors/ast/attributes.py
--- a/wemake_python_styleguide/visitors/ast/attributes.py
+++ b/wemake_python_styleguide/visitors/ast/attributes.py
@@ -3,7 +3,9 @@
 
 from typing_extensions import final
 
+from wemake_python_styleguide.compat.aliases import FunctionNodes
 from wemake_python_styleguide.constants import ALL_MAGIC_METHODS
+from wemake_python_styleguide.logic import nodes
 from wemake_python_styleguide.logic.naming import access
 from wemake_python_styleguide.violations.best_practices import (
     ProtectedAttributeViolation,
@@ -50,6 +52,15 @@ def _check_protected_attribute(self, node: ast.Attribute) -> None:
 
     def _check_magic_attribute(self, node: ast.Attribute) -> None:
         if access.is_magic(node.attr):
+            # If "magic" method being called has the same name as
+            # the enclosing function, then it is a "wrapper" and thus
+            # a "false positive".
+
+            ctx = nodes.get_context(node)
+            if isinstance(ctx, FunctionNodes):
+                if node.attr == ctx.name:
+                    return
+
             if node.attr in ALL_MAGIC_METHODS:
                 self._ensure_attribute_type(
                     node, DirectMagicAttributeAccessViolation,


Test patch:
diff --git a/tests/test_visitors/test_ast/test_attributes/test_magic_attributes.py b/tests/test_visitors/test_ast/test_attributes/test_magic_attributes.py
--- a/tests/test_visitors/test_ast/test_attributes/test_magic_attributes.py
+++ b/tests/test_visitors/test_ast/test_attributes/test_magic_attributes.py
@@ -1,5 +1,6 @@
 import pytest
 
+from wemake_python_styleguide.constants import ALL_MAGIC_METHODS
 from wemake_python_styleguide.violations.oop import (
     DirectMagicAttributeAccessViolation,
 )
@@ -107,6 +108,24 @@ def method(cls):
         super().{0}()
 """
 
+magic_wrapper_method = """
+class Test(object):
+
+    def {0}(cls):
+        self.conn.{0}()
+"""
+
+magic_wrapper_method_inside_stacked_cls = """
+class StackedTest(object):
+
+    def something(cls):
+        class Test(object):
+            def {0}(cls):
+                self.conn.{0}()
+
+        anything()
+"""
+
 
 @pytest.mark.parametrize('attribute', [
     '__truediv__',
@@ -225,7 +244,7 @@ def test_magic_attribute_correct_contexts(
     magic_super_cls_attribute,
     magic_super_cls_method,
 ])
-def test_other_magic_attributs_allowed(
+def test_other_magic_attributes_allowed(
     assert_errors,
     parse_ast_tree,
     code,
@@ -245,3 +264,30 @@ def test_other_magic_attributs_allowed(
     visitor.run()
 
     assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize('attribute', ALL_MAGIC_METHODS)
+@pytest.mark.parametrize('code', [
+    magic_wrapper_method,
+    magic_wrapper_method_inside_stacked_cls,
+])
+def test_happy_little_magic_wrapper_methods(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    attribute,
+    default_options,
+    mode,
+):
+    """
+    Test calling magic methods with the same name.
+
+    Ensure that calling magic method inside magic method with the same
+    name does not trigger any violations.
+    """
+    tree = parse_ast_tree(mode(code.format(attribute)))
+
+    visitor = WrongAttributeVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])