On Tue, 2012-01-17 at 11:27 +0100, Martin Kosek wrote:
> On Fri, 2012-01-13 at 21:02 +0100, Martin Kosek wrote:
> > This patch fixes RHEL 6.2 build issue.
> > ----
> > Having float type as a base type for floating point parameter in
> > ipalib introduces several issues, e.g. problem with representation
> > or value comparison. Python language provides Decimal type which
> > help overcome these issue.
> > 
> > This patch replaces a float type with Decimal type in Float
> > parameter. A precision attribute was added to Float 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
> > number on server side.
> > 
> > https://fedorahosted.org/freeipa/ticket/2260
> > 
> 
> Sending an improved version of the patch with following major changes:
> 
> 1) Float parameter was renamed to Decimal as it base type is different
> and would confuse users otherwise.
> 
> 2) Parameter maxvalue, minvalue and default can be also passed as a
> string and not just as a decimal.Decimal value. Parameter definition is
> then much simpler.
> 
> 3) LDAP backend encoder was enhanced to support this new type (it
> converts it to string just like a float value).
> 
> Martin

I forgot to add an encoding rule to JSON xmlrpc server. This can be
useful in a future when Decimal type is actually used for a real LDAP
attribute.

Martin
>From 7c3ebfa08df24be75644b0c66a109930a80c27e8 Mon Sep 17 00:00:00 2001
From: Martin Kosek <mko...@redhat.com>
Date: Tue, 17 Jan 2012 11:19:00 +0100
Subject: [PATCH] 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
---
 API.txt                              |   36 +++++++-------
 doc/guide/guide.org                  |    8 ++--
 ipalib/__init__.py                   |    2 +-
 ipalib/encoder.py                    |    6 ++-
 ipalib/parameters.py                 |   85 +++++++++++++++++++++++++++-------
 ipalib/plugins/dns.py                |   51 ++++++++++++--------
 ipalib/rpc.py                        |    4 ++
 ipaserver/rpcserver.py               |    3 +
 make-lint                            |    2 +-
 tests/test_ipalib/test_parameters.py |   47 ++++++++++---------
 tests/test_xmlrpc/test_dns_plugin.py |    2 +-
 11 files changed, 159 insertions(+), 87 deletions(-)

diff --git a/API.txt b/API.txt
index 9048231bb1f349047f9790e5335778d4c3d637b0..2937c24f4d6aa53b1028f430a05ef6453544ea7c 100644
--- a/API.txt
+++ b/API.txt
@@ -654,16 +654,16 @@ option: Str('kx_part_exchanger', attribute=False, cli_name='kx_exchanger', multi
 option: LOCRecord('locrecord', attribute=True, cli_name='loc_rec', csv=True, multivalue=True, option_group=u'LOC Record', required=False)
 option: Int('loc_part_lat_deg', attribute=False, cli_name='loc_lat_deg', maxvalue=90, minvalue=0, multivalue=False, option_group=u'LOC Record', required=False)
 option: Int('loc_part_lat_min', attribute=False, cli_name='loc_lat_min', maxvalue=59, minvalue=0, multivalue=False, option_group=u'LOC Record', required=False)
-option: Float('loc_part_lat_sec', attribute=False, cli_name='loc_lat_sec', maxvalue=59.999, minvalue=0.0, multivalue=False, option_group=u'LOC Record', required=False)
+option: Decimal('loc_part_lat_sec', attribute=False, cli_name='loc_lat_sec', maxvalue=Decimal('59.999'), minvalue=Decimal('0.0'), multivalue=False, option_group=u'LOC Record', precision=3, required=False)
 option: StrEnum('loc_part_lat_dir', attribute=False, cli_name='loc_lat_dir', multivalue=False, option_group=u'LOC Record', required=False, values=(u'N', u'S'))
 option: Int('loc_part_lon_deg', attribute=False, cli_name='loc_lon_deg', maxvalue=180, minvalue=0, multivalue=False, option_group=u'LOC Record', required=False)
 option: Int('loc_part_lon_min', attribute=False, cli_name='loc_lon_min', maxvalue=59, minvalue=0, multivalue=False, option_group=u'LOC Record', required=False)
-option: Float('loc_part_lon_sec', attribute=False, cli_name='loc_lon_sec', maxvalue=59.999, minvalue=0.0, multivalue=False, option_group=u'LOC Record', required=False)
+option: Decimal('loc_part_lon_sec', attribute=False, cli_name='loc_lon_sec', maxvalue=Decimal('59.999'), minvalue=Decimal('0.0'), multivalue=False, option_group=u'LOC Record', precision=3, required=False)
 option: StrEnum('loc_part_lon_dir', attribute=False, cli_name='loc_lon_dir', multivalue=False, option_group=u'LOC Record', required=False, values=(u'E', u'W'))
-option: Float('loc_part_altitude', attribute=False, cli_name='loc_altitude', maxvalue=42849672.95, minvalue=-100000.0, multivalue=False, option_group=u'LOC Record', required=False)
-option: Float('loc_part_size', attribute=False, cli_name='loc_size', maxvalue=90000000.0, minvalue=0.0, multivalue=False, option_group=u'LOC Record', required=False)
-option: Float('loc_part_h_precision', attribute=False, cli_name='loc_h_precision', maxvalue=90000000.0, minvalue=0.0, multivalue=False, option_group=u'LOC Record', required=False)
-option: Float('loc_part_v_precision', attribute=False, cli_name='loc_v_precision', maxvalue=90000000.0, minvalue=0.0, multivalue=False, option_group=u'LOC Record', required=False)
+option: Decimal('loc_part_altitude', attribute=False, cli_name='loc_altitude', maxvalue=Decimal('42849672.95'), minvalue=Decimal('-100000.00'), multivalue=False, option_group=u'LOC Record', precision=2, required=False)
+option: Decimal('loc_part_size', attribute=False, cli_name='loc_size', maxvalue=Decimal('90000000.00'), minvalue=Decimal('0.0'), multivalue=False, option_group=u'LOC Record', precision=2, required=False)
+option: Decimal('loc_part_h_precision', attribute=False, cli_name='loc_h_precision', maxvalue=Decimal('90000000.00'), minvalue=Decimal('0.0'), multivalue=False, option_group=u'LOC Record', precision=2, required=False)
+option: Decimal('loc_part_v_precision', attribute=False, cli_name='loc_v_precision', maxvalue=Decimal('90000000.00'), minvalue=Decimal('0.0'), multivalue=False, option_group=u'LOC Record', precision=2, required=False)
 option: MXRecord('mxrecord', attribute=True, cli_name='mx_rec', csv=True, multivalue=True, option_group=u'MX Record', required=False)
 option: Int('mx_part_preference', attribute=False, cli_name='mx_preference', maxvalue=65535, minvalue=0, multivalue=False, option_group=u'MX Record', required=False)
 option: Str('mx_part_exchanger', attribute=False, cli_name='mx_exchanger', multivalue=False, option_group=u'MX Record', required=False)
@@ -831,16 +831,16 @@ option: Str('kx_part_exchanger', attribute=False, autofill=False, cli_name='kx_e
 option: LOCRecord('locrecord', attribute=True, autofill=False, cli_name='loc_rec', csv=True, multivalue=True, option_group=u'LOC Record', query=True, required=False)
 option: Int('loc_part_lat_deg', attribute=False, autofill=False, cli_name='loc_lat_deg', maxvalue=90, minvalue=0, multivalue=False, option_group=u'LOC Record', query=True, required=False)
 option: Int('loc_part_lat_min', attribute=False, autofill=False, cli_name='loc_lat_min', maxvalue=59, minvalue=0, multivalue=False, option_group=u'LOC Record', query=True, required=False)
-option: Float('loc_part_lat_sec', attribute=False, autofill=False, cli_name='loc_lat_sec', maxvalue=59.999, minvalue=0.0, multivalue=False, option_group=u'LOC Record', query=True, required=False)
+option: Decimal('loc_part_lat_sec', attribute=False, autofill=False, cli_name='loc_lat_sec', maxvalue=Decimal('59.999'), minvalue=Decimal('0.0'), multivalue=False, option_group=u'LOC Record', precision=3, query=True, required=False)
 option: StrEnum('loc_part_lat_dir', attribute=False, autofill=False, cli_name='loc_lat_dir', multivalue=False, option_group=u'LOC Record', query=True, required=False, values=(u'N', u'S'))
 option: Int('loc_part_lon_deg', attribute=False, autofill=False, cli_name='loc_lon_deg', maxvalue=180, minvalue=0, multivalue=False, option_group=u'LOC Record', query=True, required=False)
 option: Int('loc_part_lon_min', attribute=False, autofill=False, cli_name='loc_lon_min', maxvalue=59, minvalue=0, multivalue=False, option_group=u'LOC Record', query=True, required=False)
-option: Float('loc_part_lon_sec', attribute=False, autofill=False, cli_name='loc_lon_sec', maxvalue=59.999, minvalue=0.0, multivalue=False, option_group=u'LOC Record', query=True, required=False)
+option: Decimal('loc_part_lon_sec', attribute=False, autofill=False, cli_name='loc_lon_sec', maxvalue=Decimal('59.999'), minvalue=Decimal('0.0'), multivalue=False, option_group=u'LOC Record', precision=3, query=True, required=False)
 option: StrEnum('loc_part_lon_dir', attribute=False, autofill=False, cli_name='loc_lon_dir', multivalue=False, option_group=u'LOC Record', query=True, required=False, values=(u'E', u'W'))
-option: Float('loc_part_altitude', attribute=False, autofill=False, cli_name='loc_altitude', maxvalue=42849672.95, minvalue=-100000.0, multivalue=False, option_group=u'LOC Record', query=True, required=False)
-option: Float('loc_part_size', attribute=False, autofill=False, cli_name='loc_size', maxvalue=90000000.0, minvalue=0.0, multivalue=False, option_group=u'LOC Record', query=True, required=False)
-option: Float('loc_part_h_precision', attribute=False, autofill=False, cli_name='loc_h_precision', maxvalue=90000000.0, minvalue=0.0, multivalue=False, option_group=u'LOC Record', query=True, required=False)
-option: Float('loc_part_v_precision', attribute=False, autofill=False, cli_name='loc_v_precision', maxvalue=90000000.0, minvalue=0.0, multivalue=False, option_group=u'LOC Record', query=True, required=False)
+option: Decimal('loc_part_altitude', attribute=False, autofill=False, cli_name='loc_altitude', maxvalue=Decimal('42849672.95'), minvalue=Decimal('-100000.00'), multivalue=False, option_group=u'LOC Record', precision=2, query=True, required=False)
+option: Decimal('loc_part_size', attribute=False, autofill=False, cli_name='loc_size', maxvalue=Decimal('90000000.00'), minvalue=Decimal('0.0'), multivalue=False, option_group=u'LOC Record', precision=2, query=True, required=False)
+option: Decimal('loc_part_h_precision', attribute=False, autofill=False, cli_name='loc_h_precision', maxvalue=Decimal('90000000.00'), minvalue=Decimal('0.0'), multivalue=False, option_group=u'LOC Record', precision=2, query=True, required=False)
+option: Decimal('loc_part_v_precision', attribute=False, autofill=False, cli_name='loc_v_precision', maxvalue=Decimal('90000000.00'), minvalue=Decimal('0.0'), multivalue=False, option_group=u'LOC Record', precision=2, query=True, required=False)
 option: MXRecord('mxrecord', attribute=True, autofill=False, cli_name='mx_rec', csv=True, multivalue=True, option_group=u'MX Record', query=True, required=False)
 option: Int('mx_part_preference', attribute=False, autofill=False, cli_name='mx_preference', maxvalue=65535, minvalue=0, multivalue=False, option_group=u'MX Record', query=True, required=False)
 option: Str('mx_part_exchanger', attribute=False, autofill=False, cli_name='mx_exchanger', multivalue=False, option_group=u'MX Record', query=True, required=False)
@@ -952,16 +952,16 @@ option: Str('kx_part_exchanger', attribute=False, autofill=False, cli_name='kx_e
 option: LOCRecord('locrecord', attribute=True, autofill=False, cli_name='loc_rec', csv=True, multivalue=True, option_group=u'LOC Record', required=False)
 option: Int('loc_part_lat_deg', attribute=False, autofill=False, cli_name='loc_lat_deg', maxvalue=90, minvalue=0, multivalue=False, option_group=u'LOC Record', required=False)
 option: Int('loc_part_lat_min', attribute=False, autofill=False, cli_name='loc_lat_min', maxvalue=59, minvalue=0, multivalue=False, option_group=u'LOC Record', required=False)
-option: Float('loc_part_lat_sec', attribute=False, autofill=False, cli_name='loc_lat_sec', maxvalue=59.999, minvalue=0.0, multivalue=False, option_group=u'LOC Record', required=False)
+option: Decimal('loc_part_lat_sec', attribute=False, autofill=False, cli_name='loc_lat_sec', maxvalue=Decimal('59.999'), minvalue=Decimal('0.0'), multivalue=False, option_group=u'LOC Record', precision=3, required=False)
 option: StrEnum('loc_part_lat_dir', attribute=False, autofill=False, cli_name='loc_lat_dir', multivalue=False, option_group=u'LOC Record', required=False, values=(u'N', u'S'))
 option: Int('loc_part_lon_deg', attribute=False, autofill=False, cli_name='loc_lon_deg', maxvalue=180, minvalue=0, multivalue=False, option_group=u'LOC Record', required=False)
 option: Int('loc_part_lon_min', attribute=False, autofill=False, cli_name='loc_lon_min', maxvalue=59, minvalue=0, multivalue=False, option_group=u'LOC Record', required=False)
-option: Float('loc_part_lon_sec', attribute=False, autofill=False, cli_name='loc_lon_sec', maxvalue=59.999, minvalue=0.0, multivalue=False, option_group=u'LOC Record', required=False)
+option: Decimal('loc_part_lon_sec', attribute=False, autofill=False, cli_name='loc_lon_sec', maxvalue=Decimal('59.999'), minvalue=Decimal('0.0'), multivalue=False, option_group=u'LOC Record', precision=3, required=False)
 option: StrEnum('loc_part_lon_dir', attribute=False, autofill=False, cli_name='loc_lon_dir', multivalue=False, option_group=u'LOC Record', required=False, values=(u'E', u'W'))
-option: Float('loc_part_altitude', attribute=False, autofill=False, cli_name='loc_altitude', maxvalue=42849672.95, minvalue=-100000.0, multivalue=False, option_group=u'LOC Record', required=False)
-option: Float('loc_part_size', attribute=False, autofill=False, cli_name='loc_size', maxvalue=90000000.0, minvalue=0.0, multivalue=False, option_group=u'LOC Record', required=False)
-option: Float('loc_part_h_precision', attribute=False, autofill=False, cli_name='loc_h_precision', maxvalue=90000000.0, minvalue=0.0, multivalue=False, option_group=u'LOC Record', required=False)
-option: Float('loc_part_v_precision', attribute=False, autofill=False, cli_name='loc_v_precision', maxvalue=90000000.0, minvalue=0.0, multivalue=False, option_group=u'LOC Record', required=False)
+option: Decimal('loc_part_altitude', attribute=False, autofill=False, cli_name='loc_altitude', maxvalue=Decimal('42849672.95'), minvalue=Decimal('-100000.00'), multivalue=False, option_group=u'LOC Record', precision=2, required=False)
+option: Decimal('loc_part_size', attribute=False, autofill=False, cli_name='loc_size', maxvalue=Decimal('90000000.00'), minvalue=Decimal('0.0'), multivalue=False, option_group=u'LOC Record', precision=2, required=False)
+option: Decimal('loc_part_h_precision', attribute=False, autofill=False, cli_name='loc_h_precision', maxvalue=Decimal('90000000.00'), minvalue=Decimal('0.0'), multivalue=False, option_group=u'LOC Record', precision=2, required=False)
+option: Decimal('loc_part_v_precision', attribute=False, autofill=False, cli_name='loc_v_precision', maxvalue=Decimal('90000000.00'), minvalue=Decimal('0.0'), multivalue=False, option_group=u'LOC Record', precision=2, required=False)
 option: MXRecord('mxrecord', attribute=True, autofill=False, cli_name='mx_rec', csv=True, multivalue=True, option_group=u'MX Record', required=False)
 option: Int('mx_part_preference', attribute=False, autofill=False, cli_name='mx_preference', maxvalue=65535, minvalue=0, multivalue=False, option_group=u'MX Record', required=False)
 option: Str('mx_part_exchanger', attribute=False, autofill=False, cli_name='mx_exchanger', multivalue=False, option_group=u'MX Record', required=False)
diff --git a/doc/guide/guide.org b/doc/guide/guide.org
index 68858166e6f2bf0292d3bc15f486e7715bc8405c..bca7b34fabfbab02e68a62382a59cdef54a098d3 100644
--- a/doc/guide/guide.org
+++ b/doc/guide/guide.org
@@ -227,7 +227,7 @@ verbose = Flag('verbose', default=True)
            specified when constructing =Int= parameter:
            - /minvalue/ :: minimal value that this parameter accepts, defaults to =MININT=
            - /maxvalue/ :: maximum value this parameter can accept, defaults to =MAXINT=
-- /Float/ :: floating point parameters that are stored in Python's float type. =Float= has
+- /Decimal/ :: floating point parameters that are stored in Python's Decimal type. =Decimal= has
              the same two additional properties as =Int=. Unlike =Int=, there are no
              default values for the minimal and maximum boundaries.
 - /Bytes/ :: a parameter to represent binary data.
@@ -294,9 +294,9 @@ class tank(Object):
     takes_params = (
         StrEnum('species*', label=u'Species', doc=u'Fish species',
                  values=(u'Angelfish', u'Betta', u'Cichlid', u'Firemouth')),
-        Float('height', label=u'Height', doc=u'height in mm', default=400.0),
-        Float('width', label=u'Width', doc=u'width in mm', default=400.0),
-        Float('depth', label=u'Depth', doc=u'Depth in mm', default=300.0)
+        Decimal('height', label=u'Height', doc=u'height in mm', default='400.0'),
+        Decimal('width', label=u'Width', doc=u'width in mm', default='400.0'),
+        Decimal('depth', label=u'Depth', doc=u'Depth in mm', default='300.0')
     )
 
 api.register(tank) (ref:register)
diff --git a/ipalib/__init__.py b/ipalib/__init__.py
index 29ba0bb90d7d8d32dd1f375696b16b0bacaa7b5e..1efeeab4a6c5cef8f625c3964be253baf208dd29 100644
--- a/ipalib/__init__.py
+++ b/ipalib/__init__.py
@@ -878,7 +878,7 @@ from backend import Backend
 from frontend import Command, LocalOrRemote, Updater
 from frontend import Object, Method, Property
 from crud import Create, Retrieve, Update, Delete, Search
-from parameters import DefaultFrom, Bool, Flag, Int, Float, Bytes, Str, IA5Str, Password
+from parameters import DefaultFrom, Bool, Flag, Int, Decimal, Bytes, Str, IA5Str, Password
 from parameters import BytesEnum, StrEnum, AccessTime, File
 from errors import SkipPluginModule
 from text import _, ngettext, GettextFactory, NGettextFactory
diff --git a/ipalib/encoder.py b/ipalib/encoder.py
index f23e5659e848d37db1072ff59aa7e11796b0836c..8d59bd3161466e5d96ff03894a9b43871b8bb19e 100644
--- a/ipalib/encoder.py
+++ b/ipalib/encoder.py
@@ -20,6 +20,8 @@
 Encoding capabilities.
 """
 
+from decimal import Decimal
+
 class EncoderSettings(object):
     """
     Container for encoder settings.
@@ -77,7 +79,7 @@ class Encoder(object):
             return self.encoder_settings.encode_postprocessor(
                 var.encode(self.encoder_settings.encode_to)
             )
-        elif isinstance(var, (bool, float, int, long)):
+        elif isinstance(var, (bool, float, Decimal, int, long)):
             return self.encoder_settings.encode_postprocessor(
                 str(var).encode(self.encoder_settings.encode_to)
             )
@@ -131,7 +133,7 @@ class Encoder(object):
             return self.encoder_settings.decode_postprocessor(
                 var.decode(self.encoder_settings.decode_from)
             )
-        elif isinstance(var, (bool, float, int, long)):
+        elif isinstance(var, (bool, float, Decimal, int, long)):
             return var
         elif isinstance(var, list):
             return [self.decode(m) for m in var]
diff --git a/ipalib/parameters.py b/ipalib/parameters.py
index be210864f465668f5cd5fdb107838efd9a66ad41..d918a573778f533b37318622b2fd0ce2d265adf4 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)
+        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
 
-        if (self.minvalue > self.maxvalue) and (self.minvalue is not None and self.maxvalue is not None):
+        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,
diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py
index 8042c3a1b3e6bd4517930411f9eeabb877f2998f..abb2e90b80f06ec0247532928ba71bd6d2b9c800 100644
--- a/ipalib/plugins/dns.py
+++ b/ipalib/plugins/dns.py
@@ -25,7 +25,7 @@ import re
 from ipalib.request import context
 from ipalib import api, errors, output
 from ipalib import Command
-from ipalib.parameters import Flag, Bool, Int, Float, Str, StrEnum, Any
+from ipalib.parameters import Flag, Bool, Int, Decimal, Str, StrEnum, Any
 from ipalib.plugins.baseldap import *
 from ipalib import _, ngettext
 from ipalib.util import validate_zonemgr, normalize_zonemgr, validate_hostname
@@ -345,7 +345,12 @@ class DNSRecord(Str):
             if not values:
                 return value
 
-            new_values = [ part.normalize(values[part_id]) \
+            converted_values = [ part._convert_scalar(values[part_id]) \
+                                 if values[part_id] is not None else None
+                                 for part_id, part in enumerate(self.parts)
+                               ]
+
+            new_values = [ part.normalize(converted_values[part_id]) \
                             for part_id, part in enumerate(self.parts) ]
 
             value = self._convert_scalar(new_values)
@@ -626,10 +631,11 @@ class LOCRecord(DNSRecord):
             minvalue=0,
             maxvalue=59,
         ),
-        Float('lat_sec?',
+        Decimal('lat_sec?',
             label=_('Seconds Latitude'),
-            minvalue=0.0,
-            maxvalue=59.999,
+            minvalue='0.0',
+            maxvalue='59.999',
+            precision=3,
         ),
         StrEnum('lat_dir',
             label=_('Direction Latitude'),
@@ -645,34 +651,39 @@ class LOCRecord(DNSRecord):
             minvalue=0,
             maxvalue=59,
         ),
-        Float('lon_sec?',
+        Decimal('lon_sec?',
             label=_('Seconds Longtitude'),
-            minvalue=0.0,
-            maxvalue=59.999,
+            minvalue='0.0',
+            maxvalue='59.999',
+            precision=3,
         ),
         StrEnum('lon_dir',
             label=_('Direction Longtitude'),
             values=(u'E', u'W',),
         ),
-        Float('altitude',
+        Decimal('altitude',
             label=_('Altitude'),
-            minvalue=-100000.00,
-            maxvalue=42849672.95,
+            minvalue='-100000.00',
+            maxvalue='42849672.95',
+            precision=2,
         ),
-        Float('size?',
+        Decimal('size?',
             label=_('Size'),
-            minvalue=0.0,
-            maxvalue=90000000.00,
+            minvalue='0.0',
+            maxvalue='90000000.00',
+            precision=2,
         ),
-        Float('h_precision?',
+        Decimal('h_precision?',
             label=_('Horizontal Precision'),
-            minvalue=0.0,
-            maxvalue=90000000.00,
+            minvalue='0.0',
+            maxvalue='90000000.00',
+            precision=2,
         ),
-        Float('v_precision?',
+        Decimal('v_precision?',
             label=_('Vertical Precision'),
-            minvalue=0.0,
-            maxvalue=90000000.00,
+            minvalue='0.0',
+            maxvalue='90000000.00',
+            precision=2,
         ),
     )
 
diff --git a/ipalib/rpc.py b/ipalib/rpc.py
index 8ec3a2f2706f6a18216ea8cfc74bc50b21159d31..5a59ae654496e356b35e9d82bca8d81088683972 100644
--- a/ipalib/rpc.py
+++ b/ipalib/rpc.py
@@ -31,6 +31,7 @@ Also see the `ipaserver.rpcserver` module.
 """
 
 from types import NoneType
+from decimal import Decimal
 import threading
 import sys
 import os
@@ -86,6 +87,9 @@ def xml_wrap(value):
         )
     if type(value) is str:
         return Binary(value)
+    if type(value) is Decimal:
+        # transfer Decimal as a string
+        return unicode(value)
     assert type(value) in (unicode, int, float, bool, NoneType)
     return value
 
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
index 26850db55969a4de2f1d6d5da319f092a36cfe9a..955c11b7ff59f7ba327008c95f296f798d7f0ba9 100644
--- a/ipaserver/rpcserver.py
+++ b/ipaserver/rpcserver.py
@@ -37,6 +37,7 @@ from ipapython.version import VERSION
 import base64
 import os
 import string
+from decimal import Decimal
 _not_found_template = """<html>
 <head>
 <title>404 Not Found</title>
@@ -385,6 +386,8 @@ def json_encode_binary(val):
         return new_list
     elif isinstance(val, str):
         return {'__base64__' : base64.b64encode(val)}
+    elif isinstance(val, Decimal):
+        return {'__base64__' : base64.b64encode(str(val))}
     else:
         return val
 
diff --git a/make-lint b/make-lint
index ef2414970c5d843921ca06afd23d9cca8a3bc35a..5826c32294643cc7a7cf27e219ad63c934604911 100755
--- a/make-lint
+++ b/make-lint
@@ -61,7 +61,7 @@ class IPATypeChecker(TypeChecker):
             'sortorder', 'csv', 'csv_separator', 'csv_skipspace'],
         'ipalib.parameters.Bool': ['truths', 'falsehoods'],
         'ipalib.parameters.Int': ['minvalue', 'maxvalue'],
-        'ipalib.parameters.Float': ['minvalue', 'maxvalue'],
+        'ipalib.parameters.Decimal': ['minvalue', 'maxvalue', 'precision'],
         'ipalib.parameters.Data': ['minlength', 'maxlength', 'length',
             'pattern', 'pattern_errmsg'],
         'ipalib.parameters.Enum': ['values'],
diff --git a/tests/test_ipalib/test_parameters.py b/tests/test_ipalib/test_parameters.py
index 5cb7abf2a09f5d97d6046e56732dd204a1c6ae8c..ad8d8404496f1631cfdfc2db4e6f849ecdd09afe 100644
--- a/tests/test_ipalib/test_parameters.py
+++ b/tests/test_ipalib/test_parameters.py
@@ -25,6 +25,7 @@ Test the `ipalib.parameters` module.
 import re
 import sys
 from types import NoneType
+from decimal import Decimal
 from inspect import isclass
 from tests.util import raises, ClassChecker, read_only
 from tests.util import dummy_ugettext, assert_equal
@@ -1300,77 +1301,77 @@ class test_Int(ClassChecker):
         assert o._convert_scalar(u'0x10')  == 16
         assert o._convert_scalar(u'020')   == 16
 
-class test_Float(ClassChecker):
+class test_Decimal(ClassChecker):
     """
-    Test the `ipalib.parameters.Float` class.
+    Test the `ipalib.parameters.Decimal` class.
     """
-    _cls = parameters.Float
+    _cls = parameters.Decimal
 
     def test_init(self):
         """
-        Test the `ipalib.parameters.Float.__init__` method.
+        Test the `ipalib.parameters.Decimal.__init__` method.
         """
         # Test with no kwargs:
         o = self.cls('my_number')
-        assert o.type is float
-        assert isinstance(o, parameters.Float)
+        assert o.type is Decimal
+        assert isinstance(o, parameters.Decimal)
         assert o.minvalue is None
         assert o.maxvalue is None
 
         # Test when min > max:
-        e = raises(ValueError, self.cls, 'my_number', minvalue=22.5, maxvalue=15.1)
+        e = raises(ValueError, self.cls, 'my_number', minvalue=Decimal('22.5'), maxvalue=Decimal('15.1'))
         assert str(e) == \
-            "Float('my_number'): minvalue > maxvalue (minvalue=22.5, maxvalue=15.1)"
+            "Decimal('my_number'): minvalue > maxvalue (minvalue=22.5, maxvalue=15.1)"
 
     def test_rule_minvalue(self):
         """
-        Test the `ipalib.parameters.Float._rule_minvalue` method.
+        Test the `ipalib.parameters.Decimal._rule_minvalue` method.
         """
-        o = self.cls('my_number', minvalue=3.1)
-        assert o.minvalue == 3.1
+        o = self.cls('my_number', minvalue='3.1')
+        assert o.minvalue == Decimal('3.1')
         rule = o._rule_minvalue
-        translation = u'minvalue=%(minvalue)r'
+        translation = u'minvalue=%(minvalue)s'
         dummy = dummy_ugettext(translation)
         assert dummy.translation is translation
 
         # Test with passing values:
-        for value in (3.2, 99.0):
+        for value in (Decimal('3.2'), Decimal('99.0')):
             assert rule(dummy, value) is None
             assert dummy.called() is False
 
         # Test with failing values:
-        for value in (-1.2, 0.0, 3.0):
+        for value in (Decimal('-1.2'), Decimal('0.0'), Decimal('3.0')):
             assert_equal(
                 rule(dummy, value),
-                translation % dict(minvalue=3.1)
+                translation % dict(minvalue=Decimal('3.1'))
             )
-            assert dummy.message == 'must be at least %(minvalue)f'
+            assert dummy.message == 'must be at least %(minvalue)s'
             assert dummy.called() is True
             dummy.reset()
 
     def test_rule_maxvalue(self):
         """
-        Test the `ipalib.parameters.Float._rule_maxvalue` method.
+        Test the `ipalib.parameters.Decimal._rule_maxvalue` method.
         """
-        o = self.cls('my_number', maxvalue=4.7)
-        assert o.maxvalue == 4.7
+        o = self.cls('my_number', maxvalue='4.7')
+        assert o.maxvalue == Decimal('4.7')
         rule = o._rule_maxvalue
         translation = u'maxvalue=%(maxvalue)r'
         dummy = dummy_ugettext(translation)
         assert dummy.translation is translation
 
         # Test with passing values:
-        for value in (-1.0, 0.1, 4.2):
+        for value in (Decimal('-1.0'), Decimal('0.1'), Decimal('4.2')):
             assert rule(dummy, value) is None
             assert dummy.called() is False
 
         # Test with failing values:
-        for value in (5.3, 99.9):
+        for value in (Decimal('5.3'), Decimal('99.9')):
             assert_equal(
                 rule(dummy, value),
-                translation % dict(maxvalue=4.7)
+                translation % dict(maxvalue=Decimal('4.7'))
             )
-            assert dummy.message == 'can be at most %(maxvalue)f'
+            assert dummy.message == 'can be at most %(maxvalue)s'
             assert dummy.called() is True
             dummy.reset()
 
diff --git a/tests/test_xmlrpc/test_dns_plugin.py b/tests/test_xmlrpc/test_dns_plugin.py
index 00d4f9b7dbd0c401cce891b0bc054c6bf85d7d8c..bded2ad42f1813cf101faf9ba5895bdf78f6699c 100644
--- a/tests/test_xmlrpc/test_dns_plugin.py
+++ b/tests/test_xmlrpc/test_dns_plugin.py
@@ -598,7 +598,7 @@ class test_dns(Declarative):
                     'idnsname': [dnszone1],
                     'mxrecord': [u"0 %s" % dnszone1_mname],
                     'nsrecord': [dnszone1_mname],
-                    'locrecord': [u"49 11 42.4 N 16 36 29.6 E 227.64"],
+                    'locrecord': [u"49 11 42.400 N 16 36 29.600 E 227.64"],
                 },
             },
         ),
-- 
1.7.7.5

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to