跳转到主要内容

Python模式匹配

项目描述

高性能Python模式匹配和对象验证

用于Python的可复用模式匹配,由Cython实现。我最初为Ibis项目开发了这个系统,但希望它对其他人也有用。

实现旨在尽可能快,纯Python实现已经相当快,但利用Cython可以减轻Python解释器的开销。我也尝试了PyO3,但它的开销比Cython大。当前的实现使用Cython的纯Python模式,允许快速迭代和测试,然后可以将其Cython化并编译为扩展模块,从而显著提高速度。基准测试显示,与用Rust编写的pydantic模型验证相比,速度提高了2倍以上。

安装

该软件包已发布到PyPI,因此可以使用pip安装

pip install koerce

库组件

该库包含三个主要组件,可以独立使用或一起使用

1. 延迟对象构建器

这些允许延迟评估给定上下文中的Python表达式

In [1]: from koerce import var, resolve

In [2]: a, b = var("a"), var("b")

In [3]: expr = (a + 1) * b["field"]

In [4]: expr
Out[4]: (($a + 1) * $b['field'])

In [5]: resolve(expr, {"a": 2, "b": {"field": 3}})
Out[5]: 9

延迟对象提供的语法糖允许以简洁和自然的方式定义复杂对象转换。

2. 操作各种Python对象的模式匹配器

模式是该库的核心,它们允许在Python对象中搜索和替换特定的结构。该库提供了一种可扩展且简单的方法来定义模式和将值与其匹配。

In [1]: from koerce import match, NoMatch, Anything

In [2]: context = {}

In [3]: match([1, 2, 3, int, "a" @ Anything()], [1, 2, 3, 4, 5], context)
Out[3]: [1, 2, 3, 4, 5]

In [4]: context
Out[4]: {'a': 5}

请注意,可以使用from koerce import koerce函数代替match(),以避免与内置Python match混淆。

from dataclasses import dataclass
from koerce import Object, match

@dataclass
class B:
    x: int
    y: int
    z: float

match(Object(B, y=1, z=2), B(1, 1, 2))
# B(x=1, y=1, z=2)

其中,Object模式检查传递的对象是否是B的实例,以及value.y == 1value.z == 2(忽略x字段)。

模式还可以捕获值作为变量,使匹配过程更加灵活。

from koerce import var

x = var("x")

# `+x` means to capture that object argument as variable `x`
# then the `z` argument must match that captured value
match(Object(B, +x, z=x), B(1, 2, 1))
# it is a match because x and z are equal: B(x=1, y=2, z=1)

match(Object(B, +x, z=x), B(1, 2, 0))
# is is a NoMatch because x and z are unequal

模式也适用于匹配和替换任务,因为它们可以生成新的值。

# >> operator constructs a `Replace` pattern where the right
# hand side is a deferred object
match(Object(B, +x, z=x) >> (x, x + 1), B(1, 2, 1))
# result: (1, 2)

模式还可以组合使用,可以通过重载运算符自由组合。

In [1]: from koerce import match, Is, Eq, NoMatch

In [2]: pattern = Is(int) | Is(str)
   ...: assert match(pattern, 1) == 1
   ...: assert match(pattern, "1") == "1"
   ...: assert match(pattern, 3.14) is NoMatch

In [3]: pattern = Is(int) | Eq(1)
   ...: assert match(pattern, 1) == 1
   ...: assert match(pattern, None) is NoMatch

模式也可以从Python类型提示中构建。

In [1]: from koerce import match

In [2]: class Ordinary:
   ...:     def __init__(self, x, y):
   ...:         self.x = x
   ...:         self.y = y
   ...:
   ...:
   ...: class Coercible(Ordinary):
   ...:
   ...:     @classmethod
   ...:     def __coerce__(cls, value):
   ...:         if isinstance(value, tuple):
   ...:             return Coercible(value[0], value[1])
   ...:         else:
   ...:             raise ValueError("Cannot coerce value to Coercible")
   ...:

In [3]: match(Ordinary, Ordinary(1, 2))
Out[3]: <__main__.Ordinary at 0x105194fe0>

In [4]: match(Ordinary, (1, 2))
Out[4]: koerce.patterns.NoMatch

In [5]: match(Coercible, (1, 2))
Out[5]: <__main__.Coercible at 0x109ebb320>

模式创建逻辑还通过轻量级类型参数推理处理泛型类型。实现相当紧凑,可在Pattern.from_typehint()下使用。

3. 数据类对象的高级别验证系统

这种抽象类似于attrs或pydantic提供的内容,但也有一些区别(待列出)。

In [1]: from typing import Optional
   ...: from koerce import Annotable
   ...:
   ...:
   ...: class MyClass(Annotable):
   ...:     x: int
   ...:     y: float
   ...:     z: Optional[list[str]] = None
   ...:

In [2]: MyClass(1, 2.0, ["a", "b"])
Out[2]: MyClass(x=1, y=2.0, z=['a', 'b'])

In [3]: MyClass(1, 2, ["a", "b"])
Out[3]: MyClass(x=1, y=2.0, z=['a', 'b'])

In [4]: MyClass("invalid", 2, ["a", "b"])
Out[4]: # raises validation error

可注解对象默认是可变的,但可以通过将immutable=True传递给Annotable基类来使其不可变。通常,将不可变对象也变为可散列也是很有用的,这可以通过将hashable=True传递给Annotable基类来实现,在这种情况下,散列值在初始化期间预先计算并存储在对象中,这使得字典查找变得廉价。

In [1]: from typing import Optional
   ...: from koerce import Annotable
   ...:
   ...:
   ...: class MyClass(Annotable, immutable=True, hashable=True):
   ...:     x: int
   ...:     y: float
   ...:     z: Optional[tuple[str, ...]] = None
   ...:

In [2]: a = MyClass(1, 2.0, ["a", "b"])

In [3]: a
Out[3]: MyClass(x=1, y=2.0, z=('a', 'b'))

In [4]: a.x = 2
AttributeError: Attribute 'x' cannot be assigned to immutable instance of type <class '__main__.MyClass'>

In [5]: {a: 1}
Out[5]: {MyClass(x=1, y=2.0, z=('a', 'b')): 1}

可用的模式匹配器

这是一个不完整的匹配器列表,更多细节和示例请参阅koerce/patterns.pykoerce/tests/test_patterns.py

AnythingNothing

In [1]: from koerce import match, Anything, Nothing

In [2]: match(Anything(), "a")
Out[2]: 'a'

In [3]: match(Anything(), 1)
Out[3]: 1

In [4]: match(Nothing(), 1)
Out[4]: koerce._internal.NoMatch

Eq 用于相等匹配

In [1]: from koerce import Eq, match, var

In [2]: x = var("x")

In [3]: match(Eq(1), 1)
Out[3]: 1

In [4]: match(Eq(1), 2)
Out[4]: koerce._internal.NoMatch

In [5]: match(Eq(x), 2, context={"x": 2})
Out[5]: 2

In [6]: match(Eq(x), 2, context={"x": 3})
Out[6]: koerce._internal.NoMatch

Is 用于实例匹配

以下是一些简单的示例

In [1]: from koerce import match, Is

In [2]: class A: pass

In [3]: match(Is(A), A())
Out[3]: <__main__.A at 0x1061070e0>

In [4]: match(Is(A), "A")
Out[4]: koerce._internal.NoMatch

In [5]: match(Is(int), 1)
Out[5]: 1

In [6]: match(Is(int), 3.14)
Out[6]: koerce._internal.NoMatch

In [7]: from typing import Optional

In [8]: match(Is(Optional[int]), 1)
Out[8]: 1

In [9]: match(Is(Optional[int]), None)

泛型类型也通过检查属性/属性的类型得到支持

from koerce import match, Is, NoMatch
from typing import Generic, TypeVar, Any
from dataclasses import dataclass


T = TypeVar("T", covariant=True)
S = TypeVar("S", covariant=True)

@dataclass
class My(Generic[T, S]):
    a: T
    b: S
    c: str


MyAlias = My[T, str]

b_int = My(1, 2, "3")
b_float = My(1, 2.0, "3")
b_str = My("1", "2", "3")

# b_int.a must be an instance of int
# b_int.b must be an instance of Any
assert match(My[int, Any], b_int) is b_int

# both b_int.a and b_int.b must be an instance of int
assert match(My[int, int], b_int) is b_int

# b_int.b should be an instance of a float but it isn't
assert match(My[int, float], b_int) is NoMatch

# now b_float.b is actually a float so it is a match
assert match(My[int, float], b_float) is b_float

# type aliases are also supported
assert match(MyAlias[str], b_str) is b_str

As 模式尝试将值强制转换为给定的类型

from koerce import match, As, NoMatch
from typing import Generic, TypeVar, Any
from dataclasses import dataclass

class MyClass:
    pass

class MyInt(int):
    @classmethod
    def __coerce__(cls, other):
        return MyInt(int(other))


class MyNumber(Generic[T]):
    value: T

    def __init__(self, value):
        self.value = value

    @classmethod
    def __coerce__(cls, other, T):
        return cls(T(other))


assert match(As(int), 1.0) == 1
assert match(As(str), 1.0) == "1.0"
assert match(As(float), 1.0) == 1.0
assert match(As(MyClass), "myclass") is NoMatch

# by implementing the coercible protocol objects can be transparently
# coerced to the given type
assert match(As(MyInt), 3.14) == MyInt(3)

# coercible protocol also supports generic types where the `__coerce__`
# method should be implemented on one of the base classes and the
# type parameters are passed as keyword arguments to `cls.__coerce__()`
assert match(As(MyNumber[float]), 8).value == 8.0

AsIs 可以省略,因为 match() 尝试使用 koerce.pattern() 函数将其第一个参数转换为模式

from koerce import pattern, As, Is

assert pattern(int, allow_coercion=False) == Is(int)
assert pattern(int, allow_coercion=True) == As(int)

assert match(int, 1, allow_coercion=False) == 1
assert match(int, 1.1, allow_coercion=False) is NoMatch
# lossy coercion is not allowed
assert match(int, 1.1, allow_coercion=True) is NoMatch

# default is allow_coercion=False
assert match(int, 1.1) is NoMatch

As[typehint]Is[typehint] 可以用于创建模式

from koerce import Pattern, As, Is

assert match(As[int], '1') == 1
assert match(Is[int], 1) == 1
assert match(Is[int], '1') is NoMatch

If 模式用于条件表达式

允许基于对象的值或其他上下文中的变量进行条件匹配

from koerce import match, If, Is, var, NoMatch, Capture

x = var("x")

pattern = Capture(x) & If(x > 0)
assert match(pattern, 1) == 1
assert match(pattern, -1) is NoMatch

Custom 用于用户定义的匹配逻辑

传递给 match()pattern() 的函数被视为 Custom 模式

from koerce import match, Custom, NoMatch, NoMatchError

def is_even(value):
    if value % 2:
        raise NoMatchError("Value is not even")
    else:
        return value

assert match(is_even, 2) == 2
assert match(is_even, 3) is NoMatch

Capture 用于在上下文中记录值

捕获模式可以以多种方式定义

from koerce import Capture, Is, var

x = var("x")

Capture("x")  # captures anything as "x" in the context
Capture(x)  # same as above but using a variable
Capture("x", Is(int))  # captures only integers as "x" in the context
Capture("x", Is(int) | Is(float))  # captures integers and floats as "x" in the context
"x" @ Is(int)  # syntax sugar for Capture("x", Is(int))
+x  # syntax sugar for Capture(x, Anything())
from koerce import match, Capture, var

# context is a mutable dictionary passed along the matching process
context = {}
assert match("x" @ Is(int), 1, context) == 1
assert context["x"] == 1

Replace 用于替换匹配的值

允许用新值替换匹配的值

from koerce import match, Replace, var

x = var("x")

pattern = Replace(Capture(x), x + 1)
assert match(pattern, 1) == 2
assert match(pattern, 2) == 3

存在 Replace 模式的语法糖,上面的示例可以写成

from koerce import match, Replace, var

x = var("x")

assert match(+x >> x + 1, 1) == 2
assert match(+x >> x + 1, 2) == 3

替换模式在匹配对象时特别有用

from dataclasses import dataclass
from koerce import match, Replace, var, namespace

x = var("x")

@dataclass
class A:
    x: int
    y: int

@dataclass
class B:
    x: int
    y: int
    z: float


p, d = namespace(__name__)
x, y = var("x"), var("y")

