Skip to content
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
Merged

Add multiple_of attribute to constrained numerics #371

merged 5 commits into from Feb 3, 2019

Conversation

@StephenBrown2
Copy link
Collaborator

@StephenBrown2 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>
@samuelcolvin
Copy link
Owner

@samuelcolvin 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
Copy link
Collaborator Author

@StephenBrown2 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
Copy link

@codecov 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
Copy link
Owner

@samuelcolvin 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
Copy link
Collaborator Author

@StephenBrown2 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
Copy link
Owner

@samuelcolvin samuelcolvin commented Jan 24, 2019

yup, looks pretty marginal to me.

@StephenBrown2
Copy link
Collaborator Author

@StephenBrown2 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
Copy link
Owner

@samuelcolvin 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.

Copy link
Owner

@samuelcolvin 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}'
Copy link
Owner

@samuelcolvin samuelcolvin Jan 26, 2019

rename limit_value to multiple_of

Copy link
Collaborator Author

@StephenBrown2 StephenBrown2 Jan 28, 2019

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

Copy link
Owner

@samuelcolvin samuelcolvin Jan 29, 2019

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
Copy link
Collaborator

@tiangolo tiangolo commented Jan 29, 2019

👏 🎉 🌮

@StephenBrown2
Copy link
Collaborator Author

@StephenBrown2 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
7 checks passed
@samuelcolvin
Copy link
Owner

@samuelcolvin 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
Labels
None yet
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

3 participants