python-3.x pydantic中基于isinstance的自定义验证器,用于来自第三方库的自定义类

kb5ga3dv  于 2023-05-30  发布在  Python
关注(0)|答案(1)|浏览(237)

在我的自定义工作包中,我想使用pydantic验证输入。然而,我的大多数函数的输入不是原生类型,而是来自其他库的类的示例,例如。pandas.DataFramesqlalchemy.Engine。将这些作为类型提示并添加pydantic.validate_arguments装饰器会失败。
让我们假设我的函数的输入应该是来自CustomPackage库的CustomClass类型。

import CustomPackage
import pydantic

@pydantic.validate_arguments
def custom_function(custom_argument: CustomPackage.CustomClass) -> None:
    pass

这将导致以下错误:
RuntimeError:未找到<class 'CustomPackage. CustomClass'>的验证程序,请参阅配置中的arbitrary_types_allowed
为了解决这个问题,我可以使用@pydantic.validate_arguments(config={"arbitrary_types_allowed": True})来代替,它允许任何东西。这不是我的意图,所以我遵循文档中的自定义类型部分,并创建了以下内容:

import collections.abc
import typing

import CustomPackage

class CustomClassWithValidator(CustomPackage.CustomClass):
    @classmethod
    def __get_validators__(cls: typing.Type["CustomClassWithValidator"]) -> collections.abc.Iterable[collections.abc.Callable]:
        yield cls.validate_custom_class

    @classmethod
    def validate_custom_class(cls: typing.Type["CustomClassWithValidator"], passed_value: typing.Any) -> CustomPackage.CustomClass:
        if isinstance(passed_value, CustomPackage.CustomClass):
            return passed_value

        raise ValueError

在此之后,以下工作正常:

@pydantic.validate_arguments
def custom_function(custom_argument: CustomClassWithValidator) -> None:
    pass

但是我有相当多的第三方依赖项,每个依赖项都有很多我正在使用的自定义类。如上所述,为它们中的每一个创建几乎相同的类将起作用,但这似乎不是最佳的。pydantic中有没有什么功能可以减少重复性?

u2nhd7ah

u2nhd7ah1#

我不知道在版本1中有任何内置的Pydantic方法来做到这一点,除了将__get_validators__生成器函数添加到您已经找到的类型中。
我认为其中一个主要原因是,* 通常 * 您不仅希望字段类型被验证为正确的类型,而且还希望 * 解析 * 或序列化/反序列化,因为在大多数应用Pydantic的领域中,您要么从Python原语或内置类型的对象,甚至从JSON文本示例化Pydantic模型。
因此Pydantic不仅为最常见的类型提供了默认的验证器,而且还为这些类型提供了合理且灵活的初始化和(反)序列化函数。
这意味着,如果你想使用一些自定义/外来类型,* 你 * 负责告诉Pydantic如何示例化和验证它。

解决方法

选项A:简单混合

如果你想要的只是一个简单的“愚蠢”验证器,只是检查isinstance是否为任何给定类型,你可以重写你已经拥有的作为一个不可知的混合类,以最大限度地减少重复:

from collections.abc import Callable, Iterator
from typing import Any, Self

class ValidatorMixin:
    @classmethod
    def __get_validators__(cls) -> Iterator[Callable[..., Any]]:
        yield cls.__validate__

    @classmethod
    def __validate__(cls, v: object) -> Self:
        if isinstance(v, cls):
            return v
        raise TypeError(f"Not an instance of {cls.__name__}: {v}")

然后,你可以简单地从它和任何你想要的特定类一起继承:

from pydantic import validate_arguments

# ... import ValidatorMixin

class Bar:  # from foo
    ...

class Eggs:  # from spam
    ...

class MyBar(ValidatorMixin, Bar):
    pass

class MyEggs(ValidatorMixin, Eggs):
    pass

@validate_arguments
def f(bar: MyBar, eggs: MyEggs) -> None:
    print(bar, eggs)

现在的问题是,你的参数现在必须是MyBarMyEggs类型,而不能只是BarEggs类型。

选项B:泛型混入

为了允许指定要针对哪个类进行验证,同时将重复保持在最低限度,我们需要发挥创造性。我建议的一种方法是在检查类方面使ValidatorMixin通用。你可以做一些魔术,然后自动提取传递给它的类型参数并对其进行验证:(详情请看这里)

from collections.abc import Callable, Iterator
from typing import Any, Generic, TypeVar, get_args, get_origin

T = TypeVar("T")

class ValidatorMixin(Generic[T]):
    _type_arg: type[T] | None = None

    @classmethod
    def _get_type_arg(cls) -> type[T]:
        if cls._type_arg is not None:
            return cls._type_arg
        raise AttributeError(f"{cls.__name__} is generic; type arg unspecified")

    @classmethod
    def __init_subclass__(cls, **kwargs: Any) -> None:
        super().__init_subclass__(**kwargs)
        for base in cls.__orig_bases__:  # type: ignore[attr-defined]
            origin = get_origin(base)
            if origin is None or not issubclass(origin, ValidatorMixin):
                continue
            type_arg = get_args(base)[0]
            if not isinstance(type_arg, TypeVar):
                cls._type_arg = type_arg
                return

    @classmethod
    def __get_validators__(cls) -> Iterator[Callable[..., Any]]:
        yield cls.__validate__

    @classmethod
    def __validate__(cls, v: object) -> T:
        if isinstance(v, cls._get_type_arg()):
            return v
        raise TypeError(f"Not an instance of {cls.__name__}: {v}")

现在你可以这样使用它:

from pydantic import validate_arguments

# ... import ValidatorMixin

class Bar:  # from foo
    ...

class Eggs:  # from spam
    ...

class MyClass(ValidatorMixin[Eggs], Bar, Eggs):
    pass

@validate_arguments
def f(obj: MyClass) -> None:
    print(obj)

f(Eggs())

正如您所看到的,这现在还允许您使用多重继承,但仍然精确地指定要验证的对象。因此,最后一行中的f调用将通过验证。
然而,这仍然有一个缺陷。静态类型检查器会抱怨这个调用,因为Eggs不是MyClass的子类型(相反),所以这个调用会被视为错误。

选项C:对类本身进行Monkey-patch

如果这让你感到困扰,在我看来,唯一合理的替代方案就是对你想要使用的类进行猴子补丁,而不是子类化和混合。就像这样:

from collections.abc import Callable, Iterable, Iterator
from typing import Any, TypeVar

T = TypeVar("T")

def add_validation(
    cls: type[T],
    validators: Iterable[Callable[..., Any]] = (),
    force_patch: bool = False,
) -> type[T]:
    method_name = "__get_validators__"
    if not force_patch and hasattr(cls, method_name):
        raise AttributeError(f"{cls.__name__} already has `{method_name}`")
    if not validators:
        def __validate__(v: object) -> T:
            if isinstance(v, cls):
                return v
            raise TypeError(f"Not an instance of {cls.__name__}: {v}")
        validators = (__validate__, )

    def __get_validators__(_cls: type) -> Iterator[Callable[..., Any]]:
        yield from validators
    setattr(cls, method_name, classmethod(__get_validators__))
    return cls

现在,这既可以用作对某些第三方类进行monkey-patching的简单函数,也可以用作您自己的类的装饰器。
(You也可以通过可选的validators参数自由添加其他验证函数。)
用途:

from pydantic import validate_arguments

# ... import add_validation

class Bar:  # from foo
    ...

add_validation(Bar)

@add_validation
class SomeCustomClass:
    ...

@validate_arguments
def f(bar: Bar, obj: SomeCustomClass) -> None:
    print(bar, obj)

f(Bar(), SomeCustomClass())

如果你愿意,你可以做一个更复杂的装饰器版本,允许使用或不使用额外的参数:

from collections.abc import Callable, Iterable, Iterator
from typing import Any, TypeVar, overload

T = TypeVar("T")

@overload
def add_validation(
    cls: None = None,
    *,
    validators: Iterable[Callable[..., Any]] = (),
    force_patch: bool = False,
) -> Callable[[type[T]], type[T]]: ...

@overload
def add_validation(cls: type[T]) -> type[T]: ...

def add_validation(
    cls: type[T] | None = None,
    *,
    validators: Iterable[Callable[..., Any]] = (),
    force_patch: bool = False,
) -> type[T] | Callable[[type[T]], type[T]]:
    def decorator(cls_: type[T]) -> type[T]:
        nonlocal validators
        method_name = "__get_validators__"
        if not force_patch and hasattr(cls_, method_name):
            raise AttributeError(f"{cls_.__name__} already has `{method_name}`")
        if not validators:
            def __validate__(v: object) -> T:
                if isinstance(v, cls_):
                    return v
                raise TypeError(f"Not an instance of {cls_.__name__}: {v}")
            validators = (__validate__, )

        def __get_validators__(_cls: type) -> Iterator[Callable[..., Any]]:
            yield from validators
        setattr(cls_, method_name, classmethod(__get_validators__))
        return cls_
    return decorator if cls is None else decorator(cls)

相关问题