summaryrefslogtreecommitdiffstats
path: root/ipalib/parameters.py
diff options
context:
space:
mode:
authorMartin Kosek <mkosek@redhat.com>2012-01-17 11:19:00 +0100
committerMartin Kosek <mkosek@redhat.com>2012-01-20 08:13:44 +0100
commit092dd8db1293599a4b7f0ab33ea43e8082ec554f (patch)
tree4de216fe1b4a69f8d2acb4550dee8716dd1e0b53 /ipalib/parameters.py
parentd906fa50c1fd56ceb1a602fe50129becb6a304d4 (diff)
downloadfreeipa.git-092dd8db1293599a4b7f0ab33ea43e8082ec554f.tar.gz
freeipa.git-092dd8db1293599a4b7f0ab33ea43e8082ec554f.tar.xz
freeipa.git-092dd8db1293599a4b7f0ab33ea43e8082ec554f.zip
Replace float with Decimal
Having float type as a base type for floating point parameters in ipalib introduces several issues, e.g. problem with representation or value comparison. Python language provides a Decimal type which help overcome these issues. This patch replaces a float type and Float parameter with a decimal.Decimal type in Decimal parameter. A precision attribute was added to Decimal parameter that can be used to limit a number of decimal places in parameter representation. This approach fixes a problem with API.txt validation where comparison of float values may fail on different architectures due to float representation error. In order to safely transfer the parameter value over RPC it is being converted to string which is then converted back to decimal.Decimal number on a server side. https://fedorahosted.org/freeipa/ticket/2260
Diffstat (limited to 'ipalib/parameters.py')
-rw-r--r--ipalib/parameters.py87
1 files changed, 69 insertions, 18 deletions
diff --git a/ipalib/parameters.py b/ipalib/parameters.py
index be210864..d918a573 100644
--- a/ipalib/parameters.py
+++ b/ipalib/parameters.py
@@ -100,6 +100,7 @@ a more detailed description for clarity.
"""
import re
+import decimal
from types import NoneType
from util import make_repr
from text import _ as ugettext
@@ -723,8 +724,6 @@ class Param(ReadOnly):
else:
newval += (v,)
value = newval
- if self.normalizer is None:
- return value
if self.multivalue:
return tuple(
self._normalize_scalar(v) for v in value
@@ -740,6 +739,8 @@ class Param(ReadOnly):
"""
if type(value) is not unicode:
return value
+ if self.normalizer is None:
+ return value
try:
return self.normalizer(value)
except StandardError:
@@ -1100,7 +1101,7 @@ class Flag(Bool):
class Number(Param):
"""
- Base class for the `Int` and `Float` parameters.
+ Base class for the `Int` and `Decimal` parameters.
"""
def _convert_scalar(self, value, index=None):
@@ -1225,36 +1226,59 @@ class Int(Number):
)
-class Float(Number):
+class Decimal(Number):
"""
- A parameter for floating-point values (stored in the ``float`` type).
+ A parameter for floating-point values (stored in the ``Decimal`` type).
+
+ Python Decimal type helps overcome problems tied to plain "float" type,
+ e.g. problem with representation or value comparison. In order to safely
+ transfer the value over RPC libraries, it is being converted to string
+ which is then converted back to Decimal number.
"""
- type = float
+ type = decimal.Decimal
type_error = _('must be a decimal number')
kwargs = Param.kwargs + (
- ('minvalue', float, None),
- ('maxvalue', float, None),
+ ('minvalue', decimal.Decimal, None),
+ ('maxvalue', decimal.Decimal, None),
+ ('precision', int, None),
)
def __init__(self, name, *rules, **kw):
- #pylint: disable=E1003
- super(Number, self).__init__(name, *rules, **kw)
-
- if (self.minvalue > self.maxvalue) and (self.minvalue is not None and self.maxvalue is not None):
+ for kwparam in ('minvalue', 'maxvalue', 'default'):
+ value = kw.get(kwparam)
+ if value is None:
+ continue
+ if isinstance(value, (basestring, float)):
+ try:
+ value = decimal.Decimal(value)
+ except Exception, e:
+ raise ValueError(
+ '%s: cannot parse kwarg %s: %s' % (
+ name, kwparam, str(e)))
+ kw[kwparam] = value
+
+ super(Decimal, self).__init__(name, *rules, **kw)
+
+ if (self.minvalue > self.maxvalue) \
+ and (self.minvalue is not None and \
+ self.maxvalue is not None):
raise ValueError(
- '%s: minvalue > maxvalue (minvalue=%r, maxvalue=%r)' % (
+ '%s: minvalue > maxvalue (minvalue=%s, maxvalue=%s)' % (
self.nice, self.minvalue, self.maxvalue)
)
+ if self.precision is not None and self.precision < 0:
+ raise ValueError('%s: precision must be at least 0' % self.nice)
+
def _rule_minvalue(self, _, value):
"""
Check min constraint.
"""
- assert type(value) is float
+ assert type(value) is decimal.Decimal
if value < self.minvalue:
- return _('must be at least %(minvalue)f') % dict(
+ return _('must be at least %(minvalue)s') % dict(
minvalue=self.minvalue,
)
@@ -1262,12 +1286,39 @@ class Float(Number):
"""
Check max constraint.
"""
- assert type(value) is float
+ assert type(value) is decimal.Decimal
if value > self.maxvalue:
- return _('can be at most %(maxvalue)f') % dict(
+ return _('can be at most %(maxvalue)s') % dict(
maxvalue=self.maxvalue,
)
+ 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)
+
+ return value
+
+ def _convert_scalar(self, value, index=None):
+ if isinstance(value, (basestring, float)):
+ try:
+ value = decimal.Decimal(value)
+ except Exception, e:
+ raise ConversionError(name=self.name, index=index,
+ error=unicode(e))
+
+ if isinstance(value, decimal.Decimal):
+ x = self._enforce_precision(value)
+ return x
+
+ return super(Decimal, self)._convert_scalar(value, index)
+
+ def _normalize_scalar(self, value):
+ if isinstance(value, decimal.Decimal):
+ value = self._enforce_precision(value)
+
+ return super(Decimal, self)._normalize_scalar(value)
class Data(Param):
"""
@@ -1423,7 +1474,7 @@ class Str(Data):
"""
if type(value) is self.type:
return value
- if type(value) in (int, float):
+ if type(value) in (int, float, decimal.Decimal):
return self.type(value)
if type(value) in (tuple, list):
raise ConversionError(name=self.name, index=index,