Skip to content

Commit a143277

Browse files
committed
functools: Add functools.total_ordering.
This commit is the result of copying the total_ordering code and tests over from CPython v3.7.17. One test is disabled because it expects builtin objects to have attributes (__lt__, __gt__, etc.). Another test for compatibility with pickle is also disabled because pickle compatibility is currently broken. Bumped package version to 0.0.8. The functools code in CPython has the following credits: Written by Nick Coghlan <ncoghlan at gmail.com>, Raymond Hettinger <python at rcn.com>, and Łukasz Langa <lukasz at langa.pl>. Copyright (C) 2006-2013 Python Software Foundation. See C source code for _functools credits/copyright This work was donated by W Winfried Kretzschmar. Signed-off-by: W Winfried Kretzschmar <[email protected]>
1 parent 2242465 commit a143277

File tree

3 files changed

+359
-1
lines changed

3 files changed

+359
-1
lines changed

python-stdlib/functools/functools.py

+128
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,131 @@ def reduce(function, iterable, initializer=None):
2626
for element in it:
2727
value = function(value, element)
2828
return value
29+
30+
31+
################################################################################
32+
### total_ordering class decorator
33+
################################################################################
34+
# Extracted from the CPython v3.7.17 implementation of Lib/functools.py
35+
#
36+
# Written by Nick Coghlan <ncoghlan at gmail.com>,
37+
# Raymond Hettinger <python at rcn.com>,
38+
# and Łukasz Langa <lukasz at langa.pl>.
39+
# Copyright (C) 2006-2013 Python Software Foundation.
40+
# See C source code for _functools credits/copyright
41+
42+
# The total ordering functions all invoke the root magic method directly
43+
# rather than using the corresponding operator. This avoids possible
44+
# infinite recursion that could occur when the operator dispatch logic
45+
# detects a NotImplemented result and then calls a reflected method.
46+
47+
def _gt_from_lt(self, other, NotImplemented=NotImplemented):
48+
'Return a > b. Computed by @total_ordering from (not a < b) and (a != b).'
49+
op_result = self.__lt__(other)
50+
if op_result is NotImplemented:
51+
return op_result
52+
return not op_result and self != other
53+
54+
def _le_from_lt(self, other, NotImplemented=NotImplemented):
55+
'Return a <= b. Computed by @total_ordering from (a < b) or (a == b).'
56+
op_result = self.__lt__(other)
57+
return op_result or self == other
58+
59+
def _ge_from_lt(self, other, NotImplemented=NotImplemented):
60+
'Return a >= b. Computed by @total_ordering from (not a < b).'
61+
op_result = self.__lt__(other)
62+
if op_result is NotImplemented:
63+
return op_result
64+
return not op_result
65+
66+
def _ge_from_le(self, other, NotImplemented=NotImplemented):
67+
'Return a >= b. Computed by @total_ordering from (not a <= b) or (a == b).'
68+
op_result = self.__le__(other)
69+
if op_result is NotImplemented:
70+
return op_result
71+
return not op_result or self == other
72+
73+
def _lt_from_le(self, other, NotImplemented=NotImplemented):
74+
'Return a < b. Computed by @total_ordering from (a <= b) and (a != b).'
75+
op_result = self.__le__(other)
76+
if op_result is NotImplemented:
77+
return op_result
78+
return op_result and self != other
79+
80+
def _gt_from_le(self, other, NotImplemented=NotImplemented):
81+
'Return a > b. Computed by @total_ordering from (not a <= b).'
82+
op_result = self.__le__(other)
83+
if op_result is NotImplemented:
84+
return op_result
85+
return not op_result
86+
87+
def _lt_from_gt(self, other, NotImplemented=NotImplemented):
88+
'Return a < b. Computed by @total_ordering from (not a > b) and (a != b).'
89+
op_result = self.__gt__(other)
90+
if op_result is NotImplemented:
91+
return op_result
92+
return not op_result and self != other
93+
94+
def _ge_from_gt(self, other, NotImplemented=NotImplemented):
95+
'Return a >= b. Computed by @total_ordering from (a > b) or (a == b).'
96+
op_result = self.__gt__(other)
97+
return op_result or self == other
98+
99+
def _le_from_gt(self, other, NotImplemented=NotImplemented):
100+
'Return a <= b. Computed by @total_ordering from (not a > b).'
101+
op_result = self.__gt__(other)
102+
if op_result is NotImplemented:
103+
return op_result
104+
return not op_result
105+
106+
def _le_from_ge(self, other, NotImplemented=NotImplemented):
107+
'Return a <= b. Computed by @total_ordering from (not a >= b) or (a == b).'
108+
op_result = self.__ge__(other)
109+
if op_result is NotImplemented:
110+
return op_result
111+
return not op_result or self == other
112+
113+
def _gt_from_ge(self, other, NotImplemented=NotImplemented):
114+
'Return a > b. Computed by @total_ordering from (a >= b) and (a != b).'
115+
op_result = self.__ge__(other)
116+
if op_result is NotImplemented:
117+
return op_result
118+
return op_result and self != other
119+
120+
def _lt_from_ge(self, other, NotImplemented=NotImplemented):
121+
'Return a < b. Computed by @total_ordering from (not a >= b).'
122+
op_result = self.__ge__(other)
123+
if op_result is NotImplemented:
124+
return op_result
125+
return not op_result
126+
127+
_convert = {
128+
'__lt__': [('__gt__', _gt_from_lt),
129+
('__le__', _le_from_lt),
130+
('__ge__', _ge_from_lt)],
131+
'__le__': [('__ge__', _ge_from_le),
132+
('__lt__', _lt_from_le),
133+
('__gt__', _gt_from_le)],
134+
'__gt__': [('__lt__', _lt_from_gt),
135+
('__ge__', _ge_from_gt),
136+
('__le__', _le_from_gt)],
137+
'__ge__': [('__le__', _le_from_ge),
138+
('__gt__', _gt_from_ge),
139+
('__lt__', _lt_from_ge)]
140+
}
141+
142+
def total_ordering(cls):
143+
"""Class decorator that fills in missing ordering methods"""
144+
# Find user-defined comparisons (not those inherited from object).
145+
roots = {op for op in _convert if getattr(cls, op, None) is not getattr(object, op, None)}
146+
if not roots:
147+
raise ValueError('must define at least one ordering operation: < > <= >=')
148+
root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__
149+
for opname, opfunc in _convert[root]:
150+
if opname not in roots:
151+
# function objects have no attributes in micropython
152+
# opfunc.__name__ = opname
153+
setattr(cls, opname, opfunc)
154+
return cls
155+
156+