# if value is an instance of A then capture A.0 as x and A.1 as y
# then construct a new B object with arguments x=x, y=1, z=y
pattern = p.A(+x, +y) >> d.B(x=x, y=1, z=y)
value = A(1, 2)
expected = B(x=1, y=1, z=2)
assert match(pattern, value) == expected

替换也可以用于嵌套结构中

from koerce import match, Replace, var, namespace, NoMatch

@dataclass
class Foo:
    value: str

@dataclass
class Bar:
    foo: Foo
    value: int

p, d = namespace(__name__)

pattern = p.Bar(p.Foo("a") >> d.Foo("b"))
value = Bar(Foo("a"), 123)
expected = Bar(Foo("b"), 123)

assert match(pattern, value) == expected
assert match(pattern, Bar(Foo("c"), 123)) is NoMatch

SequenceOf / ListOf / TupleOf

from koerce import Is, NoMatch, match, ListOf, TupleOf

pattern = ListOf(str)
assert match(pattern, ["foo", "bar"]) == ["foo", "bar"]
assert match(pattern, [1, 2]) is NoMatch
assert match(pattern, 1) is NoMatch

MappingOf / DictOf / FrozenDictOf

from koerce import DictOf, Is, match

pattern = DictOf(Is(str), Is(int))
assert match(pattern, {"a": 1, "b": 2}) == {"a": 1, "b": 2}
assert match(pattern, {"a": 1, "b": "2"}) is NoMatch

模式列表

from koerce import match, NoMatch, SomeOf, ListOf, pattern

four = [1, 2, 3, 4]
three = [1, 2, 3]

assert match([1, 2, 3, SomeOf(int, at_least=1)], four) == four
assert match([1, 2, 3, SomeOf(int, at_least=1)], three) is NoMatch

integer = pattern(int, allow_coercion=False)
floating = pattern(float, allow_coercion=False)

assert match([1, 2, *floating], [1, 2, 3]) is NoMatch
assert match([1, 2, *floating], [1, 2, 3.0]) == [1, 2, 3.0]
assert match([1, 2, *floating], [1, 2, 3.0, 4.0]) == [1, 2, 3.0, 4.0]

模式映射

from koerce import match, NoMatch, Is, As

pattern = {
    "a": Is(int),
    "b": As(int),
    "c": Is(str),
    "d": ListOf(As(int)),
}
value = {
    "a": 1,
    "b": 2.0,
    "c": "three",
    "d": (4.0, 5.0, 6.0),
}
assert match(pattern, value) == {
    "a": 1,
    "b": 2,
    "c": "three",
    "d": [4, 5, 6],
}
assert match(pattern, {"a": 1, "b": 2, "c": "three"}) is NoMatch

可注解对象

可注解对象与数据类类似,但有一些不同

  • 可注解对象默认是可变的,但可以通过将immutable=True传递给Annotable基类来使其不可变。
  • 可以通过将hashable=True传递给Annotable基类来使可注解对象可散列,在这种情况下,散列值在初始化期间预先计算并存储在对象中,这使得字典查找变得廉价。
  • 可以通过传递allow_coercion=False来控制验证严格性。当allow_coercion=True时,注释被视为As模式,允许将值强制转换为给定的类型。当allow_coercion=False时,注释被视为Is模式,并且值必须正好是给定的类型。默认值为allow_coercion=True
  • 可注解对象支持继承,注释从基类继承,并合并签名,提供无缝体验。
  • 可注解对象可以使用位置参数或关键字参数或两者一起调用,位置参数按照顺序与注释匹配,关键字参数按名称与注释匹配。
from typing import Optional
from koerce import Annotable

class MyBase(Annotable):
    x: int
    y: float
    z: Optional[str] = None

class MyClass(MyBase):
    a: str
    b: bytes
    c: tuple[str, ...] = ("a", "b")
    x: int = 1


print(MyClass.__signature__)
# (y: float, a: str, b: bytes, c: tuple = ('a', 'b'), x: int = 1, z: Optional[str] = None)

print(MyClass(2.0, "a", b"b"))
# MyClass(y=2.0, a='a', b=b'b', c=('a', 'b'), x=1, z=None)

print(MyClass(2.0, "a", b"b", c=("c", "d")))
# MyClass(y=2.0, a='a', b=b'b', c=('c', 'd'), x=1, z=None)

print(MyClass(2.0, "a", b"b", c=("c", "d"), x=2))
# MyClass(y=2.0, a='a', b=b'b', c=('c', 'd'), x=2, z=None)

print(MyClass(2.0, "a", b"b", c=("c", "d"), x=2, z="z"))
# MyClass(y=2.0, a='a', b=b'b', c=('c', 'd'), x=2, z='z')

MyClass()
# TypeError: missing a required argument: 'y'

MyClass(2.0, "a", b"b", c=("c", "d"), x=2, z="z", invalid="invalid")
# TypeError: got an unexpected keyword argument 'invalid'

MyClass(2.0, "a", b"b", c=("c", "d"), x=2, z="z", y=3.0)
# TypeError: multiple values for argument 'y'

MyClass("asd", "a", b"b")
# ValidationError

性能

koerce 的性能至少与 pydantic 相当。由于使用 PyO3 绑定,pydantic-core 是用 Rust 编写的,使其成为一个性能相当不错的库。还有一个来自 Jim Crist-Harif 的更快验证/序列化库,称为 msgspec,它是直接使用 Python 的 C API 手工编写的 C 语言代码。

koercepydanticmsgpec 不完全相同,但它们是良好的基准测试候选者。

koerce/tests/test_y.py::test_pydantic PASSED
koerce/tests/test_y.py::test_msgspec PASSED
koerce/tests/test_y.py::test_annotated PASSED


------------------------------------------------------------------------------------------- benchmark: 3 tests ------------------------------------------------------------------------------------------
Name (time in ns)            Min                   Max                  Mean              StdDev                Median                IQR            Outliers  OPS (Kops/s)            Rounds  Iterations
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
test_msgspec            230.2801 (1.0)      6,481.4200 (1.60)       252.1706 (1.0)       97.0572 (1.0)        238.1600 (1.0)       5.0002 (1.0)      485;1616    3,965.5694 (1.0)       20000          50
test_annotated          525.6401 (2.28)     4,038.5600 (1.0)        577.7090 (2.29)     132.9966 (1.37)       553.9799 (2.33)     34.9300 (6.99)      662;671    1,730.9752 (0.44)      20000          50
test_pydantic         1,185.0201 (5.15)     6,027.9400 (1.49)     1,349.1259 (5.35)     320.3790 (3.30)     1,278.5601 (5.37)     75.5100 (15.10)   1071;1424      741.2206 (0.19)      20000          50

我尝试使用 msgspecpydantic 中性能最好的 API,将参数作为字典接收。

我计划进行更彻底的比较,但 koerce 的类似模型的注解 API 的性能大约是 pydantic 的两倍,但大约是 msgspec 的一半。考虑到实现方式,这也很有道理,因为 PyO3 可能比 Cython 有更高的开销,但它们都无法与手工编写的 Python C-API 代码的性能相媲美。

这种性能结果可以略有改善,但有两个巨大的优势是其他两个库所没有的。

  1. 它使用纯 Python 和 Cython 装饰器实现,因此即使不编译它也可以使用。它还可以启用 JIT 编译器,如 PyPy 或 CPython 3.13 带来的新复制和修补 JIT 编译器,以更好地优化热点路径。
  2. 完全在 Python 中开发使其更容易贡献。不需要学习 Rust 或 Python 的 C API 就可以修复错误或贡献新功能。

待办事项

README 正在建设中,计划进行改进

  • 使用 @annotated 装饰器验证函数的示例
  • 解释 allow_coercible 标志
  • 为每个模式提供适当的错误消息

开发

  • 该项目使用 poetry 进行依赖管理和打包。
  • Python 版本支持遵循 https://numpy.com.cn/neps/nep-0029-deprecation_policy.html
  • 轮子是通过使用 cibuildwheel 项目构建的。
  • 实现是纯 Python 和 Cython 注释。
  • 该项目使用 ruff 进行代码格式化。
  • 该项目使用 pytest 进行测试。

开发者指南将很快提供。

参考文献

该项目主要受到以下项目的启发

项目详细信息


下载文件

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

源分布

koerce-0.5.1.tar.gz (71.7 kB 查看哈希值)

