From bcc1e1e10c53d8d1f5de1b747057b35af9b4713d Mon Sep 17 00:00:00 2001 From: Dylan Pulver Date: Tue, 1 Sep 2026 22:17:09 +0300 Subject: [PATCH] Reject NaN against a declared float bound is_float compared with `value < min_val` / `value > max_val`. Every comparison against NaN is False, so a NaN satisfied neither test and was returned as valid: `float(0, 10)` accepts the string 'nan', while 'inf' and '-inf' are correctly rejected by those same bounds. docs/validate.rst describes these parameters as "any value from 3 to 9" (integer, and float "has the same parameters"), which NaN is not. Phrase the checks as "must satisfy the bound" instead, so NaN fails them. An unbounded `float` still accepts NaN, unchanged. is_integer is unaffected: it converts through int() first, which rejects NaN before any bound is compared. --- CHANGES.rst | 3 +++ src/configobj/validate.py | 7 +++++-- src/tests/test_validate.py | 24 +++++++++++++++++++++++- 3 files changed, 31 insertions(+), 3 deletions(-) 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'))