|
"""Provides partial support for compatibility with Pydantic v1.""" |
|
|
|
from __future__ import annotations |
|
|
|
import json |
|
from functools import lru_cache |
|
from inspect import signature |
|
from operator import attrgetter |
|
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal, overload |
|
|
|
import pydantic |
|
|
|
from .utils import IS_PYDANTIC_V2, to_json |
|
|
|
if TYPE_CHECKING: |
|
from typing import Protocol |
|
|
|
class V1Model(Protocol): |
|
|
|
|
|
|
|
@classmethod |
|
def _dump_json_vals(cls, values: dict, by_alias: bool) -> dict: ... |
|
|
|
|
|
|
|
|
|
__config__: ClassVar[type] |
|
__fields__: ClassVar[dict[str, Any]] |
|
__fields_set__: set[str] |
|
|
|
@classmethod |
|
def update_forward_refs(cls, *args: Any, **kwargs: Any) -> None: ... |
|
@classmethod |
|
def construct(cls, *args: Any, **kwargs: Any) -> V1Model: ... |
|
@classmethod |
|
def parse_obj(cls, *args: Any, **kwargs: Any) -> V1Model: ... |
|
@classmethod |
|
def parse_raw(cls, *args: Any, **kwargs: Any) -> V1Model: ... |
|
|
|
def dict(self, **kwargs: Any) -> dict[str, Any]: ... |
|
def json(self, **kwargs: Any) -> str: ... |
|
def copy(self, **kwargs: Any) -> V1Model: ... |
|
|
|
|
|
|
|
|
|
_V1_CONFIG_KEYS = { |
|
"populate_by_name": "allow_population_by_field_name", |
|
"str_to_lower": "anystr_lower", |
|
"str_strip_whitespace": "anystr_strip_whitespace", |
|
"str_to_upper": "anystr_upper", |
|
"ignored_types": "keep_untouched", |
|
"str_max_length": "max_anystr_length", |
|
"str_min_length": "min_anystr_length", |
|
"from_attributes": "orm_mode", |
|
"json_schema_extra": "schema_extra", |
|
"validate_default": "validate_all", |
|
} |
|
|
|
|
|
def convert_v2_config(v2_config: dict[str, Any]) -> dict[str, Any]: |
|
"""Internal helper: Return a copy of the v2 ConfigDict with renamed v1 keys.""" |
|
return {_V1_CONFIG_KEYS.get(k, k): v for k, v in v2_config.items()} |
|
|
|
|
|
@lru_cache(maxsize=None) |
|
def allowed_arg_names(func: Callable) -> set[str]: |
|
"""Internal helper: Return the names of args accepted by the given function.""" |
|
return set(signature(func).parameters) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PydanticModelMetaclass: type = type(pydantic.BaseModel) |
|
|
|
|
|
class V1MixinMetaclass(PydanticModelMetaclass): |
|
def __new__( |
|
cls, |
|
name: str, |
|
bases: tuple[type, ...], |
|
namespace: dict[str, Any], |
|
**kwargs: Any, |
|
): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if config_dict := namespace.pop("model_config", None): |
|
namespace["Config"] = type("Config", (), convert_v2_config(config_dict)) |
|
return super().__new__(cls, name, bases, namespace, **kwargs) |
|
|
|
@property |
|
def model_fields(self) -> dict[str, Any]: |
|
return self.__fields__ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class V1Mixin(metaclass=V1MixinMetaclass): |
|
|
|
@classmethod |
|
def _dump_json_vals(cls, values: dict[str, Any], by_alias: bool) -> dict[str, Any]: |
|
"""Reserialize values from `Json`-typed fields after dumping the model to dict.""" |
|
|
|
|
|
json_fields = (f for f in cls.__fields__.values() if f.parse_json) |
|
get_key = attrgetter("alias" if by_alias else "name") |
|
json_field_keys = set(map(get_key, json_fields)) |
|
|
|
return { |
|
|
|
k: to_json(v) if ((v is not None) and (k in json_field_keys)) else v |
|
for k, v in values.items() |
|
} |
|
|
|
|
|
@classmethod |
|
def __try_update_forward_refs__(cls: type[V1Model], **localns: Any) -> None: |
|
if hasattr(sup := super(), "__try_update_forward_refs__"): |
|
sup.__try_update_forward_refs__(**localns) |
|
|
|
@classmethod |
|
def model_rebuild(cls, *args: Any, **kwargs: Any) -> None: |
|
return cls.update_forward_refs(*args, **kwargs) |
|
|
|
@classmethod |
|
def model_construct(cls, *args: Any, **kwargs: Any) -> V1Model: |
|
return cls.construct(*args, **kwargs) |
|
|
|
@classmethod |
|
def model_validate(cls, *args: Any, **kwargs: Any) -> V1Model: |
|
return cls.parse_obj(*args, **kwargs) |
|
|
|
@classmethod |
|
def model_validate_json(cls, *args: Any, **kwargs: Any) -> V1Model: |
|
return cls.parse_raw(*args, **kwargs) |
|
|
|
def model_dump(self: V1Model, **kwargs: Any) -> dict[str, Any]: |
|
|
|
allowed_keys = allowed_arg_names(self.dict) & kwargs.keys() |
|
dict_ = self.dict(**{k: kwargs[k] for k in allowed_keys}) |
|
|
|
|
|
if kwargs.get("round_trip", False): |
|
by_alias: bool = kwargs.get("by_alias", False) |
|
return self._dump_json_vals(dict_, by_alias=by_alias) |
|
|
|
return dict_ |
|
|
|
def model_dump_json(self: V1Model, **kwargs: Any) -> str: |
|
|
|
allowed_keys = allowed_arg_names(self.json) & kwargs.keys() |
|
json_ = self.json(**{k: kwargs[k] for k in allowed_keys}) |
|
|
|
|
|
if kwargs.get("round_trip", False): |
|
by_alias: bool = kwargs.get("by_alias", False) |
|
dict_ = json.loads(json_) |
|
return json.dumps(self._dump_json_vals(dict_, by_alias=by_alias)) |
|
|
|
return json_ |
|
|
|
def model_copy(self: V1Model, **kwargs: Any) -> V1Model: |
|
|
|
allowed_keys = allowed_arg_names(self.copy) & kwargs.keys() |
|
return self.copy(**{k: kwargs[k] for k in allowed_keys}) |
|
|
|
@property |
|
def model_fields_set(self: V1Model) -> set[str]: |
|
return self.__fields_set__ |
|
|
|
|
|
|
|
class V2Mixin: |
|
pass |
|
|
|
|
|
|
|
PydanticCompatMixin: type = V2Mixin if IS_PYDANTIC_V2 else V1Mixin |
|
|
|
|
|
|
|
|
|
|
|
if IS_PYDANTIC_V2: |
|
field_validator = pydantic.field_validator |
|
model_validator = pydantic.model_validator |
|
AliasChoices = pydantic.AliasChoices |
|
computed_field = pydantic.computed_field |
|
|
|
else: |
|
|
|
|
|
def field_validator( |
|
field: str, |
|
/, |
|
*fields: str, |
|
mode: Literal["before", "after", "wrap", "plain"] = "after", |
|
check_fields: bool | None = None, |
|
**_: Any, |
|
) -> Callable: |
|
return pydantic.validator( |
|
field, |
|
*fields, |
|
pre=(mode == "before"), |
|
always=True, |
|
check_fields=bool(check_fields), |
|
allow_reuse=True, |
|
) |
|
|
|
|
|
|
|
def model_validator( |
|
*, |
|
mode: Literal["before", "after", "wrap", "plain"], |
|
**_: Any, |
|
) -> Callable: |
|
if mode == "after": |
|
|
|
|
|
|
|
|
|
def _decorator(v2_method: Callable) -> Any: |
|
def v1_method( |
|
cls: type[V1Model], values: dict[str, Any] |
|
) -> dict[str, Any]: |
|
|
|
|
|
|
|
validated = v2_method(cls.construct(**values)) |
|
|
|
|
|
return { |
|
name: getattr(validated, name) for name in validated.__fields__ |
|
} |
|
|
|
return pydantic.root_validator(pre=False, allow_reuse=True)( |
|
classmethod(v1_method) |
|
) |
|
|
|
return _decorator |
|
else: |
|
return pydantic.root_validator(pre=(mode == "before"), allow_reuse=True) |
|
|
|
@overload |
|
def computed_field(func: Callable | property, /) -> property: ... |
|
@overload |
|
def computed_field( |
|
func: None, /, **_: Any |
|
) -> Callable[[Callable | property], property]: ... |
|
|
|
def computed_field( |
|
func: Callable | property | None = None, /, **_: Any |
|
) -> property | Callable[[Callable | property], property]: |
|
"""Compatibility wrapper for Pydantic v2's `computed_field` in v1.""" |
|
|
|
def always_property(f: Callable | property) -> property: |
|
|
|
return f if isinstance(f, property) else property(f) |
|
|
|
|
|
return always_property if (func is None) else always_property(func) |
|
|
|
class AliasChoices: |
|
"""Placeholder class for Pydantic v2's AliasChoices for partial v1 compatibility.""" |
|
|
|
aliases: list[str] |
|
|
|
def __init__(self, *aliases: str): |
|
self.aliases = list(aliases) |
|
|