Using ZyncIO – Standalone Interface

Now that we understand how it’s possible to write code that is both sync and async, let’s take a look at how ZyncIO makes this process more ergonomic.

@zyncio.zfunc

ZyncIO provides a number of classes that can be used as function decorators, the simplest of which is zyncio.zfunc:

@zyncio.zfunc
async def zync_sleep(zync_mode: zyncio.Mode, duration: float) -> None:
    if zync_mode is zyncio.SYNC:
        time.sleep(duration)
    else:
        await asyncio.sleep(duration)
>>> zync_sleep.call_sync(0.1)  # Runs in sync mode
>>> asyncio.run(zync_sleep.call_async(0.1))  # Runs in async mode

Composing ZyncIO functions with call_zync

If we want to compose ZyncIO functions, we can branch on zync_mode:

@zyncio.zfunc
async def slow_print(zync_mode: zyncio.Mode, message: str) -> None:
    for c in message:
        if zync_mode is zyncio.SYNC:
            zync_sleep.call_sync(0.1)
        else:
            await zync_sleep.call_async(0.1)
        print(c, end='', flush=True)
    print()

This works, but it’s verbose and error-prone. Instead, zfunc (and the other ZyncIO decorators we’ll look at) provide an additional method, call_zync. call_zync takes the mode as an argument, and always returns a coroutine (so it can always be awaited):

@zyncio.zfunc
async def slow_print(zync_mode: zyncio.Mode, message: str) -> None:
    for c in message:
        await zync_sleep.call_zync(zync_mode, 0.1)
        print(c, end='', flush=True)
    print()
>>> slow_print.call_sync('Hello')
Hello
>>> asyncio.run(slow_print.call_async('world'))
world

@zyncio.zgenerator

The zyncio.zgenerator decorator is similar to zfunc, but for generator functions. It provides the same call_sync, call_async, and call_zync methods, but they return generators instead of coroutines:

@zyncio.zgenerator
async def countdown(zync_mode: zyncio.Mode, start: int) -> AsyncGenerator[int]:
    for i in range(start, 0, -1):
        await zync_sleep.call_zync(zync_mode, 1.0)
        yield i
>>> for n in countdown.call_sync(3):
...     print(n)
3
2
1

>>> async def main() -> None:
...     async for n in countdown.call_async(3):
...         print(n)
>>> asyncio.run(main())
3
2
1

@zyncio.zcontextmanager

The zyncio.zcontextmanager decorator can be used to create context managers using generator functions, similar to contextlib.contextmanager:

@zyncio.zcontextmanager
async def run_process(zync_mode: zyncio.Mode, command: str) -> AsyncGenerator[int]:
    if zync_mode is zyncio.SYNC:
        process = subprocess.Popen(command, shell=True)
    else:
        process = await asyncio.create_subprocess_shell(command)

    print('Process started.')

    try:
        yield process.pid
    finally:
        process.terminate()
        if zync_mode is zyncio.SYNC:
            process.wait()
        else:
            await process.wait()


        print('Process terminated.')
>>> with run_process.call_sync('sleep 60') as pid:
...     print('PID:', pid)
Process started.
PID: ...
Process terminated.

>>> async def main() -> None:
...     async with run_process.call_async('sleep 60') as pid:
...         print('PID:', pid)
>>> asyncio.run(main())
Process started.
PID: ...
Process terminated.

These decorators are useful, but we’ll introduce the real magic of ZyncIO in the next chapter: Using ZyncIO – Class-Based Interface.