类型注释,用于在Python中使用Pydantic将JSON文件中的字符串解析为范围



我已经设置了一个Pydantic类,用于解析JSON文件。range属性是从形式为"11-34"的字符串(或者更准确地说是从所示的正则表达式)解析的:

RANGE_STRING_REGEX = r"^(?P<first>[1-6]+)(-(?P<last>[1-6]+))?$"
class RandomTableEvent(BaseModel):
name: str
range: Annotated[str, Field(regex=RANGE_STRING_REGEX)]

@validator("range", allow_reuse=True)
def convert_range_string_to_range(cls, r) -> "range":
match_groups = re.fullmatch(RANGE_STRING_REGEX, r).groupdict()
first = int(match_groups["first"])
last = int(match_groups["last"]) if match_groups["last"] else first
return range(first, last + 1)

生成的模式工作并且验证通过。

然而,严格来说,类中range属性的类型注释是不正确的,因为range属性在验证器函数中从字符串(类型注释)转换为range对象。

注释这个的正确方法是什么,并且仍然保持模式生成?是否有另一种方法来处理这种隐式类型转换(例如,字符串在Pydantic中自动转换为int -是否有类似的自定义类型)?

range不是pydantic支持的类型,使用它作为字段的类型会在尝试创建JSON模式时导致错误,但pydantic支持自定义数据类型:

您还可以定义自己的自定义数据类型。有几种方法可以达到这个目的。

带有get_validators的类

您使用具有类方法__get_validators__的自定义类。它将被调用以获得验证器来解析和验证输入数据。

但是这个自定义数据类型不能从range继承,因为它是final。因此,您可以创建一个自定义数据类型,在内部使用range并公开范围方法:它将像range一样工作,但它不会是range(isinstance(..., range)将是False)。

同样的pydantic文档展示了如何使用__modify_schema__方法自定义自定义数据类型的JSON模式。

完整的示例:

import re
from typing import Any, Callable, Dict, Iterator, SupportsIndex, Union
from pydantic import BaseModel

class Range:
_RANGE_STRING_REGEX = r"^(?P<first>[1-6]+)(-(?P<last>[1-6]+))?$"
@classmethod
def __get_validators__(cls) -> Iterator[Callable[[Any], Any]]:
yield cls.validate
@classmethod
def validate(cls, v: Any) -> "Range":
if not isinstance(v, str):
raise ValueError("expected string")
match = re.fullmatch(cls._RANGE_STRING_REGEX, v)
if not match:
raise ValueError("invalid string")
match_groups = match.groupdict()
first = int(match_groups["first"])
last = int(match_groups["last"]) if match_groups["last"] else first
return cls(range(first, last + 1))
def __init__(self, r: range) -> None:
self._range = r
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
# Customize the JSON schema as you want
field_schema["pattern"] = cls._RANGE_STRING_REGEX
field_schema["type"] = "string"
# Implement the range methods and use self._range
@property
def start(self) -> int:
return self._range.start
@property
def stop(self) -> int:
return self._range.stop
@property
def step(self) -> int:
return self._range.step
def count(self, value: int) -> int:
return self._range.count(value)
def index(self, value: int) -> int:
return self._range.index(value)
def __len__(self) -> int:
return self._range.__len__()
def __contains__(self, o: object) -> bool:
return self._range.__contains__(o)
def __iter__(self) -> Iterator[int]:
return self._range.__iter__()
def __getitem__(self, key: Union[SupportsIndex, slice]) -> int:
return self._range.__getitem__(key)
def __reversed__(self) -> Iterator[int]:
return self._range.__reversed__()
def __repr__(self) -> str:
return self._range.__repr__()

class RandomTableEvent(BaseModel):
name: str
range: Range

event = RandomTableEvent(name="foo", range="11-34")
print("event:", event)
print("event.range:", event.range)
print("schema:", event.schema_json(indent=2))
print("is instance of range:", isinstance(event.range, range))
print("event.range.start:", event.range.start)
print("event.range.stop:", event.range.stop)
print("event.range[0:5]", event.range[0:5])
print("last 3 elements:", list(event.range[-3:]))
输出:

event: name='foo' range=range(11, 35)
event.range: range(11, 35)
schema: {
"title": "RandomTableEvent",
"type": "object",
"properties": {
"name": {
"title": "Name",
"type": "string"
},
"range": {
"title": "Range",
"pattern": "^(?P<first>[1-6]+)(-(?P<last>[1-6]+))?$",
"type": "string"
}
},
"required": [
"name",
"range"
]
}
is instance of range: False
event.range.start: 11
event.range.stop: 35
event.range[0:5] range(11, 16)
last 3 elements: [32, 33, 34]

最新更新