Skip to content

Fix cmp function to act more like python2 cmp function #573

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 22, 2021
73 changes: 70 additions & 3 deletions src/past/builtins/misc.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import unicode_literals

import inspect
import math
import numbers

from future.utils import PY2, PY3, exec_

Expand Down Expand Up @@ -29,8 +31,67 @@ def cmp(x, y):
cmp(x, y) -> integer

Return negative if x<y, zero if x==y, positive if x>y.
Python2 had looser comparison allowing cmp None and non Numerical types and collections.
Try to match the old behavior
"""
return (x > y) - (x < y)
if isinstance(x, set) and isinstance(y, set):
raise TypeError('cannot compare sets using cmp()',)
try:
if isinstance(x, numbers.Number) and math.isnan(x):
if not isinstance(y, numbers.Number):
raise TypeError('cannot compare float("nan"), {type_y} with cmp'.format(type_y=type(y)))
if isinstance(y, int):
return 1
else:
return -1
if isinstance(y, numbers.Number) and math.isnan(y):
if not isinstance(x, numbers.Number):
raise TypeError('cannot compare {type_x}, float("nan") with cmp'.format(type_x=type(x)))
if isinstance(x, int):
return -1
else:
return 1
return (x > y) - (x < y)
except TypeError:
if x == y:
return 0
type_order = [
type(None),
numbers.Number,
dict, list,
set,
(str, bytes),
]
x_type_index = y_type_index = None
for i, type_match in enumerate(type_order):
if isinstance(x, type_match):
x_type_index = i
if isinstance(y, type_match):
y_type_index = i
if cmp(x_type_index, y_type_index) == 0:
if isinstance(x, bytes) and isinstance(y, str):
return cmp(x.decode('ascii'), y)
if isinstance(y, bytes) and isinstance(x, str):
return cmp(x, y.decode('ascii'))
elif isinstance(x, list):
# if both arguments are lists take the comparison of the first non equal value
for x_elem, y_elem in zip(x, y):
elem_cmp_val = cmp(x_elem, y_elem)
if elem_cmp_val != 0:
return elem_cmp_val
# if all elements are equal, return equal/0
return 0
elif isinstance(x, dict):
if len(x) != len(y):
return cmp(len(x), len(y))
else:
x_key = min(a for a in x if a not in y or x[a] != y[a])
y_key = min(b for b in y if b not in x or x[b] != y[b])
if x_key != y_key:
return cmp(x_key, y_key)
else:
return cmp(x[x_key], y[y_key])
return cmp(x_type_index, y_type_index)

from sys import intern

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

raw_input = input
from imp import reload

try:
from importlib import reload
except ImportError:
# for python2, python3 <= 3.4
from imp import reload

unicode = str
unichr = chr
xrange = range
Expand Down Expand Up @@ -82,7 +149,7 @@ def execfile(filename, myglobals=None, mylocals=None):
if not isinstance(mylocals, Mapping):
raise TypeError('locals must be a mapping')
with open(filename, "rb") as fin:
source = fin.read()
source = fin.read()
code = compile(source, filename, "exec")
exec_(code, myglobals, mylocals)

Expand Down
53 changes: 53 additions & 0 deletions tests/test_past/test_misc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""
Tests for the resurrected Py2-like cmp function
"""

from __future__ import absolute_import, unicode_literals, print_function

import os.path
import sys
import traceback
from contextlib import contextmanager

from future.tests.base import unittest
from future.utils import PY3, PY26

if PY3:
from past.builtins import cmp

_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(_dir)
import test_values


@contextmanager
def empty_context_manager(*args, **kwargs):
yield dict(args=args, kwargs=kwargs)


class TestCmp(unittest.TestCase):
def test_cmp(self):
for x, y, cmp_python2_value in test_values.cmp_python2_value:
if PY26:
# set cmp works a bit differently in 2.6, we try to emulate 2.7 behavior, so skip set cmp tests
if isinstance(x, set) or isinstance(y, set):
continue
# to get this to run on python <3.4 which lacks subTest
with getattr(self, 'subTest', empty_context_manager)(x=x, y=y):
try:
past_cmp_value = cmp(x, y)
except Exception:
past_cmp_value = traceback.format_exc().strip().split('\n')[-1]

self.assertEqual(cmp_python2_value, past_cmp_value,
"expected result matching python2 __builtins__.cmp({x!r},{y!r}) "
"== {cmp_python2_value} "
"got past.builtins.cmp({x!r},{y!r}) "
"== {past_cmp_value} "
"".format(x=x, y=y, past_cmp_value=past_cmp_value,
cmp_python2_value=cmp_python2_value))


if __name__ == '__main__':
unittest.main()
Loading