summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--oslo_config/cfg.py11
-rw-r--r--oslo_config/tests/test_cfg.py79
-rw-r--r--oslo_config/tests/test_types.py78
-rw-r--r--oslo_config/types.py97
-rw-r--r--releasenotes/notes/add-float-min-max-b1a2e16301c8435c.yaml3
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