[英]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.