在运行时验证 Python TypedDict

问题描述

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

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

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

class PersonData(TypedDict):
    first: str
    last: str
    email: str

然后运行如下代码

person_dict: PersonData = PersonSerializer(Person.objects.first()).data

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

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

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

class PersonData(TypedDict):
    email: Optional[str]
    affiliations: Union[List[str],Dict[int,str]]

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

我想知道的是,是否已经存在一个调用函数/方法(在 mypy 或其他检查器中)可以让我验证 TypedDict(甚至是单个变量,因为我可以自己迭代一个 dict)注释并查看它是否有效?

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

解决方法

有点小技巧,但您可以使用 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
,

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

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}")

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

,

您可能想看看https://pypi.org/project/strongtyping/。这可能会有所帮助。

在文档中你可以找到这个例子:

from typing import List,TypedDict

from strongtyping.strong_typing import match_class_typing


@match_class_typing
class SalesSummary(TypedDict):
    sales: int
    country: str
    product_codes: List[str]

# works like expected
SalesSummary({"sales": 10,"country": "Foo","product_codes": ["1","2","3"]})

# will raise a TypeMisMatch
SalesSummary({"sales": "Foo","country": 10,"product_codes": [1,2,3]})