Add min and max values to Float type and Opt

Just like Integers, Floats configuration should also have a minimum and
maximum possible values.
For example, the vmware-nsx plugin needs it for the QoS support.
See https://review.openstack.org/#/c/344763/

Change-Id: If1c47444e0c12b68d9d9cb645b8251e4462cfd49
This commit is contained in:
Adit Sarfaty 2016-07-20 15:14:05 +03:00
parent 61224ce932
commit 15d3ab88f2
5 changed files with 273 additions and 89 deletions

View File

@ -1156,13 +1156,20 @@ class FloatOpt(Opt):
"""Option with Float type
Option with ``type`` :class:`oslo_config.types.Float`
:param min: minimum value the float can take
:param max: maximum value the float can take
:param name: the option's name
:param \*\*kwargs: arbitrary keyword arguments passed to :class:`Opt`
.. versionchanged:: 3.14
Added *min* and *max* parameters.
"""
def __init__(self, name, **kwargs):
super(FloatOpt, self).__init__(name, type=types.Float(), **kwargs)
def __init__(self, name, min=None, max=None, **kwargs):
super(FloatOpt, self).__init__(name, type=types.Float(min, max),
**kwargs)
class ListOpt(Opt):

View File

@ -1150,6 +1150,85 @@ class ConfigFileOptsTestCase(BaseTestCase):
def test_conf_file_float_ignore_dgroup_and_dname(self):
self._do_dgroup_and_dname_test_ignore(cfg.FloatOpt, '64.54', 64.54)
def test_conf_file_float_min_max_above_max(self):
self.conf.register_opt(cfg.FloatOpt('foo', min=1.1, max=5.5))
paths = self.create_tempfiles([('test',
'[DEFAULT]\n'
'foo = 10.5\n')])
self.conf(['--config-file', paths[0]])
self.assertRaises(cfg.ConfigFileValueError, self.conf._get, 'foo')
def test_conf_file_float_only_max_above_max(self):
self.conf.register_opt(cfg.FloatOpt('foo', max=5.5))
paths = self.create_tempfiles([('test',
'[DEFAULT]\n'
'foo = 10.5\n')])
self.conf(['--config-file', paths[0]])
self.assertRaises(cfg.ConfigFileValueError, self.conf._get, 'foo')
def test_conf_file_float_min_max_below_min(self):
self.conf.register_opt(cfg.FloatOpt('foo', min=1.1, max=5.5))
paths = self.create_tempfiles([('test',
'[DEFAULT]\n'
'foo = 0.5\n')])
self.conf(['--config-file', paths[0]])
self.assertRaises(cfg.ConfigFileValueError, self.conf._get, 'foo')
def test_conf_file_float_only_min_below_min(self):
self.conf.register_opt(cfg.FloatOpt('foo', min=1.1))
paths = self.create_tempfiles([('test',
'[DEFAULT]\n'
'foo = 0.5\n')])
self.conf(['--config-file', paths[0]])
self.assertRaises(cfg.ConfigFileValueError, self.conf._get, 'foo')
def test_conf_file_float_min_max_in_range(self):
self.conf.register_opt(cfg.FloatOpt('foo', min=1.1, max=5.5))
paths = self.create_tempfiles([('test',
'[DEFAULT]\n'
'foo = 4.5\n')])
self.conf(['--config-file', paths[0]])
self.assertTrue(hasattr(self.conf, 'foo'))
self.assertEqual(4.5, self.conf.foo)
def test_conf_file_float_only_max_in_range(self):
self.conf.register_opt(cfg.FloatOpt('foo', max=5.5))
paths = self.create_tempfiles([('test',
'[DEFAULT]\n'
'foo = 4.5\n')])
self.conf(['--config-file', paths[0]])
self.assertTrue(hasattr(self.conf, 'foo'))
self.assertEqual(4.5, self.conf.foo)
def test_conf_file_float_only_min_in_range(self):
self.conf.register_opt(cfg.FloatOpt('foo', min=3.5))
paths = self.create_tempfiles([('test',
'[DEFAULT]\n'
'foo = 4.5\n')])
self.conf(['--config-file', paths[0]])
self.assertTrue(hasattr(self.conf, 'foo'))
self.assertEqual(4.5, self.conf.foo)
def test_conf_file_float_min_greater_max(self):
self.assertRaises(ValueError, cfg.FloatOpt, 'foo', min=5.5, max=1.5)
def test_conf_file_list_default(self):
self.conf.register_opt(cfg.ListOpt('foo', default=['bar']))

View File

@ -394,12 +394,90 @@ class FloatTypeTests(TypeTestHelper, unittest.TestCase):
def test_repr(self):
self.assertEqual('Float', repr(types.Float()))
def test_repr_with_min(self):
t = types.Float(min=1.1)
self.assertEqual('Float(min=1.1)', repr(t))
def test_repr_with_max(self):
t = types.Float(max=2.2)
self.assertEqual('Float(max=2.2)', repr(t))
def test_repr_with_min_and_max(self):
t = types.Float(min=1.1, max=2.2)
self.assertEqual('Float(min=1.1, max=2.2)', repr(t))
t = types.Float(min=1.0, max=2)
self.assertEqual('Float(min=1, max=2)', repr(t))
t = types.Float(min=0, max=0)
self.assertEqual('Float(min=0, max=0)', repr(t))
def test_equal(self):
self.assertTrue(types.Float() == types.Float())
def test_equal_with_same_min_and_no_max(self):
self.assertTrue(types.Float(min=123.1) == types.Float(min=123.1))
def test_equal_with_same_max_and_no_min(self):
self.assertTrue(types.Float(max=123.1) == types.Float(max=123.1))
def test_not_equal(self):
self.assertFalse(types.Float(min=123.1) == types.Float(min=456.1))
self.assertFalse(types.Float(max=123.1) == types.Float(max=456.1))
self.assertFalse(types.Float(min=123.1) == types.Float(max=123.1))
self.assertFalse(types.Float(min=123.1, max=456.1) ==
types.Float(min=123.1, max=456.2))
def test_not_equal_to_other_class(self):
self.assertFalse(types.Float() == types.Integer())
def test_equal_with_same_min_and_max(self):
t1 = types.Float(min=1.1, max=2.2)
t2 = types.Float(min=1.1, max=2.2)
self.assertTrue(t1 == t2)
def test_min_greater_max(self):
self.assertRaises(ValueError,
types.Float,
min=100.1, max=50)
self.assertRaises(ValueError,
types.Float,
min=-50, max=-100.1)
self.assertRaises(ValueError,
types.Float,
min=0.1, max=-50.0)
self.assertRaises(ValueError,
types.Float,
min=50.0, max=0.0)
def test_with_max_and_min(self):
t = types.Float(min=123.45, max=678.9)
self.assertRaises(ValueError, t, 123)
self.assertRaises(ValueError, t, 123.1)
t(124.1)
t(300)
t(456.0)
self.assertRaises(ValueError, t, 0)
self.assertRaises(ValueError, t, 800.5)
def test_with_min_zero(self):
t = types.Float(min=0, max=456.1)
self.assertRaises(ValueError, t, -1)
t(0.0)
t(123.1)
t(300.2)
t(456.1)
self.assertRaises(ValueError, t, -201.0)
self.assertRaises(ValueError, t, 457.0)
def test_with_max_zero(self):
t = types.Float(min=-456.1, max=0)
self.assertRaises(ValueError, t, 1)
t(0.0)
t(-123.0)
t(-300.0)
t(-456.0)
self.assertRaises(ValueError, t, 201.0)
self.assertRaises(ValueError, t, -457.0)
class ListTypeTests(TypeTestHelper, unittest.TestCase):
type = types.List()

View File

@ -243,7 +243,99 @@ class Boolean(ConfigType):
return str(value).lower()
class Integer(ConfigType):
class Number(ConfigType):
"""Number class, base for Integer and Float.
:param min: Optional check that value is greater than or equal to min.
Mutually exclusive with 'choices'.
:param max: Optional check that value is less than or equal to max.
Mutually exclusive with 'choices'.
:param type_name: Type name to be used in the sample config file.
:param choices: Optional sequence of valid values. Mutually exclusive
with 'min/max'.
:param num_type: the type of number used for casting (i.e int, float)
.. versionadded:: 3.14
"""
def __init__(self, num_type, type_name,
min=None, max=None, choices=None):
super(Number, self).__init__(type_name=type_name)
# Validate the choices and limits
if choices is not None:
if min is not None or max is not None:
raise ValueError("'choices' and 'min/max' cannot both be "
"specified")
else:
if min is not None and max is not None and max < min:
raise ValueError('Max value is less than min value')
self.min = min
self.max = max
self.choices = choices
self.num_type = num_type
def __call__(self, value):
if not isinstance(value, self.num_type):
s = str(value).strip()
if s == '':
value = None
else:
value = self.num_type(value)
if value is not None:
if self.choices is not None:
self._check_choices(value)
else:
self._check_range(value)
return value
def _check_choices(self, value):
if value in self.choices:
return
else:
raise ValueError('Valid values are %r, but found %g' % (
self.choices, value))
def _check_range(self, value):
if self.min is not None and value < self.min:
raise ValueError('Should be greater than or equal to %g' %
self.min)
if self.max is not None and value > self.max:
raise ValueError('Should be less than or equal to %g' % self.max)
def __repr__(self):
props = []
if self.choices is not None:
props.append("choices=%r" % (self.choices,))
else:
if self.min is not None:
props.append('min=%g' % self.min)
if self.max is not None:
props.append('max=%g' % self.max)
if props:
return self.__class__.__name__ + '(%s)' % ', '.join(props)
return self.__class__.__name__
def __eq__(self, other):
return (
(self.__class__ == other.__class__) and
(self.min == other.min) and
(self.max == other.max) and
(set(self.choices) == set(other.choices) if
self.choices and other.choices else
self.choices == other.choices)
)
def _formatter(self, value):
return str(value)
class Integer(Number):
"""Integer type.
@ -270,104 +362,29 @@ class Integer(ConfigType):
def __init__(self, min=None, max=None, type_name='integer value',
choices=None):
super(Integer, self).__init__(type_name=type_name)
if choices is not None:
if min is not None or max is not None:
raise ValueError("'choices' and 'min/max' cannot both be "
"specified")
else:
if min is not None and max is not None and max < min:
raise ValueError('Max value is less than min value')
self.min = min
self.max = max
self.choices = choices
def __call__(self, value):
if not isinstance(value, int):
s = str(value).strip()
if s == '':
value = None
else:
value = int(value)
if value is not None:
if self.choices is not None:
self._check_choices(value)
else:
self._check_range(value)
return value
def _check_choices(self, value):
if value in self.choices:
return
else:
raise ValueError('Valid values are %r, but found %d' % (
self.choices, value))
def _check_range(self, value):
if self.min is not None and value < self.min:
raise ValueError('Should be greater than or equal to %d' %
self.min)
if self.max is not None and value > self.max:
raise ValueError('Should be less than or equal to %d' % self.max)
def __repr__(self):
props = []
if self.choices is not None:
props.append("choices=%r" % (self.choices,))
else:
if self.min is not None:
props.append('min=%d' % self.min)
if self.max is not None:
props.append('max=%d' % self.max)
if props:
return 'Integer(%s)' % ', '.join(props)
return 'Integer'
def __eq__(self, other):
return (
(self.__class__ == other.__class__) and
(self.min == other.min) and
(self.max == other.max) and
(set(self.choices) == set(other.choices) if
self.choices and other.choices else
self.choices == other.choices)
)
def _formatter(self, value):
return str(value)
super(Integer, self).__init__(int, type_name, min=min, max=max,
choices=choices)
class Float(ConfigType):
class Float(Number):
"""Float type.
:param type_name: Type name to be used in the sample config file.
:param min: Optional check that value is greater than or equal to min.
:param max: Optional check that value is less than or equal to max.
.. versionchanged:: 2.7
Added *type_name* parameter.
.. versionchanged:: 3.14
Added *min* and *max* parameters.
"""
def __init__(self, type_name='floating point value'):
super(Float, self).__init__(type_name=type_name)
def __call__(self, value):
if isinstance(value, float):
return value
return float(value)
def __repr__(self):
return 'Float'
def __eq__(self, other):
return self.__class__ == other.__class__
def _formatter(self, value):
return str(value)
def __init__(self, min=None, max=None, type_name='floating point value'):
super(Float, self).__init__(float, type_name, min=min, max=max)
class List(ConfigType):

View File

@ -0,0 +1,3 @@
---
features:
- Added minimum and maximum value limits to FloatOpt.