diff --git a/CHANGES.rst b/CHANGES.rst index 26a9037..7e87b06 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,9 @@ Unreleased * raise ``InterpolationError`` instead of leaking a raw ``TypeError`` when a value interpolates a reference to an option whose value is a list +* reject ``NaN`` against a declared ``float`` bound; every comparison against + ``NaN`` is False, so it previously satisfied neither the ``min`` nor the + ``max`` check and was accepted as valid Release 5.0.9 """"""""""""" diff --git a/src/configobj/validate.py b/src/configobj/validate.py index fd90899..8ccc603 100644 --- a/src/configobj/validate.py +++ b/src/configobj/validate.py @@ -840,9 +840,12 @@ def is_float(value, min=None, max=None): value = float(value) except ValueError: raise VdtTypeError(value) - if (min_val is not None) and (value < min_val): + # Phrased as "must satisfy the bound" rather than "must not violate it", + # so that a NaN -- for which every comparison is False -- fails a declared + # bound instead of slipping past both checks. + if (min_val is not None) and not (value >= min_val): raise VdtValueTooSmallError(value) - if (max_val is not None) and (value > max_val): + if (max_val is not None) and not (value <= max_val): raise VdtValueTooBigError(value) return value diff --git a/src/tests/test_validate.py b/src/tests/test_validate.py index dae0dea..d7c4b5e 100644 --- a/src/tests/test_validate.py +++ b/src/tests/test_validate.py @@ -1,6 +1,8 @@ +import math + from configobj import ConfigObj import pytest -from configobj.validate import VdtValueTooSmallError +from configobj.validate import VdtValueTooBigError, VdtValueTooSmallError class TestImporting: @@ -173,3 +175,23 @@ def change(section, key): 'test3': 3, 'test4': 6.0 }}} + + +class TestFloatBounds: + """A declared bound must reject NaN, for which every comparison is False.""" + + def test_nan_fails_min(self, val): + with pytest.raises(VdtValueTooSmallError): + val.check('float(0, 10)', 'nan') + + def test_nan_fails_max_only(self, val): + with pytest.raises(VdtValueTooBigError): + val.check('float(max=10)', 'nan') + + def test_bounded_floats_still_pass(self, val): + assert val.check('float(0, 10)', '5.0') == 5.0 + assert val.check('float(0, 10)', '0') == 0.0 + assert val.check('float(0, 10)', '10') == 10.0 + + def test_unbounded_float_is_unchanged(self, val): + assert math.isnan(val.check('float', 'nan'))