在运行时验证 Python TypedDict

Mic*_*ert 21 django-rest-framework mypy python-typing typeddict

我正在 Python 3.8+ Django/Rest-Framework 环境中工作,在新代码中强制执行类型,但建立在许多无类型的遗留代码和数据之上。我们广泛使用 TypedDicts 来确保我们生成的数据以正确的数据类型传递到 TypeScript 前端。

MyPy/PyCharm/等。在检查我们的新代码是否输出符合要求的数据方面做得很好,但我们想测试我们的许多 RestSerializers/ModelSerializers 的输出是否符合 TypeDict。如果我有一个序列化器并输入如下字典:

class PersonSerializer(ModelSerializer):
    class Meta:
        model = Person
        fields = ['first', 'last']

class PersonData(TypedDict):
    first: str
    last: str
    email: str
Run Code Online (Sandbox Code Playgroud)

然后运行如下代码:

person_dict: PersonData = PersonSerializer(Person.objects.first()).data
Run Code Online (Sandbox Code Playgroud)

静态类型检查器无法发现person_dict缺少所需email密钥,因为(根据 PEP-589 的设计)它只是一个普通的dict. 但我可以写这样的东西:

annotations = PersonData.__annotations__
for k in annotations:
    assert k in person_dict  # or something more complex.
    assert isinstance(person_dict[k], annotations[k])
Run Code Online (Sandbox Code Playgroud)

它会发现email序列化器的数据丢失了。在这种情况下,这很好,我没有引入任何更改from __future__ import annotations(不确定这是否会破坏它),并且我的所有类型注释都是裸类型。但如果PersonData定义如下:

class PersonData(TypedDict):
    email: Optional[str]
    affiliations: Union[List[str], Dict[int, str]]
Run Code Online (Sandbox Code Playgroud)

thenisinstance不足以检查数据是否通过(因为“下标泛型不能与类和实例检查一起使用”)。

我想知道是否已经存在一个可调用函数/方法(在 mypy 或另一个检查器中),它允许我根据注释验证 TypedDict (甚至单个变量,因为我可以自己迭代字典)并查看如果它有效?

我不关心速度等,因为这样做的目的是检查一次所有数据/方法/函数,然后在我们对当前数据验证感到满意后删除检查。

小智 11

我发现最简单的解决方案可以使用 pydantic。

pydantic v2 的解决方案

import pydantic

from pydantic import TypeAdapter, ValidationError
from typing_extensions import TypedDict # Required by pydantic for python < 3.12

class SomeDict(TypedDict):
    val: int
    name: str
    
SomeDictValidator = TypeAdapter(SomeDict)

# this could be a valid/invalid declaration
obj: SomeDict = {
    'val': 12,
    'name': 'John',
}

# validate with pydantic
try:
    obj = SomeDictValidator.validate_python(obj)
except ValidationError as exc: 
    print(f"ERROR: Invalid schema: {exc}")
Run Code Online (Sandbox Code Playgroud)

请参阅TypeAdapter文档以获取更多信息。

pydantic v1 的解决方案

from typing import cast, TypedDict 

import pydantic

class SomeDict(TypedDict):
    val: int
    name: str

# this could be a valid/invalid declaration
obj: SomeDict = {
    'val': 12,
    'name': 'John',
}

# validate with pydantic
try:
    obj = cast(SomeDict, pydantic.create_model_from_typeddict(SomeDict)(**obj).dict())
except pydantic.ValidationError as exc: 
    print(f"ERROR: Invalid schema: {exc}")
Run Code Online (Sandbox Code Playgroud)

编辑:当类型检查时,它当前返回一个错误,但按预期工作。请参阅此处: https: //github.com/samuelcolvin/pydantic/issues/3008

  • 你有这个与嵌套 TypedDicts 一起使用吗?也就是说,假设 name 的定义类似于“name: SomeOtherDict”,其中 SomeOtherDict 也是 TypedDict? (2认同)

小智 1

有点黑客,但您可以使用 mypy 命令行-c选项检查两种类型。只需将其包装在 python 函数中即可:

import subprocess

def is_assignable(type_to, type_from) -> bool:
    """
    Returns true if `type_from` can be assigned to `type_to`,
    e. g. type_to := type_from

    Example:
    >>> is_assignable(bool, str) 
    False
    >>> from typing import *
    >>> is_assignable(Union[List[str], Dict[int, str]], List[str])
    True
    """
    code = "\n".join((
        f"import typing",
        f"type_to: {type_to}",
        f"type_from: {type_from}",
        f"type_to = type_from",
    ))
    return subprocess.call(("mypy", "-c", code)) == 0
Run Code Online (Sandbox Code Playgroud)