Write dual sync/async interfaces with minimal duplication.
Project description
Write dual sync/async interfaces with minimal duplication.
What is ZyncIO?
If I had a nickel for every variation of my library that I maintain, I'd have two nickels... which isn't a lot, but it's weird that I had to write everything 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 coroutineis effectively the same as invoking a regular, synchronous Python function.
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.call_sync(3)
asyncio.run(zync_sleep.call_async(3))
@zyncio.zfunc
async def sleep_3(zync_mode: zyncio.Mode) -> None:
await zync_sleep.call_zync(zync_mode, 3)
The real magic: SyncMixin/AsyncMixin, zyncio.zmethod, and zyncio.zproperty
The real power of ZyncIO comes out when implementing client interfaces:
-
Implement a single base client, using the
zyncio.zmethodandzyncio.zpropertydecorators. -
Create two subclasses a sync client and an async client, adding the
zyncio.SyncMixinandzyncio.AsyncMixinmixins respectively. -
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, data: bytes) -> None:
if zyncio.is_sync(self):
self.sock.sendall(data)
else:
loop = asyncio.get_running_loop()
await loop.sock_sendall(self.sock, data)
@zyncio.zmethod
async def recv_msg(self, n: int) -> bytes:
buf = b''
if zyncio.is_sync(self):
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) -> None:
# `.z` (or `.call_zync`) on bound `zmethod`'s (and similar callables)
# always returns a coroutine, so you can `await` it regardless of the
# running mode.
await self.send_msg.z(HANDSHAKE_REQ)
response = await self.recv_msg.z(len(HANDSHAKE_RESP))
if response != HANDSHAKE_RESP:
raise RuntimeError('Handshake failed')
@zyncio.zproperty
async def status(self) -> str:
await self.send_msg.z(STATUS_REQ)
return (await self.recv_msg.z(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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file zyncio-0.17.1.tar.gz.
File metadata
- Download URL: zyncio-0.17.1.tar.gz
- Upload date:
- Size: 2.8 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c4cec6b1f4b77be5d0a3ac1bf999fd530fa4bdb8918110b5cce17d22aa074ec4
|
|
| MD5 |
a3dda2c12026aae67c45dc6110352e70
|
|
| BLAKE2b-256 |
ce10d8a293aed7ea6864b776ed12b0b26cc650e6afc0db47e2ffc6b9d83285ad
|
Provenance
The following attestation bundles were made for zyncio-0.17.1.tar.gz:
Publisher:
release.yml on BenjyWiener/zyncio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zyncio-0.17.1.tar.gz -
Subject digest:
c4cec6b1f4b77be5d0a3ac1bf999fd530fa4bdb8918110b5cce17d22aa074ec4 - Sigstore transparency entry: 1311480558
- Sigstore integration time:
-
Permalink:
BenjyWiener/zyncio@e6a48c3f20db227526c0fa8492367d79f57f37cd -
Branch / Tag:
refs/tags/v0.17.1 - Owner: https://github.com/BenjyWiener
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e6a48c3f20db227526c0fa8492367d79f57f37cd -
Trigger Event:
push
-
Statement type:
File details
Details for the file zyncio-0.17.1-py3-none-any.whl.
File metadata
- Download URL: zyncio-0.17.1-py3-none-any.whl
- Upload date:
- Size: 10.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9bb9e6e7f13de07405fed9225d5185b837a4e6eeb142876920d95cb2941a55bd
|
|
| MD5 |
696f0efc215ce258b4b9a026768c0611
|
|
| BLAKE2b-256 |
ea60e3e51ba70f2f7d9ad7daa2de028c045bf0d0eca9aefa96337705edb89e34
|
Provenance
The following attestation bundles were made for zyncio-0.17.1-py3-none-any.whl:
Publisher:
release.yml on BenjyWiener/zyncio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zyncio-0.17.1-py3-none-any.whl -
Subject digest:
9bb9e6e7f13de07405fed9225d5185b837a4e6eeb142876920d95cb2941a55bd - Sigstore transparency entry: 1311480689
- Sigstore integration time:
-
Permalink:
BenjyWiener/zyncio@e6a48c3f20db227526c0fa8492367d79f57f37cd -
Branch / Tag:
refs/tags/v0.17.1 - Owner: https://github.com/BenjyWiener
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e6a48c3f20db227526c0fa8492367d79f57f37cd -
Trigger Event:
push
-
Statement type: