Repository: wemake-services/wemake-python-styleguide

Issue:
Restrict variable names with more than one consecutive underscore
# Rule request

## Thesis

Currently it is possible to name your variables like: `some__value`. This should be forbidden.

Mind, that `__magic__` variables can still be used.

## Reasoning

This is done to gain extra readability. This naming rule already exist for module names.



Discussion:
Can I take it?
@Roxe322 sure!
@sobolevn Hello! I've created some tests in `test_variable_names.py` and added some logics to `/logics/variables.py` but now I have `Z202 Found too many module members` in both of files. How can I solve this and then where I should add my logics and tests if it is a part of naming rules?
Test can be created in a new file named `test_protected_variable.py`.
And logic can be refactored like this:
1. create a package instead of a single module named `variables`
2. create required modules
3. place a new function in the appropriate module

Thank you, will work on this

Resolving patch:
diff --git a/wemake_python_styleguide/logics/variables/naming.py b/wemake_python_styleguide/logics/variables/naming.py
--- a/wemake_python_styleguide/logics/variables/naming.py
+++ b/wemake_python_styleguide/logics/variables/naming.py
@@ -104,6 +104,31 @@ def is_too_short_variable_name(
     return name != constants.UNUSED_VARIABLE and len(name) < min_length
 
 
+def is_private_variable(name: Optional[str]) -> bool:
+    """
+    Checks if variable has private name pattern.
+
+    >>> is_private_variable(None)
+    False
+
+    >>> is_private_variable('regular')
+    False
+
+    >>> is_private_variable('__private')
+    True
+
+    >>> is_private_variable('_protected')
+    False
+
+    >>> is_private_variable('__magic__')
+    False
+
+    """
+    return (
+        name is not None and name.startswith('__') and not name.endswith('__')
+    )
+
+
 def is_variable_name_with_underscored_number(name: str) -> bool:
     """
     Checks for variable names with underscored number.
@@ -135,3 +160,44 @@ def is_variable_name_with_underscored_number(name: str) -> bool:
     """
     pattern = constants.UNDERSCORED_NUMBER_PATTERN
     return name is not None and pattern.match(name) is not None
+
+
+def is_variable_name_contains_consecutive_underscores(name: str) -> bool:
+    """
+    Checks if variable contains consecutive underscores in middle of name.
+
+    >>> is_variable_name_contains_consecutive_underscores('name')
+    False
+
+    >>> is_variable_name_contains_consecutive_underscores('__magic__')
+    False
+
+    >>> is_variable_name_contains_consecutive_underscores('__private')
+    False
+
+    >>> is_variable_name_contains_consecutive_underscores(None)
+    False
+
+    >>> is_variable_name_contains_consecutive_underscores('name')
+    False
+
+    >>> is_variable_name_contains_consecutive_underscores('some__value')
+    True
+
+    >>> is_variable_name_contains_consecutive_underscores('some_value__')
+    True
+
+    """
+    if name is None:
+        return False
+
+    if name.endswith('__') and name.startswith('__'):
+        return False
+
+    if name.startswith('__'):
+        return False
+
+    if '__' in name:
+        return True
+
+    return False
diff --git a/wemake_python_styleguide/violations/naming.py b/wemake_python_styleguide/violations/naming.py
--- a/wemake_python_styleguide/violations/naming.py
+++ b/wemake_python_styleguide/violations/naming.py
@@ -96,6 +96,7 @@
 
 - Variables should use ``snake_case``
 - When some variable is unused it should be prefixed with an underscore
+- Variables should not contain more than one consecutive underscore
 
 Type aliases
 ~~~~~~~~~~~~
@@ -126,8 +127,10 @@
    SameAliasImportViolation
    UnderScoredNumberNameViolation
    UpperCaseAttributeViolation
+   ConsecutiveUnderscoresInNameViolation
    ProtectedModuleViolation
 
+
 Module names
 ------------
 
@@ -146,7 +149,8 @@
 .. autoclass:: PrivateNameViolation
 .. autoclass:: SameAliasImportViolation
 .. autoclass:: UnderScoredNumberNameViolation
-.. autoclass:: UpperCaseAttributeViolation
+.. autoclass:: UpperCaseAttributeViolations
+.. autoclass:: ConsecutiveUnderscoresInNameViolation
 
 """
 
@@ -547,6 +551,37 @@ class A(object):
     code = 115
 
 
+@final
+class ConsecutiveUnderscoresInNameViolation(ASTViolation):
+    """
+    Forbids to use more than one consecutive underscore in variable names.
+
+    Reasoning:
+        This is done to gain extra readability.
+        This naming rule already exist for module names.
+
+    Example::
+
+        # Correct:
+        some_value = 5
+        __magic__ = 5
+
+        # Wrong:
+        some__value = 5
+
+    .. versionadded:: 0.3.0
+
+    Note:
+        Returns Z116 as error code
+
+    """
+
+    #: Error message shown to the user.
+    error_template = 'Found consecutive underscores in a variable "{0}"'
+
+    code = 116
+
+
 @final
 class ProtectedModuleViolation(ASTViolation):
     """
@@ -571,9 +606,10 @@ class ProtectedModuleViolation(ASTViolation):
     .. versionadded:: 0.3.0
 
     Note:
-        Returns Z116 as error code
+        Returns Z117 as error code
     """
 
     #: Error message shown to the user
     error_template = 'Found protected module import "{0}"'
-    code = 116
+
+    code = 117
diff --git a/wemake_python_styleguide/visitors/ast/naming.py b/wemake_python_styleguide/visitors/ast/naming.py
--- a/wemake_python_styleguide/visitors/ast/naming.py
+++ b/wemake_python_styleguide/visitors/ast/naming.py
@@ -14,6 +14,7 @@
 from wemake_python_styleguide.logics.variables.naming import (
     is_too_short_variable_name,
     is_upper_case_name,
+    is_variable_name_contains_consecutive_underscores,
     is_variable_name_with_underscored_number,
     is_wrong_variable_name,
 )
@@ -23,6 +24,7 @@
     WrongModuleMetadataViolation,
 )
 from wemake_python_styleguide.violations.naming import (
+    ConsecutiveUnderscoresInNameViolation,
     PrivateNameViolation,
     TooShortVariableNameViolation,
     UnderScoredNumberNameViolation,
@@ -55,6 +57,7 @@ class WrongNameVisitor(BaseNodeVisitor):
     """Performs checks based on variable names."""
 
     def _check_name(self, node: ast.AST, name: str) -> None:
+
         if is_wrong_variable_name(name, VARIABLE_NAMES_BLACKLIST):
             self.add_violation(WrongVariableNameViolation(node, text=name))
 
@@ -67,6 +70,10 @@ def _check_name(self, node: ast.AST, name: str) -> None:
 
         if is_variable_name_with_underscored_number(name):
             self.add_violation(UnderScoredNumberNameViolation())
+        if is_variable_name_contains_consecutive_underscores(name):
+            self.add_violation(
+                ConsecutiveUnderscoresInNameViolation(node, text=name),
+            )
 
     def _check_function_signature(self, node: AnyFunctionDef) -> None:
         for arg in node.args.args:
@@ -148,6 +155,7 @@ def visit_variable(self, node: VariableDef) -> None:
 
         """
         variable_name = get_assigned_name(node)
+
         if variable_name is not None:
             self._check_name(node, variable_name)
         self.generic_visit(node)


Test patch:
diff --git a/tests/test_visitors/test_ast/test_naming/test_consecutive_underscore_in_variable_name.py b/tests/test_visitors/test_ast/test_naming/test_consecutive_underscore_in_variable_name.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_naming/test_consecutive_underscore_in_variable_name.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+
+import pytest
+
+from wemake_python_styleguide.violations.naming import (
+    ConsecutiveUnderscoresInNameViolation,
+)
+from wemake_python_styleguide.visitors.ast.naming import WrongNameVisitor
+
+variable_test = """
+{0} = 'test'
+"""
+
+underscore_variable_test1 = """
+_{0} = 'test'
+"""
+
+underscore_variable_test2 = """
+{0}_ = 'test'
+"""
+
+for_variable_test = """
+for {0} in []:
+    print()
+"""
+
+async_for_variable_test = """
+async def container():
+    async for {0} in []:
+        print()
+"""
+
+with_variable_test = """
+with open('test.py') as {0}:
+    raise ValueError()
+"""
+
+async_with_variable_test = """
+async def container():
+    async with open('test.py') as {0}:
+        raise ValueError()
+"""
+
+exception_test = """
+try:
+    1 / 0
+except Exception as {0}:
+    raise
+"""
+
+
+@pytest.mark.parametrize('code', [
+    variable_test,
+    for_variable_test,
+    async_for_variable_test,
+    with_variable_test,
+    async_with_variable_test,
+    exception_test,
+])
+def test_consecutive_underscores_in_variable_name(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    default_options,
+):
+    """Testing that variable can not have private names."""
+    tree = parse_ast_tree(code.format('some__value'))
+
+    visitor = WrongNameVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [ConsecutiveUnderscoresInNameViolation])