Commit b4b86a4b authored by dmitry.mashoshin's avatar dmitry.mashoshin
Browse files

Merge branch 'mashony/update-decimal-number-field-type-precision' into 'master'

Update DecimalIntegerType validation

See merge request !41
parents 4c546a7e bb1d1a92
......@@ -105,6 +105,7 @@ class BaseSchemaBuilder:
'minItems': 'min_size',
'maxItems': 'max_size',
'x-serialize_when_none': 'serialize_when_none',
'x-strict': 'strict',
}
self.custom_default = {
'hex': lambda: uuid4().hex,
......@@ -171,10 +172,14 @@ class BaseSchemaBuilder:
return module
def _map_params(self, prop_data: dict) -> dict:
params = {}
params, metadata = {}, {}
for name, value in prop_data.items():
if name in self.known_props:
params[self.known_props[name]] = value
else:
valid_name = name.replace("x-", "")
metadata.update({valid_name: value})
params['metadata'] = metadata
if 'x-default' in prop_data:
params['default'] = self.custom_default[prop_data['x-default']]
return params
......
import json
import operator
from ..translator import _
from .base import NumberType, UTCDateTimeType, BaseType
from schematics.exceptions import ValidationError
......@@ -8,6 +10,11 @@ class DecimalIntegerType(NumberType):
"""
Custom type for integer representation of decimal values
"""
MESSAGES = {
'number_coerce': _("Value '{0}' is not {1}."),
'number_min': _("{0} value should be greater than{1} {2}."),
'number_max': _("{0} value should be less than{1} {2}."),
}
number_type = 'Decimal'
_max_int = 999999999999999
......@@ -15,6 +22,11 @@ class DecimalIntegerType(NumberType):
"""
Override init for set default strict to 2
"""
self.strict = strict
if max_value:
max_value = max_value * self.get_strict()
if min_value:
min_value = min_value * self.get_strict()
if not max_value or max_value > self._max_int:
max_value = self._max_int
super().__init__(min_value, max_value, strict, **kwargs)
......@@ -30,20 +42,47 @@ class DecimalIntegerType(NumberType):
def convert(self, value, context=None):
if not isinstance(value, (int, float)):
raise ValidationError(f'Require int or float type, but {type(value).__name__} found')
precision = self.__get_value_precision(value)
if precision > self.strict:
raise ValidationError(f"Value should have less than or equal to {self.strict} numbers after decimal point")
return super().convert(value, context)
def to_native(self, value, context=None):
if 'oo' in context and context.oo: # TODO: Sorry, it is temporary fix. Will be refactored.
if context and 'oo' in context and context.oo: # TODO: Sorry, it is temporary fix. Will be refactored.
return round(value * self.get_strict())
return value
def __get_value_precision(self, value):
# todo: find more correct way to calculate this
decimal_number = "{:.15f}".format(float(value)).strip("0").split(".")[1]
return len(decimal_number)
def validate_range(self, value, context=None):
if self.min_value is not None and value < self.min_value:
min_value = self.min_value / self.get_strict()
raise ValidationError(self.messages['number_min'].format(self.number_type, min_value))
if self.max_value is not None and value > self.max_value:
max_value = self.max_value / self.get_strict()
raise ValidationError(self.messages['number_max'].format(self.number_type, max_value))
"""
validate value greater than min_value and less than max_value
"""
if self.min_value is not None:
comp = operator.le if self.metadata.get('exclusiveMinimum', False) else operator.lt
if comp(value, self.min_value):
min_value = self.min_value / self.get_strict()
raise ValidationError(
self.messages['number_min'].format(
self.number_type,
"" if self.metadata.get('exclusiveMinimum', False) else " or equal to",
min_value
)
)
if self.max_value is not None:
comp = operator.ge if self.metadata.get('exclusiveMaximum', False) else operator.gt
if comp(value, self.max_value):
max_value = self.max_value / self.get_strict()
raise ValidationError(
self.messages['number_max'].format(
self.number_type,
"" if self.metadata.get('exclusiveMaximum', False) else " or equal to",
max_value
)
)
return value
......
import pytest
from schematics import exceptions, types, Model
from contextlib import contextmanager
from schematics.swagger_generator import BaseSchemaBuilder
@contextmanager
def does_not_raise():
yield
@pytest.fixture()
def make_model_spec():
def _wr(properties):
return {
'models.A': {
'title': 'A',
'x-baseClass': 'schematics.models.Model',
'type': 'object',
'description': 'A model',
'properties': properties,
'required': list(properties.keys())
}
}
return _wr
class TestDecimalIntegerType:
def test_max_value_validation(self):
class A(Model):
amount = types.DecimalIntegerType()
with pytest.raises(exceptions.DataError) as ex:
A({'amount': 10000000000000})
max_int = A.fields['amount'].max_value / 100
assert ex == f'["Decimal value should be less than or equal to {max_int}."]'
def test_override_max_value(self):
max_value = 10000
class A(Model):
amount = types.DecimalIntegerType(max_value=max_value)
assert A.fields['amount'].max_value == max_value * 100
with pytest.raises(exceptions.DataError) as ex:
A({'amount': max_value + 1})
max_int = A.fields['amount'].max_value / 100
assert ex == f'["Decimal value should be less than or equal to {max_int}."]'
def test_cannot_override_max_value(self):
max_value = types.DecimalIntegerType._max_int + 1
class A(Model):
amount = types.DecimalIntegerType(max_value=max_value)
assert A.fields['amount'].max_value == types.DecimalIntegerType._max_int
@pytest.mark.parametrize("min_value, test_value, exclusive_min, expectation", [
(10, 10, False, does_not_raise()), # default
(10, 10, True, pytest.raises(exceptions.DataError)),
(10, 9, False, pytest.raises(exceptions.DataError)),
(10, 9, True, pytest.raises(exceptions.DataError)),
])
def test_value_exclusiveMinimum_validation(self, min_value, test_value, exclusive_min,
expectation):
class A(Model):
amount = types.DecimalIntegerType(min_value=min_value, metadata={
"exclusiveMinimum": exclusive_min
})
with expectation:
A({'amount': test_value})
def test_value_exclusiveMinimum_default_validation(self):
min_value = 10
test_value = 10
class A(Model):
amount = types.DecimalIntegerType(min_value=min_value)
A({'amount': test_value})
@pytest.mark.parametrize("max_value, test_value, exclusive_max, expectation", [
(10, 10, False, does_not_raise()), # default
(10, 10, True, pytest.raises(exceptions.DataError)),
(10, 11, False, pytest.raises(exceptions.DataError)),
(10, 11, True, pytest.raises(exceptions.DataError)),
])
def test_value_exclusiveMaximum_validation(self, max_value, test_value, exclusive_max,
expectation):
class A(Model):
amount = types.DecimalIntegerType(max_value=max_value, metadata={
"exclusiveMaximum": exclusive_max
})
with expectation:
A({'amount': test_value})
def test_value_exclusiveMaximum_default_validation(self):
max_value = 10
test_value = 10
class A(Model):
amount = types.DecimalIntegerType(max_value=max_value)
A({'amount': test_value})
def test_swagger_generator(self, make_model_spec):
props = {
'amount': {
'type': 'number',
'format': 'float',
'x-format': 'decimal-float',
'x-legalNameUa': 'Сума',
'x-legalNameEn': 'Amount',
'minimum': 0,
'exclusiveMinimum': True
}
}
test_class_schema = make_model_spec(props)
schema = BaseSchemaBuilder(test_class_schema, 'models')
schema.generate_schema_from_swagger()
cls = getattr(schema.module, 'A')
amount_field = cls.fields['amount']
assert isinstance(amount_field, types.DecimalIntegerType)
assert 'exclusiveMinimum' in amount_field.metadata
@pytest.mark.parametrize("value, expected_precision", [
(10, 0),
(10., 0),
(10.0, 0),
(10.000, 0),
(10.1, 1),
(10.01, 2),
(10.000000000001, 12),
(3.55, 2),
])
def test_value_precision(self, value, expected_precision):
test_field = types.DecimalIntegerType()
assert test_field._DecimalIntegerType__get_value_precision(value) == expected_precision
@pytest.mark.parametrize("value, strict, expectation", [
(10, 2, does_not_raise()), # default
(10., 2, does_not_raise()),
(10.0, 2, does_not_raise()),
(10.000, 2, does_not_raise()),
(10.1, 2, does_not_raise()),
(10.001, 2, pytest.raises(exceptions.ValidationError)),
(10.009, 2, pytest.raises(exceptions.ValidationError)),
(3.55, 2, does_not_raise()),
(3., 0, does_not_raise()),
(3.0, 0, does_not_raise()),
])
def test_precision_validation(self, value, strict, expectation):
test_field = types.DecimalIntegerType(strict=strict)
with expectation:
assert test_field.convert(value)
def test_xstrict_swagger_generator(self, make_model_spec):
expected_strict = 1
props = {
'amount': {
'type': 'number',
'format': 'float',
'x-format': 'decimal-float',
'x-legalNameUa': 'Сума',
'x-legalNameEn': 'Amount',
'x-strict': expected_strict
}
}
test_class_schema = make_model_spec(props)
schema = BaseSchemaBuilder(test_class_schema, 'models')
schema.generate_schema_from_swagger()
cls = getattr(schema.module, 'A')
amount_field = cls.fields['amount']
assert isinstance(amount_field, types.DecimalIntegerType)
assert amount_field.strict == expected_strict
import pytest
from schematics import exceptions, types, Model
def test_DecimalIntegerType():
class A(Model):
amount = types.DecimalIntegerType()
with pytest.raises(exceptions.DataError) as ex:
A({'amount': 10000000000000})
max_int = A.fields['amount'].max_value / 100
assert ex == f'["Decimal value should be less than or equal to {max_int}."]'
a = A({'amount': 9999999999999.99})
assert A.fields['amount'].max_value == a.amount
class A(Model):
amount = types.DecimalIntegerType(max_value=10000)
assert A.fields['amount'].max_value == 10000
class A(Model):
amount = types.DecimalIntegerType(max_value=10000000000000000000)
assert A.fields['amount'].max_value == 999999999999999
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment