Skip to content

Commit

Permalink
🐛 fix for floating point multiple_of values
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
  • Loading branch information
justindujardin committed Jul 10, 2019
1 parent b52d877 commit 05b2307
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 8 deletions.
10 changes: 8 additions & 2 deletions pydantic/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,14 @@ 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:
v_places = abs(Decimal(str(v)).as_tuple().exponent)
schema_places = abs(Decimal(str(field_type.multiple_of)).as_tuple().exponent)
max_places = max(v_places, schema_places)
multiplier = 10 ** max_places
remainder = round(v * multiplier) % round(field_type.multiple_of * multiplier)
if remainder != 0:
raise errors.NumberNotMultipleError(multiple_of=field_type.multiple_of)

return v

Expand Down
30 changes: 24 additions & 6 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1422,16 +1422,34 @@ class Model(BaseModel):
Model(a=6)


def test_number_multiple_of():
@pytest.mark.parametrize('value,is_valid', ((10, True), (1337, False), (23, False), (100, True), (20, True)))
def test_number_multiple_of(value, is_valid):
class Model(BaseModel):
a: conint(multiple_of=5)

assert Model(a=10).dict() == {'a': 10}
if is_valid:
assert Model(a=value).dict() == {'a': value}
else:
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=value)

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)

@pytest.mark.parametrize(
'value,is_valid', ((0.2, True), (0.3, True), (0.4, True), (0.5, True), (0.07, False), (1.27, False), (1, True))
)
def test_number_multiple_of_float(value, is_valid):
class Model(BaseModel):
a: confloat(multiple_of=0.1)

if is_valid:
assert Model(a=value).dict() == {'a': value}
else:
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 05b2307

Please sign in to comment.