簡體   English   中英

在運行時驗證 Python TypedDict

[英]Validate Python TypedDict at runtime

我在 Python 3.8+ Django/Rest-Framework 環境中工作,在新代碼中強制執行類型,但基於大量無類型的遺留代碼和數據。 我們廣泛使用 TypedDicts 來確保我們生成的數據以正確的數據類型傳遞到我們的 TypeScript 前端。

MyPy/PyCharm/等。 在檢查我們的新代碼是否吐出符合要求的數據方面做得很好,但我們想測試我們許多 RestSerializers/ModelSerializers 的 output 是否適合 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

Static 類型檢查器無法確定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])

它會發現序列化器的數據中缺少email 在這種情況下,這很好,我沒有from __future__ import annotations引入的任何更改(不確定這是否會破壞它),並且我所有的類型注釋都是裸類型。 但是如果PersonData被定義為:

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

那么isinstance不足以檢查數據是否通過(因為“下標 generics 不能與 class 和實例檢查一起使用”)。

我想知道是否已經存在一個可調用的函數/方法(在 mypy 或其他檢查器中),它允許我針對注釋驗證 TypedDict (甚至是單個變量,因為我可以自己迭代一個 dict)並查看如果它驗證?

我不關心速度等,因為這樣做的目的是檢查我們所有的數據/方法/函數一次,然后在我們對當前數據驗證感到高興時刪除檢查。

有點小技巧,但您可以使用 mypy 命令行-c選項檢查兩種類型。 只需將其包裝在 python function 中:

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

你可以這樣做:

def validate(typ: Any, instance: Any) -> bool:
    for property_name, property_type in typ.__annotations__.items():
        value = instance.get(property_name, None)
        if value is None:
            # Check for missing keys
            print(f"Missing key: {property_name}")
            return False
        elif property_type not in (int, float, bool, str):
            # check if property_type is object (e.g. not a primitive)
            result = validate(property_type, value)
            if result is False:
                return False
        elif not isinstance(value, property_type):
            # Check for type equality
            print(f"Wrong type: {property_name}. Expected {property_type}, got {type(value)}")
            return False
    return True

然后測試一些 object,例如傳遞給您的 REST 端點的一個:

class MySubModel(TypedDict):
    subfield: bool


class MyModel(TypedDict):
    first: str
    last: str
    email: str
    sub: MySubModel

m = {
    'email': 'JohnDoeAtDoeishDotCom',
    'first': 'John'
}
assert validate(MyModel, m) is False

這個打印第一個錯誤並返回 bool,您可以將其更改為異常,可能包含所有缺少的鍵。 您還可以將其擴展為在 model 定義的其他鍵上失敗。

我喜歡您的解決方案。,為了避免某些用戶的迭代修復:我在您的解決方案中添加了一些代碼:D

def validate_custom_typed_dict(instance: Any, custom_typed_dict:TypedDict) -> bool|Exception:
    key_errors = []
    type_errors = []
    for property_name, type_ in my_typed_dict.__annotations__.items():
        value = instance.get(property_name, None)
        if value is None:
            # Check for missing keys
            key_errors.append(f"\t- Missing property: '{property_name}' \n")
        elif type_ not in (int, float, bool, str):
            # check if type is object (e.g. not a primitive)
            result = validate_custom_typed_dict(type_, value)
            if result is False:
                type_errors.append(f"\t- '{property_name}' expected {type_}, got {type(value)}\n")
        elif not isinstance(value, type_):
            # Check for type equality
            type_errors.append(f"\t- '{property_name}' expected {type_}, got {type(value)}\n")

    if len(key_errors) > 0 or len(type_errors) > 0:
        error_message = f'\n{"".join(key_errors)}{"".join(type_errors)}'
        raise Exception(error_message)
    
    return True

一些控制台 output:異常:-缺少屬性:'Combined_cycle'-缺少屬性:'Solar_PV'-缺少屬性:'Hydro'-'timestamp'預期 <class 'str'>,得到 <class 'int'> -'Diesel_engines'預期 <class 'float'>,得到 <class 'int'>

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM