从 AnyIO 3 迁移到 AnyIO 4

Migrating from AnyIO 3 to AnyIO 4

非标准异常组类已被删除

The non-standard exception group class was removed

AnyIO 3 之前有一个自定义的 ExceptionGroup 类,它在 PEP 654 异常组类出现之前就已存在。这个类现在已经被移除,改为使用内置的 BaseExceptionGroupExceptionGroup 类。如果您的代码曾经抛出旧的 ExceptionGroup 异常或捕获它,您需要切换到这些标准类。否则,您可以忽略这一部分。

如果您正在针对 Python 3.11 之前的版本,您需要使用 exceptiongroup_ 回溯,并从 exceptiongroup 导入这些类之一。 BaseExceptionGroupExceptionGroup 之间的唯一区别是,后者只能包含从 Exception 派生的异常,因此也可以通过 except Exception: 来捕获。

任务组现在将单个异常包装在组中

Task groups now wrap single exceptions in groups

AnyIO 4 中最显著的不兼容变更是,任务组现在总是会在主任务或任何子任务抛出异常时(除了取消异常)抛出异常组。以前,只有当任务组需要抛出多个异常时,才会抛出异常组。实际上,这意味着如果您的代码以前期望捕获从任务组中抛出的特定类型异常,您现在需要切换到 except* 语法(如果您恰好使用的是 Python 3.11 或更高版本),或者使用 exceptiongroup_ 回溯中的 catch() 上下文管理器。

因此,如果您的代码像这样:

try:
    await function_using_a_taskgroup()
except ValueError as exc:
    ...

那么 Python 3.11+ 版本的等效代码几乎是一样的:

try:
    await function_using_a_taskgroup()
except* ValueError as excgrp:
    # 注意:excgrp 现在是一个 ExceptionGroup!
    ...

如果您需要保持与较旧 Python 版本的兼容性,您需要使用回溯版本:

from exceptiongroup import ExceptionGroup, catch

def handle_value_errors(excgrp: ExceptionGroup) -> None:
    ...

with catch({ValueError: handle_value_errors}):
    await function_using_a_taskgroup()

这个差异通常也会出现在测试套件中。例如,如果您以前在基于 pytest 的测试套件中有如下代码:

with pytest.raises(ValueError):
    await function_using_a_taskgroup()

现在需要改成:

from exceptiongroup import ExceptionGroup

with pytest.raises(ExceptionGroup) as exc:
    await function_using_a_taskgroup()

assert len(exc.value.exceptions) == 1
assert isinstance(exc.value.exceptions[0], ValueError)

如果您需要保持与 AnyIO 3 和 4 的兼容性,可以使用以下兼容性代码,通过解包来“折叠”单一异常组:

import sys
from contextlib import contextmanager
from typing import Generator

has_exceptiongroups = True
if sys.version_info < (3, 11):
    try:
        from exceptiongroup import BaseExceptionGroup
    except ImportError:
        has_exceptiongroups = False


@contextmanager
def collapse_excgroups() -> Generator[None, None, None]:
    try:
        yield
    except BaseException as exc:
        if has_exceptiongroups:
            while isinstance(exc, BaseExceptionGroup) and len(exc.exceptions) == 1:
                exc = exc.exceptions[0]

        raise exc

类型注释内存对象流的语法已更改changed

**Syntax for type annotated memory object streams has **

以前,创建类型注解的内存对象流是通过将所需的类型作为第二个参数传递来实现的:

send, receive = create_memory_object_stream(100, int)

在 4.0 中,create_memory_object_stream() 是一个伪装成函数的类,因此你需要为它提供类型参数化:

send, receive = create_memory_object_stream

如果您以前没有为您的内存对象流进行类型参数化,那么在这方面不需要进行任何更改。

事件循环工厂而不是事件循环策略

Event loop factories instead of event loop policies

如果你正在使用自定义的 asyncio 事件循环策略与 run() 一起,你需要改为传递一个 事件循环工厂(event loop factory),即一个返回新事件循环的可调用对象。

uvloop 为例,像以下这样的代码:

