Skip to content

Migration Guide: curio → anyio (3.0.0)

async-btree 3.0.0 drops curio entirely. anyio replaces it as the sole async backend.

Why

curio is no longer maintained at the same pace. anyio provides a stable, backend-agnostic API that runs on asyncio, trio, and asyncio+uvloop — tested uniformly across all three.

Dependency changes

Remove:

# pyproject.toml — remove these
"async-btree[curio]"
curio
pytest-curio

Add:

"async-btree >= 3.0.0"

Optional extras for non-asyncio backends:

# trio backend
trio >= 0.26

# uvloop backend (asyncio only)
uvloop >= 0.21

API changes

BTreeRunner

BTreeRunner no longer accepts disable_curio. It now accepts backend:

# before
with BTreeRunner() as runner:            # used curio if installed
    runner.run(my_tree)                  # callable + args, unchanged

with BTreeRunner(disable_curio=True) as runner:  # forced asyncio
    runner.run(my_tree)

# after
with BTreeRunner() as runner:            # asyncio by default
    runner.run(my_tree)                  # same callable signature

with BTreeRunner(backend="trio") as runner:
    runner.run(my_tree)

with BTreeRunner(backend="asyncio+uvloop") as runner:
    runner.run(my_tree)

runner.run(target, *args, **kwargs) signature is unchanged — it takes a callable, not a coroutine, in both 2.x and 3.0.0.

run()

The old run(kernel, target, *args) curio helper is gone. New run() is a one-shot convenience wrapper:

# before
import curio
from async_btree import run
run(curio.Kernel(), my_tree)

# after
from async_btree import run
run(my_tree)                          # asyncio default
run(my_tree, backend="trio")
run(my_tree, backend="asyncio+uvloop")

has_curio

Removed entirely. No replacement — the curio detection path no longer exists.

# before
from async_btree.utils import has_curio
if has_curio():
    ...

# after — delete this code

parallele — success_threshold typo fixed

The parameter was previously succes_threshold (one 's'). It is now success_threshold.

# before
parallele(children=[a, b], succes_threshold=1)

# after
parallele(children=[a, b], success_threshold=1)

sequence — success_threshold typo fixed

Same typo fix applies to sequence:

# before
sequence(children=[a, b, c], succes_threshold=2)

# after
sequence(children=[a, b, c], success_threshold=2)

The property shown in stringify_analyze output changes from succes_threshold: N to success_threshold: N.

ControlFlowException.instanciate → instantiate

The method was renamed to fix a typo:

# before
ControlFlowException.instanciate(e)

# after
ControlFlowException.instantiate(e)

Behavior is unchanged — pure rename.

repeat_until semantics corrected — repeat_while added

The old repeat_until looped while condition was truthy, which is the opposite of standard BT semantics. 3.0.0 corrects this:

Function Loops while condition is...
repeat_while(condition, child) truthy (replaces old repeat_until behavior)
repeat_until(condition, child) falsy (standard BT semantics — stops when condition fires)
# before — looped while condition was truthy
repeat_until(condition=my_condition, child=my_child)

# after — same behavior, correct name
repeat_while(condition=my_condition, child=my_child)

# new repeat_until — loops until condition becomes truthy
repeat_until(condition=my_condition, child=my_child)

ContextVar isolation

With curio, BTreeRunner kept a persistent kernel across run() calls. ContextVar mutations accumulated from tick to tick.

With anyio, each runner.run() call is isolated — mutations inside a tick do not propagate to the next tick. The context is snapshotted once at __enter__.

var = ContextVar("x", default=0)
var.set(1)

with BTreeRunner() as runner:
    runner.run(mutate_var)   # sets var to 99 inside
    # var is still 1 here — isolation is intentional

var.get()  # → 1

See tutorial_3_context.py for details.

pytest migration

Replace pytest-curio with pytest-anyio (bundled with anyio):

# before
@pytest.mark.curio
async def test_something():
    ...

# after
import pytest
pytestmark = pytest.mark.anyio

async def test_something():
    ...

Parametrize over backends with the anyio_backend fixture:

@pytest.fixture(params=["asyncio", "trio", "asyncio+uvloop"])
def anyio_backend(request):
    backend = request.param
    if backend == "asyncio+uvloop":
        return "asyncio", {"use_uvloop": True}
    return backend, {}