From 369da3e42e64429de7bb9f79b32fc4c7dd2a6a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 Apr 2022 21:36:05 +0200 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20Add=20JSON-compatible=20float?= =?UTF-8?q?=20constraints=20for=20NaN=20and=20Inf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pydantic/__init__.py | 3 +++ pydantic/errors.py | 10 ++++++++++ pydantic/fields.py | 14 ++++++++++++++ pydantic/schema.py | 2 ++ pydantic/types.py | 28 +++++++++++++++++++++++++++- pydantic/validators.py | 15 +++++++++++++++ 6 files changed, 71 insertions(+), 1 deletion(-) diff --git a/pydantic/__init__.py b/pydantic/__init__.py index 982ea4755c..66f3140dc3 100644 --- a/pydantic/__init__.py +++ b/pydantic/__init__.py @@ -98,6 +98,9 @@ 'NegativeFloat', 'NonNegativeFloat', 'NonPositiveFloat', + 'NonNanFloat', + 'NonInfFloat', + 'JSONFloat', 'ConstrainedDecimal', 'condecimal', 'UUID1', diff --git a/pydantic/errors.py b/pydantic/errors.py index 322862e056..48247d3ec1 100644 --- a/pydantic/errors.py +++ b/pydantic/errors.py @@ -417,6 +417,16 @@ class NumberNotLeError(_NumberBoundError): msg_template = 'ensure this value is less than or equal to {limit_value}' +class NumberIsNanError(PydanticValueError): + code = 'number.is_nan' + msg_template = 'ensure this value is a number and not NaN' + + +class NumberIsInfError(PydanticValueError): + code = 'number.is_inf' + msg_template = 'ensure this value is a number and not infinity (+inf or -inf)' + + class NumberNotMultipleError(PydanticValueError): code = 'number.not_multiple' msg_template = 'ensure this value is a multiple of {multiple_of}' diff --git a/pydantic/fields.py b/pydantic/fields.py index e6736f0239..d9cc2689d1 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -110,6 +110,8 @@ class FieldInfo(Representation): 'lt', 'le', 'multiple_of', + 'allow_nan', + 'allow_inf', 'max_digits', 'decimal_places', 'min_items', @@ -134,6 +136,8 @@ class FieldInfo(Representation): 'ge': None, 'le': None, 'multiple_of': None, + 'allow_nan': None, + 'allow_inf': None, 'max_digits': None, 'decimal_places': None, 'min_items': None, @@ -157,6 +161,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.allow_nan = kwargs.pop('allow_nan', None) + self.allow_inf = kwargs.pop('allow_inf', 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) @@ -226,6 +232,8 @@ def Field( lt: float = None, le: float = None, multiple_of: float = None, + allow_nan: bool = None, + allow_inf: bool = None, max_digits: int = None, decimal_places: int = None, min_items: int = None, @@ -265,6 +273,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 allow_nan: only applies to numbers, allows the field to be NaN, which is a valid Python float. Set + it to False for compatibility with JSON. + :param allow_inf: only applies to numbers, allows the field to be infinity (+inf or -inf), which is a valid + Python float. Set it to False for compatibility with JSON. :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 @@ -302,6 +314,8 @@ def Field( lt=lt, le=le, multiple_of=multiple_of, + allow_nan=allow_nan, + allow_inf=allow_inf, max_digits=max_digits, decimal_places=decimal_places, min_items=min_items, diff --git a/pydantic/schema.py b/pydantic/schema.py index e979678c22..1e3140e92e 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -1104,6 +1104,8 @@ def constraint_func(**kw: Any) -> Type[Any]: ): # Is numeric type attrs = ('gt', 'lt', 'ge', 'le', 'multiple_of') + if issubclass(type_, float): + attrs += ('allow_nan', 'allow_inf') 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 diff --git a/pydantic/types.py b/pydantic/types.py index 2d0cc18f8d..c122865427 100644 --- a/pydantic/types.py +++ b/pydantic/types.py @@ -36,6 +36,8 @@ constr_lower, constr_strip_whitespace, decimal_validator, + float_inf_validator, + float_nan_validator, float_validator, frozenset_validator, int_validator, @@ -81,6 +83,9 @@ 'NegativeFloat', 'NonNegativeFloat', 'NonPositiveFloat', + 'NonNanFloat', + 'NonInfFloat', + 'JSONFloat', 'ConstrainedDecimal', 'condecimal', 'UUID1', @@ -257,6 +262,8 @@ class ConstrainedFloat(float, metaclass=ConstrainedNumberMeta): lt: OptionalIntFloat = None le: OptionalIntFloat = None multiple_of: OptionalIntFloat = None + allow_nan: Optional[bool] = None + allow_inf: Optional[bool] = None @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: @@ -283,6 +290,8 @@ def __get_validators__(cls) -> 'CallableGenerator': yield strict_float_validator if cls.strict else float_validator yield number_size_validator yield number_multiple_validator + yield float_nan_validator + yield float_inf_validator def confloat( @@ -293,9 +302,13 @@ def confloat( lt: float = None, le: float = None, multiple_of: float = None, + allow_nan: bool = None, + allow_inf: bool = None, ) -> Type[float]: # use kwargs then define conf in a dict to aid with IDE type hinting - namespace = dict(strict=strict, gt=gt, ge=ge, lt=lt, le=le, multiple_of=multiple_of) + namespace = dict( + strict=strict, gt=gt, ge=ge, lt=lt, le=le, multiple_of=multiple_of, allow_nan=allow_nan, allow_inf=allow_inf + ) return type('ConstrainedFloatValue', (ConstrainedFloat,), namespace) @@ -305,6 +318,9 @@ def confloat( NonPositiveFloat = float NonNegativeFloat = float StrictFloat = float + NonNanFloat = float + NonInfFloat = float + JSONFloat = float else: class PositiveFloat(ConstrainedFloat): @@ -322,6 +338,16 @@ class NonNegativeFloat(ConstrainedFloat): class StrictFloat(ConstrainedFloat): strict = True + class NonNanFloat(ConstrainedFloat): + allow_nan = False + + class NonInfFloat(ConstrainedFloat): + allow_inf = False + + class JSONFloat(ConstrainedFloat): + allow_nan = False + allow_inf = False + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BYTES TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/pydantic/validators.py b/pydantic/validators.py index d4783d97b1..bb9352b2f7 100644 --- a/pydantic/validators.py +++ b/pydantic/validators.py @@ -1,3 +1,4 @@ +import math import re from collections import OrderedDict, deque from collections.abc import Hashable as CollectionsHashable @@ -151,6 +152,20 @@ def strict_float_validator(v: Any) -> float: raise errors.FloatError() +def float_nan_validator(v: 'Number', field: 'ModelField') -> 'Number': + field_type: ConstrainedFloat = field.type_ + if field_type.allow_nan is False and math.isnan(v): + raise errors.NumberIsNanError() + return v + + +def float_inf_validator(v: 'Number', field: 'ModelField') -> 'Number': + field_type: ConstrainedFloat = field.type_ + if field_type.allow_inf is False and math.isinf(v): + raise errors.NumberIsInfError() + return v + + def number_multiple_validator(v: 'Number', field: 'ModelField') -> 'Number': field_type: ConstrainedNumber = field.type_ if field_type.multiple_of is not None: From 3391c00c0aaf4623d029cacba0dbdbc65f189715 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sun, 21 Aug 2022 15:24:55 +0100 Subject: [PATCH 2/5] switching to a single "allow_inf_nan" --- pydantic/errors.py | 11 +++-------- pydantic/fields.py | 23 ++++++++--------------- pydantic/schema.py | 2 +- pydantic/types.py | 36 ++++++++++-------------------------- pydantic/validators.py | 14 +++----------- tests/test_types.py | 38 +++++++++++++++++++++++++++++++++++--- 6 files changed, 60 insertions(+), 64 deletions(-) diff --git a/pydantic/errors.py b/pydantic/errors.py index 48247d3ec1..7bdafdd17f 100644 --- a/pydantic/errors.py +++ b/pydantic/errors.py @@ -417,14 +417,9 @@ class NumberNotLeError(_NumberBoundError): msg_template = 'ensure this value is less than or equal to {limit_value}' -class NumberIsNanError(PydanticValueError): - code = 'number.is_nan' - msg_template = 'ensure this value is a number and not NaN' - - -class NumberIsInfError(PydanticValueError): - code = 'number.is_inf' - msg_template = 'ensure this value is a number and not infinity (+inf or -inf)' +class NumberNotFiniteError(PydanticValueError): + code = 'number.not_finite_number' + msg_template = 'ensure this value is a finite number' class NumberNotMultipleError(PydanticValueError): diff --git a/pydantic/fields.py b/pydantic/fields.py index d9cc2689d1..f256a11b61 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -110,8 +110,7 @@ class FieldInfo(Representation): 'lt', 'le', 'multiple_of', - 'allow_nan', - 'allow_inf', + 'allow_inf_nan', 'max_digits', 'decimal_places', 'min_items', @@ -136,8 +135,7 @@ class FieldInfo(Representation): 'ge': None, 'le': None, 'multiple_of': None, - 'allow_nan': None, - 'allow_inf': None, + 'allow_inf_nan': None, 'max_digits': None, 'decimal_places': None, 'min_items': None, @@ -161,8 +159,7 @@ 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.allow_nan = kwargs.pop('allow_nan', None) - self.allow_inf = kwargs.pop('allow_inf', None) + self.allow_inf_nan = kwargs.pop('allow_inf_nan', 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) @@ -232,8 +229,7 @@ def Field( lt: float = None, le: float = None, multiple_of: float = None, - allow_nan: bool = None, - allow_inf: bool = None, + allow_inf_nan: bool = None, max_digits: int = None, decimal_places: int = None, min_items: int = None, @@ -273,10 +269,8 @@ 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 allow_nan: only applies to numbers, allows the field to be NaN, which is a valid Python float. Set - it to False for compatibility with JSON. - :param allow_inf: only applies to numbers, allows the field to be infinity (+inf or -inf), which is a valid - Python float. Set it to False for compatibility with JSON. + :param allow_inf_nan: only applies to numbers, allows the field to be NaN or infinity (+inf or -inf), + which is a valid Python float. Default True, set to False for compatibility with JSON. :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 @@ -285,7 +279,7 @@ def Field( 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 elements. The schema will have a ``maxItems`` validation keyword - :param max_items: only applies to lists, requires the field not to have duplicated + :param unique_items: only applies to lists, requires the field not to have duplicated elements. The schema will have a ``uniqueItems`` validation keyword :param min_length: only applies to strings, requires the field to have a minimum length. The schema will have a ``maximum`` validation keyword @@ -314,8 +308,7 @@ def Field( lt=lt, le=le, multiple_of=multiple_of, - allow_nan=allow_nan, - allow_inf=allow_inf, + allow_inf_nan=allow_inf_nan, max_digits=max_digits, decimal_places=decimal_places, min_items=min_items, diff --git a/pydantic/schema.py b/pydantic/schema.py index 1e3140e92e..362f52cde1 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -1105,7 +1105,7 @@ def constraint_func(**kw: Any) -> Type[Any]: # Is numeric type attrs = ('gt', 'lt', 'ge', 'le', 'multiple_of') if issubclass(type_, float): - attrs += ('allow_nan', 'allow_inf') + attrs += ('allow_inf_nan',) 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 diff --git a/pydantic/types.py b/pydantic/types.py index c122865427..d151748403 100644 --- a/pydantic/types.py +++ b/pydantic/types.py @@ -36,8 +36,7 @@ constr_lower, constr_strip_whitespace, decimal_validator, - float_inf_validator, - float_nan_validator, + float_finite_validator, float_validator, frozenset_validator, int_validator, @@ -83,9 +82,7 @@ 'NegativeFloat', 'NonNegativeFloat', 'NonPositiveFloat', - 'NonNanFloat', - 'NonInfFloat', - 'JSONFloat', + 'FiniteFloat', 'ConstrainedDecimal', 'condecimal', 'UUID1', @@ -262,8 +259,7 @@ class ConstrainedFloat(float, metaclass=ConstrainedNumberMeta): lt: OptionalIntFloat = None le: OptionalIntFloat = None multiple_of: OptionalIntFloat = None - allow_nan: Optional[bool] = None - allow_inf: Optional[bool] = None + allow_inf_nan: Optional[bool] = None @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: @@ -290,8 +286,8 @@ def __get_validators__(cls) -> 'CallableGenerator': yield strict_float_validator if cls.strict else float_validator yield number_size_validator yield number_multiple_validator - yield float_nan_validator - yield float_inf_validator + if cls.allow_inf_nan is False: + yield float_finite_validator def confloat( @@ -302,13 +298,10 @@ def confloat( lt: float = None, le: float = None, multiple_of: float = None, - allow_nan: bool = None, - allow_inf: bool = None, + allow_inf_nan: bool = True, ) -> Type[float]: # use kwargs then define conf in a dict to aid with IDE type hinting - namespace = dict( - strict=strict, gt=gt, ge=ge, lt=lt, le=le, multiple_of=multiple_of, allow_nan=allow_nan, allow_inf=allow_inf - ) + namespace = dict(strict=strict, gt=gt, ge=ge, lt=lt, le=le, multiple_of=multiple_of, allow_inf_nan=allow_inf_nan) return type('ConstrainedFloatValue', (ConstrainedFloat,), namespace) @@ -318,9 +311,7 @@ def confloat( NonPositiveFloat = float NonNegativeFloat = float StrictFloat = float - NonNanFloat = float - NonInfFloat = float - JSONFloat = float + FiniteFloat = float else: class PositiveFloat(ConstrainedFloat): @@ -338,15 +329,8 @@ class NonNegativeFloat(ConstrainedFloat): class StrictFloat(ConstrainedFloat): strict = True - class NonNanFloat(ConstrainedFloat): - allow_nan = False - - class NonInfFloat(ConstrainedFloat): - allow_inf = False - - class JSONFloat(ConstrainedFloat): - allow_nan = False - allow_inf = False + class FiniteFloat(ConstrainedFloat): + allow_info_nan = False # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BYTES TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/pydantic/validators.py b/pydantic/validators.py index bb9352b2f7..e01510231a 100644 --- a/pydantic/validators.py +++ b/pydantic/validators.py @@ -152,17 +152,9 @@ def strict_float_validator(v: Any) -> float: raise errors.FloatError() -def float_nan_validator(v: 'Number', field: 'ModelField') -> 'Number': - field_type: ConstrainedFloat = field.type_ - if field_type.allow_nan is False and math.isnan(v): - raise errors.NumberIsNanError() - return v - - -def float_inf_validator(v: 'Number', field: 'ModelField') -> 'Number': - field_type: ConstrainedFloat = field.type_ - if field_type.allow_inf is False and math.isinf(v): - raise errors.NumberIsInfError() +def float_finite_validator(v: 'Number') -> 'Number': + if math.isnan(v) or math.isinf(v): + raise errors.NumberNotFiniteError() return v diff --git a/tests/test_types.py b/tests/test_types.py index 8e5481719c..5d03a95318 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,4 +1,5 @@ import itertools +import math import os import re import sys @@ -1536,12 +1537,13 @@ class Model(BaseModel): e: confloat(gt=4, lt=12.2) = None f: confloat(ge=0, le=9.9) = None g: confloat(multiple_of=0.5) = None + h: confloat(allow_inf_nan=False) = None - m = Model(a=5.1, b=-5.2, c=0, d=0, e=5.3, f=9.9, g=2.5) - assert m.dict() == {'a': 5.1, 'b': -5.2, 'c': 0, 'd': 0, 'e': 5.3, 'f': 9.9, 'g': 2.5} + m = Model(a=5.1, b=-5.2, c=0, d=0, e=5.3, f=9.9, g=2.5, h=42) + assert m.dict() == {'a': 5.1, 'b': -5.2, 'c': 0, 'd': 0, 'e': 5.3, 'f': 9.9, 'g': 2.5, 'h': 42} with pytest.raises(ValidationError) as exc_info: - Model(a=-5.1, b=5.2, c=-5.1, d=5.1, e=-5.3, f=9.91, g=4.2) + Model(a=-5.1, b=5.2, c=-5.1, d=5.1, e=-5.3, f=9.91, g=4.2, h=float('nan')) assert exc_info.value.errors() == [ { 'loc': ('a',), @@ -1585,6 +1587,36 @@ class Model(BaseModel): 'type': 'value_error.number.not_multiple', 'ctx': {'multiple_of': 0.5}, }, + { + 'loc': ('h',), + 'msg': 'ensure this value is a finite number', + 'type': 'value_error.number.not_finite_number', + }, + ] + + +def test_finite_float_validation(): + class Model(BaseModel): + a: float = None + + assert Model(a=float('inf')).a == float('inf') + assert Model(a=float('-inf')).a == float('-inf') + assert math.isnan(Model(a=float('nan')).a) + + +@pytest.mark.parametrize('value', [float('inf'), float('-inf'), float('nan')]) +def test_finite_float_validation_error(value): + class Model(BaseModel): + a: confloat(allow_inf_nan=False) + + with pytest.raises(ValidationError) as exc_info: + Model(a=value) + assert exc_info.value.errors() == [ + { + 'loc': ('a',), + 'msg': 'ensure this value is a finite number', + 'type': 'value_error.number.not_finite_number', + }, ] From b1d29b76694a723730b4a6fbe230bd6da176719a Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 22 Aug 2022 10:41:51 +0100 Subject: [PATCH 3/5] fix tests --- pydantic/__init__.py | 4 +--- tests/mypy/test_mypy.py | 9 ++++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/pydantic/__init__.py b/pydantic/__init__.py index 66f3140dc3..d6282ce8f7 100644 --- a/pydantic/__init__.py +++ b/pydantic/__init__.py @@ -98,9 +98,7 @@ 'NegativeFloat', 'NonNegativeFloat', 'NonPositiveFloat', - 'NonNanFloat', - 'NonInfFloat', - 'JSONFloat', + 'FiniteFloat', 'ConstrainedDecimal', 'condecimal', 'UUID1', diff --git a/tests/mypy/test_mypy.py b/tests/mypy/test_mypy.py index 4e6d173ae4..18a19b3ec4 100644 --- a/tests/mypy/test_mypy.py +++ b/tests/mypy/test_mypy.py @@ -110,7 +110,7 @@ def test_success_cases_run(module: str) -> None: importlib.import_module(f'tests.mypy.modules.{module}') -def test_explicit_reexports() -> None: +def test_explicit_reexports(): from pydantic import __all__ as root_all from pydantic.main import __all__ as main from pydantic.networks import __all__ as networks @@ -120,3 +120,10 @@ def test_explicit_reexports() -> None: for name, export_all in [('main', main), ('network', networks), ('tools', tools), ('types', types)]: for export in export_all: assert export in root_all, f'{export} is in {name}.__all__ but missing from re-export in __init__.py' + + +def test_explicit_reexports_exist(): + import pydantic + + for name in pydantic.__all__: + assert hasattr(pydantic, name), f'{name} is in pydantic.__all__ but missing from pydantic' From c4579e3dea365e75a78356d302d9aeaafd7cd5d7 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 22 Aug 2022 11:14:19 +0100 Subject: [PATCH 4/5] add change and docs --- changes/3994-tiangolo.md | 1 + docs/usage/types.md | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 changes/3994-tiangolo.md diff --git a/changes/3994-tiangolo.md b/changes/3994-tiangolo.md new file mode 100644 index 0000000000..e4bffdd652 --- /dev/null +++ b/changes/3994-tiangolo.md @@ -0,0 +1 @@ +Add JSON-compatible float constraint `allow_inf_nan` diff --git a/docs/usage/types.md b/docs/usage/types.md index 86a437ba6e..1756531ff5 100644 --- a/docs/usage/types.md +++ b/docs/usage/types.md @@ -878,6 +878,9 @@ The following arguments are available when using the `confloat` type function - `lt: float = None`: enforces float to be less than the set value - `le: float = None`: enforces float to be less than or equal to the set value - `multiple_of: float = None`: enforces float to be a multiple of the set value +- `allow_inf_nan: bool = True`: whether to allows infinity (`+inf` an `-inf`) and NaN values, defaults to `True`, + set to `False` for compatibility with `JSON`, + see [#3994](https://github.com/pydantic/pydantic/pull/3994) for more details, added in **V1.10** ### Arguments to `condecimal` The following arguments are available when using the `condecimal` type function From b7319ae2aedd38170b22e4e5783e52bc845798c3 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 22 Aug 2022 13:13:18 +0100 Subject: [PATCH 5/5] add allow_inf_nan to Config --- docs/usage/model_config.md | 5 +++++ pydantic/config.py | 2 ++ pydantic/types.py | 7 +++---- pydantic/validators.py | 15 ++++++++++----- tests/test_types.py | 26 +++++++++++++++++++++++++- 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/docs/usage/model_config.md b/docs/usage/model_config.md index 749f894362..f73f3ff971 100644 --- a/docs/usage/model_config.md +++ b/docs/usage/model_config.md @@ -121,6 +121,11 @@ not be included in the model schemas. **Note**: this means that attributes on th : whether stdlib dataclasses `__post_init__` should be run before (default behaviour with value `'before_validation'`) or after (value `'after_validation'`) parsing and validation when they are [converted](dataclasses.md#stdlib-dataclasses-and-_pydantic_-dataclasses). +**`allow_inf_nan`** +: whether to allows infinity (`+inf` an `-inf`) and NaN values to float fields, defaults to `True`, + set to `False` for compatibility with `JSON`, + see [#3994](https://github.com/pydantic/pydantic/pull/3994) for more details, added in **V1.10** + ## Change behaviour globally If you wish to change the behaviour of _pydantic_ globally, you can create your own custom `BaseModel` diff --git a/pydantic/config.py b/pydantic/config.py index 7e21c13a1f..74687ca036 100644 --- a/pydantic/config.py +++ b/pydantic/config.py @@ -67,6 +67,7 @@ class ConfigDict(TypedDict, total=False): json_dumps: AnyArgTCallable[str] json_encoders: Dict[Type[object], AnyCallable] underscore_attrs_are_private: bool + allow_inf_nan: bool # whether or not inherited models as fields should be reconstructed as base model copy_on_model_validation: bool @@ -103,6 +104,7 @@ class BaseConfig: json_dumps: Callable[..., str] = json.dumps json_encoders: Dict[Union[Type[Any], str, ForwardRef], AnyCallable] = {} underscore_attrs_are_private: bool = False + allow_inf_nan: bool = True # whether inherited models as fields should be reconstructed as base model, # and whether such a copy should be shallow or deep diff --git a/pydantic/types.py b/pydantic/types.py index a59a7c64eb..f98dba3de4 100644 --- a/pydantic/types.py +++ b/pydantic/types.py @@ -294,8 +294,7 @@ def __get_validators__(cls) -> 'CallableGenerator': yield strict_float_validator if cls.strict else float_validator yield number_size_validator yield number_multiple_validator - if cls.allow_inf_nan is False: - yield float_finite_validator + yield float_finite_validator def confloat( @@ -306,7 +305,7 @@ def confloat( lt: float = None, le: float = None, multiple_of: float = None, - allow_inf_nan: bool = True, + allow_inf_nan: Optional[bool] = None, ) -> Type[float]: # use kwargs then define conf in a dict to aid with IDE type hinting namespace = dict(strict=strict, gt=gt, ge=ge, lt=lt, le=le, multiple_of=multiple_of, allow_inf_nan=allow_inf_nan) @@ -338,7 +337,7 @@ class StrictFloat(ConstrainedFloat): strict = True class FiniteFloat(ConstrainedFloat): - allow_info_nan = False + allow_inf_nan = False # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BYTES TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/pydantic/validators.py b/pydantic/validators.py index 4d2b102ab9..1c19fc9239 100644 --- a/pydantic/validators.py +++ b/pydantic/validators.py @@ -152,8 +152,12 @@ def strict_float_validator(v: Any) -> float: raise errors.FloatError() -def float_finite_validator(v: 'Number') -> 'Number': - if math.isnan(v) or math.isinf(v): +def float_finite_validator(v: 'Number', field: 'ModelField', config: 'BaseConfig') -> 'Number': + allow_inf_nan = getattr(field.type_, 'allow_inf_nan', None) + if allow_inf_nan is None: + allow_inf_nan = config.allow_inf_nan + + if allow_inf_nan is False and (math.isnan(v) or math.isinf(v)): raise errors.NumberNotFiniteError() return v @@ -618,12 +622,13 @@ def typeddict_validator(values: 'TypedDict') -> Dict[str, Any]: # type: ignore[ class IfConfig: - def __init__(self, validator: AnyCallable, *config_attr_names: str) -> None: + def __init__(self, validator: AnyCallable, *config_attr_names: str, ignored_value: Any = False) -> None: self.validator = validator self.config_attr_names = config_attr_names + self.ignored_value = ignored_value def check(self, config: Type['BaseConfig']) -> bool: - return any(getattr(config, name) not in {None, False} for name in self.config_attr_names) + return any(getattr(config, name) not in {None, self.ignored_value} for name in self.config_attr_names) # order is important here, for example: bool is a subclass of int so has to come first, datetime before date same, @@ -653,7 +658,7 @@ def check(self, config: Type['BaseConfig']) -> bool: ), (bool, [bool_validator]), (int, [int_validator]), - (float, [float_validator]), + (float, [float_validator, IfConfig(float_finite_validator, 'allow_inf_nan', ignored_value=True)]), (Path, [path_validator]), (datetime, [parse_datetime]), (date, [parse_date]), diff --git a/tests/test_types.py b/tests/test_types.py index 8fb75a854a..af4a91ef1d 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -43,6 +43,7 @@ EmailStr, Field, FilePath, + FiniteFloat, FutureDate, Json, NameEmail, @@ -1571,6 +1572,9 @@ class Model(BaseModel): m = Model(a=5.1, b=-5.2, c=0, d=0, e=5.3, f=9.9, g=2.5, h=42) assert m.dict() == {'a': 5.1, 'b': -5.2, 'c': 0, 'd': 0, 'e': 5.3, 'f': 9.9, 'g': 2.5, 'h': 42} + assert Model(a=float('inf')).a == float('inf') + assert Model(b=float('-inf')).b == float('-inf') + with pytest.raises(ValidationError) as exc_info: Model(a=-5.1, b=5.2, c=-5.1, d=5.1, e=-5.3, f=9.91, g=4.2, h=float('nan')) assert exc_info.value.errors() == [ @@ -1636,8 +1640,9 @@ class Model(BaseModel): @pytest.mark.parametrize('value', [float('inf'), float('-inf'), float('nan')]) def test_finite_float_validation_error(value): class Model(BaseModel): - a: confloat(allow_inf_nan=False) + a: FiniteFloat + assert Model(a=42).a == 42 with pytest.raises(ValidationError) as exc_info: Model(a=value) assert exc_info.value.errors() == [ @@ -1649,6 +1654,25 @@ class Model(BaseModel): ] +def test_finite_float_config(): + class Model(BaseModel): + a: float + + class Config: + allow_inf_nan = False + + assert Model(a=42).a == 42 + with pytest.raises(ValidationError) as exc_info: + Model(a=float('nan')) + assert exc_info.value.errors() == [ + { + 'loc': ('a',), + 'msg': 'ensure this value is a finite number', + 'type': 'value_error.number.not_finite_number', + }, + ] + + def test_strict_bytes(): class Model(BaseModel): v: StrictBytes