-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
validators in custom subclasses of Mapping are not called #1457
Comments
I think your guess at the problem is probably (without digging in to it in detail now) spot on. The short answer therefore is probably "you can't". I'd be open to changing this (e.g. put the
If it does become clear it's backwards incompatible it would need to wait for v2. |
Got it, thank you! A quick hack on my installed Pydantic to reorder those branches allows the tests to pass, and I have to implement the recursive checking myself. That means that it's technically a breaking change since mapping types will no longer be validated automatically if you define a Design question: assuming this can be done backwards-compatibly, I'm thinking the best approach would be to check for @classmethod
def validate(cls, v):
if isinstance(v, Mapping):
v, errors = validate_mapping(v)
if errors:
raise ValidationError(errors, v)
else:
return FrozenOrderedDict(v)
else:
raise TypeError(
f"cannot construct a FrozenOrderedDict from non-Mapping value of type {type(v).__name__}"
) Or do you think that that's too much API surface area, and would rather people follow the pattern for this outlined in the docs? My quick and dirty @classmethod
def validate(cls, v, field: ModelField):
if isinstance(v, Mapping):
if field.sub_fields:
validated_dict = {}
errors = []
key_type, value_type = field.sub_fields
# Based on https://pydantic-docs.helpmanual.io/usage/types/#generic-classes-as-types.
for k, v in v.items():
key_result, key_error = key_type.validate(k, {}, loc="__key__")
if key_error:
errors.append(key_error)
value_result, value_error = value_type.validate(v, {}, loc=str(k))
if value_error:
errors.append(value_error)
if not errors:
validated_dict[key_result] = value_result
if errors:
raise ValidationError(errors, cls)
else:
return FrozenOrderedDict(validated_dict)
else:
return FrozenOrderedDict(v)
else:
raise TypeError(
f"cannot construct a FrozenOrderedDict from non-Mapping value of type {type(v).__name__}"
) |
Humm, difficult. One option would be to insert validators from the class as either pre or post "whole" validators. This would mean that the standard validation (including recursive checking) would still be called, just your validators would be called before or after that entire process. - This is probably be easier than what you're suggesting. My main reservation is that I an idea for quite a big rewrite of validators in v2 - I don't want you to do lots of work on this (and perhaps break some edge case usage) then change it all again in v2 which I'm hoping to work on fairly soon. I haven't yet sat down and written out a description of the new validators, I'll do that in the next few days and put a link here. |
Sounds good, I'll wait for your thoughts here! |
As a workaround I currently use this: @classmethod
def __get_validators__(cls) -> Generator: # noqa: B902
yield from super().__get_validators__()
... # your own extra validators |
This now works well in V2: from typing import Any, Dict, Hashable, TypeVar
from pydantic_core import CoreSchema, core_schema
from typing_extensions import get_args, get_origin
from pydantic.annotated import GetCoreSchemaHandler
K = TypeVar("K", bound=Hashable)
V = TypeVar("V")
class FrozenOrderedDict(Dict[K, V]):
@classmethod
def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
origin = get_origin(source_type)
if origin is None:
key_type = value_type = Any
origin = source_type
else:
key_type, value_type = get_args(source_type)
return core_schema.no_info_after_validator_function(
FrozenOrderedDict,
handler.generate_schema(Dict[key_type, value_type])
)
def __repr__(self) -> str:
return f'{self.__class__.__name__}({super().__repr__()})'
from pydantic import BaseModel
class Model(BaseModel):
x: FrozenOrderedDict[str, int]
print(Model(x={'a': '1'}).x)
#> FrozenOrderedDict({'a': 1}) # note that value was correctly coerced! I don't think we'll be able to fix this in V1. |
Bug
Output of
python -c "import pydantic.utils; print(pydantic.utils.version_info())"
:I'm defining a
FrozenOrderedDict
(need to support hashable dicts for Reasons), and I want to give it Pydantic support. Here's the implementation at the moment:I wrote a couple tests for this.
As far as I can tell, specifying type parameters hits a different code path: Pydantic no longer sees some duck-type that adheres to
__get_validators__
and instead sees a generic type alias that descends fromMapping
, and does its own validation. To go out on a limb, I'm guessing it's thathttps://github.com/samuelcolvin/pydantic/blob/3cd8b1ee2d5aac76528dbe627f40fe1c27bf59f6/pydantic/fields.py#L464
comes before
https://github.com/samuelcolvin/pydantic/blob/3cd8b1ee2d5aac76528dbe627f40fe1c27bf59f6/pydantic/fields.py#L476
or something along those lines.
I tried some workarounds, most notably including not descending from
Mapping
but instead justGeneric
and implementing the recursive validation myself. This wasn't too bad on the Pydantic side (good support for inspecting and validating generic type parameters!), but it caused other problems around interop withdict
and failures of duck-typing. And it was just weird to have a mapping that wasn't a Mapping.How can I descend from generic
Mapping
and support validation that respects type parameters, but still have my own (additional) validators be used? I don't mind implementing the recursive step for key/value validation, if necessary (though of course, it's nice if I don't have to, like now).I took a long look at #380 but could not see how it applies to my case despite apparent similarity.
The text was updated successfully, but these errors were encountered: