Decimal parameter conversion and normalization

Parameter Decimal does not have a sufficient value checks. Some values
cause Decimal parameter with a custom precision to crash with
an unhandled exception.

Improve parameter conversion and normalization operations to handle
decimal exceptions more gracefully. Decimal parameter now also has
new attributes enabling 2 new validation/normalization methods:
 * exponential: when False, decimal number is normalized to its
                non-exponential form
 * numberclass: a set of allowed decimal number classes
                (e.g. +Infinity, -Normal, ...) that are enforced
                for every Decimal parameter value

https://fedorahosted.org/freeipa/ticket/2705
This commit is contained in:
Martin Kosek
2012-06-07 09:25:19 +02:00
committed by Rob Crittenden
parent 8f051c978e
commit 1484ccc404
2 changed files with 141 additions and 6 deletions

View File

@@ -1206,7 +1206,12 @@ class Decimal(Number):
kwargs = Param.kwargs + (
('minvalue', decimal.Decimal, None),
('maxvalue', decimal.Decimal, None),
# round Decimal to given precision
('precision', int, None),
# when False, number is normalized to non-exponential form
('exponential', bool, False),
# set of allowed decimal number classes
('numberclass', tuple, ('-Normal', '+Zero', '+Normal')),
)
def __init__(self, name, *rules, **kw):
@@ -1256,31 +1261,70 @@ class Decimal(Number):
maxvalue=self.maxvalue,
)
def _enforce_numberclass(self, value):
#pylint: disable=E1101
numberclass = value.number_class()
if numberclass not in self.numberclass:
raise ValidationError(name=self.get_param_name(),
error=_("number class '%(cls)s' is not included in a list "
"of allowed number classes: %(allowed)s") \
% dict(cls=numberclass,
allowed=u', '.join(self.numberclass))
)
def _enforce_precision(self, value):
assert type(value) is decimal.Decimal
if self.precision is not None:
quantize_exp = decimal.Decimal(10) ** -self.precision
return value.quantize(quantize_exp)
try:
value = value.quantize(quantize_exp)
except decimal.DecimalException, e:
raise ConversionError(name=self.get_param_name(),
error=unicode(e))
return value
def _remove_exponent(self, value):
assert type(value) is decimal.Decimal
if not self.exponential: #pylint: disable=E1101
try:
# adopted from http://docs.python.org/library/decimal.html
value = value.quantize(decimal.Decimal(1)) \
if value == value.to_integral() \
else value.normalize()
except decimal.DecimalException, e:
raise ConversionError(name=self.get_param_name(),
error=unicode(e))
return value
def _test_and_normalize(self, value):
"""
This method is run in conversion and normalization methods to test
that the Decimal number conforms to Parameter boundaries and then
normalizes the value.
"""
self._enforce_numberclass(value)
value = self._remove_exponent(value)
value = self._enforce_precision(value)
return value
def _convert_scalar(self, value, index=None):
if isinstance(value, (basestring, float)):
try:
value = decimal.Decimal(value)
except Exception, e:
except decimal.DecimalException, e:
raise ConversionError(name=self.get_param_name(), index=index,
error=unicode(e))
if isinstance(value, decimal.Decimal):
x = self._enforce_precision(value)
return x
return self._test_and_normalize(value)
return super(Decimal, self)._convert_scalar(value, index)
def _normalize_scalar(self, value):
if isinstance(value, decimal.Decimal):
value = self._enforce_precision(value)
return self._test_and_normalize(value)
return super(Decimal, self)._normalize_scalar(value)

View File

@@ -32,7 +32,7 @@ from tests.util import dummy_ugettext, assert_equal
from tests.data import binary_bytes, utf8_bytes, unicode_str
from ipalib import parameters, text, errors, config
from ipalib.constants import TYPE_ERROR, CALLABLE_ERROR, NULLS
from ipalib.errors import ValidationError
from ipalib.errors import ValidationError, ConversionError
from ipalib import _
from xmlrpclib import MAXINT, MININT
@@ -1358,6 +1358,97 @@ class test_Decimal(ClassChecker):
assert dummy.called() is True
dummy.reset()
def test_precision(self):
"""
Test the `ipalib.parameters.Decimal` precision attribute
"""
# precission is None
param = self.cls('my_number')
for value in (Decimal('0'), Decimal('4.4'), Decimal('4.67')):
assert_equal(
param(value),
value)
# precision is 0
param = self.cls('my_number', precision=0)
for original,expected in ((Decimal('0'), '0'),
(Decimal('1.1'), '1'),
(Decimal('4.67'), '5')):
assert_equal(
str(param(original)),
expected)
# precision is 1
param = self.cls('my_number', precision=1)
for original,expected in ((Decimal('0'), '0.0'),
(Decimal('1.1'), '1.1'),
(Decimal('4.67'), '4.7')):
assert_equal(
str(param(original)),
expected)
# value has too many digits
param = self.cls('my_number', precision=1)
e = raises(ConversionError, param, '123456789012345678901234567890')
assert str(e) == \
"invalid 'my_number': quantize result has too many digits for current context"
def test_exponential(self):
"""
Test the `ipalib.parameters.Decimal` exponential attribute
"""
param = self.cls('my_number', exponential=True)
for original,expected in ((Decimal('0'), '0'),
(Decimal('1E3'), '1E+3'),
(Decimal('3.4E2'), '3.4E+2')):
assert_equal(
str(param(original)),
expected)
param = self.cls('my_number', exponential=False)
for original,expected in ((Decimal('0'), '0'),
(Decimal('1E3'), '1000'),
(Decimal('3.4E2'), '340')):
assert_equal(
str(param(original)),
expected)
def test_numberclass(self):
"""
Test the `ipalib.parameters.Decimal` numberclass attribute
"""
# test default value: '-Normal', '+Zero', '+Normal'
param = self.cls('my_number')
for value,raises_verror in ((Decimal('0'), False),
(Decimal('-0'), True),
(Decimal('1E8'), False),
(Decimal('-1.1'), False),
(Decimal('-Infinity'), True),
(Decimal('+Infinity'), True),
(Decimal('NaN'), True)):
if raises_verror:
raises(ValidationError, param, value)
else:
param(value)
param = self.cls('my_number', exponential=True,
numberclass=('-Normal', '+Zero', '+Infinity'))
for value,raises_verror in ((Decimal('0'), False),
(Decimal('-0'), True),
(Decimal('1E8'), True),
(Decimal('-1.1'), False),
(Decimal('-Infinity'), True),
(Decimal('+Infinity'), False),
(Decimal('NaN'), True)):
if raises_verror:
raises(ValidationError, param, value)
else:
param(value)
class test_AccessTime(ClassChecker):
"""
Test the `ipalib.parameters.AccessTime` class.