上传时间

构建分布

koerce-0.5.1-cp312-cp312-win_amd64.whl (496.9 kB 查看哈希值)

上传时间 CPython 3.12 Windows x86-64

koerce-0.5.1-cp312-cp312-win32.whl (411.1 kB 查看哈希值)

上传时间 CPython 3.12 Windows x86

koerce-0.5.1-cp312-cp312-musllinux_1_2_x86_64.whl (3.9 MB 查看哈希值)

上传时间 CPython 3.12 musllinux: musl 1.2+ x86_64

koerce-0.5.1-cp312-cp312-musllinux_1_2_i686.whl (3.7 MB 查看哈希值)

上传时间 CPython 3.12 musllinux: musl 1.2+ i686

koerce-0.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.2 MB 查看哈希值)

上传时间 CPython 3.12 manylinux: glibc 2.17+ x86_64

koerce-0.5.1-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl (3.9 MB 查看哈希值)

上传时间 CPython 3.12 manylinux: glibc 2.17+ i686 manylinux: glibc 2.5+ i686

koerce-0.5.1-cp312-cp312-macosx_14_0_arm64.whl (561.7 kB 查看哈希值)

上传时间 CPython 3.12 macOS 14.0+ ARM64

koerce-0.5.1-cp312-cp312-macosx_13_0_x86_64.whl (614.4 kB 查看哈希值)

上传时间 CPython 3.12 macOS 13.0+ x86_64

koerce-0.5.1-cp311-cp311-win_amd64.whl (517.2 kB 查看哈希值)

上传时间 CPython 3.11 Windows x86_64

koerce-0.5.1-cp311-cp311-win32.whl (425.0 kB 查看哈希值)

上传时间 CPython 3.11 Windows x86

koerce-0.5.1-cp311-cp311-musllinux_1_2_x86_64.whl (4.0 MB 查看哈希值)

上传时间 CPython 3.11 musllinux: musl 1.2+ x86_64

koerce-0.5.1-cp311-cp311-musllinux_1_2_i686.whl (3.8 MB 查看哈希值)

上传时间 CPython 3.11 musllinux: musl 1.2+ i686

koerce-0.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.3 MB 查看哈希值)

上传于 CPython 3.11 manylinux: glibc 2.17+ x86-64

koerce-0.5.1-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl (4.0 MB 查看哈希值)

上传于 CPython 3.11 manylinux: glibc 2.17+ i686 manylinux: glibc 2.5+ i686

koerce-0.5.1-cp311-cp311-macosx_14_0_arm64.whl (559.7 kB 查看哈希值)

上传于 CPython 3.11 macOS 14.0+ ARM64

koerce-0.5.1-cp311-cp311-macosx_13_0_x86_64.whl (630.5 kB 查看哈希值)

上传于 CPython 3.11 macOS 13.0+ x86-64

koerce-0.5.1-cp310-cp310-win_amd64.whl (513.3 kB 查看哈希值)

上传于 CPython 3.10 Windows x86-64

koerce-0.5.1-cp310-cp310-win32.whl (425.1 kB 查看哈希值)

上传于 CPython 3.10 Windows x86

koerce-0.5.1-cp310-cp310-musllinux_1_2_x86_64.whl (3.4 MB 查看哈希值)

上传于 CPython 3.10 musllinux: musl 1.2+ x86-64

koerce-0.5.1-cp310-cp310-musllinux_1_2_i686.whl (3.3 MB 查看哈希值)

上传于 CPython 3.10 musllinux: musl 1.2+ i686

koerce-0.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.8 MB 查看哈希值)

上传于 CPython 3.10 manylinux: glibc 2.17+ x86-64

koerce-0.5.1-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl (3.5 MB 查看哈希值)

上传于 CPython 3.10 manylinux: glibc 2.17+ i686 manylinux: glibc 2.5+ i686

koerce-0.5.1-cp310-cp310-macosx_14_0_arm64.whl (558.8 kB 查看哈希值)

上传时间 CPython 3.10 macOS 14.0+ ARM64

koerce-0.5.1-cp310-cp310-macosx_13_0_x86_64.whl (628.3 kB 查看哈希值)

上传时间 CPython 3.10 macOS 13.0+ x86-64

由以下支持