diff --git a/HISTORY.rst b/HISTORY.rst index 8510aa0..7db7226 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,7 @@ History v0.31 (unreleased) .................. +* better support for floating point `multiple_of` values, #652 by @justindujardin * fix schema generation for ``NewType`` and ``Literal``, #649 by @dmontagu * add documentation for Literal type, #651 by @dmontagu diff --git a/pydantic/validators.py b/pydantic/validators.py index f3d3aca..9913030 100644 --- a/pydantic/validators.py +++ b/pydantic/validators.py @@ -29,6 +29,7 @@ from .utils import ( AnyCallable, AnyType, ForwardRef, + almost_equal_floats, change_exception, display_as_type, is_callable_type, @@ -121,9 +122,10 @@ def float_validator(v: Any) -> float: def number_multiple_validator(v: 'Number', field: 'Field') -> 'Number': field_type: ConstrainedNumber = field.type_ # type: ignore - if field_type.multiple_of is not None and v % field_type.multiple_of != 0: # type: ignore - raise errors.NumberNotMultipleError(multiple_of=field_type.multiple_of) - + if field_type.multiple_of is not None: + mod = float(v) / float(field_type.multiple_of) % 1 + if not almost_equal_floats(mod, 0.0) and not almost_equal_floats(mod, 1.0): + raise errors.NumberNotMultipleError(multiple_of=field_type.multiple_of) return v diff --git a/tests/test_types.py b/tests/test_types.py index 83f4a06..d22daee 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1422,16 +1422,42 @@ def test_number_le(): Model(a=6) -def test_number_multiple_of(): +@pytest.mark.parametrize('value', ((10), (100), (20))) +def test_number_multiple_of_int_valid(value): class Model(BaseModel): a: conint(multiple_of=5) - assert Model(a=10).dict() == {'a': 10} + assert Model(a=value).dict() == {'a': value} + + +@pytest.mark.parametrize('value', ((1337), (23), (6), (14))) +def test_number_multiple_of_int_invalid(value): + class Model(BaseModel): + a: conint(multiple_of=5) multiple_message = base_message.replace('limit_value', 'multiple_of') message = multiple_message.format(msg='a multiple of 5', ty='multiple', value=5) with pytest.raises(ValidationError, match=message): - Model(a=42) + Model(a=value) + + +@pytest.mark.parametrize('value', ((0.2), (0.3), (0.4), (0.5), (1))) +def test_number_multiple_of_float_valid(value): + class Model(BaseModel): + a: confloat(multiple_of=0.1) + + assert Model(a=value).dict() == {'a': value} + + +@pytest.mark.parametrize('value', ((0.07), (1.27), (1.003))) +def test_number_multiple_of_float_invalid(value): + class Model(BaseModel): + a: confloat(multiple_of=0.1) + + multiple_message = base_message.replace('limit_value', 'multiple_of') + message = multiple_message.format(msg='a multiple of 0.1', ty='multiple', value=0.1) + with pytest.raises(ValidationError, match=message): + Model(a=value) @pytest.mark.parametrize('fn', [conint, confloat, condecimal])