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/tree.py b/wemake_python_styleguide/presets/types/tree.py
--- a/wemake_python_styleguide/presets/types/tree.py
+++ b/wemake_python_styleguide/presets/types/tree.py
@@ -84,6 +84,7 @@
     compares.UnaryCompareVisitor,
     compares.WrongConstantCompareVisitor,
     compares.InCompareSanityVisitor,
+    compares.WrongFloatComplexCompareVisitor,
 
     conditions.IfStatementVisitor,
     conditions.BooleanConditionVisitor,
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
@@ -75,6 +75,7 @@
    FloatingNanViolation
    InfiniteWhileLoopViolation
    ImportCollisionViolation
+   FloatComplexCompareViolation
 
 Best practices
 --------------
@@ -138,6 +139,7 @@
 .. autoclass:: FloatingNanViolation
 .. autoclass:: InfiniteWhileLoopViolation
 .. autoclass:: ImportCollisionViolation
+.. autoclass:: FloatComplexCompareViolation
 
 """
 
@@ -2294,3 +2296,38 @@ class ImportCollisionViolation(ASTViolation):
 
     error_template = 'Found imports collision: {0}'
     code = 458
+
+
+@final
+class FloatComplexCompareViolation(ASTViolation):
+    """
+    Forbids comparisons with ``float`` and ``complex``.
+
+    Reasoning:
+        This is a best practice rule, as ``float`` and ``complex``
+        suffer from representation error, leading to possibly
+        incorrect results during comparison.
+
+    Solution:
+        Use fuzzy operators.
+        1. ``abs(f1 - f2) <= allowed_error``
+        2. ``math.isclose(f1, f2)`` (for ``float``)
+        3. ``cmath.isclose(c1, c2)`` (for ``complex``)
+        4. Custom logic, not using operators
+
+    Example::
+
+        # Correct:
+        math.isclose(3.0, 0.3 / 0.1)
+        cmath.isclose(3 + 4j, (0.3 + 0.4j) / 0.1)
+
+        # Wrong:
+        3.0 == 0.3 / 0.1
+        3 + 4j == (0.3 + 0.4j) / 0.1
+
+    .. versionadded:: 0.15.0
+
+    """
+
+    error_template = 'Found comparison with float or complex number'
+    code = 459
diff --git a/wemake_python_styleguide/visitors/ast/compares.py b/wemake_python_styleguide/visitors/ast/compares.py
--- a/wemake_python_styleguide/visitors/ast/compares.py
+++ b/wemake_python_styleguide/visitors/ast/compares.py
@@ -16,6 +16,7 @@
 from wemake_python_styleguide.logic.walrus import get_assigned_expr
 from wemake_python_styleguide.types import AnyIf, AnyNodes
 from wemake_python_styleguide.violations.best_practices import (
+    FloatComplexCompareViolation,
     HeterogenousCompareViolation,
 )
 from wemake_python_styleguide.violations.consistency import (
@@ -40,17 +41,6 @@
 from wemake_python_styleguide.visitors.decorators import alias
 
 
-def _is_correct_len(sign: ast.cmpop, comparator: ast.AST) -> bool:
-    """This is a helper function to tell what calls to ``len()`` are valid."""
-    if isinstance(operators.unwrap_unary_node(comparator), ast.Num):
-        numeric_value = ast.literal_eval(comparator)
-        if numeric_value == 0:
-            return False
-        if numeric_value == 1:
-            return not isinstance(sign, (ast.GtE, ast.Lt))
-    return True
-
-
 @final
 class CompareSanityVisitor(BaseNodeVisitor):
     """Restricts the incorrect compares."""
@@ -74,6 +64,16 @@ def visit_Compare(self, node: ast.Compare) -> None:
         self._check_reversed_complex_compare(node)
         self.generic_visit(node)
 
+    def _is_correct_len(self, sign: ast.cmpop, comparator: ast.AST) -> bool:
+        """Helper function which tells what calls to ``len()`` are valid."""
+        if isinstance(operators.unwrap_unary_node(comparator), ast.Num):
+            numeric_value = ast.literal_eval(comparator)
+            if numeric_value == 0:
+                return False
+            if numeric_value == 1:
+                return not isinstance(sign, (ast.GtE, ast.Lt))
+        return True
+
     def _check_literal_compare(self, node: ast.Compare) -> None:
         last_was_literal = nodes.is_literal(get_assigned_expr(node.left))
         for comparator in map(get_assigned_expr, node.comparators):
@@ -102,7 +102,7 @@ def _check_unpythonic_compare(self, node: ast.Compare) -> None:
 
             if functions.given_function_called(compare, {'len'}):
                 ps = index - len(all_nodes) + 1
-                if not _is_correct_len(node.ops[ps], node.comparators[ps]):
+                if not self._is_correct_len(node.ops[ps], node.comparators[ps]):
                     self.add_violation(UselessLenCompareViolation(node))
 
     def _check_heterogenous_operators(self, node: ast.Compare) -> None:
@@ -464,3 +464,34 @@ def _check_single_item_container(self, node: ast.AST) -> None:
     def _check_wrong_comparators(self, node: ast.AST) -> None:
         if isinstance(node, self._wrong_in_comparators):
             self.add_violation(WrongInCompareTypeViolation(node))
+
+
+@final
+class WrongFloatComplexCompareVisitor(BaseNodeVisitor):
+    """Restricts incorrect compares with ``float`` and ``complex``."""
+
+    def visit_Compare(self, node: ast.Compare) -> None:
+        """
+        Ensures that compares are written correctly.
+
+        Raises:
+            FloatComplexCompareViolation
+
+        """
+        self._check_float_complex_compare(node)
+        self.generic_visit(node)
+
+    def _is_float_or_complex(self, node: ast.AST) -> bool:
+        node = operators.unwrap_unary_node(node)
+        return (
+            isinstance(node, ast.Num) and
+            isinstance(node.n, (float, complex))
+        )
+
+    def _check_float_complex_compare(self, node: ast.Compare) -> None:
+        any_float_or_complex = any(
+            self._is_float_or_complex(comparator)
+            for comparator in node.comparators
+        ) or self._is_float_or_complex(node.left)
+        if any_float_or_complex:
+            self.add_violation(FloatComplexCompareViolation(node))


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
@@ -726,6 +726,7 @@ def consecutive_yields():
         break
     my_print(4)
 
+
 def raise_bad_exception():
     raise Exception  # noqa: WPS454
 
@@ -742,6 +743,8 @@ def infinite_loop():
     while True:  # noqa: WPS457
         my_print('forever')
 
+
+my_print(some_float == 1.0)  # noqa: WPS459
 unnecessary_raw_string = r'no backslashes.' # noqa: WPS360
 
 
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
@@ -231,6 +231,7 @@
     'WPS456': 1,
     'WPS457': 1,
     'WPS458': 1,
+    'WPS459': 1,
 
     'WPS500': 1,
     'WPS501': 1,
diff --git a/tests/test_visitors/test_ast/test_compares/test_float_complex_compare.py b/tests/test_visitors/test_ast/test_compares/test_float_complex_compare.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_compares/test_float_complex_compare.py
@@ -0,0 +1,61 @@
+import pytest
+
+from wemake_python_styleguide.violations.best_practices import (
+    FloatComplexCompareViolation,
+)
+from wemake_python_styleguide.visitors.ast.compares import (
+    WrongFloatComplexCompareVisitor,
+)
+
+
+@pytest.mark.parametrize('code', [
+    '3.0 > 2.0 + 1.0',
+    'f2/f1 != 1.0',
+    '3.0 in item_list',
+    '2.0 < x < 3.0',
+    '5.0 not in abc_list',
+    '3.0 == 0.1*3',
+    '2j != (0.2j)/0.1',
+    '2j in comp_list',
+    '1j != 2 != 3.0',
+    'x == 4.0j',
+])
+def test_float_complex_compare(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    default_options,
+):
+    """Testing that compares with ``float`` and ``complex`` raise violations."""
+    tree = parse_ast_tree(code)
+
+    visitor = WrongFloatComplexCompareVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [FloatComplexCompareViolation])
+
+
+@pytest.mark.parametrize('code', [
+    'x > 3',
+    'x <= y',
+    'abs(x - y) <= eps',
+    'isclose(x, 5.0)',
+    'isclose(y, 3j)',
+    '3 in item_list',
+    '3 != 5',
+    '3.0 + x',
+    '4.5 + y > z',
+])
+def test_correct_compares(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    default_options,
+):
+    """Testing safe compares."""
+    tree = parse_ast_tree(code)
+
+    visitor = WrongFloatComplexCompareVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])