Skip to content

Commit

Permalink
bump pydantic-core to 2.5.0 (#7077)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidhewitt committed Aug 14, 2023
1 parent 84a8014 commit b903d72
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 327 deletions.
109 changes: 109 additions & 0 deletions docs/errors/validation_errors.md
Expand Up @@ -479,6 +479,115 @@ except ValidationError as exc:

This error is also raised for strict fields when the input value is not an instance of `datetime`.

## `decimal_max_digits`

This error is raised when the value provided for a `Decimal` has too many digits:

```py
from decimal import Decimal

from pydantic import BaseModel, Field, ValidationError


class Model(BaseModel):
x: Decimal = Field(max_digits=3)


try:
Model(x='42.1234')
except ValidationError as exc:
print(repr(exc.errors()[0]['type']))
#> 'decimal_max_digits'
```

## `decimal_max_places`

This error is raised when the value provided for a `Decimal` has too many digits after the decimal point:

```py
from decimal import Decimal

from pydantic import BaseModel, Field, ValidationError


class Model(BaseModel):
x: Decimal = Field(decimal_places=3)


try:
Model(x='42.1234')
except ValidationError as exc:
print(repr(exc.errors()[0]['type']))
#> 'decimal_max_places'
```

## `decimal_parsing`

This error is raised when the value provided for a `Decimal` could not be parsed as a decimal number:

```py
from decimal import Decimal

from pydantic import BaseModel, Field, ValidationError


class Model(BaseModel):
x: Decimal = Field(decimal_places=3)


try:
Model(x='test')
except ValidationError as exc:
print(repr(exc.errors()[0]['type']))
#> 'decimal_parsing'
```

## `decimal_type`

This error is raised when the value provided for a `Decimal` is of the wrong type:

```py
from decimal import Decimal

from pydantic import BaseModel, Field, ValidationError


class Model(BaseModel):
x: Decimal = Field(decimal_places=3)


try:
Model(x=[1, 2, 3])
except ValidationError as exc:
print(repr(exc.errors()[0]['type']))
#> 'decimal_type'
```

This error is also raised for strict fields when the input value is not an instance of `Decimal`.

## `decimal_whole_digits`

This error is raised when the value provided for a `Decimal` has more digits before the decimal point than `max_digits` - `decimal_places` (as long as both are specified):

```py
from decimal import Decimal

from pydantic import BaseModel, Field, ValidationError


class Model(BaseModel):
x: Decimal = Field(max_digits=6, decimal_places=3)


try:
Model(x='12345.6')
except ValidationError as exc:
print(repr(exc.errors()[0]['type']))
#> 'decimal_whole_digits'
```

This error is also raised for strict fields when the input value is not an instance of `Decimal`.

## `dict_type`

This error is raised when the input value's type is not `dict` for a `dict` field:
Expand Down
212 changes: 107 additions & 105 deletions pdm.lock

Large diffs are not rendered by default.

196 changes: 14 additions & 182 deletions pydantic/_internal/_std_types_schema.py
Expand Up @@ -21,7 +21,6 @@
CoreSchema,
MultiHostUrl,
PydanticCustomError,
PydanticKnownError,
PydanticOmit,
Url,
core_schema,
Expand Down Expand Up @@ -137,170 +136,24 @@ def get_json_schema(_, handler: GetJsonSchemaHandler) -> JsonSchemaValue:


@dataclasses.dataclass(**slots_true)
class DecimalValidator:
gt: decimal.Decimal | None = None
ge: decimal.Decimal | None = None
lt: decimal.Decimal | None = None
le: decimal.Decimal | None = None
max_digits: int | None = None
decimal_places: int | None = None
multiple_of: decimal.Decimal | None = None
allow_inf_nan: bool = False
check_digits: bool = False
strict: bool = False
class InnerSchemaValidator:
"""Use a fixed CoreSchema, avoiding interference from outward annotations."""

def __post_init__(self) -> None:
self.check_digits = self.max_digits is not None or self.decimal_places is not None
self.gt = decimal.Decimal(self.gt) if self.gt is not None else None
self.ge = decimal.Decimal(self.ge) if self.ge is not None else None
self.lt = decimal.Decimal(self.lt) if self.lt is not None else None
self.le = decimal.Decimal(self.le) if self.le is not None else None
self.multiple_of = decimal.Decimal(self.multiple_of) if self.multiple_of is not None else None
if self.check_digits and self.allow_inf_nan:
raise ValueError('allow_inf_nan=True cannot be used with max_digits or decimal_places')
core_schema: CoreSchema
js_schema: JsonSchemaValue | None = None
js_core_schema: CoreSchema | None = None
js_schema_update: JsonSchemaValue | None = None

def __get_pydantic_json_schema__(self, _schema: CoreSchema, handler: GetJsonSchemaHandler) -> JsonSchemaValue:
string_schema = handler(core_schema.str_schema())

if handler.mode == 'validation':
float_schema = handler(
core_schema.float_schema(
allow_inf_nan=self.allow_inf_nan,
multiple_of=None if self.multiple_of is None else float(self.multiple_of),
le=None if self.le is None else float(self.le),
ge=None if self.ge is None else float(self.ge),
lt=None if self.lt is None else float(self.lt),
gt=None if self.gt is None else float(self.gt),
)
)
return {'anyOf': [float_schema, string_schema]}
else:
return string_schema
if self.js_schema is not None:
return self.js_schema
js_schema = handler(self.js_core_schema or self.core_schema)
if self.js_schema_update is not None:
js_schema.update(self.js_schema_update)
return js_schema

def __get_pydantic_core_schema__(self, _source_type: Any, _handler: GetCoreSchemaHandler) -> CoreSchema:
Decimal = decimal.Decimal

def to_decimal(v: str) -> decimal.Decimal:
try:
return Decimal(v)
except decimal.DecimalException as e:
raise PydanticCustomError('decimal_parsing', 'Input should be a valid decimal') from e

primitive_schema = core_schema.union_schema(
[
# if it's an int keep it like that and pass it straight to Decimal
# but if it's not make it a string
# we don't use JSON -> float because parsing to any float will cause
# loss of precision
core_schema.int_schema(strict=True),
core_schema.str_schema(strict=True, strip_whitespace=True),
core_schema.no_info_plain_validator_function(str),
],
)
json_schema = core_schema.no_info_after_validator_function(to_decimal, primitive_schema)
schema = core_schema.json_or_python_schema(
json_schema=json_schema,
python_schema=core_schema.lax_or_strict_schema(
lax_schema=core_schema.union_schema([core_schema.is_instance_schema(decimal.Decimal), json_schema]),
strict_schema=core_schema.is_instance_schema(decimal.Decimal),
),
serialization=core_schema.to_string_ser_schema(when_used='json'),
)

if not self.allow_inf_nan or self.check_digits:
schema = core_schema.no_info_after_validator_function(
self.check_digits_validator,
schema,
)

if self.multiple_of is not None:
schema = core_schema.no_info_after_validator_function(
partial(_validators.multiple_of_validator, multiple_of=self.multiple_of),
schema,
)

if self.gt is not None:
schema = core_schema.no_info_after_validator_function(
partial(_validators.greater_than_validator, gt=self.gt),
schema,
)

if self.ge is not None:
schema = core_schema.no_info_after_validator_function(
partial(_validators.greater_than_or_equal_validator, ge=self.ge),
schema,
)

if self.lt is not None:
schema = core_schema.no_info_after_validator_function(
partial(_validators.less_than_validator, lt=self.lt),
schema,
)

if self.le is not None:
schema = core_schema.no_info_after_validator_function(
partial(_validators.less_than_or_equal_validator, le=self.le),
schema,
)

return schema

def check_digits_validator(self, value: decimal.Decimal) -> decimal.Decimal:
if not value.is_finite():
# Either check_digits is true or allow_inf_nan is False,
# either way we cannot allow nan / infinity
raise PydanticKnownError('finite_number')

if self.check_digits:
try:
normalized_value = value.normalize()
except decimal.InvalidOperation:
normalized_value = value
_1, digit_tuple, exponent = normalized_value.as_tuple()

# Already checked for finite value above
assert isinstance(exponent, int)

if exponent >= 0:
# A positive exponent adds that many trailing zeros.
digits = len(digit_tuple) + exponent
decimals = 0
else:
# If the absolute value of the negative exponent is larger than the
# number of digits, then it's the same as the number of digits,
# because it'll consume all the digits in digit_tuple and then
# add abs(exponent) - len(digit_tuple) leading zeros after the
# decimal point.
if abs(exponent) > len(digit_tuple):
digits = decimals = abs(exponent)
else:
digits = len(digit_tuple)
decimals = abs(exponent)

if self.max_digits is not None and digits > self.max_digits:
raise PydanticCustomError(
'decimal_max_digits',
'ensure that there are no more than {max_digits} digits in total',
{'max_digits': self.max_digits},
)

if self.decimal_places is not None and decimals > self.decimal_places:
raise PydanticCustomError(
'decimal_max_places',
'ensure that there are no more than {decimal_places} decimal places',
{'decimal_places': self.decimal_places},
)

if self.max_digits is not None and self.decimal_places is not None:
whole_digits = digits - decimals
expected = self.max_digits - self.decimal_places
if whole_digits > expected:
raise PydanticCustomError(
'decimal_whole_digits',
'ensure that there are no more than {whole_digits} digits before the decimal point',
{'whole_digits': expected},
)
return value
return self.core_schema


def decimal_prepare_pydantic_annotations(
Expand All @@ -318,28 +171,7 @@ def decimal_prepare_pydantic_annotations(
_known_annotated_metadata.check_metadata(
metadata, {*_known_annotated_metadata.FLOAT_CONSTRAINTS, 'max_digits', 'decimal_places'}, decimal.Decimal
)
return source, [DecimalValidator(**metadata), *remaining_annotations]


@dataclasses.dataclass(**slots_true)
class InnerSchemaValidator:
"""Use a fixed CoreSchema, avoiding interference from outward annotations."""

core_schema: CoreSchema
js_schema: JsonSchemaValue | None = None
js_core_schema: CoreSchema | None = None
js_schema_update: JsonSchemaValue | None = None

def __get_pydantic_json_schema__(self, _schema: CoreSchema, handler: GetJsonSchemaHandler) -> JsonSchemaValue:
if self.js_schema is not None:
return self.js_schema
js_schema = handler(self.js_core_schema or self.core_schema)
if self.js_schema_update is not None:
js_schema.update(self.js_schema_update)
return js_schema

def __get_pydantic_core_schema__(self, _source_type: Any, _handler: GetCoreSchemaHandler) -> CoreSchema:
return self.core_schema
return source, [InnerSchemaValidator(core_schema.decimal_schema(**metadata)), *remaining_annotations]


def datetime_prepare_pydantic_annotations(
Expand Down
33 changes: 33 additions & 0 deletions pydantic/json_schema.py
Expand Up @@ -594,6 +594,39 @@ def float_schema(self, schema: core_schema.FloatSchema) -> JsonSchemaValue:
json_schema = {k: v for k, v in json_schema.items() if v not in {math.inf, -math.inf}}
return json_schema

def decimal_schema(self, schema: core_schema.DecimalSchema) -> JsonSchemaValue:
"""Generates a JSON schema that matches a decimal value.
Args:
schema: The core schema.
Returns:
The generated JSON schema.
"""
json_schema = self.str_schema(core_schema.str_schema())
if self.mode == 'validation':
multiple_of = schema.get('multiple_of')
le = schema.get('le')
ge = schema.get('ge')
lt = schema.get('lt')
gt = schema.get('gt')
json_schema = {
'anyOf': [
self.float_schema(
core_schema.float_schema(
allow_inf_nan=schema.get('allow_inf_nan'),
multiple_of=None if multiple_of is None else float(multiple_of),
le=None if le is None else float(le),
ge=None if ge is None else float(ge),
lt=None if lt is None else float(lt),
gt=None if gt is None else float(gt),
)
),
json_schema,
],
}
return json_schema

def str_schema(self, schema: core_schema.StringSchema) -> JsonSchemaValue:
"""Generates a JSON schema that matches a string value.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Expand Up @@ -61,7 +61,7 @@ requires-python = '>=3.7'
dependencies = [
'typing-extensions>=4.6.1',
'annotated-types>=0.4.0',
"pydantic-core==2.4.0",
"pydantic-core==2.5.0",
]
dynamic = ['version', 'readme']

Expand Down

0 comments on commit b903d72

Please sign in to comment.