Repository: python/mypy

Issue:
attrs plugin doesn't support new `eq` and `order` arguments
attrs 19.2.0 has added `eq` and `order` arguments to `attr.ib`, and deprecated `cmp`. I think mypy needs to be updated to take this into account.

```python
@attr.s(auto_attribs=True, frozen=True)
class Context:
    _original: Dict[str, Any] = attr.ib(order=False)
```

```
_context.py:37: error: No overload variant matches argument type "bool"
_context.py:37: note: Possible overload variants:
_context.py:37: note:     def (default: None = ..., validator: None = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: None = ..., metadata: Optional[Mapping[Any, Any]] = ..., type: None = ..., converter: None = ..., factory: None = ..., kw_only: bool = ...) -> Any
_context.py:37: note:     def [_T] (default: None = ..., validator: Union[Callable[[Any, Attribute[_T], _T], Any], Sequence[Callable[[Any, Attribute[_T], _T], Any]], None] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[Callable[[Any], _T]] = ..., metadata: Optional[Mapping[Any, Any]] = ..., type: Optional[Type[_T]] = ..., converter: Optional[Callable[[Any], _T]] = ..., factory: Optional[Callable[[], _T]] = ..., kw_only: bool = ...) -> _T
_context.py:37: note:     <2 more similar overloads not shown, out of 4 total overloads>
```

mypy 0.711 and 0.730

I expect this to pass type checking, as attrs 19.2.0 adds `eq` and `order` parameters to attr.ib: http://www.attrs.org/en/19.2.0/changelog.html#deprecations

Without this, using `cmp=` generates a DeprecationWarning. 

I haven't tried installing mypy from Git master.

```
[mypy]
python_version = 3.7
strict_optional = true
warn_redundant_casts = true
namespace_packages = true
```



Discussion:
Oh looks like attrs finally shipped.  I can work on fixing this in my spare time.  Unless someone feels like looking at it!
@euresti Great, thanks!

Resolving patch:
diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py
--- a/mypy/plugins/attrs.py
+++ b/mypy/plugins/attrs.py
@@ -176,6 +176,62 @@ def deserialize(cls, info: TypeInfo, data: JsonDict) -> 'Attribute':
         )
 
 
