diff options
-rw-r--r-- | oslo_config/cfg.py | 11 | ||||
-rw-r--r-- | oslo_config/tests/test_cfg.py | 79 | ||||
-rw-r--r-- | oslo_config/tests/test_types.py | 78 | ||||
-rw-r--r-- | oslo_config/types.py | 97 | ||||
-rw-r--r-- | releasenotes/notes/add-float-min-max-b1a2e16301c8435c.yaml | 3 |
5 files changed, 226 insertions, 42 deletions
diff --git a/oslo_config/cfg.py b/oslo_config/cfg.py index 0bb219c..08175a4 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 ac00219..aaa07b9 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 b1b18cb..b53817d 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 4eaabea..892b589 100644 --- a/oslo_config/types.py +++ b/oslo_config/types.py @@ -243,12 +243,9 @@ class Boolean(ConfigType): return str(value).lower() -class Integer(ConfigType): +class Number(ConfigType): - """Integer type. - - Converts value to an integer optionally doing range checking. - If value is whitespace or empty string will return None. + """Number class, base for Integer and Float. :param min: Optional check that value is greater than or equal to min. Mutually exclusive with 'choices'. @@ -257,20 +254,16 @@ class Integer(ConfigType): :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) - .. versionchanged:: 2.4 - The class now honors zero for *min* and *max* parameters. - - .. versionchanged:: 2.7 - Added *type_name* parameter. - - .. versionchanged:: 3.2 - Added *choices* parameter. + .. versionadded:: 3.14 """ - def __init__(self, min=None, max=None, type_name='integer value', - choices=None): - super(Integer, self).__init__(type_name=type_name) + 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 " @@ -278,17 +271,19 @@ class Integer(ConfigType): 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, int): + if not isinstance(value, self.num_type): s = str(value).strip() if s == '': value = None else: - value = int(value) + value = self.num_type(value) if value is not None: if self.choices is not None: @@ -302,15 +297,15 @@ class Integer(ConfigType): if value in self.choices: return else: - raise ValueError('Valid values are %r, but found %d' % ( + 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 %d' % + 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 %d' % self.max) + raise ValueError('Should be less than or equal to %g' % self.max) def __repr__(self): props = [] @@ -318,13 +313,13 @@ class Integer(ConfigType): props.append("choices=%r" % (self.choices,)) else: if self.min is not None: - props.append('min=%d' % self.min) + props.append('min=%g' % self.min) if self.max is not None: - props.append('max=%d' % self.max) + props.append('max=%g' % self.max) if props: - return 'Integer(%s)' % ', '.join(props) - return 'Integer' + return self.__class__.__name__ + '(%s)' % ', '.join(props) + return self.__class__.__name__ def __eq__(self, other): return ( @@ -340,34 +335,56 @@ class Integer(ConfigType): return str(value) -class Float(ConfigType): +class Integer(Number): - """Float type. + """Integer type. + + Converts value to an integer optionally doing range checking. + If value is whitespace or empty string will return None. + :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'. - .. versionchanged:: 2.7 + .. versionchanged:: 2.4 + The class now honors zero for *min* and *max* parameters. + .. versionchanged:: 2.7 Added *type_name* parameter. + + .. versionchanged:: 3.2 + Added *choices* parameter. """ - def __init__(self, type_name='floating point value'): - super(Float, self).__init__(type_name=type_name) + def __init__(self, min=None, max=None, type_name='integer value', + choices=None): + super(Integer, self).__init__(int, type_name, min=min, max=max, + choices=choices) - def __call__(self, value): - if isinstance(value, float): - return value - return float(value) +class Float(Number): - def __repr__(self): - return 'Float' + """Float type. - def __eq__(self, other): - return self.__class__ == other.__class__ + :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. - def _formatter(self, value): - return str(value) + .. versionchanged:: 2.7 + + Added *type_name* parameter. + + .. versionchanged:: 3.14 + + Added *min* and *max* parameters. + """ + + 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 0000000..dffb4ad --- /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 |