freeipa/ipalib/encoder.py
Martin Kosek 092dd8db12 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
2012-01-20 08:13:44 +01:00

206 lines
7.7 KiB
Python

# Authors:
# Pavel Zuna <pzuna@redhat.com>
#
# Copyright (C) 2009 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Encoding capabilities.
"""
from decimal import Decimal
class EncoderSettings(object):
"""
Container for encoder settings.
"""
encode_to = 'utf-8'
encode_none = False
encode_dict_keys = False
encode_dict_keys_postprocess = True
encode_dict_vals = True
encode_dict_vals_postprocess = True
encode_postprocessor = staticmethod(lambda x: x)
decode_from = 'utf-8'
decode_none = False
decode_dict_keys = False
decode_dict_keys_postprocess = True
decode_dict_vals = True
decode_dict_vals_postprocess = True
decode_dict_vals_table = dict()
decode_dict_vals_table_keygen = staticmethod(lambda x, y: x)
decode_postprocessor = staticmethod(lambda x: x)
class Encoder(object):
"""
Base class implementing encoding of python scalar types to strings
and vise-versa.
"""
encoder_settings = EncoderSettings()
def __init__(self):
# each instance should have its own settings
self.encoder_settings = EncoderSettings()
def _decode_dict_val(self, key, val):
f = self.encoder_settings.decode_dict_vals_table.get(
self.encoder_settings.decode_dict_vals_table_keygen(key, val)
)
if f:
return val
return self.decode(val)
def encode(self, var):
"""
Encode any python built-in python type variable into `self.encode_to`.
Compound types have their individual members encoded.
Returns an encoded copy of 'var'.
"""
if isinstance(var, str):
return var
elif isinstance(var, unicode):
return self.encoder_settings.encode_postprocessor(
var.encode(self.encoder_settings.encode_to)
)
elif isinstance(var, (bool, float, Decimal, int, long)):
return self.encoder_settings.encode_postprocessor(
str(var).encode(self.encoder_settings.encode_to)
)
elif isinstance(var, list):
return [self.encode(m) for m in var]
elif isinstance(var, tuple):
return tuple(self.encode(m) for m in var)
elif isinstance(var, dict):
if self.encoder_settings.encode_dict_keys:
dct = dict()
if not self.encoder_settings.encode_dict_keys_postprocess:
tmp = self.encoder_settings.encode_postprocessor
self.encoder_settings.encode_postprocessor = lambda x: x
for (k, v) in var.iteritems():
dct[self.encode(k)] = v
if not self.encoder_settings.encode_dict_keys_postprocess:
self.encoder_settings.encode_postprocessor = tmp
else:
dct = dict(var)
if self.encoder_settings.encode_dict_vals:
if not self.encoder_settings.encode_dict_vals_postprocess:
tmp = self.encoder_settings.encode_postprocessor
self.encoder_settings.encode_postprocessor = lambda x: x
for (k, v) in dct.iteritems():
dct[k] = self.encode(v)
if not self.encoder_settings.encode_dict_vals_postprocess:
self.encoder_settings.encode_postprocessor = tmp
return dct
elif var is None:
if self.encoder_settings.encode_none:
return self.encoder_settings.encode_postprocessor(
str(var).encode(self.encoder_settings.encode_to)
)
return None
raise TypeError('python built-in type expected, got \'%s\'', type(var))
def decode(self, var):
"""
Decode strings in `self.decode_from` into python strings.
Compound types have their individual members decoded.
Dictionaries can have their values decoded into other types
by looking up keys in `self.decode_dict_vals_table`.
Returns a decoded copy of 'var'.
"""
if isinstance(var, unicode):
return var
elif isinstance(var, str):
return self.encoder_settings.decode_postprocessor(
var.decode(self.encoder_settings.decode_from)
)
elif isinstance(var, (bool, float, Decimal, int, long)):
return var
elif isinstance(var, list):
return [self.decode(m) for m in var]
elif isinstance(var, tuple):
return tuple(self.decode(m) for m in var)
elif isinstance(var, dict):
if self.encoder_settings.decode_dict_keys:
dct = dict()
if not self.encoder_settings.decode_dict_keys_postprocess:
tmp = self.encoder_settings.decode_postprocessor
self.encoder_settings.decode_postprocessor = lambda x: x
for (k, v) in var.iteritems():
dct[self.decode(k)] = v
if not self.encoder_settings.decode_dict_keys_postprocess:
self.encoder_settings.decode_postprocessor = tmp
else:
dct = dict(var)
if self.encoder_settings.decode_dict_vals:
if not self.encoder_settings.decode_dict_vals_postprocess:
tmp = self.encoder_settings.decode_postprocessor
self.encoder_settings.decode_postprocessor = lambda x: x
for (k, v) in dct.iteritems():
dct[k] = self._decode_dict_val(k, v)
if not self.encoder_settings.decode_dict_vals_postprocess:
self.encoder_settings.decode_postprocessor = tmp
return dct
elif var is None:
if self.encoder_settings.decode_none:
return self.encoder_settings.decode_postprocessor(
str(var).decode(self.encoder_settings.decode_from)
)
return None
raise TypeError('python built-in type expected, got \'%s\'', type(var))
## ENCODER METHOD DECORATORS
def encode_args(*outer_args):
def decorate(f):
def new_f(*args, **kwargs):
assert isinstance(args[0], Encoder), \
'first argument not Encoder instance'
new_args = list(args)
for a in outer_args:
if isinstance(a, int):
if a < len(args):
new_args[a] = args[0].encode(args[a])
elif isinstance(a, basestring):
if a in kwargs:
kwargs[a] = args[0].encode(kwargs[a])
else:
raise TypeError(
'encode_args takes a list of ints and basestrings'
)
return f(*new_args, **kwargs)
new_f.func_name = f.func_name
return new_f
return decorate
def decode_retval():
def decorate(f):
def new_f(*args, **kwargs):
assert isinstance(args[0], Encoder), \
'first argument not Encoder instance'
return args[0].decode(f(*args, **kwargs))
new_f.func_name = f.func_name
return new_f
return decorate