From dc07277017ce3c086d490748ceec7ce0bb2e2525 Mon Sep 17 00:00:00 2001 From: Nikita Grishko Date: Sat, 28 Apr 2018 20:40:28 +0300 Subject: [PATCH] add ``ConstrainedFloat``, ``confloat``, ``PositiveFloat`` and ``NegativeFloat`` types #166 (#166) --- HISTORY.rst | 1 + docs/examples/exotic.py | 14 ++++++++++++-- pydantic/types.py | 36 ++++++++++++++++++++++++++++++++++++ tests/test_types.py | 18 ++++++++++++++++-- 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 8fd7a17..540af6d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -8,6 +8,7 @@ v0.8.1 (2018-XX-XX) * tweak email-validator import error message #145 * fix parse error of parse_date() and parse_datetime() when input is 0 #144 * add ``Config.anystr_strip_whitespace`` and ``strip_whitespace`` kwarg to ``constr``, by default values is `False` #163 +* add ``ConstrainedFloat``, ``confloat``, ``PositiveFloat`` and ``NegativeFloat`` types #166 v0.8.0 (2018-03-25) ................... diff --git a/docs/examples/exotic.py b/docs/examples/exotic.py index d7e64c1..9fbdc5d 100644 --- a/docs/examples/exotic.py +++ b/docs/examples/exotic.py @@ -1,8 +1,8 @@ from pathlib import Path from uuid import UUID -from pydantic import (DSN, BaseModel, EmailStr, NameEmail, PyObject, conint, - constr, PositiveInt, NegativeInt) +from pydantic import (DSN, BaseModel, EmailStr, NameEmail, NegativeFloat, NegativeInt, PositiveFloat, PositiveInt, + PyObject, confloat, conint, constr) class Model(BaseModel): @@ -17,6 +17,10 @@ class Model(BaseModel): pos_int: PositiveInt = None neg_int: NegativeInt = None + big_float: confloat(gt=1000, lt=1024) = None + pos_float: PositiveFloat = None + neg_float: NegativeFloat = None + email_address: EmailStr = None email_and_name: NameEmail = None @@ -39,6 +43,9 @@ m = Model( big_int=1001, pos_int=1, neg_int=-1, + big_float=1002.1, + pos_float=2.2, + neg_float=-2.3, email_address='Samuel Colvin ', email_and_name='Samuel Colvin ', uuid='ebcdab58-6eb8-46fb-a190-d07a33e9eac8' @@ -54,6 +61,9 @@ print(m.dict()) 'big_int': 1001, 'pos_int': 1, 'neg_int': -1, + 'big_float': 1002.1, + 'pos_float': 2.2, + 'neg_float': -2.3, 'email_address': 's@muelcolvin.com', 'email_and_name': ")>, ... diff --git a/pydantic/types.py b/pydantic/types.py index 2b25f32..7526076 100644 --- a/pydantic/types.py +++ b/pydantic/types.py @@ -25,6 +25,10 @@ __all__ = [ 'conint', 'PositiveInt', 'NegativeInt', + 'ConstrainedFloat', + 'confloat', + 'PositiveFloat', + 'NegativeFloat', ] NoneStr = Optional[str] @@ -201,4 +205,36 @@ class NegativeInt(ConstrainedInt): lt = 0 +class ConstrainedFloat(float): + gt: Union[int, float] = None + lt: Union[int, float] = None + + @classmethod + def get_validators(cls): + yield float + yield cls.validate + + @classmethod + def validate(cls, value: float) -> float: + if cls.gt is not None and value <= cls.gt: + raise ValueError(f'size less than minimum allowed: {cls.gt}') + elif cls.lt is not None and value >= cls.lt: + raise ValueError(f'size greater than maximum allowed: {cls.lt}') + return value + + +def confloat(*, gt=None, lt=None) -> Type[float]: + # use kwargs then define conf in a dict to aid with IDE type hinting + namespace = dict(gt=gt, lt=lt) + return type('ConstrainedFloatValue', (ConstrainedFloat,), namespace) + + +class PositiveFloat(ConstrainedFloat): + gt = 0 + + +class NegativeFloat(ConstrainedFloat): + lt = 0 + + # TODO, JsonEither, JsonList, JsonDict diff --git a/tests/test_types.py b/tests/test_types.py index 3f1a83b..4e7149b 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -6,8 +6,8 @@ from uuid import UUID import pytest -from pydantic import (DSN, BaseModel, EmailStr, NameEmail, NegativeInt, PositiveInt, PyObject, StrictStr, - ValidationError, conint, constr) +from pydantic import (DSN, BaseModel, EmailStr, NameEmail, NegativeFloat, NegativeInt, PositiveFloat, PositiveInt, + PyObject, StrictStr, ValidationError, confloat, conint, constr) try: import email_validator @@ -410,6 +410,20 @@ def test_int_validation(): assert exc_info.value.message == '3 errors validating input' +class FloatModel(BaseModel): + a: PositiveFloat = None + b: NegativeFloat = None + c: confloat(gt=4, lt=12.2) = None + + +def test_float_validation(): + m = FloatModel(a=5.1, b=-5.2, c=5.3) + assert m == {'a': 5.1, 'b': -5.2, 'c': 5.3} + with pytest.raises(ValidationError) as exc_info: + FloatModel(a=-5.1, b=5.2, c=-5.3) + assert exc_info.value.message == '3 errors validating input' + + def test_set(): class SetModel(BaseModel): v: set = ...