From d9c0a7ae94ef81e69d927d834131bad760d4d99d Mon Sep 17 00:00:00 2001
From: Raymond Hettinger <python@rcn.com>
Date: Sat, 3 Jul 2004 10:02:28 +0000
Subject: Work through several open todos: * Added test for pickling contexts *
 Renamed ExceptionList to Signals (to match wording in the spec) * Simplified
 Context constructor by allowing flags=None to automatically   generate a
 zeroed-out flags dictionary. * inlined _convertString() which was used only
 once * _rounding_decision is private, so excluded its contants from __all__.
 * added an XXX comment with concerns about subclassing signals results in   a
 deviation from the spec (maybe important, maybe not). * Taught the test_suite
 to determine its own directory (modeled after code   in regrtest.py). 
 Enables it to be run when the current directory is not   the test directory.
 * Added a clear_flags() method to the Context API to make it easier to do   a
 common operation with flags. * Fixed the trap_enablers defaults in
 BasicDefaultContext to match the spec.

---
 Lib/decimal.py           | 75 +++++++++++++++++++++---------------------------
 Lib/test/test_decimal.py | 30 ++++++++++++++-----
 2 files changed, 55 insertions(+), 50 deletions(-)

diff --git a/Lib/decimal.py b/Lib/decimal.py
index 5565052..973f110 100644
--- a/Lib/decimal.py
+++ b/Lib/decimal.py
@@ -9,13 +9,8 @@
 
 
 # Todo:
-#    Add deepcopy and pickle support for contexts
-#    Consider having a SimpleDecimal subclass implementing X3.274 semantics
-#    Improve the Context API
-#         Especially with respect to setting flags and traps
-#         Consider adding a clear_flags() method to Context
 #    Provide a clean way of attaching monetary format representations
-#    Review all exposed constants for utility vs. namespace clutter
+#    Make tests independent of DefaultContext.prec == 9
 
 
 """
@@ -139,8 +134,7 @@ __all__ = [
     # Constants for use in setting up contexts
     'ROUND_DOWN', 'ROUND_HALF_UP', 'ROUND_HALF_EVEN', 'ROUND_CEILING',
     'ROUND_FLOOR', 'ROUND_UP', 'ROUND_HALF_DOWN',
-    'NEVER_ROUND', 'ALWAYS_ROUND',
-    'ExceptionList',    # <-- Used for building trap/flag dictionaries
+    'Signals',    # <-- Used for building trap/flag dictionaries
 
     # Functions for manipulating contexts
     'setcontext', 'getcontext',
@@ -244,6 +238,12 @@ class InvalidOperation(DecimalException):
                 return Decimal( (args[1]._sign, args[1]._int, 'n') )
         return NaN
 
+# XXX Is there a logic error in subclassing InvalidOperation?
+# Setting the InvalidOperation trap to zero does not preclude ConversionSyntax.
+# Also, incrementing Conversion syntax flag will not increment InvalidOperation.
+# Both of these issues interfere with cross-language portability because
+# code following the spec would not know about the Python subclasses.
+
 class ConversionSyntax(InvalidOperation):
     """Trying to convert badly formed string.
 
@@ -410,8 +410,8 @@ def _filterfunc(obj):
     except TypeError:
         return False
 
-#ExceptionList holds the exceptions
-ExceptionList = filter(_filterfunc, globals().values())
+#Signals holds the exceptions
+Signals = filter(_filterfunc, globals().values())
 
 del _filterfunc
 
@@ -492,7 +492,10 @@ class Decimal(object):
                 self._sign = sign
                 self._int = tuple(map(int, diag)) #Diagnostic info
                 return
-            self._convertString(value, context)
+            try:
+                self._sign, self._int, self._exp = _string2exact(value)
+            except ValueError:
+                self._sign, self._int, self._exp = context._raise_error(ConversionSyntax)
             return
 
         # tuple/list conversion (possibly from as_tuple())
@@ -594,19 +597,6 @@ class Decimal(object):
             return other
         return 0
 
-    def _convertString(self, value, context=None):
-        """Changes self's value to that in a string.
-
-        A bad string causes a ConversionSyntax error.
-        """
-        if context is None:
-            context = getcontext()
-        try:
-            self._sign, self._int, self._exp = _string2exact(value)
-        except ValueError:
-            self._sign, self._int, self._exp = context._raise_error(ConversionSyntax)
-        return
-
     def __nonzero__(self):
         """Is the number non-zero?
 
@@ -1433,8 +1423,6 @@ class Decimal(object):
 
     def __int__(self):
         """Converts self to a int, truncating if necessary."""
-        # XXX This should be implemented in terms of tested
-        # functions in the standard
         if self._isnan():
             context = getcontext()
             return context._raise_error(InvalidContext)
@@ -2115,6 +2103,8 @@ class Decimal(object):
             return self     # My components are also immutable
         return self.__class__(str(self))
 
+##### Context class ###########################################
+
 
 # get rounding method function:
 rounding_functions = [name for name in Decimal.__dict__.keys() if name.startswith('_round_')]
@@ -2152,6 +2142,8 @@ class Context(object):
                  Emin=DEFAULT_MIN_EXPONENT, Emax=DEFAULT_MAX_EXPONENT,
                  capitals=1, _clamp=0,
                  _ignored_flags=[]):
+        if flags is None:
+            flags = dict.fromkeys(Signals, 0)
         DefaultLock.acquire()
         for name, val in locals().items():
             if val is None:
@@ -2161,13 +2153,17 @@ class Context(object):
         DefaultLock.release()
         del self.self
 
+    def clear_flags(self):
+        """Reset all flags to zero"""
+        for flag in self.flags:
+            self.flag = 0
+
     def copy(self):
         """Returns a copy from self."""
         nc = Context(self.prec, self.rounding, self.trap_enablers, self.flags,
                          self._rounding_decision, self.Emin, self.Emax,
                          self.capitals, self._clamp, self._ignored_flags)
         return nc
-    __copy__ = copy
 
     def _raise_error(self, error, explanation = None, *args):
         """Handles an error
@@ -2192,7 +2188,7 @@ class Context(object):
 
     def _ignore_all_flags(self):
         """Ignore all flags, if they are raised"""
-        return self._ignore_flags(*ExceptionList)
+        return self._ignore_flags(*Signals)
 
     def _ignore_flags(self, *flags):
         """Ignore the flags, if they are raised"""
@@ -2959,20 +2955,16 @@ def isnan(num):
 
 ##### Setup Specific Contexts ################################
 
-def _zero_exceptions():
-    "Helper function mapping all exceptions to zero."
-    d = {}
-    for exception in ExceptionList:
-        d[exception] = 0
-    return d
+_basic_traps = dict.fromkeys(Signals, 1)
+_basic_traps.update({Inexact:0, Rounded:0, Subnormal:0})
 
 # The default context prototype used by Context()
 # Is mutable, so than new contexts can have different default values
 
 DefaultContext = Context(
         prec=SINGLE_PRECISION, rounding=ROUND_HALF_EVEN,
-        trap_enablers=_zero_exceptions(),
-        flags=_zero_exceptions(),
+        trap_enablers=dict.fromkeys(Signals, 0),
+        flags=None,
         _rounding_decision=ALWAYS_ROUND,
 )
 
