Skip to main content

Write dual sync/async interfaces with minimal duplication.

Project description

ZyncIO

Write dual sync/async interfaces with minimal duplication.

PyPI - Version PyPI - Python Version Coverage


What is ZyncIO?

If I had a nickel for every almost identical interface I had to write, I'd have two nickels... which isn't a lot, but it's weird that I had to write it twice.

– Dr. Doofenshmirtz, before discovering ZyncIO.

ZyncIO allows you to write interfaces that can be used synchronously and asynchronously, while avoiding the code duplication this usually entails.

How does it work?

ZyncIO works due to the fact that in Python you can actually run a coroutine without an event loop, as long as your chain of awaits consists exclusively of other coroutines (i.e. no Futures or Tasks):

The behavior of await coroutine is effectively the same as invoking a regular, synchronous Python function.

A Conceptual Overview of asyncio

To run such a coroutine, we simply call send(None), catch the StopIteration, and extract its value:

coro = pure_coroutine_func()
try:
    coro.send(None)
except StopIteration as e:
    ret = e.value

This means that a single async def function can be made to run in both synchronous and asynchronous contexts, as long as we have a way to determine which mode we're currently using:

async def zync_sleep(zync_mode: zyncio.Mode, secs: float) -> None:
    if zync_mode is zyncio.SYNC:
        time.sleep(secs)
    else:
        await asyncio.sleep(secs)

But this isn't very convenient; you need to pass an additional parameter, and running in sync mode is pretty clunky. That's where zyncio.zfunc comes in:

@zyncio.zfunc
async def zync_sleep(zync_mode: zyncio.Mode, secs: float) -> None:
    ...

zync_sleep.run_sync(3)
asyncio.run(zync_sleep.run_async(3))

@zyncio.zfunc
async def sleep_3(zync_mode: zyncio.Mode) -> None:
    await zync_sleep.run_zync(zync_mode, 3)
    # or
    await zync_sleep[zync_mode](3)

The real magic: SyncMixin/AsyncMixin, zyncio.zmethod, and zyncio.zproperty

The real power of ZyncIO comes out when implementing client interfaces:

  1. Implement a single base client, using the zyncio.zmethod and zyncio.zproperty decorators.

  2. Create two subclasses a sync client and an async client, adding the zyncio.SyncMixin and zyncio.AsyncMixin mixins respectively.

  3. All of your zyncio.zmethods magically become sync methods on the sync client and async methods on the async client.

    All of the zyncio.zpropertys magically become properties on the sync client, and async methods on the async client.

class BaseClient:
    def __init__(self, sock: socket.socket) -> None:
        self.sock: socket.socket = sock

    @zyncio.zmethod
    async def send_msg(self, zync_mode: zyncio.Mode, data: bytes) -> None:
        if zync_mode is zyncio.SYNC:
            self.sock.sendall(data)
        else:
            loop = asyncio.get_running_loop()
            await loop.sock_sendall(self.sock, data)

    @zyncio.zmethod
    async def recv_msg(self, zync_mode: zyncio.Mode, n: int) -> bytes:
        buf = b''
        if zync_mode is zyncio.SYNC:
            while len(buf) < n:
                buf += self.sock.recv(n)
        else:
            loop = asyncio.get_running_loop()
            while len(buf) < n:
                buf += await loop.sock_recv(self.sock, n)
        return buf

    @zyncio.zmethod
    async def do_handshake(self, zync_mode: zyncio.Mode) -> None:
        await self.send_msg[zync_mode](HANDSHAKE_REQ)
        response = await self.recv_msg[zync_mode](len(HANDSHAKE_RESP))
        if response != HANDSHAKE_RESP:
            raise RuntimeError('Handshake failed')

    @zyncio.zproperty
    async def status(self, zync_mode: zyncio.Mode) -> str:
        await self.send_msg[zync_mode](STATUS_REQ)
        return (await self.recv_msg[zync_mode](STATUS_RESP_LEN)).decode()


class SyncClient(BaseClient, zyncio.SyncMixin):
    pass


class AsyncClient(BaseClient, zyncio.AsyncMixin):
    def __init__(self, sock: socket.socket) -> None:
        super().__init__(sock)
        self.sock.setblocking(False)


sync_client = SyncClient(sock)
sync_client.do_handshake()  # Magically sync!
print('Status:', sync_client.status)  # Sync property


async def use_async_client():
    async_client = AsyncClient(sock)
    await async_client.do_handshake()  # Magically async!
    print('Status:', await sync_client.status())  # Async func

asyncio.run(use_async_client())

Typing

ZyncIO is fully typed, and built specifically for typed projects. If you're getting unexepcted type checking errors, please open an issue.

License

zyncio is distributed under the terms of the MIT license.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zyncio-0.4.0.tar.gz (27.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

zyncio-0.4.0-py3-none-any.whl (6.2 kB view details)

Uploaded Python 3

File details

Details for the file zyncio-0.4.0.tar.gz.

File metadata

  • Download URL: zyncio-0.4.0.tar.gz
  • Upload date:
  • Size: 27.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for zyncio-0.4.0.tar.gz
Algorithm Hash digest
SHA256 8a905669e247fdce4571188b99c1479af4a811b105c8c0ec55db0748e7c164ea
MD5 51882fdeea978c638c0f4870bed3041d
BLAKE2b-256 389ab372c7dbf0aa269bea87c3d60e0a5f47542ba5d29cec0184923b448e89b7

See more details on using hashes here.

Provenance

The following attestation bundles were made for zyncio-0.4.0.tar.gz:

Publisher: release.yml on BenjyWiener/zyncio

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file zyncio-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: zyncio-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 6.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for zyncio-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 dfd051b40309caa1579b94dc1de69f7eaf3e9ba9ff473f4ac63f698718a26eee
MD5 e79a59004854653406d1714b914f7628
BLAKE2b-256 35dfbf39d0a2a6fa8c309d545fea452ef4ea5e1cb19a4728f50288a3c3bacf66

See more details on using hashes here.

Provenance

The following attestation bundles were made for zyncio-0.4.0-py3-none-any.whl:

Publisher: release.yml on BenjyWiener/zyncio

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page