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:
codecov[bot]: # [Codecov](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/2529?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services) Report
> Merging [#2529](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/2529?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services) (fdeb409) into [master](https://codecov.io/gh/wemake-services/wemake-python-styleguide/commit/4c02204d845eca1c96e989b2033999098f96f543?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services) (4c02204) will **not change** coverage.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff            @@
##            master     #2529   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          121       121           
  Lines         6471      6486   +15     
  Branches      1453      1456    +3     
=========================================
+ Hits          6471      6486   +15     
```


| [Impacted Files](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/2529?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services) | Coverage Δ | |
|---|---|---|
| [...emake\_python\_styleguide/visitors/ast/attributes.py](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/2529/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services#diff-d2VtYWtlX3B5dGhvbl9zdHlsZWd1aWRlL3Zpc2l0b3JzL2FzdC9hdHRyaWJ1dGVzLnB5) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services)

lensvol: @sobolevn I've rewritten it using context, thanks for the suggestion!

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e2e99c24..9f405738 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,7 +19,7 @@ Semantic versioning in our case means:
 
 ## WIP
 
-## Features
+### Features
 
 - Bump `flake8` to version `5.x`
 - Bump `flake8-bandit` to version `^4.1`
@@ -27,6 +27,7 @@ Semantic versioning in our case means:
 
 ### Bugfixes
 - Make `generic_visit()` check script properly handle `with` statements.
+- Allow calling magic methods with the same name as the enclosing method #2381
 
 ### Misc
 
diff --git a/wemake_python_styleguide/visitors/ast/attributes.py b/wemake_python_styleguide/visitors/ast/attributes.py
index fa3bb8c5..3230171d 100644
--- a/wemake_python_styleguide/visitors/ast/attributes.py
+++ b/wemake_python_styleguide/visitors/ast/attributes.py
@@ -3,7 +3,9 @@ from typing import ClassVar, FrozenSet
 
 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 @@ class WrongAttributeVisitor(BaseNodeVisitor):
 
     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
index d2088214..14ed467c 100644
--- 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 @@ class Test(object):
         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, [])