-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Add multiple_of attribute to constrained numerics #371
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add multiple_of attribute to constrained numerics #371
Conversation
|
In general this looks good to me. Will have a small impact on performance, but I think we can live with that. |
|
Just ran the benchmarks: |
Codecov Report
@@ Coverage Diff @@
## master #371 +/- ##
=====================================
Coverage 100% 100%
=====================================
Files 14 14
Lines 1894 1910 +16
Branches 369 370 +1
=====================================
+ Hits 1894 1910 +16 |
|
You need to run them before and after this change to see the change. You can also use |
|
Agreed. Here's the "before", on master: |
|
yup, looks pretty marginal to me. |
|
One thing I wanted to ask about was the use of |
|
good question, I think stick with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
otherwise LGTM.
pydantic/errors.py
Outdated
| @@ -190,6 +190,11 @@ class NumberNotLeError(_NumberBoundError): | |||
| msg_template = 'ensure this value is less than or equal to {limit_value}' | |||
|
|
|||
|
|
|||
| class NumberNotMultipleError(_NumberBoundError): | |||
| code = 'number.not_multiple' | |||
| msg_template = 'ensure this value is a multiple of {limit_value}' | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename limit_value to multiple_of
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get KeyErrors when testing with that change:
git diff
1 diff --git a/pydantic/errors.py b/pydantic/errors.py
2 index 7a18e1c..043a5ba 100644
3 --- a/pydantic/errors.py
4 +++ b/pydantic/errors.py
5 @@ -192,7 +192,7 @@ class NumberNotLeError(_NumberBoundError):
6
7 class NumberNotMultipleError(_NumberBoundError):
8 code = 'number.not_multiple'
9 - msg_template = 'ensure this value is a multiple of {limit_value}'
10 + msg_template = 'ensure this value is a multiple of {multiple_of}'
11
12
13 class DecimalError(PydanticTypeError):
pipenv run make test
pytest --cov=pydantic
Test session starts (platform: linux, Python 3.7.2, pytest 4.0.2, pytest-sugar 0.9.2)
rootdir: /home/stephen/git/hub/pydantic, inifile: setup.cfg
plugins: sugar-0.9.2, mock-1.10.0, isort-0.2.1, cov-2.6.0
collecting ...
tests/__init__.py s 0%
tests/check_tag.py s 0%
tests/conftest.py s 0% ▏
tests/mypy_test_fails.py s 1% ▏
tests/mypy_test_success.py s 1% ▏
tests/test_abc.py s✓✓ 1% ▏
tests/test_construction.py s✓✓✓✓✓✓✓✓✓✓✓✓ 3% ▍
tests/test_create_model.py s✓✓✓✓✓✓✓✓✓✓✓✓✓✓ 5% ▌
tests/test_dataclasses.py s✓✓✓✓✓✓✓✓✓✓✓✓✓✓ 7% ▋
tests/test_datetime_parse.py s✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓ 16% █▋
tests/test_edge_cases.py s✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓ 23% ██▍
tests/test_error_wrappers.py s✓✓✓✓✓✓✓✓ 24% ██▌
tests/test_errors.py s✓ 25% ██▌
tests/test_json.py s✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓ 27% ██▊
tests/test_main.py s✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓ 32% ███▎
tests/test_parse.py s✓✓✓✓✓✓✓✓✓✓✓✓✓ 34% ███▍
tests/test_py37.py s✓✓✓✓✓✓ 35% ███▌
tests/test_schema.py ✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓ 54% █████▌
tests/test_settings.py s✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓ 56% █████▋
tests/test_types.py ✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓ss✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓ 69% ██████▉
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― test_int_validation ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
def test_int_validation():
class Model(BaseModel):
a: PositiveInt = None
b: NegativeInt = None
c: conint(gt=4, lt=10) = None
d: conint(ge=0, le=10) = None
e: conint(multiple_of=5) = None
m = Model(a=5, b=-5, c=5, d=0, e=25)
assert m == {'a': 5, 'b': -5, 'c': 5, 'd': 0, 'e': 25}
with pytest.raises(ValidationError) as exc_info:
Model(a=-5, b=5, c=-5, d=11, e=42)
> assert exc_info.value.errors() == [
{
'loc': ('a',),
'msg': 'ensure this value is greater than 0',
'type': 'value_error.number.not_gt',
'ctx': {'limit_value': 0},
},
{
'loc': ('b',),
'msg': 'ensure this value is less than 0',
'type': 'value_error.number.not_lt',
'ctx': {'limit_value': 0},
},
{
'loc': ('c',),
'msg': 'ensure this value is greater than 4',
'type': 'value_error.number.not_gt',
'ctx': {'limit_value': 4},
},
{
'loc': ('d',),
'msg': 'ensure this value is less than or equal to 10',
'type': 'value_error.number.not_le',
'ctx': {'limit_value': 10},
},
{
'loc': ('e',),
'msg': 'ensure this value is a multiple of 5',
'type': 'value_error.number.not_multiple',
'ctx': {'limit_value': 5},
},
]
tests/test_types.py:564:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
pydantic/error_wrappers.py:51: in errors
return list(flatten_errors(self.raw_errors))
pydantic/error_wrappers.py:85: in flatten_errors
yield error.dict(loc_prefix=loc)
pydantic/error_wrappers.py:35: in dict
d = {'loc': loc, 'msg': self.msg, 'type': self.type_}
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <pydantic.error_wrappers.ErrorWrapper object at 0x7fb91e577088>
@property
def msg(self):
default_msg_template = getattr(self.exc, 'msg_template', None)
msg_template = self.msg_template or default_msg_template
if msg_template:
> return msg_template.format(**self.ctx or {})
E KeyError: 'multiple_of'
pydantic/error_wrappers.py:24: KeyError
tests/test_types.py ⨯ 69% ██████▉
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― test_float_validation ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
def test_float_validation():
class Model(BaseModel):
a: PositiveFloat = None
b: NegativeFloat = None
c: confloat(gt=4, lt=12.2) = None
d: confloat(ge=0, le=9.9) = None
e: confloat(multiple_of=0.5) = None
m = Model(a=5.1, b=-5.2, c=5.3, d=9.9, e=2.5)
assert m.dict() == {'a': 5.1, 'b': -5.2, 'c': 5.3, 'd': 9.9, 'e': 2.5}
with pytest.raises(ValidationError) as exc_info:
Model(a=-5.1, b=5.2, c=-5.3, d=9.91, e=4.2)
> assert exc_info.value.errors() == [
{
'loc': ('a',),
'msg': 'ensure this value is greater than 0',
'type': 'value_error.number.not_gt',
'ctx': {'limit_value': 0},
},
{
'loc': ('b',),
'msg': 'ensure this value is less than 0',
'type': 'value_error.number.not_lt',
'ctx': {'limit_value': 0},
},
{
'loc': ('c',),
'msg': 'ensure this value is greater than 4',
'type': 'value_error.number.not_gt',
'ctx': {'limit_value': 4},
},
{
'loc': ('d',),
'msg': 'ensure this value is less than or equal to 9.9',
'type': 'value_error.number.not_le',
'ctx': {'limit_value': 9.9},
},
{
'loc': ('e',),
'msg': 'ensure this value is a multiple of 0.5',
'type': 'value_error.number.not_multiple',
'ctx': {'limit_value': 0.5},
},
]
tests/test_types.py:611:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
pydantic/error_wrappers.py:51: in errors
return list(flatten_errors(self.raw_errors))
pydantic/error_wrappers.py:85: in flatten_errors
yield error.dict(loc_prefix=loc)
pydantic/error_wrappers.py:35: in dict
d = {'loc': loc, 'msg': self.msg, 'type': self.type_}
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <pydantic.error_wrappers.ErrorWrapper object at 0x7fb91e4a9b48>
@property
def msg(self):
default_msg_template = getattr(self.exc, 'msg_template', None)
msg_template = self.msg_template or default_msg_template
if msg_template:
> return msg_template.format(**self.ctx or {})
E KeyError: 'multiple_of'
pydantic/error_wrappers.py:24: KeyError
tests/test_types.py ⨯✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓ 76% ███████▋
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― test_decimal_validation[ConstrainedDecimalValue-value45-result45] ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
type_ = <class 'pydantic.types.ConstrainedDecimalValue'>, value = Decimal('42'), result = [{'ctx': {'limit_value': Decimal('5')}, 'loc': ('foo',), 'msg': 'ensure this value is a multiple of 5', 'type': 'value_error.number.not_multiple'}]
@pytest.mark.parametrize(
'type_,value,result',
[
(condecimal(gt=Decimal('42.24')), Decimal('43'), Decimal('43')),
(
condecimal(gt=Decimal('42.24')),
Decimal('42'),
[
{
'loc': ('foo',),
'msg': 'ensure this value is greater than 42.24',
'type': 'value_error.number.not_gt',
'ctx': {'limit_value': Decimal('42.24')},
}
],
),
(condecimal(lt=Decimal('42.24')), Decimal('42'), Decimal('42')),
(
condecimal(lt=Decimal('42.24')),
Decimal('43'),
[
{
'loc': ('foo',),
'msg': 'ensure this value is less than 42.24',
'type': 'value_error.number.not_lt',
'ctx': {'limit_value': Decimal('42.24')},
}
],
),
(condecimal(ge=Decimal('42.24')), Decimal('43'), Decimal('43')),
(condecimal(ge=Decimal('42.24')), Decimal('42.24'), Decimal('42.24')),
(
condecimal(ge=Decimal('42.24')),
Decimal('42'),
[
{
'loc': ('foo',),
'msg': 'ensure this value is greater than or equal to 42.24',
'type': 'value_error.number.not_ge',
'ctx': {'limit_value': Decimal('42.24')},
}
],
),
(condecimal(le=Decimal('42.24')), Decimal('42'), Decimal('42')),
(condecimal(le=Decimal('42.24')), Decimal('42.24'), Decimal('42.24')),
(
condecimal(le=Decimal('42.24')),
Decimal('43'),
[
{
'loc': ('foo',),
'msg': 'ensure this value is less than or equal to 42.24',
'type': 'value_error.number.not_le',
'ctx': {'limit_value': Decimal('42.24')},
}
],
),
(condecimal(max_digits=2, decimal_places=2), Decimal('0.99'), Decimal('0.99')),
(
condecimal(max_digits=2, decimal_places=1),
Decimal('0.99'),
[
{
'loc': ('foo',),
'msg': 'ensure that there are no more than 1 decimal places',
'type': 'value_error.decimal.max_places',
'ctx': {'decimal_places': 1},
}
],
),
(
condecimal(max_digits=3, decimal_places=1),
Decimal('999'),
[
{
'loc': ('foo',),
'msg': 'ensure that there are no more than 2 digits before the decimal point',
'type': 'value_error.decimal.whole_digits',
'ctx': {'whole_digits': 2},
}
],
),
(condecimal(max_digits=4, decimal_places=1), Decimal('999'), Decimal('999')),
(condecimal(max_digits=20, decimal_places=2), Decimal('742403889818000000'), Decimal('742403889818000000')),
(condecimal(max_digits=20, decimal_places=2), Decimal('7.42403889818E+17'), Decimal('7.42403889818E+17')),
(
condecimal(max_digits=20, decimal_places=2),
Decimal('7424742403889818000000'),
[
{
'loc': ('foo',),
'msg': 'ensure that there are no more than 20 digits in total',
'type': 'value_error.decimal.max_digits',
'ctx': {'max_digits': 20},
}
],
),
(condecimal(max_digits=5, decimal_places=2), Decimal('7304E-1'), Decimal('7304E-1')),
(
condecimal(max_digits=5, decimal_places=2),
Decimal('7304E-3'),
[
{
'loc': ('foo',),
'msg': 'ensure that there are no more than 2 decimal places',
'type': 'value_error.decimal.max_places',
'ctx': {'decimal_places': 2},
}
],
),
(condecimal(max_digits=5, decimal_places=5), Decimal('70E-5'), Decimal('70E-5')),
(
condecimal(max_digits=5, decimal_places=5),
Decimal('70E-6'),
[
{
'loc': ('foo',),
'msg': 'ensure that there are no more than 5 digits in total',
'type': 'value_error.decimal.max_digits',
'ctx': {'max_digits': 5},
}
],
),
*[
(
condecimal(decimal_places=2, max_digits=10),
value,
[{'loc': ('foo',), 'msg': 'value is not a valid decimal', 'type': 'value_error.decimal.not_finite'}],
)
for value in (
'NaN',
'-NaN',
'+NaN',
'sNaN',
'-sNaN',
'+sNaN',
'Inf',
'-Inf',
'+Inf',
'Infinity',
'-Infinity',
'-Infinity',
)
],
*[
(
condecimal(decimal_places=2, max_digits=10),
Decimal(value),
[{'loc': ('foo',), 'msg': 'value is not a valid decimal', 'type': 'value_error.decimal.not_finite'}],
)
for value in (
'NaN',
'-NaN',
'+NaN',
'sNaN',
'-sNaN',
'+sNaN',
'Inf',
'-Inf',
'+Inf',
'Infinity',
'-Infinity',
'-Infinity',
)
],
(
condecimal(multiple_of=Decimal('5')),
Decimal('42'),
[
{
'loc': ('foo',),
'msg': 'ensure this value is a multiple of 5',
'type': 'value_error.number.not_multiple',
'ctx': {'limit_value': Decimal('5')},
}
],
),
],
)
def test_decimal_validation(type_, value, result):
model = create_model('DecimalModel', foo=(type_, ...))
if not isinstance(result, Decimal):
with pytest.raises(ValidationError) as exc_info:
model(foo=value)
> assert exc_info.value.errors() == result
tests/test_types.py:927:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
pydantic/error_wrappers.py:51: in errors
return list(flatten_errors(self.raw_errors))
pydantic/error_wrappers.py:85: in flatten_errors
yield error.dict(loc_prefix=loc)
pydantic/error_wrappers.py:35: in dict
d = {'loc': loc, 'msg': self.msg, 'type': self.type_}
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <pydantic.error_wrappers.ErrorWrapper object at 0x7fb91e2e91c8>
@property
def msg(self):
default_msg_template = getattr(self.exc, 'msg_template', None)
msg_template = self.msg_template or default_msg_template
if msg_template:
> return msg_template.format(**self.ctx or {})
E KeyError: 'multiple_of'
pydantic/error_wrappers.py:24: KeyError
tests/test_types.py ⨯✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓ 78% ███████▉
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― test_number_multiple_of ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
def test_number_multiple_of():
class Model(BaseModel):
a: conint(multiple_of=5)
assert Model(a=10).dict() == {'a': 10}
message = base_message.format(msg='a multiple of 5', ty='multiple', value=5)
with pytest.raises(ValidationError, match=message):
> Model(a=42)
tests/test_types.py:1139:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <[RecursionError("maximum recursion depth exceeded in comparison") raised in repr()] Model object at 0x7fb91e4cab08>, data = {'a': 42}
def __init__(self, **data):
> self.__setstate__(self._process_values(data))
pydantic/main.py:142:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <[RecursionError("maximum recursion depth exceeded in comparison") raised in repr()] Model object at 0x7fb91e4cab08>, input_data = {'a': 42}
def _process_values(self, input_data: dict) -> Dict[str, Any]:
> return validate_model(self, input_data)
pydantic/main.py:312:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
model = <[RecursionError("maximum recursion depth exceeded in comparison") raised in repr()] Model object at 0x7fb91e4cab08>, input_data = {'a': 42}, raise_exc = True
def validate_model(model: BaseModel, input_data: dict, raise_exc=True): # noqa: C901 (ignore complexity)
"""
validate data against a model.
"""
values = {}
errors = []
names_used = set()
check_extra = (not model.__config__.ignore_extra) or model.__config__.allow_extra
for name, field in model.__fields__.items():
if type(field.type_) == ForwardRef:
raise ConfigError(
f"field {field.name} not yet prepared and type is still a ForwardRef, "
f"you'll need to call {model.__class__.__name__}.update_forward_refs()"
)
value = input_data.get(field.alias, _missing)
using_name = False
if value is _missing and model.__config__.allow_population_by_alias and field.alt_alias:
value = input_data.get(field.name, _missing)
using_name = True
if value is _missing:
if model.__config__.validate_all or field.validate_always:
value = deepcopy(field.default)
else:
if field.required:
errors.append(ErrorWrapper(MissingError(), loc=field.alias, config=model.__config__))
else:
values[name] = deepcopy(field.default)
continue
elif check_extra:
names_used.add(field.name if using_name else field.alias)
v_, errors_ = field.validate(value, values, loc=field.alias, cls=model.__class__)
if isinstance(errors_, ErrorWrapper):
errors.append(errors_)
elif isinstance(errors_, list):
errors.extend(errors_)
else:
values[name] = v_
if check_extra:
extra = input_data.keys() - names_used
if extra:
if model.__config__.allow_extra:
for field in extra:
values[field] = input_data[field]
else:
# config.ignore_extra is False
for field in sorted(extra):
errors.append(ErrorWrapper(ExtraError(), loc=field, config=model.__config__))
if not raise_exc:
return values, ValidationError(errors) if errors else None
if errors:
> raise ValidationError(errors)
E pydantic.error_wrappers.ValidationError: <unprintable ValidationError object>
pydantic/main.py:474: ValidationError
During handling of the above exception, another exception occurred:
def test_number_multiple_of():
class Model(BaseModel):
a: conint(multiple_of=5)
assert Model(a=10).dict() == {'a': 10}
message = base_message.format(msg='a multiple of 5', ty='multiple', value=5)
with pytest.raises(ValidationError, match=message):
> Model(a=42)
tests/test_types.py:1139:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
pydantic/error_wrappers.py:57: in __str__
errors = self.errors()
pydantic/error_wrappers.py:51: in errors
return list(flatten_errors(self.raw_errors))
pydantic/error_wrappers.py:85: in flatten_errors
yield error.dict(loc_prefix=loc)
pydantic/error_wrappers.py:35: in dict
d = {'loc': loc, 'msg': self.msg, 'type': self.type_}
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <pydantic.error_wrappers.ErrorWrapper object at 0x7fb91e4ca988>
@property
def msg(self):
default_msg_template = getattr(self.exc, 'msg_template', None)
msg_template = self.msg_template or default_msg_template
if msg_template:
> return msg_template.format(**self.ctx or {})
E KeyError: 'multiple_of'
pydantic/error_wrappers.py:24: KeyError
tests/test_types.py ⨯✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓ 80% ████████▏
tests/test_types_url_str.py s✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓ 89% ████████▉
tests/test_utils.py s✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓s✓✓✓✓✓✓✓✓✓✓✓✓✓ 96% █████████▋
tests/test_validators.py s✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓ 100% ██████████
----------- coverage: platform linux, python 3.7.2-final-0 -----------
Name Stmts Miss Branch BrPart Cover
-------------------------------------------------------------------
pydantic/__init__.py 11 0 0 0 100.00%
pydantic/class_validators.py 84 0 36 0 100.00%
pydantic/dataclasses.py 48 0 20 0 100.00%
pydantic/datetime_parse.py 96 0 50 0 100.00%
pydantic/env_settings.py 35 0 12 0 100.00%
pydantic/error_wrappers.py 64 0 22 0 100.00%
pydantic/errors.py 164 0 0 0 100.00%
pydantic/fields.py 241 0 123 0 100.00%
pydantic/json.py 29 0 9 0 100.00%
pydantic/main.py 275 0 130 1 99.75%
pydantic/parse.py 38 2 20 0 96.55%
pydantic/schema.py 271 0 141 0 100.00%
pydantic/types.py 262 4 50 2 98.08%
pydantic/utils.py 126 5 44 1 96.47%
pydantic/validators.py 185 0 96 0 100.00%
pydantic/version.py 3 0 0 0 100.00%
-------------------------------------------------------------------
TOTAL 1932 11 753 4 99.44%
Results (7.33s):
756 passed
4 failed
- tests/test_types.py:551 test_int_validation
- tests/test_types.py:598 test_float_validation
- tests/test_types.py:742 test_decimal_validation[ConstrainedDecimalValue-value45-result45]
- tests/test_types.py:1131 test_number_multiple_of
24 skipped
make: *** [Makefile:23: test] Error 1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ye, because you've called NumberNotMultipleError with limit_value not multiple_of as the keyword argument.
You'll also need to change the other references to limit_value > multiple_of, eg. in tests.
|
👏 🎉 🌮 |
|
Updated, and tests still pass: pipenv run make test |
|
Great, thanks a lot. |
Change Summary
Implements multiple_of validation to numeric types (int, float, Decimal) and adds the
multipleOfattribute to schemas.Related issue number
Resolves #368, but does not implement the more generic solution suggested.
Checklist
HISTORY.rsthas been updated#<number>@<whomever>