From 5f5cfdd24d20dfb1bdaa8f8315fa8cc17b4ef089 Mon Sep 17 00:00:00 2001 From: Adam Groszer Date: Fri, 6 Mar 2020 13:28:14 +0100 Subject: [PATCH] Set `IDecimal` attributes `min`, `max` and `default` as `Decimal` type instead of `Number`. --- CHANGES.rst | 4 ++ src/zope/schema/_bootstrapfields.py | 63 +++++++++++++++++ src/zope/schema/_field.py | 68 +------------------ src/zope/schema/interfaces.py | 19 ++++++ .../schema/tests/test__bootstrapfields.py | 60 +++++++++++++++- src/zope/schema/tests/test__field.py | 57 ---------------- 6 files changed, 146 insertions(+), 125 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 13cb2af..09c4157 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,10 @@ 5.0 (unreleased) ================ +- Set ``IDecimal`` attributes ``min``, ``max`` and ``default`` as ``Decimal`` + type instead of ``Number``. + See `issue 88 `_. + - Enable unicode normalization for ``Text`` fields. The default is NFC normalization. Valid forms are 'NFC', 'NFKC', 'NFD', and 'NFKD'. To disable normalization, set ``unicode_normalization`` to ``False`` diff --git a/src/zope/schema/_bootstrapfields.py b/src/zope/schema/_bootstrapfields.py index 84f6148..7152b82 100644 --- a/src/zope/schema/_bootstrapfields.py +++ b/src/zope/schema/_bootstrapfields.py @@ -901,6 +901,69 @@ class Int(Integral): _unicode_converters = (int,) +class InvalidDecimalLiteral(ValueError, ValidationError): + "Raised by decimal fields" + + +class Decimal(Number): + """ + A field representing a native :class:`decimal.Decimal` and implementing + :class:`zope.schema.interfaces.IDecimal`. + + The :meth:`fromUnicode` method only accepts values that can be parsed + by the ``Decimal`` constructor:: + + >>> from zope.schema._field import Decimal + >>> f = Decimal() + >>> f.fromUnicode("1") + Decimal('1') + >>> f.fromUnicode("125.6") + Decimal('125.6') + >>> f.fromUnicode("1+0j") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidDecimalLiteral: Invalid literal for Decimal(): 1+0j + >>> f.fromUnicode("1/2") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidDecimalLiteral: Invalid literal for Decimal(): 1/2 + >>> f.fromUnicode(str(2**31234) + '.' + str(2**256)) # doctest: +ELLIPSIS + Decimal('2349...936') + >>> f.fromUnicode("not a number") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidDecimalLiteral: could not convert string to float: not a number + + Likewise for :meth:`fromBytes`:: + + >>> from zope.schema._field import Decimal + >>> f = Decimal() + >>> f.fromBytes(b"1") + Decimal('1') + >>> f.fromBytes(b"125.6") + Decimal('125.6') + >>> f.fromBytes(b"1+0j") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidDecimalLiteral: Invalid literal for Decimal(): 1+0j + >>> f.fromBytes(b"1/2") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidDecimalLiteral: Invalid literal for Decimal(): 1/2 + >>> f.fromBytes((str(2**31234) + '.' + str(2**256)).encode("ascii")) # doctest: +ELLIPSIS + Decimal('2349...936') + >>> f.fromBytes(b"not a number") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidDecimalLiteral: could not convert string to float: not a number + + + """ + _type = decimal.Decimal + _unicode_converters = (decimal.Decimal,) + _validation_error = InvalidDecimalLiteral + + class _ObjectsBeingValidated(threading.local): def __init__(self): diff --git a/src/zope/schema/_field.py b/src/zope/schema/_field.py index 336b291..cf0f7fe 100644 --- a/src/zope/schema/_field.py +++ b/src/zope/schema/_field.py @@ -25,7 +25,6 @@ from datetime import date from datetime import timedelta from datetime import time -import decimal import re @@ -106,6 +105,8 @@ from zope.schema._bootstrapfields import Int from zope.schema._bootstrapfields import Integral from zope.schema._bootstrapfields import Number +from zope.schema._bootstrapfields import InvalidDecimalLiteral # reimport +from zope.schema._bootstrapfields import Decimal from zope.schema._bootstrapfields import Password from zope.schema._bootstrapfields import Rational from zope.schema._bootstrapfields import Real @@ -147,6 +148,7 @@ classImplements(Rational, IRational) classImplements(Integral, IIntegral) classImplements(Int, IInt) +classImplements(Decimal, IDecimal) classImplements(Object, IObject) @@ -315,70 +317,6 @@ class Float(Real): _validation_error = InvalidFloatLiteral -class InvalidDecimalLiteral(ValueError, ValidationError): - "Raised by decimal fields" - - -@implementer(IDecimal) -class Decimal(Number): - """ - A field representing a native :class:`decimal.Decimal` and implementing - :class:`zope.schema.interfaces.IDecimal`. - - The :meth:`fromUnicode` method only accepts values that can be parsed - by the ``Decimal`` constructor:: - - >>> from zope.schema._field import Decimal - >>> f = Decimal() - >>> f.fromUnicode("1") - Decimal('1') - >>> f.fromUnicode("125.6") - Decimal('125.6') - >>> f.fromUnicode("1+0j") # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - InvalidDecimalLiteral: Invalid literal for Decimal(): 1+0j - >>> f.fromUnicode("1/2") # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - InvalidDecimalLiteral: Invalid literal for Decimal(): 1/2 - >>> f.fromUnicode(str(2**31234) + '.' + str(2**256)) # doctest: +ELLIPSIS - Decimal('2349...936') - >>> f.fromUnicode("not a number") # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - InvalidDecimalLiteral: could not convert string to float: not a number - - Likewise for :meth:`fromBytes`:: - - >>> from zope.schema._field import Decimal - >>> f = Decimal() - >>> f.fromBytes(b"1") - Decimal('1') - >>> f.fromBytes(b"125.6") - Decimal('125.6') - >>> f.fromBytes(b"1+0j") # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - InvalidDecimalLiteral: Invalid literal for Decimal(): 1+0j - >>> f.fromBytes(b"1/2") # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - InvalidDecimalLiteral: Invalid literal for Decimal(): 1/2 - >>> f.fromBytes((str(2**31234) + '.' + str(2**256)).encode("ascii")) # doctest: +ELLIPSIS - Decimal('2349...936') - >>> f.fromBytes(b"not a number") # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - InvalidDecimalLiteral: could not convert string to float: not a number - - - """ - _type = decimal.Decimal - _unicode_converters = (decimal.Decimal,) - _validation_error = InvalidDecimalLiteral - - @implementer(IDatetime) class Datetime(Orderable, Field): __doc__ = IDatetime.__doc__ diff --git a/src/zope/schema/interfaces.py b/src/zope/schema/interfaces.py index b6542f3..303380a 100644 --- a/src/zope/schema/interfaces.py +++ b/src/zope/schema/interfaces.py @@ -23,6 +23,7 @@ from zope.schema._bootstrapfields import Bool from zope.schema._bootstrapfields import Complex +from zope.schema._bootstrapfields import Decimal from zope.schema._bootstrapfields import Field from zope.schema._bootstrapfields import Int from zope.schema._bootstrapfields import Integral @@ -644,6 +645,24 @@ class IFloat(IReal): class IDecimal(INumber): """Field containing a :class:`decimal.Decimal`""" + min = Decimal( + title=_("Start of the range"), + required=False, + default=None + ) + + max = Decimal( + title=_("End of the range (including the value itself)"), + required=False, + default=None + ) + + default = Decimal( + title=_("Default Value"), + description=_("""The field default value may be None or a legal + field value""") + ) + ### # End numbers ### diff --git a/src/zope/schema/tests/test__bootstrapfields.py b/src/zope/schema/tests/test__bootstrapfields.py index 1cb3562..34bdfc8 100644 --- a/src/zope/schema/tests/test__bootstrapfields.py +++ b/src/zope/schema/tests/test__bootstrapfields.py @@ -11,6 +11,7 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## +import decimal import doctest import unittest import unicodedata @@ -1245,10 +1246,7 @@ def test_validate_required(self): field.validate(-1) self.assertRaises(RequiredMissing, field.validate, None) - - def test_fromUnicode_miss(self): - txt = self._makeOne() self.assertRaises(ValueError, txt.fromUnicode, u'') self.assertRaises(ValueError, txt.fromUnicode, u'False') @@ -1278,6 +1276,62 @@ def test_ctor_defaults(self): self.assertEqual(txt._type, integer_types) +class DecimalTests(NumberTests): + + mvm_missing_value = decimal.Decimal("-1") + mvm_default = decimal.Decimal("0") + + MIN = decimal.Decimal(NumberTests.MIN) + MAX = decimal.Decimal(NumberTests.MAX) + VALID = tuple(decimal.Decimal(x) for x in NumberTests.VALID) + TOO_SMALL = tuple(decimal.Decimal(x) for x in NumberTests.TOO_SMALL) + TOO_BIG = tuple(decimal.Decimal(x) for x in NumberTests.TOO_BIG) + + def _getTargetClass(self): + from zope.schema._bootstrapfields import Decimal + return Decimal + + def _getTargetInterface(self): + from zope.schema.interfaces import IDecimal + return IDecimal + + def test_validate_not_required(self): + field = self._makeOne(required=False) + field.validate(decimal.Decimal("10.0")) + field.validate(decimal.Decimal("0.93")) + field.validate(decimal.Decimal("1000.0003")) + field.validate(None) + + def test_validate_required(self): + from zope.schema.interfaces import RequiredMissing + field = self._makeOne() + field.validate(decimal.Decimal("10.0")) + field.validate(decimal.Decimal("0.93")) + field.validate(decimal.Decimal("1000.0003")) + self.assertRaises(RequiredMissing, field.validate, None) + + def test_fromUnicode_miss(self): + from zope.schema.interfaces import ValidationError + flt = self._makeOne() + self.assertRaises(ValueError, flt.fromUnicode, u'') + self.assertRaises(ValueError, flt.fromUnicode, u'abc') + with self.assertRaises(ValueError) as exc: + flt.fromUnicode(u'1.4G') + + value_error = exc.exception + self.assertIs(value_error.field, flt) + self.assertEqual(value_error.value, u'1.4G') + self.assertIsInstance(value_error, ValidationError) + + def test_fromUnicode_hit(self): + from decimal import Decimal + + flt = self._makeOne() + self.assertEqual(flt.fromUnicode(u'0'), Decimal('0.0')) + self.assertEqual(flt.fromUnicode(u'1.23'), Decimal('1.23')) + self.assertEqual(flt.fromUnicode(u'12345.6'), Decimal('12345.6')) + + class ObjectTests(EqualityTestsMixin, WrongTypeTestsMixin, unittest.TestCase): diff --git a/src/zope/schema/tests/test__field.py b/src/zope/schema/tests/test__field.py index 7b0499f..2e78b8e 100644 --- a/src/zope/schema/tests/test__field.py +++ b/src/zope/schema/tests/test__field.py @@ -12,7 +12,6 @@ # ############################################################################## import datetime -import decimal import doctest import unittest @@ -314,62 +313,6 @@ def test_fromUnicode_hit(self): self.assertEqual(flt.fromUnicode(u'1.23e6'), 1230000.0) -class DecimalTests(NumberTests): - - mvm_missing_value = decimal.Decimal("-1") - mvm_default = decimal.Decimal("0") - - MIN = decimal.Decimal(NumberTests.MIN) - MAX = decimal.Decimal(NumberTests.MAX) - VALID = tuple(decimal.Decimal(x) for x in NumberTests.VALID) - TOO_SMALL = tuple(decimal.Decimal(x) for x in NumberTests.TOO_SMALL) - TOO_BIG = tuple(decimal.Decimal(x) for x in NumberTests.TOO_BIG) - - def _getTargetClass(self): - from zope.schema._field import Decimal - return Decimal - - def _getTargetInterface(self): - from zope.schema.interfaces import IDecimal - return IDecimal - - def test_validate_not_required(self): - field = self._makeOne(required=False) - field.validate(decimal.Decimal("10.0")) - field.validate(decimal.Decimal("0.93")) - field.validate(decimal.Decimal("1000.0003")) - field.validate(None) - - def test_validate_required(self): - from zope.schema.interfaces import RequiredMissing - field = self._makeOne() - field.validate(decimal.Decimal("10.0")) - field.validate(decimal.Decimal("0.93")) - field.validate(decimal.Decimal("1000.0003")) - self.assertRaises(RequiredMissing, field.validate, None) - - def test_fromUnicode_miss(self): - from zope.schema.interfaces import ValidationError - flt = self._makeOne() - self.assertRaises(ValueError, flt.fromUnicode, u'') - self.assertRaises(ValueError, flt.fromUnicode, u'abc') - with self.assertRaises(ValueError) as exc: - flt.fromUnicode(u'1.4G') - - value_error = exc.exception - self.assertIs(value_error.field, flt) - self.assertEqual(value_error.value, u'1.4G') - self.assertIsInstance(value_error, ValidationError) - - def test_fromUnicode_hit(self): - from decimal import Decimal - - flt = self._makeOne() - self.assertEqual(flt.fromUnicode(u'0'), Decimal('0.0')) - self.assertEqual(flt.fromUnicode(u'1.23'), Decimal('1.23')) - self.assertEqual(flt.fromUnicode(u'12345.6'), Decimal('12345.6')) - - class DatetimeTests(OrderableMissingValueMixin, OrderableTestsMixin, EqualityTestsMixin,