跳转到主要内容

可重用约束类型,用于与typing.Annotated一起使用

项目描述

annotated-types

CI pypi versions license

PEP-593typing.Annotated 添加为向现有类型添加上下文特定元数据的方式,并指定任何工具或库在不需要为 x 特殊逻辑的情况下应将 Annotated[T, x] 视为 T

此软件包提供了可以用于表示常见约束的元数据对象,例如标量值的上界和下界以及集合大小,一个用于运行时检查的 Predicate 标记,以及我们打算如何解释这些元数据的说明。在某些情况下,我们还注明了不需要此软件包的替代表示。

安装

pip install annotated-types

示例

from typing import Annotated
from annotated_types import Gt, Len, Predicate

class MyClass:
    age: Annotated[int, Gt(18)]                         # Valid: 19, 20, ...
                                                        # Invalid: 17, 18, "19", 19.0, ...
    factors: list[Annotated[int, Predicate(is_prime)]]  # Valid: 2, 3, 5, 7, 11, ...
                                                        # Invalid: 4, 8, -2, 5.0, "prime", ...

    my_list: Annotated[list[int], Len(0, 10)]           # Valid: [], [10, 20, 30, 40, 50]
                                                        # Invalid: (1, 2), ["abc"], [0] * 20

文档

尽管 annotated-types 为了性能避免运行时检查,用户不应构建无效的组合,例如 MultipleOf("non-numeric")Annotated[int, Len(3)]。如果使用以下描述的元数据对象与不兼容的类型,下游实现者可以选择引发错误、发出警告、静默忽略元数据项等——或者出于任何其他原因!

Gt, Ge, Lt, Le

表示可排序值的包含和/或非包含边界——这些值可以是数字、日期、时间、字符串、集合等。请注意,边界值不需要与注释的类型相同,只要它们可以进行比较:例如,Annotated[int, Gt(1.5)] 是可以的,它意味着值是一个整数 x,使得 x > 1.5

我们建议实现者可以将 functools.partial(operator.le, 1.5) 解释为与 Gt(1.5) 等效,以便希望避免对 annotated-types 包的运行时依赖的用户。

明确来说,这些类型具有以下含义

  • Gt(x) - 值必须大于 x - 等同于最小值
  • Ge(x) - 值必须大于或等于 x - 等同于最小值
  • Lt(x) - 值必须小于 x - 等同于最大值
  • Le(x) - 值必须小于或等于 x - 等同于最大值

区间

Interval(gt, ge, lt, le) 允许您使用单个元数据对象指定上下限。应忽略 None 属性,并按照上述单个边界处理非 None 属性。

MultipleOf

MultipleOf(multiple_of=x) 可以有两种解释方式

  1. Python 语义,意味着 value % multiple_of == 0,或者
  2. JSONschema 语义,其中 int(value / multiple_of) == value / multiple_of

我们鼓励用户了解这两种常见的解释及其不同的行为,尤其是对于非常大的或非整数数字,由于浮点数精度问题,很容易导致静默数据损坏。

我们鼓励库仔细记录它们实现了哪种解释。

MinLen, MaxLen, Len

Len() 表示 min_length <= len(value) <= max_length - 下限和上限都是包含的。

除了可选包含上下限的 Len() 之外,我们还提供了 MinLen(x)MaxLen(y),它们分别等同于 Len(min_length=x)Len(max_length=y)

LenMinLenMaxLen 可以与任何支持 len(value) 的类型一起使用。

使用示例

  • Annotated[list, MaxLen(10)](或 Annotated[list, Len(max_length=10)))- 列表长度必须为 10 或更短
  • Annotated[str, MaxLen(10)] - 字符串长度必须为 10 或更短
  • Annotated[list, MinLen(3))(或 Annotated[list, Len(min_length=3)))- 列表长度必须为 3 或更多
  • Annotated[list, Len(4, 6)] - 列表长度必须为 4、5 或 6
  • Annotated[list, Len(8, 8)] - 列表长度必须正好为 8

在 v0.4.0 中更改

  • min_inclusive 已重命名为 min_length,含义未变
  • max_exclusive 已重命名为 max_length,上界现在为 包含的 而不是 排除的
  • 有关切片的解释为 Len 的建议已被删除,因为存在歧义,并且切片与 Len 的上界语义不同

有关讨论,请参阅 问题 #23

时区

时区 可以与 datetimetime 一起使用,以表示允许哪些时区。 Annotated[datetime, Timezone(None)] 必须是 naive datetime。 Timezone[...] (文字省略号) 表示允许任何具有时区感知的 datetime。您还可以传递特定的时区字符串或 tzinfo 对象,如 Timezone(timezone.utc)Timezone("Africa/Abidjan"),以表示您只允许特定的时区,尽管我们注意到这通常是脆弱设计的一个症状。

在 v0.x.x 中更改

  • Timezone 接受 tzinfo 对象,而不是 timezone,从而扩展了兼容性到 zoneinfo 和第三方库。

单位

Unit(unit: str) 表示注释的数值是具有指定单位的量的幅度。例如,Annotated[float, Unit("m/s")] 将是一个表示每秒米数的速度的浮点数。

请注意,annotated_types 本身不尝试以任何方式解析或验证单位字符串。这完全留给下游库,例如 pintastropy.units

一个库如何使用此元数据的示例

from annotated_types import Unit
from typing import Annotated, TypeVar, Callable, Any, get_origin, get_args

# given a type annotated with a unit:
Meters = Annotated[float, Unit("m")]


# you can cast the annotation to a specific unit type with any
# callable that accepts a string and returns the desired type
T = TypeVar("T")
def cast_unit(tp: Any, unit_cls: Callable[[str], T]) -> T | None:
    if get_origin(tp) is Annotated:
        for arg in get_args(tp):
            if isinstance(arg, Unit):
                return unit_cls(arg.unit)
    return None


# using `pint`
import pint
pint_unit = cast_unit(Meters, pint.Unit)


# using `astropy.units`
import astropy.units as u
astropy_unit = cast_unit(Meters, u.Unit)

谓词

Predicate(func: Callable) 表示 func(value) 对于有效值来说是 truthy。用户应优先考虑上述静态可检查的元数据,但如果您需要任意运行时谓词的完整功能和灵活性……这里就是。

对于一些常见的约束,我们提供通用类型

  • IsLower = Annotated[T, Predicate(str.islower)]
  • IsUpper = Annotated[T, Predicate(str.isupper)]
  • IsDigit = Annotated[T, Predicate(str.isdigit)]
  • IsFinite = Annotated[T, Predicate(math.isfinite)]
  • IsNotFinite = Annotated[T, Predicate(Not(math.isfinite))]
  • IsNan = Annotated[T, Predicate(math.isnan)]
  • IsNotNan = Annotated[T, Predicate(Not(math.isnan))]
  • IsInfinite = Annotated[T, Predicate(math.isinf)]
  • IsNotInfinite = Annotated[T, Predicate(Not(math.isinf))]

因此,您可以用 x: IsFinite[float] = 2.0 来编写,而不是更长的(但完全等价)的 x: Annotated[float, Predicate(math.isfinite)] = 2.0

一些库可能有特殊的逻辑来处理已知或可理解的谓词,例如通过检查 str.isdigit 并使用其存在来调用自定义逻辑以强制执行仅包含数字的字符串,并自定义一些生成的外部模式。因此,鼓励用户避免使用 lambda s: s.lower() 这样的间接方法,而应使用可内省的方法,如 str.lowerre.compile("pattern").search

为了在不引入使实现者无法内省谓词的 introspection 的情况下启用对常用谓词(如 math.isnan)的基本否定,我们提供了一个 Not 包装器,该包装器简单地以内省方式否定谓词。上述列出的许多谓词都是以这种方式创建的。

我们没有指定应期望抛出异常的谓词的行为。例如,Annotated[int, Predicate(str.isdigit)] 可能会静默跳过无效约束,或静态引发错误;或者它可能尝试调用它,然后传播或丢弃产生的 TypeError: descriptor 'isdigit' for 'str' objects doesn't apply to a 'int' object 异常。我们鼓励库记录它们选择的行为。

文档

doc() 可以用于在 Annotated 中添加文档信息,用于函数和方法参数、变量、类属性、返回类型以及 Annotated 可以使用的任何地方。

它期望一个可以静态分析的值,因为主要用例是静态分析、编辑器、文档生成器等工具。

它返回一个具有单个属性 documentationDocInfo 类,其中包含传递给 doc() 的值。

这是早期采用者对 typing-doc 提案 的替代形式。

将下游类型与 GroupedMetadata 集成

实现者可以选择提供一种方便的包装器,将多个元数据分组。这可以帮助减少用户在认知上的负担。例如,像 Pydantic 这样的实现者可能会提供一个接受关键字参数并将它们转换为低级元数据的 FieldMeta 类型。

from dataclasses import dataclass
from typing import Iterator
from annotated_types import GroupedMetadata, Ge

@dataclass
class Field(GroupedMetadata):
    ge: int | None = None
    description: str | None = None

    def __iter__(self) -> Iterator[object]:
        # Iterating over a GroupedMetadata object should yield annotated-types
        # constraint metadata objects which describe it as fully as possible,
        # and may include other unknown objects too.
        if self.ge is not None:
            yield Ge(self.ge)
        if self.description is not None:
            yield Description(self.description)

消耗注解类型约束的库应检查 GroupedMetadata 并通过遍历对象将其展开,就像它们在 Annotated 类型中已被“展开”一样。相同的逻辑应应用于 PEP 646 Unpack 类型,以便 Annotated[T, Field(...)]Annotated[T, Unpack[Field(...)]]Annotated[T, *Field(...)] 都被一致对待。

消耗注解类型的库还应忽略来自展开 GroupedMetadata 的任何它们不认识的元数据,就像它们在 Annotated 本身中忽略不认识的元数据一样。

我们自己的 annotated_types.Interval 类是一个 GroupedMetadata,它将自己展开到 GtLt 等,因此这不是一个抽象问题。同样,annotated_types.Len 是一个 GroupedMetadata,它将自己展开到 MinLen(可选)和 MaxLen

消耗元数据

我们打算不对元数据和约束的 如何 使用进行规定,而是提供一个示例,说明如何从类型注解中解析约束,请参阅我们在 test_main.py 中的实现。

实现者需要确定如何使用这些元数据。您可以使用这些元数据用于运行时类型检查、生成模式或生成示例数据,以及其他用例。

设计与历史

这个包是由 Pydantic 和 Hypothesis 的维护者在 PyCon 2022 的冲刺中设计的,旨在让最终用户尽可能容易地为运行时库提供更多信息的注解。

它是故意最小化的,遵循 PEP-593 允许下游有相当大的自主权来决定他们选择支持什么(如果有的话)。尽管如此,我们预计保持简单并仅涵盖最常见用例将使用户和维护者获得最佳体验。如果您想为您的类型获得更多约束,请遵循我们的做法,通过定义和记录它们来实现!

项目详情


下载文件

下载适用于您的平台的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。

源分发

annotated_types-0.7.0.tar.gz (16.1 kB 查看散列)

上传时间

构建分发

annotated_types-0.7.0-py3-none-any.whl (13.6 kB 查看散列)

上传时间 Python 3

由以下机构支持