diff --git a/CHANGES.txt b/CHANGES.txt index 5ff7ec7..dc5d58f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -7,6 +7,11 @@ Next release - Allow the use of 'missing=None' for Number. See https://github.com/Pylons/colander/pull/59 . +- Create a ``colander.Money`` type that is a Decimal type with + two-decimal-point precision rounded-up. + +- Allow ``quant`` and ``rounding`` args to ``colander.Decimal`` constructor. + 0.9.8 (2012-04-27) ------------------ diff --git a/colander/__init__.py b/colander/__init__.py index a37b273..904c6bd 100644 --- a/colander/__init__.py +++ b/colander/__init__.py @@ -1000,8 +1000,42 @@ class Float(Number): num = float class Decimal(Number): - """ A type representing a decimal floating point. Deserialization - returns an instance of the Python ``decimal.Decimal`` type. + """ + A type representing a decimal floating point. Deserialization returns an + instance of the Python ``decimal.Decimal`` type. + + If the :attr:`colander.null` value is passed to the serialize + method of this class, the :attr:`colander.null` value will be + returned. + + The Decimal constructor takes two optional arguments, ``quant`` and + ``rounding``. If supplied, ``quant`` should be a string. If supplied, + ``rounding`` should be one of the Python ``decimal`` module rounding + options (e.g. ``decimal.ROUND_UP``, ``decimal.ROUND_DOWN``, etc). The + serialized and deserialized result will be quantized and rounded via + ``result.quantize(decimal.Decimal(quant), rounding)``. ``rounding`` is + ignored if ``quant`` is not supplied. + + The subnodes of the :class:`colander.SchemaNode` that wraps + this type are ignored. + """ + def __init__(self, quant=None, rounding=None): + if quant is None: + self.quant = None + else: + self.quant = decimal.Decimal(quant) + self.rounding = rounding + + def num(self, val): + result = decimal.Decimal(str(val)) + if self.quant is not None: + result = result.quantize(self.quant, self.rounding) + return result + +class Money(Decimal): + """ A type representing a money value with two digit precision. + Deserialization returns an instance of the Python ``decimal.Decimal`` + type (quantized to two decimal places, rounded up). If the :attr:`colander.null` value is passed to the serialize method of this class, the :attr:`colander.null` value will be @@ -1010,8 +1044,9 @@ class Decimal(Number): The subnodes of the :class:`colander.SchemaNode` that wraps this type are ignored. """ - def num(self, val): - return decimal.Decimal(str(val)) + def __init__(self): + self.quant = decimal.Decimal('.01') + self.rounding = decimal.ROUND_UP class Boolean(SchemaType): """ A type representing a boolean object. diff --git a/colander/tests/test_colander.py b/colander/tests/test_colander.py index 229c97f..72b8b9f 100644 --- a/colander/tests/test_colander.py +++ b/colander/tests/test_colander.py @@ -1249,9 +1249,9 @@ class TestFloat(unittest.TestCase): self.assertEqual(result, '1.0') class TestDecimal(unittest.TestCase): - def _makeOne(self): + def _makeOne(self, quant=None, rounding=None): from colander import Decimal - return Decimal() + return Decimal(quant, rounding) def test_serialize_null(self): import colander @@ -1266,8 +1266,22 @@ class TestDecimal(unittest.TestCase): val = '' node = DummySchemaNode(None) typ = self._makeOne() - result = typ.deserialize(node, val) - self.assertEqual(result, colander.null) + self.assertRaises(colander.Invalid, typ.serialize, node, val) + + def test_serialize_quantize_no_rounding(self): + val = '.000001' + node = DummySchemaNode(None) + typ = self._makeOne('.01') + result = typ.serialize(node, val) + self.assertEqual(result, '0.00') + + def test_serialize_quantize_with_rounding_up(self): + import decimal + val = '.000001' + node = DummySchemaNode(None) + typ = self._makeOne('.01', decimal.ROUND_UP) + result = typ.serialize(node, val) + self.assertEqual(result, '0.01') def test_deserialize_fails(self): val = 'P' @@ -1284,6 +1298,14 @@ class TestDecimal(unittest.TestCase): result = typ.deserialize(node, val) self.assertEqual(result, decimal.Decimal('1.0')) + def test_deserialize_with_quantize(self): + import decimal + val = '1.00000001' + node = DummySchemaNode(None) + typ = self._makeOne('.01', decimal.ROUND_UP) + result = typ.deserialize(node, val) + self.assertEqual(result, decimal.Decimal('1.01')) + def test_serialize_fails(self): val = 'P' node = DummySchemaNode(None) @@ -1298,6 +1320,26 @@ class TestDecimal(unittest.TestCase): result = typ.serialize(node, val) self.assertEqual(result, '1.0') +class TestMoney(unittest.TestCase): + def _makeOne(self): + from colander import Money + return Money() + + def test_serialize_rounds_up(self): + val = '1.000001' + node = DummySchemaNode(None) + typ = self._makeOne() + result = typ.serialize(node, val) + self.assertEqual(result, '1.01') + + def test_deserialize_rounds_up(self): + import decimal + val = '1.00000001' + node = DummySchemaNode(None) + typ = self._makeOne() + result = typ.deserialize(node, val) + self.assertEqual(result, decimal.Decimal('1.01')) + class TestBoolean(unittest.TestCase): def _makeOne(self): from colander import Boolean