From 15d3ab88f2ddf0c2b7ef60f499d029683a11d513 Mon Sep 17 00:00:00 2001 From: Adit Sarfaty Date: Wed, 20 Jul 2016 15:14:05 +0300 Subject: [PATCH] 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 --- oslo_config/cfg.py | 11 +- oslo_config/tests/test_cfg.py | 79 ++++++++ oslo_config/tests/test_types.py | 78 +++++++ oslo_config/types.py | 191 ++++++++++-------- .../add-float-min-max-b1a2e16301c8435c.yaml | 3 + 5 files changed, 273 insertions(+), 89 deletions(-) create mode 100644 releasenotes/notes/add-float-min-max-b1a2e16301c8435c.yaml diff --git a/oslo_config/cfg.py b/oslo_config/cfg.py index 0bb219c6..08175a4e 100644 --- a/oslo_config/cfg.py +++ b/oslo_config/cfg.py @@ -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): diff --git a/oslo_config/tests/test_cfg.py b/oslo_config/tests/test_cfg.py index ac00219b..aaa07b9c 100644 --- a/oslo_config/tests/test_cfg.py +++ b/oslo_config/tests/test_cfg.py @@ -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'])) diff --git a/oslo_config/tests/test_types.py b/oslo_config/tests/test_types.py index b1b18cbe..b53817d8 100644 --- a/oslo_config/tests/test_types.py +++ b/oslo_config/tests/test_types.py @@ -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() diff --git a/oslo_config/types.py b/oslo_config/types.py index 4eaabea2..892b5893 100644 --- a/oslo_config/types.py +++ b/oslo_config/types.py @@ -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): diff --git a/releasenotes/notes/add-float-min-max-b1a2e16301c8435c.yaml b/releasenotes/notes/add-float-min-max-b1a2e16301c8435c.yaml new file mode 100644 index 00000000..dffb4adb --- /dev/null +++ b/releasenotes/notes/add-float-min-max-b1a2e16301c8435c.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added minimum and maximum value limits to FloatOpt. \ No newline at end of file