+def _determine_eq_order(ctx: 'mypy.plugin.ClassDefContext') -> Tuple[bool, bool]:
+    """
+    Validate the combination of *cmp*, *eq*, and *order*. Derive the effective
+    values of eq and order.
+    """
+    cmp = _get_decorator_optional_bool_argument(ctx, 'cmp')
+    eq = _get_decorator_optional_bool_argument(ctx, 'eq')
+    order = _get_decorator_optional_bool_argument(ctx, 'order')
+
+    if cmp is not None and any((eq is not None, order is not None)):
+        ctx.api.fail("Don't mix `cmp` with `eq' and `order`", ctx.reason)
+
+    # cmp takes precedence due to bw-compatibility.
+    if cmp is not None:
+        ctx.api.fail("cmp is deprecated, use eq and order", ctx.reason)
+        return cmp, cmp
+
+    # If left None, equality is on and ordering mirrors equality.
+    if eq is None:
+        eq = True
+
+    if order is None:
+        order = eq
+
+    if eq is False and order is True:
+        ctx.api.fail("eq must be True if order is True", ctx.reason)
+
+    return eq, order
+
+
+def _get_decorator_optional_bool_argument(
+    ctx: 'mypy.plugin.ClassDefContext',
+    name: str,
+    default: Optional[bool] = None,
+) -> Optional[bool]:
+    """Return the Optional[bool] argument for the decorator.
+
+    This handles both @decorator(...) and @decorator.
+    """
+    if isinstance(ctx.reason, CallExpr):
+        attr_value = _get_argument(ctx.reason, name)
+        if attr_value:
+            if isinstance(attr_value, NameExpr):
+                if attr_value.fullname == 'builtins.True':
+                    return True
+                if attr_value.fullname == 'builtins.False':
+                    return False
+                if attr_value.fullname == 'builtins.None':
+                    return None
+            ctx.api.fail('"{}" argument must be True or False.'.format(name), ctx.reason)
+            return default
+        return default
+    else:
+        return default
+
+
 def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
                               auto_attribs_default: bool = False) -> None:
     """Add necessary dunder methods to classes decorated with attr.s.
@@ -193,7 +249,8 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
 
     init = _get_decorator_bool_argument(ctx, 'init', True)
     frozen = _get_frozen(ctx)
-    cmp = _get_decorator_bool_argument(ctx, 'cmp', True)
+    eq, order = _determine_eq_order(ctx)
+
     auto_attribs = _get_decorator_bool_argument(ctx, 'auto_attribs', auto_attribs_default)
     kw_only = _get_decorator_bool_argument(ctx, 'kw_only', False)
 
@@ -231,8 +288,10 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
     adder = MethodAdder(ctx)
     if init:
         _add_init(ctx, attributes, adder)
-    if cmp:
-        _add_cmp(ctx, adder)
+    if eq:
+        _add_eq(ctx, adder)
+    if order:
+        _add_order(ctx, adder)
     if frozen:
         _make_frozen(ctx, attributes)
 
@@ -529,8 +588,8 @@ def _parse_assignments(
     return lvalues, rvalues
 
 
-def _add_cmp(ctx: 'mypy.plugin.ClassDefContext', adder: 'MethodAdder') -> None:
-    """Generate all the cmp methods for this class."""
+def _add_eq(ctx: 'mypy.plugin.ClassDefContext', adder: 'MethodAdder') -> None:
+    """Generate __eq__ and __ne__ for this class."""
     # For __ne__ and __eq__ the type is:
     #     def __ne__(self, other: object) -> bool
     bool_type = ctx.api.named_type('__builtins__.bool')
@@ -538,7 +597,13 @@ def _add_cmp(ctx: 'mypy.plugin.ClassDefContext', adder: 'MethodAdder') -> None:
     args = [Argument(Var('other', object_type), object_type, None, ARG_POS)]
     for method in ['__ne__', '__eq__']:
         adder.add_method(method, args, bool_type)
-    # For the rest we use:
+
+
+def _add_order(ctx: 'mypy.plugin.ClassDefContext', adder: 'MethodAdder') -> None:
+    """Generate all the ordering methods for this class."""
+    bool_type = ctx.api.named_type('__builtins__.bool')
+    object_type = ctx.api.named_type('__builtins__.object')
+    # Make the types be:
     #    AT = TypeVar('AT')
     #    def __lt__(self: AT, other: AT) -> bool
     # This way comparisons with subclasses will work correctly.


Test patch:
diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test
--- a/test-data/unit/check-attr.test
+++ b/test-data/unit/check-attr.test
@@ -214,9 +214,9 @@ A(1) != 1
 1 != A(1)
 [builtins fixtures/attr.pyi]
 
-[case testAttrsCmpFalse]
+[case testAttrsEqFalse]
 from attr import attrib, attrs
-@attrs(auto_attribs=True, cmp=False)
+@attrs(auto_attribs=True, eq=False)
 class A:
     a: int
 reveal_type(A)  # N: Revealed type is 'def (a: builtins.int) -> __main__.A'
@@ -245,6 +245,57 @@ A(1) != 1
 1 != A(1)
 [builtins fixtures/attr.pyi]
 
+[case testAttrsOrderFalse]
+from attr import attrib, attrs
+@attrs(auto_attribs=True, order=False)
+class A:
+    a: int
+reveal_type(A)  # N: Revealed type is 'def (a: builtins.int) -> __main__.A'
+reveal_type(A.__eq__)  # N: Revealed type is 'def (self: __main__.A, other: builtins.object) -> builtins.bool'
+reveal_type(A.__ne__)  # N: Revealed type is 'def (self: __main__.A, other: builtins.object) -> builtins.bool'
+
+A(1) < A(2)  # E: Unsupported left operand type for < ("A")
+A(1) <= A(2)  # E: Unsupported left operand type for <= ("A")
+A(1) > A(2)  # E: Unsupported left operand type for > ("A")
+A(1) >= A(2)  # E: Unsupported left operand type for >= ("A")
+A(1) == A(2)
+A(1) != A(2)
+
+A(1) < 1  # E: Unsupported left operand type for < ("A")
+A(1) <= 1  # E: Unsupported left operand type for <= ("A")
+A(1) > 1  # E: Unsupported left operand type for > ("A")
+A(1) >= 1  # E: Unsupported left operand type for >= ("A")
+A(1) == 1
+A(1) != 1
+
+1 < A(1)  # E: Unsupported left operand type for < ("int")
+1 <= A(1)  # E: Unsupported left operand type for <= ("int")
+1 > A(1)  # E: Unsupported left operand type for > ("int")
+1 >= A(1)  # E: Unsupported left operand type for >= ("int")
+1 == A(1)
+1 != A(1)
+[builtins fixtures/attr.pyi]
+
+[case testAttrsCmpEqOrderValues]
+from attr import attrib, attrs
+@attrs(cmp=True)  # E: cmp is deprecated, use eq and order
+class DeprecatedTrue:
+   ...
+
+@attrs(cmp=False)  # E: cmp is deprecated, use eq and order
+class DeprecatedFalse:
+   ...
+
+@attrs(cmp=False, eq=True)  # E: Don't mix `cmp` with `eq' and `order`  # E: cmp is deprecated, use eq and order
+class Mixed:
+   ...
+
+@attrs(order=True, eq=False)  # E: eq must be True if order is True
+class Confused:
+   ...
+[builtins fixtures/attr.pyi]
+
+
 [case testAttrsInheritance]
 import attr
 @attr.s
diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test
--- a/test-data/unit/check-incremental.test
+++ b/test-data/unit/check-incremental.test
@@ -2937,7 +2937,7 @@ class Frozen:
 @attr.s(init=False)
 class NoInit:
     x: int = attr.ib()
-@attr.s(cmp=False)
+@attr.s(eq=False)
 class NoCmp:
     x: int = attr.ib()
 
diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test
--- a/test-data/unit/fine-grained.test
+++ b/test-data/unit/fine-grained.test
@@ -1005,7 +1005,7 @@ B(1, 2) < B(1, 2)
 [file b.py]
 from a import A
 import attr
-@attr.s(cmp=False)
+@attr.s(eq=False)
 class B(A):
     b = attr.ib()   # type: int
 [file a.py]
@@ -1016,7 +1016,7 @@ class A:
 
 [file a.py.2]
 import attr
-@attr.s(cmp=False, init=False)
+@attr.s(eq=False, init=False)
 class A:
     a = attr.ib()   # type: int
 [builtins fixtures/list.pyi]