Skip to content

Commit

Permalink
Add better support for floating point multiple_of values (#652)
Browse files Browse the repository at this point in the history
- modulo doesn't work with floating point values in many cases, e.g. `0.3 % 0.1 == 0.09999999999999998`
 - port implementation from: tdegrunt/jsonschema#187 (comment)
 - add tests for int/float multiple_of values
 - update history with pr/author
  • Loading branch information
justindujardin authored and samuelcolvin committed Jul 23, 2019
1 parent 4c9ee48 commit bc60014
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 6 deletions.
1 change: 1 addition & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 5 additions & 3 deletions pydantic/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
AnyCallable,
AnyType,
ForwardRef,
almost_equal_floats,
change_exception,
display_as_type,
is_callable_type,
Expand Down Expand Up @@ -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


Expand Down
32 changes: 29 additions & 3 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1422,16 +1422,42 @@ class Model(BaseModel):
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])
Expand Down

0 comments on commit bc60014

Please sign in to comment.