Skip to content

Commit bcb8513

Browse files
authored
Merge pull request #573 from rtaycher/fix-cmp-function
Fix cmp function to act more like python2 cmp function
2 parents 4657ad2 + ddedcb9 commit bcb8513

File tree

3 files changed

+348
-3
lines changed

3 files changed

+348
-3
lines changed

Diff for: src/past/builtins/misc.py

+70-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from __future__ import unicode_literals
22

33
import inspect
4+
import math
5+
import numbers
46

57
from future.utils import PY2, PY3, exec_
68

@@ -29,8 +31,67 @@ def cmp(x, y):
2931
cmp(x, y) -> integer
3032
3133
Return negative if x<y, zero if x==y, positive if x>y.
34+
Python2 had looser comparison allowing cmp None and non Numerical types and collections.
35+
Try to match the old behavior
3236
"""
33-
return (x > y) - (x < y)
37+
if isinstance(x, set) and isinstance(y, set):
38+
raise TypeError('cannot compare sets using cmp()',)
39+
try:
40+
if isinstance(x, numbers.Number) and math.isnan(x):
41+
if not isinstance(y, numbers.Number):
42+
raise TypeError('cannot compare float("nan"), {type_y} with cmp'.format(type_y=type(y)))
43+
if isinstance(y, int):
44+
return 1
45+
else:
46+
return -1
47+
if isinstance(y, numbers.Number) and math.isnan(y):
48+
if not isinstance(x, numbers.Number):
49+
raise TypeError('cannot compare {type_x}, float("nan") with cmp'.format(type_x=type(x)))
50+
if isinstance(x, int):
51+
return -1
52+
else:
53+
return 1
54+
return (x > y) - (x < y)
55+
except TypeError:
56+
if x == y:
57+
return 0
58+
type_order = [
59+
type(None),
60+
numbers.Number,
61+
dict, list,
62+
set,
63+
(str, bytes),
64+
]
65+
x_type_index = y_type_index = None
66+
for i, type_match in enumerate(type_order):
67+
if isinstance(x, type_match):
68+
x_type_index = i
69+
if isinstance(y, type_match):
70+
y_type_index = i
71+
if cmp(x_type_index, y_type_index) == 0:
72+
if isinstance(x, bytes) and isinstance(y, str):
73+
return cmp(x.decode('ascii'), y)
74+
if isinstance(y, bytes) and isinstance(x, str):
75+
return cmp(x, y.decode('ascii'))
76+
elif isinstance(x, list):
77+
# if both arguments are lists take the comparison of the first non equal value
78+
for x_elem, y_elem in zip(x, y):
79+
elem_cmp_val = cmp(x_elem, y_elem)
80+
if elem_cmp_val != 0:
81+
return elem_cmp_val
82+
# if all elements are equal, return equal/0
83+
return 0
84+
elif isinstance(x, dict):
85+
if len(x) != len(y):
86+
return cmp(len(x), len(y))
87+
else:
88+
x_key = min(a for a in x if a not in y or x[a] != y[a])
89+
y_key = min(b for b in y if b not in x or x[b] != y[b])
90+
if x_key != y_key:
91+
return cmp(x_key, y_key)
92+
else:
93+
return cmp(x[x_key], y[y_key])
94+
return cmp(x_type_index, y_type_index)
3495

3596
from sys import intern
3697

@@ -42,7 +103,13 @@ def oct(number):
42103
return '0' + builtins.oct(number)[2:]
43104

44105
raw_input = input
45-
from imp import reload
106+
107+
try:
108+
from importlib import reload
109+
except ImportError:
110+
# for python2, python3 <= 3.4
111+
from imp import reload
112+
46113
unicode = str
47114
unichr = chr
48115
xrange = range
@@ -82,7 +149,7 @@ def execfile(filename, myglobals=None, mylocals=None):
82149
if not isinstance(mylocals, Mapping):
83150
raise TypeError('locals must be a mapping')
84151
with open(filename, "rb") as fin:
85-
source = fin.read()
152+
source = fin.read()
86153
code = compile(source, filename, "exec")
87154
exec_(code, myglobals, mylocals)
88155

Diff for: tests/test_past/test_misc.py

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Tests for the resurrected Py2-like cmp function
4+
"""
5+
6+
from __future__ import absolute_import, unicode_literals, print_function
7+
8+
import os.path
9+
import sys
10+
import traceback
11+
from contextlib import contextmanager
12+
13+
from future.tests.base import unittest
14+
from future.utils import PY3, PY26
15+
16+
if PY3:
17+
from past.builtins import cmp
18+
19+
_dir = os.path.dirname(os.path.abspath(__file__))
20+
sys.path.append(_dir)
21+
import test_values
22+
23+
24+
@contextmanager
25+
def empty_context_manager(*args, **kwargs):
26+
yield dict(args=args, kwargs=kwargs)
27+
28+
29+
class TestCmp(unittest.TestCase):
30+
def test_cmp(self):
31+
for x, y, cmp_python2_value in test_values.cmp_python2_value:
32+
if PY26:
33+
# set cmp works a bit differently in 2.6, we try to emulate 2.7 behavior, so skip set cmp tests
34+
if isinstance(x, set) or isinstance(y, set):
35+
continue
36+
# to get this to run on python <3.4 which lacks subTest
37+
with getattr(self, 'subTest', empty_context_manager)(x=x, y=y):
38+
try:
39+
past_cmp_value = cmp(x, y)
40+
except Exception:
41+
past_cmp_value = traceback.format_exc().strip().split('\n')[-1]
42+
43+
self.assertEqual(cmp_python2_value, past_cmp_value,
44+
"expected result matching python2 __builtins__.cmp({x!r},{y!r}) "
45+
"== {cmp_python2_value} "
46+
"got past.builtins.cmp({x!r},{y!r}) "
47+
"== {past_cmp_value} "
48+
"".format(x=x, y=y, past_cmp_value=past_cmp_value,
49+
cmp_python2_value=cmp_python2_value))
50+
51+
52+
if __name__ == '__main__':
53+
unittest.main()

0 commit comments

Comments
 (0)