pydantic constr 和 mypy 检查之间的冲突

Cri*_*ujo 6 python mypy pydantic

我正在使用 pydantic 来验证 Json/Dict 输入。但我还使用 mypy 来验证代码的类型完整性。

当使用该pydantic.constr类型(其中包括验证给定字符串是否遵循正则表达式)时,我收到 mypy 错误。

这是代码:

from typing import List

import pydantic

Regex = pydantic.constr(regex="[0-9a-z_]*")


class Data(pydantic.BaseModel):
    regex: List[Regex]


data = Data(**{"regex":["abc", "123", "etc"]})
print(data, data.json())
Run Code Online (Sandbox Code Playgroud)

这是 mypy 的输出:

$ mypy main.py 
main.py:9: error: Variable "main.Regex" is not valid as a type
main.py:9: note: See https://mypy.readthedocs.io/en/latest/common_issues.html#variables-vs-type-aliases
Run Code Online (Sandbox Code Playgroud)

我检查了文档,但找不到处理此问题的方法。我知道我可以为该正则表达式创建一个静态类型,但这违背了 pydantic 的目的。我能通过的唯一方法是使用一个# type: ignore远非理想的方法。

那么有没有一种方法可以同时具有 pydantic 和 mypy 的优点来处理这个问题呢?

mih*_*ihi 6

有几种方法可以实现这一目标:

继承自pydantic.ConstrainedStr

您可以直接继承,而不是使用constr指定正则表达式约束(内部使用):pydantic.ConstrainedStrpydantic.ConstrainedStr

import re
import pydantic
from pydantic import Field
from typing import List

class Regex(pydantic.ConstrainedStr):
    regex = re.compile("^[0-9a-z_]*$")

class Data(pydantic.BaseModel):
    regex: List[Regex]

data = Data(**{"regex": ["abc", "123", "asdf"]})
print(data)
# regex=['abc', '123', 'asdf']
print(data.json())
# {"regex": ["abc", "123", "asdf"]}
Run Code Online (Sandbox Code Playgroud)

Mypy 愉快地接受了这一点,并且 pydantic 进行了正确的验证。的类型data.regex[i]Regex,但由于pydantic.ConstrainedStr它本身继承自str,所以它在大多数地方都可以用作字符串。

使用pydantic.Field

正则表达式约束也可以指定为 的参数Field

import pydantic
from pydantic import Field
from typing import List

class Regex(pydantic.BaseModel):
    __root__: str = Field(regex="^[0-9a-z_]*$")

class Data(pydantic.BaseModel):
    regex: List[Regex]

data = Data(**{"regex": ["abc", "123", "asdf"]})
print(data)
# regex=[Regex(__root__='abc'), Regex(__root__='123'), Regex(__root__='asdf')]
print(data.json())
# {"regex": ["abc", "123", "asdf"]}
Run Code Online (Sandbox Code Playgroud)

因为Regex不直接用作 pydantic 模型中的字段(而是作为示例中列表中的条目),所以我们需要强制引入模型。__root__使Regex模型在验证和序列化时充当其单个字段(更多详细信息请参见此处)。

但它有一个缺点: 的类型data.regex[i]又是Regex,但这次不是继承自str。这会导致例如foo: str = data.regex[0]不进行类型检查。foo: str = data.regex[0].__root__必须用它来代替。

我仍然在这里提到这一点,因为当约束直接应用于字段而不是列表条目时,它可能是最简单的解决方案(并且typing.Annotated不可用,请参见下文)。例如像这样:

class DataNotList(pydantic.BaseModel):
    regex: str = Field(regex="^[0-9a-z_]*$")
Run Code Online (Sandbox Code Playgroud)

typing.Annotated与使用pydantic.Field

constr您可以将其指定为 的参数,Field然后与 结合使用,而不是使用 来指定正则表达式约束typing.Annotated

import pydantic
from pydantic import Field
from typing import Annotated

Regex = Annotated[str, Field(regex="^[0-9a-z_]*$")]

class DataNotList(pydantic.BaseModel):
    regex: Regex

data = DataNotList(**{"regex": "abc"})
print(data)
# regex='abc'
print(data.json())
# {"regex": "abc"}
Run Code Online (Sandbox Code Playgroud)

Mypy 将其视为Annotated[str, Field(regex="^[0-9a-z_]*$")]的类型别名str。但它也告诉 pydantic 进行验证。这在 pydantic 文档中有描述

不幸的是,它目前不适用于以下情况:

class Data(pydantic.BaseModel):
    regex: List[Regex]
Run Code Online (Sandbox Code Playgroud)

验证根本无法运行。这是一个未解决的错误(github问题)。一旦错误被修复,这可能是最好的解决方案。

请注意,typing.Annotated仅自 Python 3.9 起可用。对于较旧的 Python 版本,typing_extensions.Annotated可以使用。


附带说明:我使用的是正规表达式^[0-9a-z_]*$而不是,因为后者会接受任何有效的字符串,就像pydantic 用于验证一样。[0-9a-z_]*re.match