8000 Properly validate parameterized mappings · pydantic/pydantic@422fb9f · GitHub
[go: up one dir, main page]

Skip to content

Commit 422fb9f

Browse files
committed
Properly validate parameterized mappings
Backport of: #11658
1 parent fdef77a commit 422fb9f

File tree

2 files changed

+40
-29
lines changed

2 files changed

+40
-29
lines changed

pydantic/_internal/_generate_schema.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -566,32 +566,38 @@ def _deque_schema(self, items_type: Any) -> CoreSchema:
566566
def _mapping_schema(self, tp: Any, keys_type: Any, values_type: Any) -> CoreSchema:
567567
from ._validators import MAPPING_ORIGIN_MAP, defaultdict_validator, get_defaultdict_default_default_factory
568568

569+
mapped_origin = MAPPING_ORIGIN_MAP[tp]
569570
keys_schema = self.generate_schema(keys_type)
570571
values_schema = self.generate_schema(values_type)
572+
dict_schema = core_schema.dict_schema(keys_schema, values_schema, strict=False)
571573

572-
dict_schema = core_schema.dict_schema(keys_schema, values_schema)
573-
check_instance = core_schema.json_or_python_schema(
574-
json_schema=dict_schema,
575-
python_schema=core_schema.is_instance_schema(tp),
576-
)
577-
578-
if tp is collections.defaultdict:
579-
default_default_factory = get_defaultdict_default_default_factory(values_type)
580-
coerce_instance_wrap = partial(
581-
core_schema.no_info_wrap_validator_function,
582-
partial(defaultdict_validator, default_default_factory=default_default_factory),
583-
)
574+
if mapped_origin is dict:
575+
schema = dict_schema
584576
else:
585-
coerce_instance_wrap = partial(core_schema.no_info_after_validator_function, MAPPING_ORIGIN_MAP[tp])
577+
check_instance = core_schema.json_or_python_schema(
578+
json_schema=dict_schema,
579+
python_schema=core_schema.is_instance_schema(mapped_origin),
580+
)
586581

587-
lax_schema = coerce_instance_wrap(dict_schema)
588-
schema = core_schema.lax_or_strict_schema(
589-
lax_schema=lax_schema,
590-
strict_schema=core_schema.union_schema([check_instance, lax_schema]),
591-
serialization=core_schema.wrap_serializer_function_ser_schema(
592-
lambda v, h: h(v), schema=dict_schema, info_arg=False
593-
),
594-
)
582+
if tp is collections.defaultdict:
583+
default_default_factory = get_defaultdict_default_default_factory(values_type)
584+
coerce_instance_wrap = partial(
585+
core_schema.no_info_wrap_validator_function,
586+
partial(defaultdict_validator, default_default_factory=default_default_factory),
587+
)
588+
else:
589+
coerce_instance_wrap = partial(core_schema.no_info_after_validator_function, mapped_origin)
590+
591+
lax_schema = coerce_instance_wrap(dict_schema)
592+
strict_schema = core_schema.chain_schema([check_instance, lax_schema])
593+
594+
schema = core_schema.lax_or_strict_schema(
595+
lax_schema=lax_schema,
596+
strict_schema=strict_schema,
597+
serialization=core_schema.wrap_serializer_function_ser_schema(
598+
lambda v, h: h(v), schema=dict_schema, info_arg=False
599+
),
600+
)
595601

596602
return schema
597603

tests/test_types.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
import sys
1010
import typing
1111
import uuid
12-
from collections import Counter, OrderedDict, defaultdict, deque
13-
from collections.abc import Iterable, Sequence
12+
from collections import Counter, OrderedDict, UserDict, defaultdict, deque
13+
from collections.abc import Iterable, Mapping, MutableMapping, Sequence
1414
from dataclasses import dataclass
1515
from datetime import date, datetime, time, timedelta, timezone
1616
from decimal import Decimal
@@ -6951,17 +6951,22 @@ class Foo(BaseModel):
69516951
assert exc_info.value.errors(include_url=False) == errors
69526952

69536953

6954-
def test_mutable_mapping() -> None:
6954+
def test_mutable_mapping_userdict_subclass() -> None:
69556955
"""Addresses https://github.com/pydantic/pydantic/issues/9549.
69566956
6957-
Note - we still don't do a good job of handling subclasses, as we convert the input to a dict
6958-
via the MappingValidator annotation's schema.
6957+
Note - we still don't do a good job of handling subclasses, as we convert the input to a dict.
69596958
"""
6960-
import collections.abc
6959+
adapter = TypeAdapter(MutableMapping, config=ConfigDict(strict=True))
69616960

6962-
adapter = TypeAdapter(collections.abc.MutableMapping, config=ConfigDict(strict=True))
6961+
assert isinstance(adapter.validate_python(UserDict()), MutableMapping)
69636962

6964-
assert isinstance(adapter.validate_python(collections.UserDict()), collections.abc.MutableMapping)
6963+
6964+
def test_mapping_parameterized() -> None:
6965+
"""https://github.com/pydantic/pydantic/issues/11650"""
6966+
adapter = TypeAdapter(Mapping[str, int])
6967+
6968+
with pytest.raises(ValidationError):
6969+
adapter.validate_python({'valid': 1, 'invalid': {}})
69656970

69666971

69676972
def test_ser_ip_with_union() -> None:

0 commit comments

Comments
 (0)
0