python-stdlib/functools/manifest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
metadata(version="0.0.7")
1+
metadata(version="0.0.8")
22

33
module("functools.py")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
""" Test code for functools.total_ordering
2+
3+
Copyright © 2001-2023 Python Software Foundation. All rights reserved.
4+
5+
This code was extracted from CPython v3.7.17 Lib/test/test_functools.py
6+
"""
7+
8+
import unittest
9+
10+
import functools
11+
12+
class TestTotalOrdering(unittest.TestCase):
13+
14+
def test_total_ordering_lt(self):
15+
@functools.total_ordering
16+
class A:
17+
def __init__(self, value):
18+
self.value = value
19+
def __lt__(self, other):
20+
return self.value < other.value
21+
def __eq__(self, other):
22+
return self.value == other.value
23+
self.assertTrue(A(1) < A(2))
24+
self.assertTrue(A(2) > A(1))
25+
self.assertTrue(A(1) <= A(2))
26+
self.assertTrue(A(2) >= A(1))
27+
self.assertTrue(A(2) <= A(2))
28+
self.assertTrue(A(2) >= A(2))
29+
self.assertFalse(A(1) > A(2))
30+
31+
def test_total_ordering_le(self):
32+
@functools.total_ordering
33+
class A:
34+
def __init__(self, value):
35+
self.value = value
36+
def __le__(self, other):
37+
return self.value <= other.value
38+
def __eq__(self, other):
39+
return self.value == other.value
40+
self.assertTrue(A(1) < A(2))
41+
self.assertTrue(A(2) > A(1))
42+
self.assertTrue(A(1) <= A(2))
43+
self.assertTrue(A(2) >= A(1))
44+
self.assertTrue(A(2) <= A(2))
45+
self.assertTrue(A(2) >= A(2))
46+
self.assertFalse(A(1) >= A(2))
47+
48+
def test_total_ordering_gt(self):
49+
@functools.total_ordering
50+
class A:
51+
def __init__(self, value):
52+
self.value = value
53+
def __gt__(self, other):
54+
return self.value > other.value
55+
def __eq__(self, other):
56+
return self.value == other.value
57+
self.assertTrue(A(1) < A(2))
58+
self.assertTrue(A(2) > A(1))
59+
self.assertTrue(A(1) <= A(2))
60+
self.assertTrue(A(2) >= A(1))
61+
self.assertTrue(A(2) <= A(2))
62+
self.assertTrue(A(2) >= A(2))
63+
self.assertFalse(A(2) < A(1))
64+
65+
def test_total_ordering_ge(self):
66+
@functools.total_ordering
67+
class A:
68+
def __init__(self, value):
69+
self.value = value
70+
def __ge__(self, other):
71+
return self.value >= other.value
72+
def __eq__(self, other):
73+
return self.value == other.value
74+
self.assertTrue(A(1) < A(2))
75+
self.assertTrue(A(2) > A(1))
76+
self.assertTrue(A(1) <= A(2))
77+
self.assertTrue(A(2) >= A(1))
78+
self.assertTrue(A(2) <= A(2))
79+
self.assertTrue(A(2) >= A(2))
80+
self.assertFalse(A(2) <= A(1))
81+
82+
# This test does not appear to work due to the lack of attributes on builtin types.
83+
# This appears to lead to the comparison operators not being inherited by default.
84+
# def test_total_ordering_no_overwrite(self):
85+
# # new methods should not overwrite existing
86+
# import sys
87+
# @functools.total_ordering
88+
# class A(int):
89+
# pass
90+
# self.assertTrue(A(1) < A(2))
91+
# self.assertTrue(A(2) > A(1))
92+
# self.assertTrue(A(1) <= A(2))
93+
# self.assertTrue(A(2) >= A(1))
94+
# self.assertTrue(A(2) <= A(2))
95+
# self.assertTrue(A(2) >= A(2))
96+
97+
def test_no_operations_defined(self):
98+
with self.assertRaises(ValueError):
99+
@functools.total_ordering
100+
class A:
101+
pass
102+
103+
def test_type_error_when_not_implemented(self):
104+
# bug 10042; ensure stack overflow does not occur
105+
# when decorated types return NotImplemented
106+
@functools.total_ordering
107+
class ImplementsLessThan:
108+
def __init__(self, value):
109+
self.value = value
110+
def __eq__(self, other):
111+
if isinstance(other, ImplementsLessThan):
112+
return self.value == other.value
113+
return False
114+
def __lt__(self, other):
115+
if isinstance(other, ImplementsLessThan):
116+
return self.value < other.value
117+
return NotImplemented
118+
119+
@functools.total_ordering
120+
class ImplementsGreaterThan:
121+
def __init__(self, value):
122+
self.value = value
123+
def __eq__(self, other):
124+
if isinstance(other, ImplementsGreaterThan):
125+
return self.value == other.value
126+
return False
127+
def __gt__(self, other):
128+
if isinstance(other, ImplementsGreaterThan):
129+
return self.value > other.value
130+
return NotImplemented
131+
132+
@functools.total_ordering
133+
class ImplementsLessThanEqualTo:
134+
def __init__(self, value):
135+
self.value = value
136+
def __eq__(self, other):
137+
if isinstance(other, ImplementsLessThanEqualTo):
138+
return self.value == other.value
139+
return False
140+
def __le__(self, other):
141+
if isinstance(other, ImplementsLessThanEqualTo):
142+
return self.value <= other.value
143+
return NotImplemented
144+
145+
@functools.total_ordering
146+
class ImplementsGreaterThanEqualTo:
147+
def __init__(self, value):
148+
self.value = value
149+
def __eq__(self, other):
150+
if isinstance(other, ImplementsGreaterThanEqualTo):
151+
return self.value == other.value
152+
return False
153+
def __ge__(self, other):
154+
if isinstance(other, ImplementsGreaterThanEqualTo):
155+
return self.value >= other.value
156+
return NotImplemented
157+
158+
@functools.total_ordering
159+
class ComparatorNotImplemented:
160+
def __init__(self, value):
161+
self.value = value
162+
def __eq__(self, other):
163+
if isinstance(other, ComparatorNotImplemented):
164+
return self.value == other.value
165+
return False
166+
def __lt__(self, other):
167+
return NotImplemented
168+
169+
with self.subTest("LT < 1"), self.assertRaises(TypeError):
170+
ImplementsLessThan(-1) < 1
171+
172+
with self.subTest("LT < LE"), self.assertRaises(TypeError):
173+
ImplementsLessThan(0) < ImplementsLessThanEqualTo(0)
174+
175+
with self.subTest("LT < GT"), self.assertRaises(TypeError):
176+
ImplementsLessThan(1) < ImplementsGreaterThan(1)
177+
178+
with self.subTest("LE <= LT"), self.assertRaises(TypeError):
179+
ImplementsLessThanEqualTo(2) <= ImplementsLessThan(2)
180+
181+
with self.subTest("LE <= GE"), self.assertRaises(TypeError):
182+
ImplementsLessThanEqualTo(3) <= ImplementsGreaterThanEqualTo(3)
183+
184+
with self.subTest("GT > GE"), self.assertRaises(TypeError):
185+
ImplementsGreaterThan(4) > ImplementsGreaterThanEqualTo(4)
186+
187+
with self.subTest("GT > LT"), self.assertRaises(TypeError):
188+
ImplementsGreaterThan(5) > ImplementsLessThan(5)
189+
190+
with self.subTest("GE >= GT"), self.assertRaises(TypeError):
191+
ImplementsGreaterThanEqualTo(6) >= ImplementsGreaterThan(6)
192+
193+
with self.subTest("GE >= LE"), self.assertRaises(TypeError):
194+
ImplementsGreaterThanEqualTo(7) >= ImplementsLessThanEqualTo(7)
195+
196+
with self.subTest("GE when equal"):
197+
a = ComparatorNotImplemented(8)
198+
b = ComparatorNotImplemented(8)
199+
self.assertEqual(a, b)
200+
with self.assertRaises(TypeError):
201+
a >= b
202+
203+
with self.subTest("LE when equal"):
204+
a = ComparatorNotImplemented(9)
205+
b = ComparatorNotImplemented(9)
206+
self.assertEqual(a, b)
207+
with self.assertRaises(TypeError):
208+
a <= b
209+
210+
# Leaving pickle support for a later date
211+
# def test_pickle(self):
212+
# for proto in range(pickle.HIGHEST_PROTOCOL + 1):
213+
# for name in '__lt__', '__gt__', '__le__', '__ge__':
214+
# with self.subTest(method=name, proto=proto):
215+
# method = getattr(Orderable_LT, name)
216+
# method_copy = pickle.loads(pickle.dumps(method, proto))
217+
# self.assertIs(method_copy, method)
218+
219+
# @functools.total_ordering
220+
# class Orderable_LT:
221+
# def __init__(self, value):
222+
# self.value = value
223+
# def __lt__(self, other):
224+
# return self.value < other.value
225+
# def __eq__(self, other):
226+
# return self.value == other.value
227+
228+
if __name__ == "__main__":
229+
unittest.main()
230+

0 commit comments

Comments
 (0)