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

Merged
merged 5 commits into from Feb 3, 2019

Conversation

Projects
None yet
3 participants
@StephenBrown2
Copy link
Contributor

StephenBrown2 commented Jan 24, 2019

Change Summary

Implements multiple_of validation to numeric types (int, float, Decimal) and adds the multipleOf attribute to schemas.

Related issue number

Resolves #368, but does not implement the more generic solution suggested.

Checklist

  • Unit tests for the changes exist
  • Tests pass on CI and coverage remains at 100%
  • Documentation reflects the changes where applicable
  • HISTORY.rst has been updated
    • if this is the first change since a release, please add a new section
    • include the issue number or this pull request number #<number>
    • include your github username @<whomever>

Stephen Brown II added some commits Jan 24, 2019

Stephen Brown II
Stephen Brown II
@samuelcolvin

This comment has been minimized.

Copy link
Owner

samuelcolvin commented Jan 24, 2019

In general this looks good to me. Will have a small impact on performance, but I think we can live with that.

@StephenBrown2

This comment has been minimized.

Copy link
Contributor Author

StephenBrown2 commented Jan 24, 2019

Just ran the benchmarks:

python benchmarks/run.py
generating test cases...
                                pydantic time=1.038s, success=49.65%
                                pydantic time=1.221s, success=49.65%
                                pydantic time=1.104s, success=49.65%
                                pydantic time=1.091s, success=49.65%
                                pydantic time=1.167s, success=49.65%
                                pydantic best=1.038s, avg=1.124s, stdev=0.071s

                     toasted-marshmallow time=1.479s, success=49.65%
                     toasted-marshmallow time=1.499s, success=49.65%
                     toasted-marshmallow time=1.442s, success=49.65%
                     toasted-marshmallow time=1.427s, success=49.65%
                     toasted-marshmallow time=1.439s, success=49.65%
                     toasted-marshmallow best=1.427s, avg=1.457s, stdev=0.030s

                             marshmallow time=1.593s, success=49.65%
                             marshmallow time=1.755s, success=49.65%
                             marshmallow time=2.263s, success=49.65%
                             marshmallow time=1.590s, success=49.65%
                             marshmallow time=3.446s, success=49.65%
                             marshmallow best=1.590s, avg=2.130s, stdev=0.786s

                                trafaret time=3.033s, success=49.65%
                                trafaret time=3.083s, success=49.65%
                                trafaret time=1.910s, success=49.65%
                                trafaret time=1.662s, success=49.65%
                                trafaret time=1.941s, success=49.65%
                                trafaret best=1.662s, avg=2.326s, stdev=0.677s

                django-restful-framework time=12.735s, success=49.70%
                django-restful-framework time=13.427s, success=49.70%
                django-restful-framework time=13.309s, success=49.70%
                django-restful-framework time=13.308s, success=49.70%
                django-restful-framework time=12.801s, success=49.70%
                django-restful-framework best=12.735s, avg=13.116s, stdev=0.322s

                                pydantic best=34.587μs/iter avg=37.469μs/iter stdev=2.366μs/iter
                     toasted-marshmallow best=47.567μs/iter avg=48.578μs/iter stdev=1.008μs/iter
                             marshmallow best=53.007μs/iter avg=70.989μs/iter stdev=26.197μs/iter
                                trafaret best=55.389μs/iter avg=77.530μs/iter stdev=22.580μs/iter
                django-restful-framework best=424.491μs/iter avg=437.194μs/iter stdev=10.744μs/iter
@codecov

This comment has been minimized.

Copy link

codecov bot commented Jan 24, 2019

Codecov Report

Merging #371 into master will not change coverage.
The diff coverage is 100%.

@@          Coverage Diff          @@
##           master   #371   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files          14     14           
  Lines        1894   1910   +16     
  Branches      369    370    +1     
=====================================
+ Hits         1894   1910   +16
@samuelcolvin

This comment has been minimized.

Copy link
Owner

samuelcolvin commented Jan 24, 2019

You need to run them before and after this change to see the change. You can also use make benchmark-pydantic to run just pydantic benchmarks.

@StephenBrown2

This comment has been minimized.

Copy link
Contributor Author

StephenBrown2 commented Jan 24, 2019

Agreed. Here's the "before", on master:

λ › git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
λ › pipenv run make benchmark-pydantic                                                                                                                                                                                                                            hub/pydantic master
python benchmarks/run.py pydantic-only
                                pydantic time=1.052s, success=49.65%
                                pydantic time=1.143s, success=49.65%
                                pydantic time=1.129s, success=49.65%
                                pydantic time=1.140s, success=49.65%
                                pydantic time=1.099s, success=49.65%
                                pydantic best=1.052s, avg=1.113s, stdev=0.038s

                                pydantic best=35.074μs/iter avg=37.096μs/iter stdev=1.270μs/iter
@samuelcolvin

This comment has been minimized.

Copy link
Owner

samuelcolvin commented Jan 24, 2019

yup, looks pretty marginal to me.

@StephenBrown2

This comment has been minimized.

Copy link
Contributor Author

StephenBrown2 commented Jan 24, 2019

One thing I wanted to ask about was the use of multiple_of as the constraint. I'd be OK with using multiple instead as it's shorter (yay less typing a la gt lt, etc.) but not sure if that makes it less clear.

@samuelcolvin

This comment has been minimized.

Copy link
Owner

samuelcolvin commented Jan 24, 2019

good question, I think stick with multiple_of it's much clearer for 3 more characters, and it's not something people will be typing a lot.

@samuelcolvin
Copy link
Owner

samuelcolvin left a comment

otherwise LGTM.

@@ -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}'

This comment has been minimized.

@samuelcolvin

samuelcolvin Jan 26, 2019

Owner

rename limit_value to multiple_of

This comment has been minimized.

@StephenBrown2

StephenBrown2 Jan 28, 2019

Author Contributor

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

This comment has been minimized.

@samuelcolvin

samuelcolvin Jan 29, 2019

Owner

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.

@tiangolo

This comment has been minimized.

Copy link
Contributor

tiangolo commented Jan 29, 2019

👏 🎉 🌮

Stephen Brown II
@StephenBrown2

This comment has been minimized.

Copy link
Contributor Author

StephenBrown2 commented Jan 30, 2019

Updated, and tests still pass:

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 s✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓                                                                                         54% █████▌
 tests/test_settings.py s✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓                                                                                                                                                                                                                                56% █████▋
 tests/test_types.py ✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓ss✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓                                                     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               166      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                           1934     11    753      4    99.44%


Results (9.63s):
     759 passed
      25 skipped

@samuelcolvin samuelcolvin merged commit 61e7589 into samuelcolvin:master Feb 3, 2019

3 checks passed

codecov/project 100% (+0%) compared to f287d41
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
deploy/netlify Deploy preview ready!
Details
@samuelcolvin

This comment has been minimized.

Copy link
Owner

samuelcolvin commented Feb 3, 2019

Great, thanks a lot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment