Skip to content

Commit 47093eb

Browse files
eurestimsullivan
authored andcommitted
[attrs] Support attr.s(eq=..., order=...) (#7619)
attrs 19.2.0 added support for eq and order and deprecated to attr.s. It also deprecated cmp. Fixes #7615
1 parent 37e9a98 commit 47093eb

File tree

5 files changed

+145
-17
lines changed

5 files changed

+145
-17
lines changed

mypy/plugins/attrs.py

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,62 @@ def deserialize(cls, info: TypeInfo, data: JsonDict) -> 'Attribute':
176176
)
177177

178178

179+
def _determine_eq_order(ctx: 'mypy.plugin.ClassDefContext') -> Tuple[bool, bool]:
180+
"""
181+
Validate the combination of *cmp*, *eq*, and *order*. Derive the effective
182+
values of eq and order.
183+
"""
184+
cmp = _get_decorator_optional_bool_argument(ctx, 'cmp')
185+
eq = _get_decorator_optional_bool_argument(ctx, 'eq')
186+
order = _get_decorator_optional_bool_argument(ctx, 'order')
187+
188+
if cmp is not None and any((eq is not None, order is not None)):
189+
ctx.api.fail("Don't mix `cmp` with `eq' and `order`", ctx.reason)
190+
191+
# cmp takes precedence due to bw-compatibility.
192+
if cmp is not None:
193+
ctx.api.fail("cmp is deprecated, use eq and order", ctx.reason)
194+
return cmp, cmp
195+
196+
# If left None, equality is on and ordering mirrors equality.
197+
if eq is None:
198+
eq = True
199+
200+
if order is None:
201+
order = eq
202+
203+
if eq is False and order is True:
204+
ctx.api.fail("eq must be True if order is True", ctx.reason)
205+
206+
return eq, order
207+
208+
209+
def _get_decorator_optional_bool_argument(
210+
ctx: 'mypy.plugin.ClassDefContext',
211+
name: str,
212+
default: Optional[bool] = None,
213+
) -> Optional[bool]:
214+
"""Return the Optional[bool] argument for the decorator.
215+
216+
This handles both @decorator(...) and @decorator.
217+
"""
218+
if isinstance(ctx.reason, CallExpr):
219+
attr_value = _get_argument(ctx.reason, name)
220+
if attr_value:
221+
if isinstance(attr_value, NameExpr):
222+
if attr_value.fullname == 'builtins.True':
223+
return True
224+
if attr_value.fullname == 'builtins.False':
225+
return False
226+
if attr_value.fullname == 'builtins.None':
227+
return None
228+
ctx.api.fail('"{}" argument must be True or False.'.format(name), ctx.reason)
229+
return default
230+
return default
231+
else:
232+
return default
233+
234+
179235
def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
180236
auto_attribs_default: bool = False) -> None:
181237
"""Add necessary dunder methods to classes decorated with attr.s.
@@ -193,7 +249,8 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
193249

194250
init = _get_decorator_bool_argument(ctx, 'init', True)
195251
frozen = _get_frozen(ctx)
196-
cmp = _get_decorator_bool_argument(ctx, 'cmp', True)
252+
eq, order = _determine_eq_order(ctx)
253+
197254
auto_attribs = _get_decorator_bool_argument(ctx, 'auto_attribs', auto_attribs_default)
198255
kw_only = _get_decorator_bool_argument(ctx, 'kw_only', False)
199256

@@ -231,8 +288,10 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
231288
adder = MethodAdder(ctx)
232289
if init:
233290
_add_init(ctx, attributes, adder)
234-
if cmp:
235-
_add_cmp(ctx, adder)
291+
if eq:
292+
_add_eq(ctx, adder)
293+
if order:
294+
_add_order(ctx, adder)
236295
if frozen:
237296
_make_frozen(ctx, attributes)
238297

@@ -529,16 +588,22 @@ def _parse_assignments(
529588
return lvalues, rvalues
530589

531590

532-
def _add_cmp(ctx: 'mypy.plugin.ClassDefContext', adder: 'MethodAdder') -> None:
533-
"""Generate all the cmp methods for this class."""
591+
def _add_eq(ctx: 'mypy.plugin.ClassDefContext', adder: 'MethodAdder') -> None:
592+
"""Generate __eq__ and __ne__ for this class."""
534593
# For __ne__ and __eq__ the type is:
535594
# def __ne__(self, other: object) -> bool
536595
bool_type = ctx.api.named_type('__builtins__.bool')
537596
object_type = ctx.api.named_type('__builtins__.object')
538597
args = [Argument(Var('other', object_type), object_type, None, ARG_POS)]
539598
for method in ['__ne__', '__eq__']:
540599
adder.add_method(method, args, bool_type)
541-
# For the rest we use:
600+
601+
602+
def _add_order(ctx: 'mypy.plugin.ClassDefContext', adder: 'MethodAdder') -> None:
603+
"""Generate all the ordering methods for this class."""
604+
bool_type = ctx.api.named_type('__builtins__.bool')
605+
object_type = ctx.api.named_type('__builtins__.object')
606+
# Make the types be:
542607
# AT = TypeVar('AT')
543608
# def __lt__(self: AT, other: AT) -> bool
544609
# This way comparisons with subclasses will work correctly.

test-data/unit/check-attr.test

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,9 @@ A(1) != 1
214214
1 != A(1)
215215
[builtins fixtures/attr.pyi]
216216

217-
[case testAttrsCmpFalse]
217+
[case testAttrsEqFalse]
218218
from attr import attrib, attrs
219-
@attrs(auto_attribs=True, cmp=False)
219+
@attrs(auto_attribs=True, eq=False)
220220
class A:
221221
a: int
222222
reveal_type(A) # N: Revealed type is 'def (a: builtins.int) -> __main__.A'
@@ -245,6 +245,57 @@ A(1) != 1
245245
1 != A(1)
246246
[builtins fixtures/attr.pyi]
247247

248+
[case testAttrsOrderFalse]
249+
from attr import attrib, attrs
250+
@attrs(auto_attribs=True, order=False)
251+
class A:
252+
a: int
253+
reveal_type(A) # N: Revealed type is 'def (a: builtins.int) -> __main__.A'
254+
reveal_type(A.__eq__) # N: Revealed type is 'def (self: __main__.A, other: builtins.object) -> builtins.bool'
255+
reveal_type(A.__ne__) # N: Revealed type is 'def (self: __main__.A, other: builtins.object) -> builtins.bool'
256+
257+
A(1) < A(2) # E: Unsupported left operand type for < ("A")
258+
A(1) <= A(2) # E: Unsupported left operand type for <= ("A")
259+
A(1) > A(2) # E: Unsupported left operand type for > ("A")
260+
A(1) >= A(2) # E: Unsupported left operand type for >= ("A")
261+
A(1) == A(2)
262+
A(1) != A(2)
263+
264+
A(1) < 1 # E: Unsupported left operand type for < ("A")
265+
A(1) <= 1 # E: Unsupported left operand type for <= ("A")
266+
A(1) > 1 # E: Unsupported left operand type for > ("A")
267+
A(1) >= 1 # E: Unsupported left operand type for >= ("A")
268+
A(1) == 1
269+
A(1) != 1
270+
271+
1 < A(1) # E: Unsupported left operand type for < ("int")
272+
1 <= A(1) # E: Unsupported left operand type for <= ("int")
273+
1 > A(1) # E: Unsupported left operand type for > ("int")
274+
1 >= A(1) # E: Unsupported left operand type for >= ("int")
275+
1 == A(1)
276+
1 != A(1)
277+
[builtins fixtures/attr.pyi]
278+
279+
[case testAttrsCmpEqOrderValues]
280+
from attr import attrib, attrs
281+
@attrs(cmp=True) # E: cmp is deprecated, use eq and order
282+
class DeprecatedTrue:
283+
...
284+
285+
@attrs(cmp=False) # E: cmp is deprecated, use eq and order
286+
class DeprecatedFalse:
287+
...
288+
289+
@attrs(cmp=False, eq=True) # E: Don't mix `cmp` with `eq' and `order` # E: cmp is deprecated, use eq and order
290+
class Mixed:
291+
...
292+
293+
@attrs(order=True, eq=False) # E: eq must be True if order is True
294+
class Confused:
295+
...
296+
[builtins fixtures/attr.pyi]
297+
298+
248299
[case testAttrsInheritance]
249300
import attr
250301
@attr.s

test-data/unit/check-incremental.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2937,7 +2937,7 @@ class Frozen:
29372937
@attr.s(init=False)
29382938
class NoInit:
29392939
x: int = attr.ib()
2940-
@attr.s(cmp=False)
2940+
@attr.s(eq=False)
29412941
class NoCmp:
29422942
x: int = attr.ib()
29432943

test-data/unit/fine-grained.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,7 +1005,7 @@ B(1, 2) < B(1, 2)
10051005
[file b.py]
10061006
from a import A
10071007
import attr
1008-
@attr.s(cmp=False)
1008+
@attr.s(eq=False)
10091009
class B(A):
10101010
b = attr.ib() # type: int
10111011
[file a.py]
@@ -1016,7 +1016,7 @@ class A:
10161016

10171017
[file a.py.2]
10181018
import attr
1019-
@attr.s(cmp=False, init=False)
1019+
@attr.s(eq=False, init=False)
10201020
class A:
10211021
a = attr.ib() # type: int
10221022
[builtins fixtures/list.pyi]

test-data/unit/lib-stub/attr.pyi

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ _ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]]
1313
def attrib(default: None = ...,
1414
validator: None = ...,
1515
repr: bool = ...,
16-
cmp: bool = ...,
16+
cmp: Optional[bool] = ...,
1717
hash: Optional[bool] = ...,
1818
init: bool = ...,
1919
convert: None = ...,
@@ -22,13 +22,15 @@ def attrib(default: None = ...,
2222
converter: None = ...,
2323
factory: None = ...,
2424
kw_only: bool = ...,
25+
eq: Optional[bool] = ...,
26+
order: Optional[bool] = ...,
2527
) -> Any: ...
2628
# This form catches an explicit None or no default and infers the type from the other arguments.
2729
@overload
2830
def attrib(default: None = ...,
2931
validator: Optional[_ValidatorArgType[_T]] = ...,
3032
repr: bool = ...,
31-
cmp: bool = ...,
33+
cmp: Optional[bool] = ...,
3234
hash: Optional[bool] = ...,
3335
init: bool = ...,
3436
convert: Optional[_ConverterType[_T]] = ...,
@@ -37,13 +39,15 @@ def attrib(default: None = ...,
3739
converter: Optional[_ConverterType[_T]] = ...,
3840
factory: Optional[Callable[[], _T]] = ...,
3941
kw_only: bool = ...,
42+
eq: Optional[bool] = ...,
43+
order: Optional[bool] = ...,
4044
) -> _T: ...
4145
# This form catches an explicit default argument.
4246
@overload
4347
def attrib(default: _T,
4448
validator: Optional[_ValidatorArgType[_T]] = ...,
4549
repr: bool = ...,
46-
cmp: bool = ...,
50+
cmp: Optional[bool] = ...,
4751
hash: Optional[bool] = ...,
4852
init: bool = ...,
4953
convert: Optional[_ConverterType[_T]] = ...,
@@ -52,13 +56,15 @@ def attrib(default: _T,
5256
converter: Optional[_ConverterType[_T]] = ...,
5357
factory: Optional[Callable[[], _T]] = ...,
5458
kw_only: bool = ...,
59+
eq: Optional[bool] = ...,
60+
order: Optional[bool] = ...,
5561
) -> _T: ...
5662
# This form covers type=non-Type: e.g. forward references (str), Any
5763
@overload
5864
def attrib(default: Optional[_T] = ...,
5965
validator: Optional[_ValidatorArgType[_T]] = ...,
6066
repr: bool = ...,
61-
cmp: bool = ...,
67+
cmp: Optional[bool] = ...,
6268
hash: Optional[bool] = ...,
6369
init: bool = ...,
6470
convert: Optional[_ConverterType[_T]] = ...,
@@ -67,14 +73,16 @@ def attrib(default: Optional[_T] = ...,
6773
converter: Optional[_ConverterType[_T]] = ...,
6874
factory: Optional[Callable[[], _T]] = ...,
6975
kw_only: bool = ...,
76+
eq: Optional[bool] = ...,
77+
order: Optional[bool] = ...,
7078
) -> Any: ...
7179

7280
@overload
7381
def attrs(maybe_cls: _C,
7482
these: Optional[Mapping[str, Any]] = ...,
7583
repr_ns: Optional[str] = ...,
7684
repr: bool = ...,
77-
cmp: bool = ...,
85+
cmp: Optional[bool] = ...,
7886
hash: Optional[bool] = ...,
7987
init: bool = ...,
8088
slots: bool = ...,
@@ -84,13 +92,15 @@ def attrs(maybe_cls: _C,
8492
auto_attribs: bool = ...,
8593
kw_only: bool = ...,
8694
cache_hash: bool = ...,
95+
eq: Optional[bool] = ...,
96+
order: Optional[bool] = ...,
8797
) -> _C: ...
8898
@overload
8999
def attrs(maybe_cls: None = ...,
90100
these: Optional[Mapping[str, Any]] = ...,
91101
repr_ns: Optional[str] = ...,
92102
repr: bool = ...,
93-
cmp: bool = ...,
103+
cmp: Optional[bool] = ...,
94104
hash: Optional[bool] = ...,
95105
init: bool = ...,
96106
slots: bool = ...,
@@ -100,6 +110,8 @@ def attrs(maybe_cls: None = ...,
100110
auto_attribs: bool = ...,
101111
kw_only: bool = ...,
102112
cache_hash: bool = ...,
113+
eq: Optional[bool] = ...,
114+
order: Optional[bool] = ...,
103115
) -> Callable[[_C], _C]: ...
104116

105117

0 commit comments

Comments
 (0)