@@ -2981,25 +2973,22 @@ DefaultContext = Context(
 # contexts and be able to reproduce results from other implementations
 # of the spec.
 
-_basic_traps = _zero_exceptions()
-_basic_traps.update({Inexact:1, Rounded:1, Subnormal:1})
-
 BasicDefaultContext = Context(
         prec=9, rounding=ROUND_HALF_UP,
         trap_enablers=_basic_traps,
-        flags=_zero_exceptions(),
+        flags=None,
         _rounding_decision=ALWAYS_ROUND,
 )
 
 ExtendedDefaultContext = Context(
         prec=SINGLE_PRECISION, rounding=ROUND_HALF_EVEN,
-        trap_enablers=_zero_exceptions(),
-        flags=_zero_exceptions(),
+        trap_enablers=dict.fromkeys(Signals, 0),
+        flags=None,
         _rounding_decision=ALWAYS_ROUND,
 )
 
 
-##### Useful Constants (internal use only######################
+##### Useful Constants (internal use only) ####################
 
 #Reusable defaults
 Inf = Decimal('Inf')
diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py
index b3fb0ad..6da658f 100644
--- a/Lib/test/test_decimal.py
+++ b/Lib/test/test_decimal.py
@@ -35,7 +35,12 @@ from test.test_support import TestSkipped, run_unittest, run_doctest, is_resourc
 import threading
 
 TESTDATADIR = 'decimaltestdata'
-dir = os.curdir + os.sep + TESTDATADIR + os.sep
+if __name__ == '__main__':
+    file = sys.argv[0]
+else:
+    file = __file__
+testdir = os.path.dirname(file) or os.curdir
+dir = testdir + os.sep + TESTDATADIR + os.sep
 
 skip_expected = not os.path.isdir(dir)
 
@@ -190,7 +195,7 @@ class DecimalTest(unittest.TestCase):
         quote = 0
         theirexceptions = [ErrorNames[x.lower()] for x in exceptions]
 
-        for exception in ExceptionList:
+        for exception in Signals:
             self.context.trap_enablers[exception] = 1 #Catch these bugs...
         for exception in theirexceptions:
             self.context.trap_enablers[exception] = 0
@@ -212,7 +217,7 @@ class DecimalTest(unittest.TestCase):
                             funct(self.context.create_decimal(v))
                         except error:
                             pass
-                        except ExceptionList, e:
+                        except Signals, e:
                             self.fail("Raised %s in %s when %s disabled" % \
                                       (e, s, error))
                         else:
@@ -232,7 +237,7 @@ class DecimalTest(unittest.TestCase):
                     funct(*vals)
                 except error:
                     pass
-                except ExceptionList, e:
+                except Signals, e:
                     self.fail("Raised %s in %s when %s disabled" % \
                               (e, s, error))
                 else:
@@ -242,7 +247,7 @@ class DecimalTest(unittest.TestCase):
             result = str(funct(*vals))
             if fname == 'same_quantum':
                 result = str(int(eval(result))) # 'True', 'False' -> '1', '0'
-        except ExceptionList, error:
+        except Signals, error:
             self.fail("Raised %s in %s" % (error, s))
         except: #Catch any error long enough to state the test case.
             print "ERROR:", s
@@ -263,13 +268,13 @@ class DecimalTest(unittest.TestCase):
 
     def getexceptions(self):
         L = []
-        for exception in ExceptionList:
+        for exception in Signals:
             if self.context.flags[exception]:
                 L.append(exception)
         return L
 
     def resetflags(self):
-        for exception in ExceptionList:
+        for exception in Signals:
             self.context.flags[exception] = 0
 
     def change_precision(self, prec):
@@ -1046,6 +1051,16 @@ class DecimalPythonAPItests(unittest.TestCase):
         e = pickle.loads(p)
         self.assertEqual(d, e)
 
+class ContextAPItests(unittest.TestCase):
+
+    def test_pickle(self):
+        c = Context()
+        e = pickle.loads(pickle.dumps(c))
+        for k in vars(c):
+            v1 = vars(c)[k]
+            v2 = vars(e)[k]
+            self.assertEqual(v1, v2)
+
 def test_main(arith=False, verbose=None):
     """ Execute the tests.
 
@@ -1059,6 +1074,7 @@ def test_main(arith=False, verbose=None):
         DecimalUseOfContextTest,
         DecimalUsabilityTest,
         DecimalPythonAPItests,
+        ContextAPItests,
     ]
 
     if arith or is_resource_enabled('decimal'):
-- 
cgit v0.12

