Repository: wemake-services/wemake-python-styleguide

Issue:
Discrepancy between isort and flake8 line length settings
I have a question about the configuration for [nitpick](https://pypi.org/project/nitpick/). Why is the line length different for flake8 and isort?

```toml
["setup.cfg".flake8]
max-line-length = 80

["setup.cfg".isort]
line_length = 79
```

`isort --help`
>   -w LINE_LENGTH, --line-width LINE_LENGTH
                        The max length of an import line (used for wrapping
                        long imports).

`flake8 --help`
>   --max-line-length=n   Maximum allowed line length for the entirety of this
                        run. (Default: 79)

If you're changing the default for flake from 79 to 80, why not do the same for isort?


Discussion:

Resolving patch:
diff --git a/wemake_python_styleguide/presets/types/file_tokens.py b/wemake_python_styleguide/presets/types/file_tokens.py
--- a/wemake_python_styleguide/presets/types/file_tokens.py
+++ b/wemake_python_styleguide/presets/types/file_tokens.py
@@ -22,6 +22,7 @@
 
     statements.ExtraIndentationVisitor,
     statements.BracketLocationVisitor,
+    statements.MultilineStringVisitor,
 
     conditions.IfElseVisitor,
 )
diff --git a/wemake_python_styleguide/violations/best_practices.py b/wemake_python_styleguide/violations/best_practices.py
--- a/wemake_python_styleguide/violations/best_practices.py
+++ b/wemake_python_styleguide/violations/best_practices.py
@@ -78,6 +78,7 @@
    FloatComplexCompareViolation
    SingleElementDestructuringViolation
    ForbiddenInlineIgnoreViolation
+   WrongMultilineStringUseViolation
 
 Best practices
 --------------
@@ -144,6 +145,7 @@
 .. autoclass:: FloatComplexCompareViolation
 .. autoclass:: SingleElementDestructuringViolation
 .. autoclass:: ForbiddenInlineIgnoreViolation
