8000 Move `deque` schema gen to `GenerateSchema` class (#11239) · pydantic/pydantic@4e055d5 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4e055d5

Browse files
Move deque schema gen to GenerateSchema class (#11239)
This contributes to removing the prepare annotations logic that leads to inconsistent patterns for annotated metadata application ordering.
1 parent 8fc509d commit 4e055d5

File tree

4 files changed

+42
-118
lines changed

4 files changed

+42
-118
lines changed

pydantic/_internal/_generate_schema.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,31 @@ def path_validator(input_value: str | bytes) -> os.PathLike[Any]: # type: ignor
524524
)
525525
return schema
526526

527+
def _deque_schema(self, items_type: Any) -> CoreSchema:
528+
from ._serializers import serialize_sequence_via_list
529+
from ._validators import deque_validator
530+
531+
item_type_schema = self.generate_schema(items_type)
532+
533+
# we have to use a lax list schema here, because we need to validate the deque's
534+
# items via a list schema, but it's ok if the deque itself is not a list
535+
list_schema = core_schema.list_schema(item_type_schema, strict=False)
536+
537+
check_instance = core_schema.json_or_python_schema(
538+
json_schema=core_schema.list_schema(),
539+
python_schema=core_schema.is_instance_schema(collections.deque, cls_repr='Deque'),
540+
)
541+
542+
lax_schema = core_schema.no_info_wrap_validator_function(deque_validator, list_schema)
543+
544+
return core_schema.lax_or_strict_schema(
545+
lax_schema=lax_schema,
546+
strict_schema=core_schema.chain_schema([check_instance, lax_schema]),
547+
serialization=core_schema.wrap_serializer_function_ser_schema(
548+
serialize_sequence_via_list, schema=item_type_schema, info_arg=True
549+
),
550+
)
551+
527552
def _fraction_schema(self) -> CoreSchema:
528553
"""Support for [`fractions.Fraction`][fractions.Fraction]."""
529554
from ._validators import fraction_validator
@@ -967,6 +992,8 @@ def match_type(self, obj: Any) -> core_schema.CoreSchema: # noqa: C901
967992
return self._dict_schema(Any, Any)
968993
elif obj in PATH_TYPES:
969994
return self._path_schema(obj, Any)
995+
elif obj in DEQUE_TYPES:
996+
return self._deque_schema(Any)
970997
elif _typing_extra.is_type_alias_type(obj):
971998
return self._type_alias_type_schema(obj)
972999
elif obj is type:
@@ -1047,6 +1074,8 @@ def _match_generic_type(self, obj: Any, origin: Any) -> CoreSchema: # noqa: C90
10471074
return self._dict_schema(*self._get_first_two_args_or_any(obj))
10481075
elif origin in PATH_TYPES:
10491076
return self._path_schema(origin, self._get_first_arg_or_any(obj))
1077+
elif origin in DEQUE_TYPES:
1078+
return self._deque_schema(self._get_first_arg_or_any(obj))
10501079
elif is_typeddict(origin):
10511080
return self._typed_dict_schema(obj, origin)
10521081
elif origin in (typing.Type, type):
@@ -2019,10 +2048,7 @@ def _annotated_schema(self, annotated_type: Any) -> core_schema.CoreSchema:
20192048
def _get_prepare_pydantic_annotations_for_known_type(
20202049
self, obj: Any, annotations: tuple[Any, ...]
20212050
) -> tuple[Any, list[Any]] | None:
2022-
from ._std_types_schema import (
2023-
deque_schema_prepare_pydantic_annotations,
2024-
mapping_like_prepare_pydantic_annotations,
2025-
)
2051+
from ._std_types_schema import mapping_like_prepare_pydantic_annotations
20262052

20272053
# Check for hashability
20282054
try:
@@ -2036,9 +2062,7 @@ def _get_prepare_pydantic_annotations_for_known_type(
20362062
# not always called from match_type, but sometimes from _apply_annotations
20372063
obj_origin = get_origin(obj) or obj
20382064

2039-
if obj_origin in DEQUE_TYPES:
2040-
return deque_schema_prepare_pydantic_annotations(obj, annotations)
2041-
elif obj_origin in MAPPING_TYPES:
2065+
if obj_origin in MAPPING_TYPES:
20422066
return mapping_like_prepare_pydantic_annotations(obj, annotations)
20432067
else:
20442068
return None

pydantic/_internal/_std_types_schema.py

Lines changed: 2 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
Import of this module is deferred since it contains imports of many standard library modules.
44
"""
55

6-
# TODO: eventually, we'd like to move all of the types handled here to have pydantic-core validators
7-
# so that we can avoid this annotation injection and just use the standard pydantic-core schema generation
6+
# TODO: working on removing these types as the annotation injection pattern we use here is inconsistent
7+
# with the rest of the library and is not well supported by the pydantic-core schema generation
88

99
from __future__ import annotations as _annotations
1010

@@ -22,7 +22,6 @@
2222
)
2323
from typing_extensions import get_args, get_origin
2424

25-
from pydantic._internal._serializers import serialize_sequence_via_list
2625
from pydantic.errors import PydanticSchemaGenerationError
2726

2827
from . import _known_annotated_metadata, _typing_extra
@@ -32,86 +31,6 @@
3231

3332
FieldInfo = import_cached_field_info()
3433

35-
if typing.TYPE_CHECKING:
36-
from ._generate_schema import GenerateSchema
37-
38-
StdSchemaFunction = Callable[[GenerateSchema, type[Any]], core_schema.CoreSchema]
39-
40-
41-
def deque_validator(
42-
input_value: Any, handler: core_schema.ValidatorFunctionWrapHandler, maxlen: None | int
43-
) -> collections.deque[Any]:
44-
if isinstance(input_value, collections.deque):
45-
maxlens = [v for v in (input_value.maxlen, maxlen) if v is not None]
46-
if maxlens:
47-
maxlen = min(maxlens)
48-
return collections.deque(handler(input_value), maxlen=maxlen)
49-
else:
50-
return collections.deque(handler(input_value), maxlen=maxlen)
51-
52-
53-
@dataclasses.dataclass(**slots_true)
54-
class DequeValidator:
55-
item_source_type: type[Any]
56-
metadata: dict[str, Any]
57-
58-
def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
59-
if _typing_extra.is_any(self.item_source_type):
60-
items_schema = None
61-
else:
62-
items_schema = handler.generate_schema(self.item_source_type)
63-
64-
# if we have a MaxLen annotation might as well set that as the default maxlen on the deque
65-
# this lets us reuse existing metadata annotations to let users set the maxlen on a dequeue
66-
# that e.g. comes from JSON
67-
coerce_instance_wrap = partial(
68-
core_schema.no_info_wrap_validator_function,
69-
partial(deque_validator, maxlen=self.metadata.get('max_length', None)),
70-
)
71-
72-
# we have to use a lax list schema here, because we need to validate the deque's
73-
# items via a list schema, but it's ok if the deque itself is not a list
74-
metadata_with_strict_override = {**self.metadata, 'strict': False}
75-
constrained_schema = core_schema.list_schema(items_schema, **metadata_with_strict_override)
76-
77-
check_instance = core_schema.json_or_python_schema(
78-
json_schema=core_schema.list_schema(),
79-
python_schema=core_schema.is_instance_schema(collections.deque),
80-
)
81-
82-
serialization = core_schema.wrap_serializer_function_ser_schema(
83-
serialize_sequence_via_list, schema=items_schema or core_schema.any_schema(), info_arg=True
84-
)
85-
86-
strict = core_schema.chain_schema([check_instance, coerce_instance_wrap(constrained_schema)])
87-
88-
if self.metadata.get('strict', False):
89-
schema = strict
90-
else:
91-
lax = coerce_instance_wrap(constrained_schema)
92-
schema = core_schema.lax_or_strict_schema(lax_schema=lax, strict_schema=strict)
93-
schema['serialization'] = serialization
94-
95-
return schema
96-
97-
98-
def deque_schema_prepare_pydantic_annotations(
99-
source_type: Any, annotations: Iterable[Any]
100-
) -> tuple[Any, list[Any]] | None:
101-
args = get_args(source_type)
102-
103-
if not args:
104-
args = typing.cast(Tuple[Any], (Any,))
105-
elif len(args) != 1:
106-
raise ValueError('Expected deque to have exactly 1 generic parameter')
107-
108-
item_source_type = args[0]
109-
110-
metadata, remaining_annotations = _known_annotated_metadata.collect_known_metadata(annotations)
111-
_known_annotated_metadata.check_metadata(metadata, _known_annotated_metadata.SEQUENCE_CONSTRAINTS, source_type)
112-
113-
return (source_type, [DequeValidator(item_source_type, metadata), *remaining_annotations])
114-
11534

11635
MAPPING_ORIGIN_MAP: dict[Any, Any] = {
11736
typing.DefaultDict: collections.defaultdict,

pydantic/_internal/_validators.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import math
99
import re
1010
import typing
11+
from collections import deque
1112
from decimal import Decimal
1213
from fractions import Fraction
1314
from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network
@@ -402,6 +403,10 @@ def decimal_places_validator(x: Any, decimal_places: Any) -> Any:
402403
raise TypeError(f"Unable to apply constraint 'decimal_places' to supplied value {x}")
403404

404405

406+
def deque_validator(input_value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> deque[Any]:
407+
return deque(handler(input_value), maxlen=getattr(input_value, 'maxlen', None))
408+
409+
405410
NUMERIC_VALIDATOR_LOOKUP: dict[str, Callable] = {
406411
'gt': greater_than_validator,
407412
'ge': greater_than_or_equal_validator,

tests/test_types.py

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5190,36 +5190,12 @@ class DequeModel3(BaseModel):
51905190
assert DequeModel3(field=deque(maxlen=8)).field.maxlen == 8
51915191

51925192

5193-
def test_deque_set_maxlen():
5193+
def test_deque_enforces_maxlen():
51945194
class DequeModel1(BaseModel):
5195-
field: Annotated[Deque[int], Field(max_length=10)]
5195+
field: Annotated[Deque[int], Field(max_length=3)]
51965196

5197-
assert DequeModel1(field=deque()).field.maxlen == 10
5198-
assert DequeModel1(field=deque(maxlen=8)).field.maxlen == 8
5199-
assert DequeModel1(field=deque(maxlen=15)).field.maxlen == 10
5200-
5201-
class DequeModel2(BaseModel):
5202-
field: Annotated[Deque[int], Field(max_length=10)] = deque()
5203-
5204-
assert DequeModel2().field.maxlen is None
5205-
assert DequeModel2(field=deque()).field.maxlen == 10
5206-
assert DequeModel2(field=deque(maxlen=8)).field.maxlen == 8
5207-
assert DequeModel2(field=deque(maxlen=15)).field.maxlen == 10
5208-
5209-
class DequeModel3(DequeModel2):
5210-
model_config = ConfigDict(validate_default=True)
5211-
5212-
assert DequeModel3().field.maxlen == 10
5213-
5214-
class DequeModel4(BaseModel):
5215-
field: Annotated[Deque[int], Field(max_length=10)] = deque(maxlen=5)
5216-
5217-
assert DequeModel4().field.maxlen == 5
5218-
5219-
class DequeModel5(DequeModel4):
5220-
model_config = ConfigDict(validate_default=True)
5221-
5222-
assert DequeModel4().field.maxlen == 5
5197+
with pytest.raises(ValidationError):
5198+
DequeModel1(field=deque([1, 2, 3, 4]))
52235199

52245200

52255201
@pytest.mark.parametrize('value_type', (None, type(None), None.__class__))

0 commit comments

Comments
 (0)
0