From 8e88e570460d5664eaa25a4e237b05c3b63a3987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 10 Dec 2021 17:53:41 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20Decimal-s?= =?UTF-8?q?pecific=20configs=20in=20Field()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pydantic/fields.py | 14 ++++++++++++++ pydantic/schema.py | 2 ++ 2 files changed, 16 insertions(+) diff --git a/pydantic/fields.py b/pydantic/fields.py index bbe97d7d23..86d72cbc48 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -101,6 +101,8 @@ class FieldInfo(Representation): 'lt', 'le', 'multiple_of', + 'max_digits', + 'decimal_places', 'min_items', 'max_items', 'unique_items', @@ -122,6 +124,8 @@ class FieldInfo(Representation): 'ge': None, 'le': None, 'multiple_of': None, + 'max_digits': None, + 'decimal_places': None, 'min_items': None, 'max_items': None, 'unique_items': None, @@ -143,6 +147,8 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: self.lt = kwargs.pop('lt', None) self.le = kwargs.pop('le', None) self.multiple_of = kwargs.pop('multiple_of', None) + self.max_digits = kwargs.pop('max_digits', None) + self.decimal_places = kwargs.pop('decimal_places', None) self.min_items = kwargs.pop('min_items', None) self.max_items = kwargs.pop('max_items', None) self.unique_items = kwargs.pop('unique_items', None) @@ -209,6 +215,8 @@ def Field( lt: float = None, le: float = None, multiple_of: float = None, + max_digits: int = None, + decimal_places: int = None, min_items: int = None, max_items: int = None, unique_items: bool = None, @@ -245,6 +253,10 @@ def Field( schema will have a ``maximum`` validation keyword :param multiple_of: only applies to numbers, requires the field to be "a multiple of". The schema will have a ``multipleOf`` validation keyword + :param max_digits: only applies to Decimals, requires the field to have a maximum number + of digits within the decimal. It does not include a zero before the decimal point or trailing decimal zeroes. + :param decimal_places: only applies to Decimals, requires the field to have at most a number of decimal places + allowed. It does not include trailing decimal zeroes. :param min_items: only applies to lists, requires the field to have a minimum number of elements. The schema will have a ``minItems`` validation keyword :param max_items: only applies to lists, requires the field to have a maximum number of @@ -276,6 +288,8 @@ def Field( lt=lt, le=le, multiple_of=multiple_of, + max_digits=max_digits, + decimal_places=decimal_places, min_items=min_items, max_items=max_items, unique_items=unique_items, diff --git a/pydantic/schema.py b/pydantic/schema.py index 431503f705..b55114d4bb 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -1056,6 +1056,8 @@ def constraint_func(**kw: Any) -> Type[Any]: ): # Is numeric type attrs = ('gt', 'lt', 'ge', 'le', 'multiple_of') + if issubclass(type_, Decimal): + attrs += ('max_digits', 'decimal_places') numeric_type = next(t for t in numeric_types if issubclass(type_, t)) # pragma: no branch constraint_func = _map_types_constraint[numeric_type] From daa5e875d9afea1da9cba06eef984b3df0e2677b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 10 Dec 2021 17:54:28 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=85=20Add/update=20tests=20for=20cond?= =?UTF-8?q?ecimal=20and=20variant=20with=20Field()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_types.py | 72 +++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/tests/test_types.py b/tests/test_types.py index fbb42ed01c..2a31613164 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1840,11 +1840,11 @@ class Config: @pytest.mark.parametrize( - 'type_,value,result', + 'type_args,value,result', [ - (condecimal(gt=Decimal('42.24')), Decimal('43'), Decimal('43')), + (dict(gt=Decimal('42.24')), Decimal('43'), Decimal('43')), ( - condecimal(gt=Decimal('42.24')), + dict(gt=Decimal('42.24')), Decimal('42'), [ { @@ -1855,9 +1855,9 @@ class Config: } ], ), - (condecimal(lt=Decimal('42.24')), Decimal('42'), Decimal('42')), + (dict(lt=Decimal('42.24')), Decimal('42'), Decimal('42')), ( - condecimal(lt=Decimal('42.24')), + dict(lt=Decimal('42.24')), Decimal('43'), [ { @@ -1868,10 +1868,10 @@ class Config: } ], ), - (condecimal(ge=Decimal('42.24')), Decimal('43'), Decimal('43')), - (condecimal(ge=Decimal('42.24')), Decimal('42.24'), Decimal('42.24')), + (dict(ge=Decimal('42.24')), Decimal('43'), Decimal('43')), + (dict(ge=Decimal('42.24')), Decimal('42.24'), Decimal('42.24')), ( - condecimal(ge=Decimal('42.24')), + dict(ge=Decimal('42.24')), Decimal('42'), [ { @@ -1882,10 +1882,10 @@ class Config: } ], ), - (condecimal(le=Decimal('42.24')), Decimal('42'), Decimal('42')), - (condecimal(le=Decimal('42.24')), Decimal('42.24'), Decimal('42.24')), + (dict(le=Decimal('42.24')), Decimal('42'), Decimal('42')), + (dict(le=Decimal('42.24')), Decimal('42.24'), Decimal('42.24')), ( - condecimal(le=Decimal('42.24')), + dict(le=Decimal('42.24')), Decimal('43'), [ { @@ -1896,9 +1896,9 @@ class Config: } ], ), - (condecimal(max_digits=2, decimal_places=2), Decimal('0.99'), Decimal('0.99')), + (dict(max_digits=2, decimal_places=2), Decimal('0.99'), Decimal('0.99')), ( - condecimal(max_digits=2, decimal_places=1), + dict(max_digits=2, decimal_places=1), Decimal('0.99'), [ { @@ -1910,7 +1910,7 @@ class Config: ], ), ( - condecimal(max_digits=3, decimal_places=1), + dict(max_digits=3, decimal_places=1), Decimal('999'), [ { @@ -1921,11 +1921,11 @@ class Config: } ], ), - (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')), + (dict(max_digits=4, decimal_places=1), Decimal('999'), Decimal('999')), + (dict(max_digits=20, decimal_places=2), Decimal('742403889818000000'), Decimal('742403889818000000')), + (dict(max_digits=20, decimal_places=2), Decimal('7.42403889818E+17'), Decimal('7.42403889818E+17')), ( - condecimal(max_digits=20, decimal_places=2), + dict(max_digits=20, decimal_places=2), Decimal('7424742403889818000000'), [ { @@ -1936,9 +1936,9 @@ class Config: } ], ), - (condecimal(max_digits=5, decimal_places=2), Decimal('7304E-1'), Decimal('7304E-1')), + (dict(max_digits=5, decimal_places=2), Decimal('7304E-1'), Decimal('7304E-1')), ( - condecimal(max_digits=5, decimal_places=2), + dict(max_digits=5, decimal_places=2), Decimal('7304E-3'), [ { @@ -1949,9 +1949,9 @@ class Config: } ], ), - (condecimal(max_digits=5, decimal_places=5), Decimal('70E-5'), Decimal('70E-5')), + (dict(max_digits=5, decimal_places=5), Decimal('70E-5'), Decimal('70E-5')), ( - condecimal(max_digits=5, decimal_places=5), + dict(max_digits=5, decimal_places=5), Decimal('70E-6'), [ { @@ -1964,7 +1964,7 @@ class Config: ), *[ ( - condecimal(decimal_places=2, max_digits=10), + dict(decimal_places=2, max_digits=10), value, [{'loc': ('foo',), 'msg': 'value is not a valid decimal', 'type': 'value_error.decimal.not_finite'}], ) @@ -1985,7 +1985,7 @@ class Config: ], *[ ( - condecimal(decimal_places=2, max_digits=10), + dict(decimal_places=2, max_digits=10), Decimal(value), [{'loc': ('foo',), 'msg': 'value is not a valid decimal', 'type': 'value_error.decimal.not_finite'}], ) @@ -2005,7 +2005,7 @@ class Config: ) ], ( - condecimal(multiple_of=Decimal('5')), + dict(multiple_of=Decimal('5')), Decimal('42'), [ { @@ -2018,16 +2018,18 @@ class Config: ), ], ) -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 - assert exc_info.value.json().startswith('[') - else: - assert model(foo=value).foo == result +def test_decimal_validation(type_args, value, result): + modela = create_model('DecimalModel', foo=(condecimal(**type_args), ...)) + modelb = create_model('DecimalModel', foo=(Decimal, Field(..., **type_args))) + + for model in (modela, modelb): + if not isinstance(result, Decimal): + with pytest.raises(ValidationError) as exc_info: + model(foo=value) + assert exc_info.value.errors() == result + assert exc_info.value.json().startswith('[') + else: + assert model(foo=value).foo == result @pytest.mark.parametrize('value,result', (('/test/path', Path('/test/path')), (Path('/test/path'), Path('/test/path')))) From a8df48f003e1fea2d55faca7d318953ad423c176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 10 Dec 2021 17:54:57 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=93=9D=20Update=20schema=20-=20Field(?= =?UTF-8?q?)=20docs=20including=20Decimal-specific=20configs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/usage/schema.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/usage/schema.md b/docs/usage/schema.md index 7618b362ff..c0062d98e3 100644 --- a/docs/usage/schema.md +++ b/docs/usage/schema.md @@ -65,6 +65,10 @@ It has the following arguments: JSON Schema * `multiple_of`: for numeric values, this adds a validation of "a multiple of" and an annotation of `multipleOf` to the JSON Schema +* `max_digits`: for `Decimal` values, this adds a validation to have a maximum number of digits within the decimal. It + does not include a zero before the decimal point or trailing decimal zeroes. +* `decimal_places`: for `Decimal` values, this adds a validation to have at most a number of decimal places allowed. It + does not include trailing decimal zeroes. * `min_items`: for list values, this adds a corresponding validation and an annotation of `minItems` to the JSON Schema * `max_items`: for list values, this adds a corresponding validation and an annotation of `maxItems` to the From 1fa86bc4e07e87e6e754de3a490a58200f4654bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 10 Dec 2021 18:10:16 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=93=9D=20Add=20PR=20changes=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changes/3507-tiangolo.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/3507-tiangolo.md diff --git a/changes/3507-tiangolo.md b/changes/3507-tiangolo.md new file mode 100644 index 0000000000..95185be18e --- /dev/null +++ b/changes/3507-tiangolo.md @@ -0,0 +1 @@ +Add support for `Decimal`-specific validation configurations in `Field()`, additionally to using `condecimal()`, to allow better suppport from editors and tooling \ No newline at end of file