From 59a4a93f43f2fa4f44b03ce0c1816eb2ae089fb9 Mon Sep 17 00:00:00 2001 From: Stefan Krah Date: Wed, 16 Jan 2013 12:58:59 +0100 Subject: Issue #16422: Use strings for rounding mode constants for better readability and pickling compatibility. --- Lib/test/test_decimal.py | 119 +++++++++++++++++------------------- Misc/NEWS | 3 + Modules/_decimal/_decimal.c | 120 +++++++++++++++++-------------------- Modules/_decimal/tests/deccheck.py | 36 +++++------ 4 files changed, 126 insertions(+), 152 deletions(-) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index e138709..37637d6 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -78,14 +78,20 @@ def assert_signals(cls, context, attr, expected): d = getattr(context, attr) cls.assertTrue(all(d[s] if s in expected else not d[s] for s in d)) -RoundingModes = { - C: (C.ROUND_UP, C.ROUND_DOWN, C.ROUND_CEILING, C.ROUND_FLOOR, - C.ROUND_HALF_UP, C.ROUND_HALF_DOWN, C.ROUND_HALF_EVEN, - C.ROUND_05UP) if C else None, - P: (P.ROUND_UP, P.ROUND_DOWN, P.ROUND_CEILING, P.ROUND_FLOOR, - P.ROUND_HALF_UP, P.ROUND_HALF_DOWN, P.ROUND_HALF_EVEN, - P.ROUND_05UP) -} +ROUND_UP = P.ROUND_UP +ROUND_DOWN = P.ROUND_DOWN +ROUND_CEILING = P.ROUND_CEILING +ROUND_FLOOR = P.ROUND_FLOOR +ROUND_HALF_UP = P.ROUND_HALF_UP +ROUND_HALF_DOWN = P.ROUND_HALF_DOWN +ROUND_HALF_EVEN = P.ROUND_HALF_EVEN +ROUND_05UP = P.ROUND_05UP + +RoundingModes = [ + ROUND_UP, ROUND_DOWN, ROUND_CEILING, ROUND_FLOOR, + ROUND_HALF_UP, ROUND_HALF_DOWN, ROUND_HALF_EVEN, + ROUND_05UP +] # Tests are built around these assumed context defaults. # test_main() restores the original context. @@ -96,7 +102,7 @@ ORIGINAL_CONTEXT = { def init(m): if not m: return DefaultTestContext = m.Context( - prec=9, rounding=m.ROUND_HALF_EVEN, traps=dict.fromkeys(Signals[m], 0) + prec=9, rounding=ROUND_HALF_EVEN, traps=dict.fromkeys(Signals[m], 0) ) m.setcontext(DefaultTestContext) @@ -229,14 +235,14 @@ class IBMTestCases(unittest.TestCase): 'xor':'logical_xor'} # Map test-case names to roundings. - self.RoundingDict = {'ceiling' : self.decimal.ROUND_CEILING, - 'down' : self.decimal.ROUND_DOWN, - 'floor' : self.decimal.ROUND_FLOOR, - 'half_down' : self.decimal.ROUND_HALF_DOWN, - 'half_even' : self.decimal.ROUND_HALF_EVEN, - 'half_up' : self.decimal.ROUND_HALF_UP, - 'up' : self.decimal.ROUND_UP, - '05up' : self.decimal.ROUND_05UP} + self.RoundingDict = {'ceiling' : ROUND_CEILING, + 'down' : ROUND_DOWN, + 'floor' : ROUND_FLOOR, + 'half_down' : ROUND_HALF_DOWN, + 'half_even' : ROUND_HALF_EVEN, + 'half_up' : ROUND_HALF_UP, + 'up' : ROUND_UP, + '05up' : ROUND_05UP} # Map the test cases' error names to the actual errors. self.ErrorNames = {'clamped' : self.decimal.Clamped, @@ -2101,9 +2107,6 @@ class UsabilityTest(unittest.TestCase): Inexact = self.decimal.Inexact Rounded = self.decimal.Rounded Clamped = self.decimal.Clamped - ROUND_HALF_EVEN = self.decimal.ROUND_HALF_EVEN - ROUND_DOWN = self.decimal.ROUND_DOWN - ROUND_UP = self.decimal.ROUND_UP with localcontext(Context()) as c: c.prec = 7 @@ -2430,7 +2433,6 @@ class PythonAPItests(unittest.TestCase): def test_int(self): Decimal = self.decimal.Decimal - ROUND_DOWN = self.decimal.ROUND_DOWN for x in range(-250, 250): s = '%0.2f' % (x / 100.0) @@ -2448,7 +2450,6 @@ class PythonAPItests(unittest.TestCase): def test_trunc(self): Decimal = self.decimal.Decimal - ROUND_DOWN = self.decimal.ROUND_DOWN for x in range(-250, 250): s = '%0.2f' % (x / 100.0) @@ -2491,8 +2492,6 @@ class PythonAPItests(unittest.TestCase): def test_create_decimal_from_float(self): Decimal = self.decimal.Decimal Context = self.decimal.Context - ROUND_DOWN = self.decimal.ROUND_DOWN - ROUND_UP = self.decimal.ROUND_UP Inexact = self.decimal.Inexact context = Context(prec=5, rounding=ROUND_DOWN) @@ -2522,7 +2521,6 @@ class PythonAPItests(unittest.TestCase): Decimal = self.decimal.Decimal Context = self.decimal.Context InvalidOperation = self.decimal.InvalidOperation - ROUND_DOWN = self.decimal.ROUND_DOWN c = Context(Emax=99999, Emin=-99999) self.assertEqual( @@ -2723,7 +2721,6 @@ class ContextAPItests(unittest.TestCase): InvalidOperation = self.decimal.InvalidOperation DivisionByZero = self.decimal.DivisionByZero Overflow = self.decimal.Overflow - ROUND_HALF_EVEN = self.decimal.ROUND_HALF_EVEN c1 = Context() c2 = Context(prec=None, rounding=None, Emax=None, Emin=None, @@ -2739,6 +2736,21 @@ class ContextAPItests(unittest.TestCase): assert_signals(self, c, 'traps', [InvalidOperation, DivisionByZero, Overflow]) + @cpython_only + def test_from_legacy_strings(self): + import _testcapi + c = self.decimal.Context() + + for rnd in RoundingModes: + c.rounding = _testcapi.unicode_legacy_string(rnd) + self.assertEqual(c.rounding, rnd) + + s = _testcapi.unicode_legacy_string('') + self.assertRaises(TypeError, setattr, c, 'rounding', s) + + s = _testcapi.unicode_legacy_string('ROUND_\x00UP') + self.assertRaises(TypeError, setattr, c, 'rounding', s) + def test_pickle(self): Context = self.decimal.Context @@ -2762,7 +2774,7 @@ class ContextAPItests(unittest.TestCase): # Test interchangeability combinations = [(C, P), (P, C)] if C else [(P, P)] for dumper, loader in combinations: - for ri, _ in enumerate(RoundingModes[dumper]): + for ri, _ in enumerate(RoundingModes): for fi, _ in enumerate(OrderedSignals[dumper]): for ti, _ in enumerate(OrderedSignals[dumper]): @@ -2776,7 +2788,7 @@ class ContextAPItests(unittest.TestCase): sys.modules['decimal'] = dumper c = dumper.Context( prec=prec, Emin=emin, Emax=emax, - rounding=RoundingModes[dumper][ri], + rounding=RoundingModes[ri], capitals=caps, clamp=clamp, flags=OrderedSignals[dumper][:fi], traps=OrderedSignals[dumper][:ti] @@ -2791,7 +2803,7 @@ class ContextAPItests(unittest.TestCase): self.assertEqual(d.prec, prec) self.assertEqual(d.Emin, emin) self.assertEqual(d.Emax, emax) - self.assertEqual(d.rounding, RoundingModes[loader][ri]) + self.assertEqual(d.rounding, RoundingModes[ri]) self.assertEqual(d.capitals, caps) self.assertEqual(d.clamp, clamp) assert_signals(self, d, 'flags', OrderedSignals[loader][:fi]) @@ -3593,7 +3605,6 @@ class ContextFlags(unittest.TestCase): Underflow = self.decimal.Underflow Clamped = self.decimal.Clamped Subnormal = self.decimal.Subnormal - ROUND_HALF_EVEN = self.decimal.ROUND_HALF_EVEN def raise_error(context, flag): if self.decimal == C: @@ -3960,17 +3971,6 @@ class ContextInputValidation(unittest.TestCase): self.assertRaises(ValueError, setattr, c, 'Emin', 1) self.assertRaises(TypeError, setattr, c, 'Emin', (1,2,3)) - # rounding: always raise TypeError in order to get consistent - # exceptions across implementations. In decimal, rounding - # modes are strings, in _decimal they are integers. The idea - # is to view rounding as an abstract type and not mind the - # implementation details. - # Hence, a user should view the rounding modes as if they - # had been defined in a language that supports abstract - # data types, e.g. ocaml: - # - # type rounding = ROUND_DOWN | ROUND_HALF_UP | ... ;; - # self.assertRaises(TypeError, setattr, c, 'rounding', -1) self.assertRaises(TypeError, setattr, c, 'rounding', 9) self.assertRaises(TypeError, setattr, c, 'rounding', 1.0) @@ -4023,8 +4023,6 @@ class ContextSubclassing(unittest.TestCase): decimal = self.decimal Decimal = decimal.Decimal Context = decimal.Context - ROUND_HALF_EVEN = decimal.ROUND_HALF_EVEN - ROUND_DOWN = decimal.ROUND_DOWN Clamped = decimal.Clamped DivisionByZero = decimal.DivisionByZero Inexact = decimal.Inexact @@ -4192,7 +4190,7 @@ class Coverage(unittest.TestCase): c.prec = 425000000 c.Emax = 425000000 c.Emin = -425000000 - c.rounding = self.decimal.ROUND_HALF_DOWN + c.rounding = ROUND_HALF_DOWN c.capitals = 0 c.clamp = 1 for sig in OrderedSignals[self.decimal]: @@ -4584,7 +4582,6 @@ class PyWhitebox(unittest.TestCase): def test_py_rescale(self): # Coverage Decimal = P.Decimal - ROUND_UP = P.ROUND_UP localcontext = P.localcontext with localcontext() as c: @@ -4594,7 +4591,6 @@ class PyWhitebox(unittest.TestCase): def test_py__round(self): # Coverage Decimal = P.Decimal - ROUND_UP = P.ROUND_UP self.assertRaises(ValueError, Decimal("3.1234")._round, 0, ROUND_UP) @@ -4663,11 +4659,6 @@ class CFunctionality(unittest.TestCase): self.assertEqual(C.DECIMAL128, 128) self.assertEqual(C.IEEE_CONTEXT_MAX_BITS, 512) - # Rounding modes - for i, v in enumerate(RoundingModes[C]): - self.assertEqual(v, i) - self.assertEqual(C.ROUND_TRUNC, 8) - # Conditions for i, v in enumerate(cond): self.assertEqual(v, 1<val)); } + /* Init string constants */ + for (i = 0; i < _PY_DEC_ROUND_GUARD; i++) { + ASSIGN_PTR(round_map[i], PyUnicode_InternFromString(mpd_round_string[i])); + Py_INCREF(round_map[i]); + CHECK_INT(PyModule_AddObject(m, mpd_round_string[i], round_map[i])); + } + /* Add specification version number */ CHECK_INT(PyModule_AddStringConstant(m, "__version__", " 1.70")); diff --git a/Modules/_decimal/tests/deccheck.py b/Modules/_decimal/tests/deccheck.py index 751ba57..a2853ad 100644 --- a/Modules/_decimal/tests/deccheck.py +++ b/Modules/_decimal/tests/deccheck.py @@ -158,17 +158,9 @@ CondMap = { C.FloatOperation: P.FloatOperation, } -RoundMap = { - C.ROUND_UP: P.ROUND_UP, - C.ROUND_DOWN: P.ROUND_DOWN, - C.ROUND_CEILING: P.ROUND_CEILING, - C.ROUND_FLOOR: P.ROUND_FLOOR, - C.ROUND_HALF_UP: P.ROUND_HALF_UP, - C.ROUND_HALF_DOWN: P.ROUND_HALF_DOWN, - C.ROUND_HALF_EVEN: P.ROUND_HALF_EVEN, - C.ROUND_05UP: P.ROUND_05UP -} -RoundModes = RoundMap.items() +RoundModes = [C.ROUND_UP, C.ROUND_DOWN, C.ROUND_CEILING, C.ROUND_FLOOR, + C.ROUND_HALF_UP, C.ROUND_HALF_DOWN, C.ROUND_HALF_EVEN, + C.ROUND_05UP] class Context(object): @@ -183,7 +175,7 @@ class Context(object): self.p.prec = self.c.prec self.p.Emin = self.c.Emin self.p.Emax = self.c.Emax - self.p.rounding = RoundMap[self.c.rounding] + self.p.rounding = self.c.rounding self.p.capitals = self.c.capitals self.settraps([sig for sig in self.c.traps if self.c.traps[sig]]) self.setstatus([sig for sig in self.c.flags if self.c.flags[sig]]) @@ -217,12 +209,12 @@ class Context(object): self.p.Emax = val def getround(self): - assert(self.c.rounding == RoundMap[self.p.rounding]) + assert(self.c.rounding == self.p.rounding) return self.c.rounding def setround(self, val): self.c.rounding = val - self.p.rounding = RoundMap[val] + self.p.rounding = val def getcapitals(self): assert(self.c.capitals == self.p.capitals) @@ -627,8 +619,12 @@ def convert(t, convstr=True): context.clear_status() - if not t.contextfunc and i == 0 or \ - convstr and isinstance(op, str): + if op in RoundModes: + t.cop.append(op) + t.pop.append(op) + + elif not t.contextfunc and i == 0 or \ + convstr and isinstance(op, str): try: c = C.Decimal(op) cex = None @@ -662,10 +658,6 @@ def convert(t, convstr=True): t.cop.append(op.c) t.pop.append(op.p) - elif op in RoundModes: - t.cop.append(op[0]) - t.pop.append(op[1]) - else: t.cop.append(op) t.pop.append(op) @@ -809,7 +801,7 @@ def test_method(method, testspecs, testfunc): log(" prec: %d emin: %d emax: %d", (context.prec, context.Emin, context.Emax)) restr_range = 9999 if context.Emax > 9999 else context.Emax+99 - for rounding in sorted(RoundMap): + for rounding in RoundModes: context.rounding = rounding context.capitals = random.randrange(2) if spec['clamp'] == 'rand': @@ -941,7 +933,7 @@ def test_round(method, prec, exprange, restricted_range, itr, stat): def test_from_float(method, prec, exprange, restricted_range, itr, stat): """Iterate the __float__ method through many test cases.""" - for rounding in sorted(RoundMap): + for rounding in RoundModes: context.rounding = rounding for i in range(1000): f = randfloat() -- cgit v0.12