+.. autoclass:: WrongMultilineStringUseViolation
 
 """
 
@@ -2390,3 +2392,41 @@ class of violations.
 
     error_template = 'Forbidden inline ignore: {0}'
     code = 461
+
+
+@final
+class WrongMultilineStringUseViolation(TokenizeViolation):
+    '''
+    Frobids direct usage of multiline strings.
+
+    Multiline strings are only allowed in docstrings
+    or assignments to variables.
+
+    Reasoning:
+        Direct usage of multiline strings is not readable.
+        One should not depend on the current indentation,
+        e.g. in comparisons or function calls.
+
+    Solution:
+        Assign a multiline string to a variable.
+
+    Example::
+
+        # Correct:
+        multiline = """
+            abc
+            abc
+        """
+
+        # Wrong:
+        function("""
+            abc
+            abc
+        """)
+
+    .. versionadded:: 0.15.0
+
+    '''
+
+    error_template = 'Wrong multiline string usage'
+    code = 462
diff --git a/wemake_python_styleguide/visitors/tokenize/statements.py b/wemake_python_styleguide/visitors/tokenize/statements.py
--- a/wemake_python_styleguide/visitors/tokenize/statements.py
+++ b/wemake_python_styleguide/visitors/tokenize/statements.py
@@ -1,7 +1,18 @@
 import tokenize
 from collections import defaultdict
-from typing import ClassVar, DefaultDict, Dict, List, Mapping, Sequence, Tuple
+from operator import attrgetter
+from typing import (
+    ClassVar,
+    DefaultDict,
+    Dict,
+    List,
+    Mapping,
+    Optional,
+    Sequence,
+    Tuple,
+)
 
+from flake8_quotes.docstring_detection import get_docstring_tokens
 from typing_extensions import final
 
 from wemake_python_styleguide.logic.tokens import (
@@ -9,9 +20,13 @@
     MATCHING,
     NEWLINES,
     get_reverse_bracket,
+    has_triple_string_quotes,
     last_bracket,
     only_contains,
 )
+from wemake_python_styleguide.violations.best_practices import (
+    WrongMultilineStringUseViolation,
+)
 from wemake_python_styleguide.violations.consistency import (
     BracketBlankLineViolation,
     ExtraIndentationViolation,
@@ -171,3 +186,73 @@ def _check_empty_line_wrap(
     def _post_visit(self) -> None:
         for tokens in self._lines.values():
             self._check_individual_line(tokens)
+
+
+@final
+class MultilineStringVisitor(BaseTokenVisitor):
+    """Checks if multiline strings are used only in assignment or docstrings."""
+
+    def __init__(self, *args, **kwargs) -> None:
+        """Creates line tracking for tokens."""
+        super().__init__(*args, **kwargs)
+        self._lines: TokenLines = defaultdict(list)
+        self._docstrings = get_docstring_tokens(self.file_tokens)
+
+    def visit(self, token: tokenize.TokenInfo) -> None:
+        """
+        Goes trough all tokens to separate them by line numbers.
+
+        Raises:
+            WrongMultilineStringUseViolation
+
+        """
+        self._lines[token.start[0]].append(token)
+
+    def _check_token(
+        self,
+        index: int,
+        tokens: List[tokenize.TokenInfo],
+        previous_token: Optional[tokenize.TokenInfo],
+        next_token: Optional[tokenize.TokenInfo],
+    ) -> None:
+        if index != 0:
+            previous_token = tokens[index - 1]
+        if previous_token and previous_token.exact_type != tokenize.EQUAL:
+            self.add_violation(WrongMultilineStringUseViolation(previous_token))
+
+        if index + 1 < len(tokens):
+            next_token = tokens[index + 1]
+        if next_token and next_token.exact_type == tokenize.DOT:
+            self.add_violation(WrongMultilineStringUseViolation(next_token))
+
+    def _check_individual_line(
+        self,
+        tokens: List[tokenize.TokenInfo],
+        previous_token: Optional[tokenize.TokenInfo],
+        next_token: Optional[tokenize.TokenInfo],
+    ) -> None:
+        for index, token in enumerate(tokens):
+            if token.exact_type != tokenize.STRING or token in self._docstrings:
+                continue
+            if has_triple_string_quotes(token.string):
+                self._check_token(index, tokens, previous_token, next_token)
+
+    def _post_visit(self) -> None:
+        linenos = sorted((self._lines.keys()))
+        for index, _ in enumerate(linenos):
+            line_tokens = sorted(
+                self._lines[linenos[index]], key=attrgetter('start'),
+            )
+            previous_line_token = None
+            next_line_token = None
+            if index != 0:
+                previous_line_token = sorted(
+                    self._lines[linenos[index - 1]], key=attrgetter('start'),
+                )[-1]
+            if index + 1 < len(linenos):
+                next_line_token = sorted(
+                    self._lines[linenos[index + 1]], key=attrgetter('start'),
+                )[0]
+            self._check_individual_line(
+                line_tokens, previous_line_token, next_line_token,
+            )


Test patch:
diff --git a/tests/fixtures/noqa/noqa.py b/tests/fixtures/noqa/noqa.py
--- a/tests/fixtures/noqa/noqa.py
+++ b/tests/fixtures/noqa/noqa.py
@@ -756,3 +756,8 @@ def many_raises_function(parameter):  # noqa: WPS238
     if parameter == 3:
         raise IndexError('3')
     raise TypeError('4')
+
+
+my_print("""
+text
+""")  # noqa: WPS462
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
@@ -234,6 +234,7 @@
     'WPS459': 1,
     'WPS460': 1,
     'WPS461': 0,  # logically unacceptable.
+    'WPS462': 1,
 
     'WPS500': 1,
     'WPS501': 1,
diff --git a/tests/test_visitors/test_ast/test_blocks/test_outer_scope_shadow.py b/tests/test_visitors/test_ast/test_blocks/test_outer_scope_shadow.py
--- a/tests/test_visitors/test_ast/test_blocks/test_outer_scope_shadow.py
+++ b/tests/test_visitors/test_ast/test_blocks/test_outer_scope_shadow.py
@@ -331,7 +331,7 @@ def test_outer_variable_double_shadow(
     mode,
 ):
     """Testing that shadowing vars are not allowed."""
-    tree = parse_ast_tree(mode("""
+    code = """
     a = 1
 
     def test1():
@@ -339,7 +339,8 @@ def test1():
 
     def test2(a):
         ...
-    """))
+    """
+    tree = parse_ast_tree(mode(code))
 
     visitor = BlockVariableVisitor(default_options, tree=tree)
     visitor.run()
diff --git a/tests/test_visitors/test_ast/test_complexity/test_nested/test_nested_classes.py b/tests/test_visitors/test_ast/test_complexity/test_nested/test_nested_classes.py
--- a/tests/test_visitors/test_ast/test_complexity/test_nested/test_nested_classes.py
+++ b/tests/test_visitors/test_ast/test_complexity/test_nested/test_nested_classes.py
@@ -216,13 +216,14 @@ def test_ordinary_class(
     mode,
 ):
     """Testing that it is possible to write basic classes."""
-    tree = parse_ast_tree(mode("""
+    code = """
     class Ordinary(object):
         def method(self): ...
 
     class Second(Ordinary):
         def method(self): ...
-    """))
+    """
+    tree = parse_ast_tree(mode(code))
 
     visitor = NestedComplexityVisitor(default_options, tree=tree)
     visitor.run()
diff --git a/tests/test_visitors/test_ast/test_complexity/test_offset_visitor.py b/tests/test_visitors/test_ast/test_complexity/test_offset_visitor.py
--- a/tests/test_visitors/test_ast/test_complexity/test_offset_visitor.py
+++ b/tests/test_visitors/test_ast/test_complexity/test_offset_visitor.py
@@ -197,10 +197,11 @@ def test_regression282(
 
     See: https://github.com/wemake-services/wemake-python-styleguide/issues/282
     """
-    tree = parse_ast_tree("""
+    code = """
     async def no_offset():
         ...
-    """)
+    """
+    tree = parse_ast_tree(code)
 
     monkeypatch.setattr(OffsetVisitor, '_max_offset_blocks', 1)
     visitor = OffsetVisitor(default_options, tree=tree)
diff --git a/tests/test_visitors/test_ast/test_functions/test_unused_variables.py b/tests/test_visitors/test_ast/test_functions/test_unused_variables.py
--- a/tests/test_visitors/test_ast/test_functions/test_unused_variables.py
+++ b/tests/test_visitors/test_ast/test_functions/test_unused_variables.py
@@ -176,12 +176,13 @@ def test_double_wrong_variables(
     mode,
 ):
     """Testing that it is possible to have two violations with wrong vars."""
-    tree = parse_ast_tree(mode("""
+    code = """
     def some_function():
         _should_not_be_used = 1
         print(_should_not_be_used)
         print(_should_not_be_used)
-    """))
+    """
+    tree = parse_ast_tree(mode(code))
 
     visitor = FunctionDefinitionVisitor(default_options, tree=tree)
     visitor.run()
diff --git a/tests/test_visitors/test_ast/test_keywords/test_raise.py b/tests/test_visitors/test_ast/test_keywords/test_raise.py
--- a/tests/test_visitors/test_ast/test_keywords/test_raise.py
+++ b/tests/test_visitors/test_ast/test_keywords/test_raise.py
@@ -118,12 +118,13 @@ def test_bare_raise(
     default_options,
 ):
     """Testing that bare `raise` is allowed."""
-    tree = parse_ast_tree("""
+    code = """
     try:
         1 / 0
     except Exception:
         raise
-    """)
+    """
+    tree = parse_ast_tree(code)
 
     visitor = WrongRaiseVisitor(default_options, tree=tree)
     visitor.run()
diff --git a/tests/test_visitors/test_tokenize/test_statements/test_multiline_string.py b/tests/test_visitors/test_tokenize/test_statements/test_multiline_string.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_tokenize/test_statements/test_multiline_string.py
@@ -0,0 +1,85 @@
+import pytest
+
+from wemake_python_styleguide.violations.best_practices import (
+    WrongMultilineStringUseViolation,
+)
+from wemake_python_styleguide.visitors.tokenize.statements import (
+    MultilineStringVisitor,
+)
+
+correct_assignment = '''
+a = """abc
+abc
+"""
+'''
+
+correct_docstring = '''
+def test():
+    """{0}"""
+'''
+
+
+@pytest.mark.parametrize('code', [
+    correct_assignment,
+    correct_docstring,
+])
+def test_correct_multiline_string_use(
+    parse_tokens,
+    assert_errors,
+    default_options,
+    code,
+):
+    """Ensures that correct multiline strings uses work."""
+    file_tokens = parse_tokens(code)
+
+    visitor = MultilineStringVisitor(default_options, file_tokens=file_tokens)
+    visitor.run()
+
+    assert_errors(visitor, [])
+
+
+wrong_compare = '''
+a = 'abc'
+if a > """ab
+cd""":
+    return 1
+'''
+
+wrong_function_call = '''
+f("""ab
+cd""")
+'''
+
+wrong_function_call_newline = '''
+f(
+    """ab
+    cd""",
+)
+'''
+
+wrong_string_function = '''
+a = """abc
+abc
+""".split('\n')
+'''
+
+
+@pytest.mark.parametrize('code', [
+    wrong_compare,
+    wrong_function_call,
+    wrong_string_function,
+    wrong_function_call_newline,
+])
+def test_wrong_multiline_string_use(
+    parse_tokens,
+    assert_errors,
+    default_options,
+    code,
+):
+    """Ensures that wrong multiline string uses raise a warning."""
+    file_tokens = parse_tokens(code)
+
+    visitor = MultilineStringVisitor(default_options, file_tokens=file_tokens)
+    visitor.run()
+
+    assert_errors(visitor, [WrongMultilineStringUseViolation])