Skip to content

Commit

Permalink
Set IDecimal attributes min, max and default as Decimal typ…
Browse files Browse the repository at this point in the history
…e instead of `Number`.
  • Loading branch information
agroszer committed Mar 6, 2020
1 parent ee3aa41 commit 5f5cfdd
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 125 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Expand Up @@ -5,6 +5,10 @@
5.0 (unreleased)
================

- Set ``IDecimal`` attributes ``min``, ``max`` and ``default`` as ``Decimal``
type instead of ``Number``.
See `issue 88 <https://github.com/zopefoundation/zope.schema/issues/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``
Expand Down
63 changes: 63 additions & 0 deletions src/zope/schema/_bootstrapfields.py
Expand Up @@ -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):
Expand Down
68 changes: 3 additions & 65 deletions src/zope/schema/_field.py
Expand Up @@ -25,7 +25,6 @@
from datetime import date
from datetime import timedelta
from datetime import time
import decimal
import re


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -147,6 +148,7 @@
classImplements(Rational, IRational)
classImplements(Integral, IIntegral)
classImplements(Int, IInt)
classImplements(Decimal, IDecimal)

classImplements(Object, IObject)

Expand Down Expand Up @@ -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__
Expand Down
19 changes: 19 additions & 0 deletions src/zope/schema/interfaces.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
###
Expand Down
60 changes: 57 additions & 3 deletions src/zope/schema/tests/test__bootstrapfields.py
Expand Up @@ -11,6 +11,7 @@
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
import decimal
import doctest
import unittest
import unicodedata
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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):
Expand Down
57 changes: 0 additions & 57 deletions src/zope/schema/tests/test__field.py
Expand Up @@ -12,7 +12,6 @@
#
##############################################################################
import datetime
import decimal
import doctest
import unittest

Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 5f5cfdd

Please sign in to comment.