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:
parent
61224ce932
commit
15d3ab88f2
@ -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):
|
||||
|
@ -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']))
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Added minimum and maximum value limits to FloatOpt.
|
Loading…
Reference in New Issue
Block a user