Repository: wemake-services/wemake-python-styleguide

Issue:
Require a space between keywords and parentheses
# Rule request

## Thesis

We should enforce this style:

```python
return (some, tuple, items)
```

Over this style:

```python
return(some, tuple, items)
```

Also fine:

```python
return some, tuple, items
```

## Reasoning

Some people use `return` and `yield` keywords as functions. The same happened to good old `print`.

We should enforce consistent parentheses usage with keywords.
I am not sure about which keywords are affected by this. One of the tasks is to try all keywords and find out which are affected.



Discussion:
@sobolevn could you provide github param link of where it's located in the repo'e source code? Are you asking for going through the entire code base and correcting any violating code lines?
@parejadan I am asking to create a new rule that checks the source code for this kind of violation. Currently we do not have it in the source code.

But, it is quite east to implement. What needs to be done?
1. Read the [API Reference](https://wemake-python-styleguide.readthedocs.io/en/latest/_pages/api.html), especially this part: https://wemake-python-styleguide.readthedocs.io/en/latest/_pages/checker.html

2. Create a new visitor (I suspect that it is going to be `tokenize` based visitor)
3. Find all keywords, take the next token, find if it is `(`
4. Check the space
5. Profit!
I already did it locally, but it's blocked by #258, since adding new visitor breaks `test_all_visitors_contained_in_checker`

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
@@ -44,6 +44,7 @@
    ComparisonOrderViolation
    MultipleInComparisonViolation
    RedundantComparisonViolation
+   MissingSpaceBetweenKeywordAndParenViolation
 
 Consistency checks
 ------------------
@@ -61,6 +62,7 @@
 .. autoclass:: ComparisonOrderViolation
 .. autoclass:: MultipleInComparisonViolation
 .. autoclass:: RedundantComparisonViolation
+.. autoclass:: MissingSpaceBetweenKeywordAndParenViolation
 
 """
 
@@ -496,3 +498,39 @@ class RedundantComparisonViolation(ASTViolation):
     #: Error message shown to the user.
     error_template = 'Found comparison between same variable'
     code = 312
+
+
+class MissingSpaceBetweenKeywordAndParenViolation(TokenizeViolation):
+    """
+    Forbid opening parenthesis from following keyword without space in between.
+
+    Reasoning:
+        Some people use ``return`` and ``yield`` keywords as functions.
+        The same happened to good old ``print`` in Python2.
+    Solution:
+        Insert space symbol between keyword and open paren.
+
+    Example::
+
+        # Wrong:
+        def func():
+            a = 1
+            b = 2
+            del(a, b)
+            yield(1, 2, 3)
+
+        # Correct:
+        def func():
+            a = 1
+            del (a, b)
+            yield (1, 2, 3)
+
+    Note:
+        Returns Z313 as error code
+
+    """
+
+    should_use_text = False
+    #: Error message shown to the user.
+    error_template = 'Found paren right after a keyword'
+    code = 313
diff --git a/wemake_python_styleguide/visitors/presets/tokens.py b/wemake_python_styleguide/visitors/presets/tokens.py
--- a/wemake_python_styleguide/visitors/presets/tokens.py
+++ b/wemake_python_styleguide/visitors/presets/tokens.py
@@ -3,6 +3,9 @@
 from wemake_python_styleguide.visitors.tokenize.comments import (
     WrongCommentVisitor,
 )
+from wemake_python_styleguide.visitors.tokenize.keywords import (
+    WrongKeywordTokenVisitor,
+)
 from wemake_python_styleguide.visitors.tokenize.primitives import (
     WrongPrimitivesVisitor,
 )
@@ -10,5 +13,6 @@
 #: Used to store all token related visitors to be later passed to checker:
 TOKENS_PRESET = (
     WrongCommentVisitor,
+    WrongKeywordTokenVisitor,
     WrongPrimitivesVisitor,
 )
diff --git a/wemake_python_styleguide/visitors/tokenize/keywords.py b/wemake_python_styleguide/visitors/tokenize/keywords.py
new file mode 100644
--- /dev/null
+++ b/wemake_python_styleguide/visitors/tokenize/keywords.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+
+import keyword
+import tokenize
+
+from wemake_python_styleguide.violations.consistency import (
+    MissingSpaceBetweenKeywordAndParenViolation,
+)
+from wemake_python_styleguide.visitors.base import BaseTokenVisitor
+
+
+class WrongKeywordTokenVisitor(BaseTokenVisitor):
+    """Visits keywords and finds violations related to their usage."""
+
+    def _check_space_before_open_paren(self, token: tokenize.TokenInfo) -> None:
+        if token.line[token.end[1]:].startswith('('):
+            self.add_violation(
+                MissingSpaceBetweenKeywordAndParenViolation(token),
+            )
+
+    def visit_name(self, token: tokenize.TokenInfo) -> None:
+        """
+        Check keywords related rules.
+
+        Raises:
+            MissingSpaceBetweenKeywordAndParenViolation
+
+        """
+        if keyword.iskeyword(token.string):
+            self._check_space_before_open_paren(token)


Test patch:
diff --git a/tests/test_visitors/test_tokenize/test_wrong_keywords/test_keywords_spaces.py b/tests/test_visitors/test_tokenize/test_wrong_keywords/test_keywords_spaces.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_tokenize/test_wrong_keywords/test_keywords_spaces.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+
+import sys
+
+import pytest
+
+from wemake_python_styleguide.violations.consistency import (
+    MissingSpaceBetweenKeywordAndParenViolation,
+)
+from wemake_python_styleguide.visitors.tokenize.keywords import (
+    WrongKeywordTokenVisitor,
+)
+
+_multiline_error_function = """
+def foo():
+    yield(1, 2, 3)
+"""
+_multiline_error_statement = """
+assert(
+    1, 2, 3,
+) in b,
+"""
+
+
+@pytest.mark.parametrize('code', [
+    'del(a)',
+    'for(a, b) in [(1, 2)]:',
+    'for (a, b) in((1, 2)):',
+    'if a in(1, 2, 3):',
+    'return(a)',
+    pytest.param(
+        'await(a)',
+        marks=pytest.mark.skipif(
+            sys.version_info < (3, 7),
+            reason='await is a keyword only since py3.7',
+        ),
+    ),
+    'with(lambda x: x)() as (a, b)',
+    'with (lambda x: x)() as(a, b)',
+    '(a, b) is(a, b)',
+    'from foo import(bar, baz, spam)',
+    'except(ValueError, KeyError)',
+    'elif(bar, baz)',
+    'else(bar, baz)',
+    _multiline_error_function,
+    _multiline_error_statement,
+])
+def test_missing_space(
+    parse_tokens,
+    assert_errors,
+    default_options,
+    code,
+):
+    """Ensures that paren right after keyword raises a warning."""
+    file_tokens = parse_tokens(code)
+
+    visitor = WrongKeywordTokenVisitor(default_options, file_tokens=file_tokens)
+    visitor.run()
+
+    assert_errors(visitor, [MissingSpaceBetweenKeywordAndParenViolation])
+
+
+_multiline_correct_function = """
+def foo():
+    yield (1, 2, 3)
+"""
+_multiline_correct_statement = """
+assert (
+    1, 2, 3,
+) in b,
+"""
+
+
+@pytest.mark.parametrize('code', [
+    'del (a, b)',
+    'for (a, b) in (1, 2, 3)',
+    'return (a)',
+    pytest.param(
+        'await (a)',
+        marks=pytest.mark.skipif(
+            sys.version_info < (3, 7),
+            reason='await is a keyword only since py3.7',
+        ),
+    ),
+    'with do_things() as (a, b)',
+    '(a, b) is (a, b)',
+    'from foo import (bar, baz, spam)',
+    'except (ValueError, KeyError)',
+    'elif (bar, baz)',
+    'else (bar, baz)',
+    _multiline_correct_function,
+    _multiline_correct_statement,
+])
+def test_fine_when_space_in_between_keyword_and_paren(
+    parse_tokens,
+    assert_errors,
+    default_options,
+    code,
+):
+    """Ensures that there's no violation if space in between."""
+    file_tokens = parse_tokens(code)
+
+    visitor = WrongKeywordTokenVisitor(default_options, file_tokens=file_tokens)
+    visitor.run()
+
+    assert_errors(visitor, [])