anyio.run(main, backend_options={"event_loop_policy": uvloop.EventLoopPolicy()})

应该转换为:

anyio.run(main, backend_options={"loop_factory": uvloop.new_event_loop})

确保不要实际调用工厂函数!

从 AnyIO 2 迁移到 AnyIO 3

Migrating from AnyIO 2 to AnyIO 3

AnyIO 3 更改了一些函数和方法,这需要你在代码中进行相应的调整。所有已弃用的函数和方法将在 AnyIO 4 中被移除。

异步函数转换为同步

Asynchronous functions converted to synchronous

AnyIO 3 将几个先前的异步函数和方法更改为常规函数,原因有以下两点:

  1. 更好地满足第三方库使用同步回调的用例需求。

  2. 更好地匹配 Trio 的 API。

以下函数和方法已被更改:

在迁移到 AnyIO 3 时,只需去掉每个调用中的 await

备注

出于向后兼容性原因,current_time()current_effective_deadline()get_running_tasks() 返回的对象是其原始类型的可等待版本(分别为 floatlist)。这些可等待版本是原始类型的子类,因此它们应与原始版本表现一致。但如果你绝对需要原始类型,可以根据需要对返回值使用 maybe_asyncfloat() / list()

以下异步上下文管理器改为常规上下文管理器:

在迁移时,只需将 async with 更改为普通的 with

除了 MemoryObjectReceiveStream.receive_nowait() 以外,它们都可以继续按原样使用——不过在 AnyIO 3 上以这种方式使用时将引发 DeprecationWarning

如果你在编写需要兼容两个主要版本的库,则需要使用 AnyIO 2.2 中添加的兼容性函数:maybe_async()maybe_async_cm()。它们分别允许你安全地使用函数/方法和上下文管理器,而不论当前安装的是哪个主要版本。

示例 1 —— 设置事件:

from anyio.abc import Event
from anyio import maybe_async


async def foo(event: Event):
    await maybe_async(event.set())
    ...

示例 2 —— 打开取消作用域:

from anyio import CancelScope, maybe_async_cm

async def foo():
    async with maybe_async_cm(CancelScope()) as scope:
        ...

启动任务

Starting tasks

TaskGroup.spawn() 协程方法已被弃用,推荐使用同步方法 TaskGroup.start_soon() (该方法与 Trio 的 “nurseries” 中的 start_soon() 方法相对应)。如果你完全迁移到 AnyIO 3,只需切换到调用新方法(并去掉 await)。

如果代码需要兼容 AnyIO 2 和 3,你可以继续使用 ``TaskGroup.spawn()``(直到 AnyIO 4)并抑制弃用警告:

import warnings

async def foo():
    async with create_task_group() as tg:
        with warnings.catch_warnings():
            await tg.spawn(otherfunc)

阻止门户更改

Blocking portal changes

AnyIO 现在**要求** from_thread.start_blocking_portal() 作为上下文管理器使用:

from anyio import sleep
from anyio.from_thread import start_blocking_portal

with start_blocking_portal() as portal:
    portal.call(sleep, 1)

TaskGroup.spawn() 类似, BlockingPortal.spawn_task() 方法也已重命名为 start_task_soon(),以便与任务组一致。

create_blocking_portal() 工厂函数也被弃用,建议直接实例化 BlockingPortal

对于需要跨版本兼容的代码,可以通过捕获弃用警告(如上所示)来处理。

同步原语

Synchronization primitives

同步原语工厂函数(如 create_event() 等)已被弃用,建议直接实例化类。因此,将如下代码:

from anyio import create_event

async def main():
    event = create_event()

转换为:

from anyio import Event

async def main():
    event = Event()

或者,如果需要兼容 AnyIO 2 和 3,则可以这样处理:

try:
    from anyio import Event
    create_event = Event
except ImportError:
    from anyio import create_event
    from anyio.abc import Event

async def foo() -> Event:
    return create_event()

线程函数已移动

Threading functions moved

线程函数已被重构至子模块,参考了 Trio 的模块化方式:

旧版本函数仍可使用,但调用时会发出弃用警告。