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

bump pydantic-core to 2.5.0 #7077

Merged
merged 4 commits into from Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
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:
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved
"""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