1# SPDX-License-Identifier: MIT

2

3

4 import functools

5 import types

6

7 from ._make import _make_ne

8

9

10_operation_names = {"eq": "==", "lt": "<", "le": "<=", "gt": ">", "ge": ">="}

11

12

13 def cmp_using(

14 eq=None,

15 lt=None,

16 le=None,

17 gt=None,

18 ge=None,

19 require_same_type=True,

20 class_name="Comparable",

21):

22 """

23 Create a class that can be passed into `attr.ib`'s ``eq``, ``order``, and

24 ``cmp`` arguments to customize field comparison.

25

26 The resulting class will have a full set of ordering methods if

27 at least one of ``{lt, le, gt, ge}`` and ``eq`` are provided.

28

29 :param Optional[callable] eq: `callable` used to evaluate equality

30 of two objects.

31 :param Optional[callable] lt: `callable` used to evaluate whether

32 one object is less than another object.

33 :param Optional[callable] le: `callable` used to evaluate whether

34 one object is less than or equal to another object.

35 :param Optional[callable] gt: `callable` used to evaluate whether

36 one object is greater than another object.

37 :param Optional[callable] ge: `callable` used to evaluate whether

38 one object is greater than or equal to another object.

39

40 :param bool require_same_type: When `True`, equality and ordering methods

41 will return `NotImplemented` if objects are not of the same type.

42

43 :param Optional[str] class_name: Name of class. Defaults to 'Comparable'.

44

45 See `comparison` for more details.

46

47 .. versionadded:: 21.1.0

48 """

49

50 body = {

51 "__slots__": ["value"],

52 "__init__": _make_init(),

53 "_requirements": [],

54 "_is_comparable_to": _is_comparable_to,

55 }

56

57 # Add operations.

58 num_order_functions = 0

59 has_eq_function = False

60

61 if eq is not None:

62 has_eq_function = True

63 body["__eq__"] = _make_operator("eq", eq)

64 body["__ne__"] = _make_ne()

65

66 if lt is not None:

67 num_order_functions += 1

68 body["__lt__"] = _make_operator("lt", lt)

69

70 if le is not None:

71 num_order_functions += 1

72 body["__le__"] = _make_operator("le", le)

73

74 if gt is not None:

75 num_order_functions += 1

76 body["__gt__"] = _make_operator("gt", gt)

77

78 if ge is not None:

79 num_order_functions += 1

80 body["__ge__"] = _make_operator("ge", ge)

81

82 type_ = types.new_class(

83 class_name, (object,), {}, lambda ns: ns.update(body)

84 )

85

86 # Add same type requirement.

87 if require_same_type:

88 type_._requirements.append(_check_same_type)

89

90 # Add total ordering if at least one operation was defined.

91 if 0 < num_order_functions < 4:

92 if not has_eq_function:

93 # functools.total_ordering requires __eq__ to be defined,

94 # so raise early error here to keep a nice stack.

95 raise ValueError(

96 "eq must be define is order to complete ordering from "

97 "lt, le, gt, ge."

98 )

99 type_ = functools.total_ordering(type_)

100

101 return type_

102

103

104 def _make_init():

105 """

106 Create __init__ method.

107 """

108

109 def __init__(self, value):

110 """

111 Initialize object with *value*.

112 """

113 self.value = value

114

115 return __init__

116

117

118 def _make_operator(name, func):

119 """

120 Create operator method.

121 """

122

123 def method(self, other):

124 if not self._is_comparable_to(other):

125 return NotImplemented

126

127 result = func(self.value, other.value)

128 if result is NotImplemented:

129 return NotImplemented

130

131 return result

132

133 method.__name__ = f"__{name}__"

134 method.__doc__ = (

135 f"Return a {_operation_names[name]} b. Computed by attrs."

136 )

137

138 return method

139

140

141 def _is_comparable_to(self, other):

142 """

143 Check whether `other` is comparable to `self`.

144 """

145 for func in self._requirements:

146 if not func(self, other):

147 return False

148 return True

149

150

151 def _check_same_type(self, other):

152 """

153 Return True if *self* and *other* are of the same type, False otherwise.

154 """

155 return other.value.__class__ is self.value.__class__