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 JSON-compatible float constraints for NaN and Inf #3994

Merged
merged 6 commits into from Aug 22, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/usage/model_config.md
Expand Up @@ -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`
Expand Down
2 changes: 2 additions & 0 deletions pydantic/config.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions pydantic/types.py
Expand Up @@ -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(
Expand All @@ -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)
Expand Down Expand Up @@ -338,7 +337,7 @@ class StrictFloat(ConstrainedFloat):
strict = True

class FiniteFloat(ConstrainedFloat):
allow_info_nan = False
allow_inf_nan = False


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BYTES TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
15 changes: 10 additions & 5 deletions pydantic/validators.py
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


if allow_inf_nan is False and (math.isnan(v) or math.isinf(v)):
raise errors.NumberNotFiniteError()
return v

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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]),
Expand Down
26 changes: 25 additions & 1 deletion tests/test_types.py
Expand Up @@ -43,6 +43,7 @@
EmailStr,
Field,
FilePath,
FiniteFloat,
FutureDate,
Json,
NameEmail,
Expand Down Expand Up @@ -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() == [
Expand Down Expand Up @@ -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() == [
Expand All @@ -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
Expand Down