Add better support for floating point multiple_of values (#652)

- modulo doesn't work with floating point values in many cases, e.g. `0.3 % 0.1 == 0.09999999999999998`
 - port implementation from: https://github.com/tdegrunt/jsonschema/issues/187#issuecomment-320664251
 - add tests for int/float multiple_of values
 - update history with pr/author
This commit is contained in:
Justin DuJardin
2019-07-23 08:41:11 -07:00
committed by Samuel Colvin
parent 4c9ee486d8
commit bc60014518
3 changed files with 35 additions and 6 deletions
+1
View File
@@ -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
+5 -3
View File
@@ -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
+29 -3
View File
@@ -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])