mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
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:
committed by
Rob Crittenden
parent
8f051c978e
commit
1484ccc404
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user