From ffa03f6566dd1ba8caba3ffe65f76dce239ba958 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 13 Sep 2016 00:22:14 +0100 Subject: [PATCH 1/6] [py23] add round2 and round3 function for simulating Python 2 and Python 3 built-in round MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The implementation is inspired by https://github.com/PythonCharmers/python-future/blob/master/src/future/builtins/newround.py It adds supportĀ for the old Python 2 round, and for negative 'ndigits' --- Lib/fontTools/misc/py23.py | 84 +++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/misc/py23.py b/Lib/fontTools/misc/py23.py index f013d6153..890447202 100644 --- a/Lib/fontTools/misc/py23.py +++ b/Lib/fontTools/misc/py23.py @@ -6,7 +6,8 @@ import sys __all__ = ['basestring', 'unicode', 'unichr', 'byteord', 'bytechr', 'BytesIO', 'StringIO', 'UnicodeIO', 'strjoin', 'bytesjoin', 'tobytes', 'tostr', - 'tounicode', 'Tag', 'open', 'range', 'xrange', 'Py23Error'] + 'tounicode', 'Tag', 'open', 'range', 'xrange', 'round2', 'round3', + 'Py23Error'] class Py23Error(NotImplementedError): @@ -255,6 +256,87 @@ def xrange(*args, **kwargs): raise Py23Error("'xrange' is not defined. Use 'range' instead.") +import decimal as _decimal + + +def round2(number, ndigits=None): + """ + See Python 2 documentation. + + Rounds a number to a given precision in decimal digits (default + 0 digits). The result is a floating point number. Values are rounded + to the closest multiple of 10 to the power minus ndigits; if two + multiples are equally close, rounding is done away from 0. + + ndigits may be negative. + """ + if ndigits is None: + ndigits = 0 + elif hasattr(ndigits, '__index__'): + # any type with an __index__ method should be permitted as + # a second argument + ndigits = ndigits.__index__() + + if ndigits < 0: + exponent = 10 ** (-ndigits) + quotient, remainder = divmod(number, exponent) + if remainder >= exponent//2 and number >= 0: + quotient += 1 + return float(quotient * exponent) + else: + exponent = _decimal.Decimal('10') ** (-ndigits) + + d = _decimal.Decimal.from_float(number).quantize( + exponent, rounding=_decimal.ROUND_HALF_UP) + + return float(d) + + +def round3(number, ndigits=None): + """ + See Python 3 documentation: uses Banker's Rounding. + + Delegates to the __round__ method if for some reason this exists. + + If not, rounds a number to a given precision in decimal digits (default + 0 digits). This returns an int when called with one argument, + otherwise the same type as the number. ndigits may be negative. + + ndigits may be negative. + + Derived from python-future: + https://github.com/PythonCharmers/python-future/blob/master/src/future/builtins/newround.py + """ + return_int = False + if ndigits is None: + return_int = True + ndigits = 0 + + if hasattr(number, '__round__'): + d = number.__round__(ndigits) + return int(d) if return_int else float(d) + + if hasattr(ndigits, '__index__'): + # any type with an __index__ method should be permitted as + # a second argument + ndigits = ndigits.__index__() + + if ndigits < 0: + exponent = 10 ** (-ndigits) + quotient, remainder = divmod(number, exponent) + half = exponent//2 + if remainder > half or (remainder == half and quotient % 2 != 0): + quotient += 1 + d = quotient * exponent + else: + exponent = _decimal.Decimal('10') ** (-ndigits) + + d = _decimal.Decimal.from_float(number).quantize( + exponent, rounding=_decimal.ROUND_HALF_EVEN) + + return int(d) if return_int else float(d) + + import logging From c7edcfec302e474d0daff34f99c4c94632f8bb0c Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 13 Sep 2016 00:23:32 +0100 Subject: [PATCH 2/6] [py23_test] borrow round() test cases from cpython 2.7 test suite --- Lib/fontTools/misc/py23_test.py | 183 ++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/Lib/fontTools/misc/py23_test.py b/Lib/fontTools/misc/py23_test.py index 2aab6593a..759be3520 100644 --- a/Lib/fontTools/misc/py23_test.py +++ b/Lib/fontTools/misc/py23_test.py @@ -65,5 +65,188 @@ class OpenFuncWrapperTest(unittest.TestCase): self.assertEqual(result, expected) +class Round2Test(unittest.TestCase): + """ + Test cases taken from cpython 2.7 test suite: + + https://github.com/python/cpython/blob/2.7/Lib/test/test_float.py#L748 + + Excludes the test cases that are not supported when using the `decimal` + module's `quantize` method. + """ + + def test_second_argument_type(self): + # any type with an __index__ method should be permitted as + # a second argument + self.assertAlmostEqual(round2(12.34, True), 12.3) + + class MyIndex(object): + def __index__(self): return 4 + self.assertAlmostEqual(round2(-0.123456, MyIndex()), -0.1235) + # but floats should be illegal + self.assertRaises(TypeError, round2, 3.14159, 2.0) + + def test_halfway_cases(self): + # Halfway cases need special attention, since the current + # implementation has to deal with them specially. Note that + # 2.x rounds halfway values up (i.e., away from zero) while + # 3.x does round-half-to-even. + self.assertAlmostEqual(round2(0.125, 2), 0.13) + self.assertAlmostEqual(round2(0.375, 2), 0.38) + self.assertAlmostEqual(round2(0.625, 2), 0.63) + self.assertAlmostEqual(round2(0.875, 2), 0.88) + self.assertAlmostEqual(round2(-0.125, 2), -0.13) + self.assertAlmostEqual(round2(-0.375, 2), -0.38) + self.assertAlmostEqual(round2(-0.625, 2), -0.63) + self.assertAlmostEqual(round2(-0.875, 2), -0.88) + + self.assertAlmostEqual(round2(0.25, 1), 0.3) + self.assertAlmostEqual(round2(0.75, 1), 0.8) + self.assertAlmostEqual(round2(-0.25, 1), -0.3) + self.assertAlmostEqual(round2(-0.75, 1), -0.8) + + self.assertEqual(round2(-6.5, 0), -7.0) + self.assertEqual(round2(-5.5, 0), -6.0) + self.assertEqual(round2(-1.5, 0), -2.0) + self.assertEqual(round2(-0.5, 0), -1.0) + self.assertEqual(round2(0.5, 0), 1.0) + self.assertEqual(round2(1.5, 0), 2.0) + self.assertEqual(round2(2.5, 0), 3.0) + self.assertEqual(round2(3.5, 0), 4.0) + self.assertEqual(round2(4.5, 0), 5.0) + self.assertEqual(round2(5.5, 0), 6.0) + self.assertEqual(round2(6.5, 0), 7.0) + + # same but without an explicit second argument; in 3.x these + # will give integers + self.assertEqual(round2(-6.5), -7.0) + self.assertEqual(round2(-5.5), -6.0) + self.assertEqual(round2(-1.5), -2.0) + self.assertEqual(round2(-0.5), -1.0) + self.assertEqual(round2(0.5), 1.0) + self.assertEqual(round2(1.5), 2.0) + self.assertEqual(round2(2.5), 3.0) + self.assertEqual(round2(3.5), 4.0) + self.assertEqual(round2(4.5), 5.0) + self.assertEqual(round2(5.5), 6.0) + self.assertEqual(round2(6.5), 7.0) + + self.assertEqual(round2(-25.0, -1), -30.0) + self.assertEqual(round2(-15.0, -1), -20.0) + self.assertEqual(round2(-5.0, -1), -10.0) + self.assertEqual(round2(5.0, -1), 10.0) + self.assertEqual(round2(15.0, -1), 20.0) + self.assertEqual(round2(25.0, -1), 30.0) + self.assertEqual(round2(35.0, -1), 40.0) + self.assertEqual(round2(45.0, -1), 50.0) + self.assertEqual(round2(55.0, -1), 60.0) + self.assertEqual(round2(65.0, -1), 70.0) + self.assertEqual(round2(75.0, -1), 80.0) + self.assertEqual(round2(85.0, -1), 90.0) + self.assertEqual(round2(95.0, -1), 100.0) + self.assertEqual(round2(12325.0, -1), 12330.0) + self.assertEqual(round2(0, -1), 0.0) + + self.assertEqual(round2(350.0, -2), 400.0) + self.assertEqual(round2(450.0, -2), 500.0) + + self.assertAlmostEqual(round2(0.5e21, -21), 1e21) + self.assertAlmostEqual(round2(1.5e21, -21), 2e21) + self.assertAlmostEqual(round2(2.5e21, -21), 3e21) + self.assertAlmostEqual(round2(5.5e21, -21), 6e21) + self.assertAlmostEqual(round2(8.5e21, -21), 9e21) + + self.assertAlmostEqual(round2(-1.5e22, -22), -2e22) + self.assertAlmostEqual(round2(-0.5e22, -22), -1e22) + self.assertAlmostEqual(round2(0.5e22, -22), 1e22) + self.assertAlmostEqual(round2(1.5e22, -22), 2e22) + + +class Round3Test(unittest.TestCase): + """ Same as above but results adapted for Python 3 round() """ + + def test_second_argument_type(self): + # any type with an __index__ method should be permitted as + # a second argument + self.assertAlmostEqual(round3(12.34, True), 12.3) + + class MyIndex(object): + def __index__(self): return 4 + self.assertAlmostEqual(round3(-0.123456, MyIndex()), -0.1235) + # but floats should be illegal + self.assertRaises(TypeError, round3, 3.14159, 2.0) + + def test_halfway_cases(self): + self.assertAlmostEqual(round3(0.125, 2), 0.12) + self.assertAlmostEqual(round3(0.375, 2), 0.38) + self.assertAlmostEqual(round3(0.625, 2), 0.62) + self.assertAlmostEqual(round3(0.875, 2), 0.88) + self.assertAlmostEqual(round3(-0.125, 2), -0.12) + self.assertAlmostEqual(round3(-0.375, 2), -0.38) + self.assertAlmostEqual(round3(-0.625, 2), -0.62) + self.assertAlmostEqual(round3(-0.875, 2), -0.88) + + self.assertAlmostEqual(round3(0.25, 1), 0.2) + self.assertAlmostEqual(round3(0.75, 1), 0.8) + self.assertAlmostEqual(round3(-0.25, 1), -0.2) + self.assertAlmostEqual(round3(-0.75, 1), -0.8) + + self.assertEqual(round3(-6.5, 0), -6.0) + self.assertEqual(round3(-5.5, 0), -6.0) + self.assertEqual(round3(-1.5, 0), -2.0) + self.assertEqual(round3(-0.5, 0), 0.0) + self.assertEqual(round3(0.5, 0), 0.0) + self.assertEqual(round3(1.5, 0), 2.0) + self.assertEqual(round3(2.5, 0), 2.0) + self.assertEqual(round3(3.5, 0), 4.0) + self.assertEqual(round3(4.5, 0), 4.0) + self.assertEqual(round3(5.5, 0), 6.0) + self.assertEqual(round3(6.5, 0), 6.0) + + # same but without an explicit second argument; in 2.x these + # will give floats + self.assertEqual(round3(-6.5), -6) + self.assertEqual(round3(-5.5), -6) + self.assertEqual(round3(-1.5), -2.0) + self.assertEqual(round3(-0.5), 0) + self.assertEqual(round3(0.5), 0) + self.assertEqual(round3(1.5), 2) + self.assertEqual(round3(2.5), 2) + self.assertEqual(round3(3.5), 4) + self.assertEqual(round3(4.5), 4) + self.assertEqual(round3(5.5), 6) + self.assertEqual(round3(6.5), 6) + + self.assertEqual(round3(-25.0, -1), -20.0) + self.assertEqual(round3(-15.0, -1), -20.0) + self.assertEqual(round3(-5.0, -1), 0.0) + self.assertEqual(round3(5.0, -1), 0.0) + self.assertEqual(round3(15.0, -1), 20.0) + self.assertEqual(round3(25.0, -1), 20.0) + self.assertEqual(round3(35.0, -1), 40.0) + self.assertEqual(round3(45.0, -1), 40.0) + self.assertEqual(round3(55.0, -1), 60.0) + self.assertEqual(round3(65.0, -1), 60.0) + self.assertEqual(round3(75.0, -1), 80.0) + self.assertEqual(round3(85.0, -1), 80.0) + self.assertEqual(round3(95.0, -1), 100.0) + self.assertEqual(round3(12325.0, -1), 12320.0) + self.assertEqual(round3(0, -1), 0.0) + + self.assertEqual(round3(350.0, -2), 400.0) + self.assertEqual(round3(450.0, -2), 400.0) + + self.assertAlmostEqual(round3(0.5e21, -21), 0.0) + self.assertAlmostEqual(round3(1.5e21, -21), 2e21) + self.assertAlmostEqual(round3(2.5e21, -21), 2e21) + self.assertAlmostEqual(round3(5.5e21, -21), 6e21) + self.assertAlmostEqual(round3(8.5e21, -21), 8e21) + + self.assertAlmostEqual(round3(-1.5e22, -22), -2e22) + self.assertAlmostEqual(round3(-0.5e22, -22), 0.0) + self.assertAlmostEqual(round3(0.5e22, -22), 0.0) + self.assertAlmostEqual(round3(1.5e22, -22), 2e22) + + if __name__ == "__main__": unittest.main() From b22f8c7310cbf79916480a0517f12a12d8e33284 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 13 Sep 2016 07:12:44 +0100 Subject: [PATCH 3/6] [py23] in PY2 shadow built-in round with round3; in PY3 simply use built-in round --- Lib/fontTools/misc/py23.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lib/fontTools/misc/py23.py b/Lib/fontTools/misc/py23.py index 890447202..f6dfbde88 100644 --- a/Lib/fontTools/misc/py23.py +++ b/Lib/fontTools/misc/py23.py @@ -14,6 +14,10 @@ class Py23Error(NotImplementedError): pass +PY3 = sys.version_info[0] == 3 +PY2 = sys.version_info[0] == 2 + + try: basestring = basestring except NameError: @@ -337,6 +341,13 @@ def round3(number, ndigits=None): return int(d) if return_int else float(d) +if PY2: + round = round3 +else: + import builtins + round = builtins.round + + import logging From f07c29c8bcc565bbb5d922422fa479f1e4153067 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 13 Sep 2016 07:16:12 +0100 Subject: [PATCH 4/6] [py23] export 'round' in __all__ (let's see if some of our tests that import * breaks now...) --- Lib/fontTools/misc/py23.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontTools/misc/py23.py b/Lib/fontTools/misc/py23.py index f6dfbde88..67764eb94 100644 --- a/Lib/fontTools/misc/py23.py +++ b/Lib/fontTools/misc/py23.py @@ -7,7 +7,7 @@ import sys __all__ = ['basestring', 'unicode', 'unichr', 'byteord', 'bytechr', 'BytesIO', 'StringIO', 'UnicodeIO', 'strjoin', 'bytesjoin', 'tobytes', 'tostr', 'tounicode', 'Tag', 'open', 'range', 'xrange', 'round2', 'round3', - 'Py23Error'] + 'round', 'Py23Error'] class Py23Error(NotImplementedError): From 8de2f44b31138ae4a81e8dbae2a185a618498519 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 13 Sep 2016 18:44:30 +0200 Subject: [PATCH 5/6] [py23] don't export 'round2' and 'round3' in __all__, only 'round' No need to pollute the namespace. If one needs one or the other, one can just import that explicitly. --- Lib/fontTools/misc/py23.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/fontTools/misc/py23.py b/Lib/fontTools/misc/py23.py index 67764eb94..48f506af1 100644 --- a/Lib/fontTools/misc/py23.py +++ b/Lib/fontTools/misc/py23.py @@ -6,8 +6,7 @@ import sys __all__ = ['basestring', 'unicode', 'unichr', 'byteord', 'bytechr', 'BytesIO', 'StringIO', 'UnicodeIO', 'strjoin', 'bytesjoin', 'tobytes', 'tostr', - 'tounicode', 'Tag', 'open', 'range', 'xrange', 'round2', 'round3', - 'round', 'Py23Error'] + 'tounicode', 'Tag', 'open', 'range', 'xrange', 'round', 'Py23Error'] class Py23Error(NotImplementedError): From 152c6d81b38ecc616f38664d16c8d1db7e80ba7f Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 13 Sep 2016 18:48:18 +0200 Subject: [PATCH 6/6] [py23_test] import round2 and round3 in py23_test module --- Lib/fontTools/misc/py23_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/fontTools/misc/py23_test.py b/Lib/fontTools/misc/py23_test.py index 759be3520..d5aa23ad2 100644 --- a/Lib/fontTools/misc/py23_test.py +++ b/Lib/fontTools/misc/py23_test.py @@ -8,6 +8,8 @@ import sys import os import unittest +from fontTools.misc.py23 import round2, round3 + PIPE_SCRIPT = """\ import sys