Using ZyncIO – Class-Based Interface¶
In the previous chapter we learned about zyncio.zfunc and its siblings in the standalone API.
Where ZyncIO really shines, however, is its class-based interface.
Let’s build a client for fetching repository information from the GitHub API. We’ll use niquests for making the HTTP requests.
Base Class¶
We start by defining BaseGitHubClient, which will serve as the basis for our sync and async clients. We’ll implement
the get_repo method. Notice that we use zyncio.zmethod instead of zyncio.zfunc, and we don’t have a
zync_mode parameter. Instead, we use zyncio.is_sync to determine if we’re in sync or async mode.
from dataclasses import dataclass
import niquests
import zyncio
@dataclass
class Repo:
full_name: str
description: str
stars: int
class BaseGitHubClient:
@zyncio.zmethod
async def get_repo(self, owner: str, repo: str) -> Repo:
url = f'https://api.github.com/repos/{owner}/{repo}'
if zyncio.is_sync(self):
data = niquests.get(url).raise_for_status().json()
else:
data = (await niquests.aget(url)).raise_for_status().json()
return Repo(
full_name=data['full_name'],
description=data['description'],
stars=data['stargazers_count'],
)
If we try using BaseGitHubClient, we get an exception:
>>> client = BaseGitHubClient()
>>> client.get_repo('BenjyWiener', 'zyncio')
Traceback (most recent call last):
...
TypeError: BoundZyncMethod is only callable on instances of classes that subclass zyncio.SyncMixin or zyncio.AsyncMixin, or that implement the zyncio.ZyncDelegator protocol
The Mixins¶
To actually use our client, we need to specialize it, using the special mixins zyncio.SyncMixin and
zyncio.AsyncMixin:
class GitHubClient(BaseGitHubClient, zyncio.SyncMixin):
pass
class AsyncGitHubClient(BaseGitHubClient, zyncio.AsyncMixin):
pass
>>> client = GitHubClient()
>>> client.get_repo('BenjyWiener', 'zyncio')
Repo(full_name='BenjyWiener/zyncio', description='...', stars=...)
>>> import asyncio
>>> async_client = AsyncGitHubClient()
>>> asyncio.run(async_client.get_repo('BenjyWiener', 'zyncio'))
Repo(full_name='BenjyWiener/zyncio', description='...', stars=...)
Composing zyncio.zmethods¶
Let’s add another method to get the language breakdown of a repository:
class BaseGitHubClient:
@zyncio.zmethod
async def get_repo(self, owner: str, repo: str) -> Repo:
url = f'https://api.github.com/repos/{owner}/{repo}'
if zyncio.is_sync(self):
data = niquests.get(url).raise_for_status().json()
else:
data = (await niquests.aget(url)).raise_for_status().json()
return Repo(
full_name=data['full_name'],
description=data['description'],
stars=data['stargazers_count'],
)
@zyncio.zmethod
async def get_languages(self, owner: str, repo: str) -> dict[str, int]:
url = f'https://api.github.com/repos/{owner}/{repo}/languages'
if zyncio.is_sync(self):
return niquests.get(url).raise_for_status().json()
else:
return (await niquests.aget(url)).raise_for_status().json()
>>> client = GitHubClient()
>>> client.get_languages('BenjyWiener', 'zyncio')
{'Python': ...}
This works, but it’s not very DRY. Let’s extract our request logic into a new method, get_url:
class BaseGitHubClient:
@zyncio.zmethod
async def get_url(self, path: str, base_url: str = 'https://api.github.com') -> Any:
url = f'{base_url}/{path}'
if zyncio.is_sync(self):
return niquests.get(url).raise_for_status().json()
else:
return (await niquests.aget(url)).raise_for_status().json()
@zyncio.zmethod
async def get_repo(self, owner: str, repo: str) -> Repo:
data = await self.get_url(f'repos/{owner}/{repo}')
return Repo(
full_name=data['full_name'],
description=data['description'],
stars=data['stargazers_count'],
)
@zyncio.zmethod
async def get_languages(self, owner: str, repo: str) -> dict[str, int]:
return await self.get_url(f'repos/{owner}/{repo}/languages')
>>> client = GitHubClient()
>>> client.get_languages('BenjyWiener', 'zyncio')
Traceback (most recent call last):
...
TypeError: 'dict' object can't be awaited
What happened?
The problem is that get_url is a zyncio.zmethod, so when we call self.get_url(...) in get_languages,
it acts like a synchronous method and returns a dict instead of a coroutine. If we remove the await our code
will work when we use GitHubClient, but then it will break when we use AsyncGitHubClient.
We can use zyncio.is_sync to determine if we need to await or not – but then we’re back to our original problem!
That’s where call_zync comes in. call_zync always returns a coroutine, so we can safely
await it regardless of self’s mode.
Let’s fix our code:
class BaseGitHubClient:
@zyncio.zmethod
async def get_url(self, path: str, base_url: str = 'https://api.github.com') -> Any:
url = f'{base_url}/{path}'
if zyncio.is_sync(self):
return niquests.get(url).raise_for_status().json()
else:
return (await niquests.aget(url)).raise_for_status().json()
@zyncio.zmethod
async def get_repo(self, owner: str, repo: str) -> Repo:
data = await self.get_url.call_zync(f'repos/{owner}/{repo}')
return Repo(
full_name=data['full_name'],
description=data['description'],
stars=data['stargazers_count'],
)
@zyncio.zmethod
async def get_languages(self, owner: str, repo: str) -> dict[str, int]:
return await self.get_url.call_zync(f'repos/{owner}/{repo}/languages')
>>> client = GitHubClient()
>>> client.get_repo('BenjyWiener', 'zyncio')
Repo(full_name='BenjyWiener/zyncio', description='...', stars=...)
>>> import asyncio
>>> async_client = AsyncGitHubClient()
>>> asyncio.run(async_client.get_repo('BenjyWiener', 'zyncio'))
Repo(full_name='BenjyWiener/zyncio', description='...', stars=...)
Tip
You can use .z(...) as an alias of .call_zync(...).
Tip
If your method is private and only ever called from other ZyncIO code, don’t decorate it with
@zyncio.zmethod. That way you can call it normally, without call_zync.
The ZyncDelegator Protocol¶
Let’s make our library a bit more object-oriented, by adding a languages property to Repo.
Instead of splitting Repo into two classes (sync and async), we can make it delegate its mode to its client, using
the zyncio.ZyncDelegator protocol:
from dataclasses import dataclass
from typing import Any, Self
import niquests
import zyncio
@dataclass
class Repo[ClientT: BaseGitHubClient]:
full_name: str
description: str
stars: int
client: ClientT
# Using a `TypeVar` for the return type of `__zync_delegate__` lets
# type checkers and IDEs infer the correct types ZyncIO descriptors
# like `zyncio.zmethod` and `zyncio.zproperty`.
def __zync_delegate__(self) -> ClientT:
return self.client
@zyncio.zproperty
async def languages(self) -> dict[str, int]:
owner, repo = self.full_name.split('/', 1)
return await self.client.get_languages.call_zync(owner, repo)
class BaseGitHubClient:
@zyncio.zmethod
async def get_url(self, path: str, base_url: str = 'https://api.github.com') -> Any:
url = f'{base_url}/{path}'
if zyncio.is_sync(self):
return niquests.get(url).raise_for_status().json()
else:
return (await niquests.aget(url)).raise_for_status().json()
@zyncio.zmethod
async def get_repo(self, owner: str, repo: str) -> Repo[Self]:
data = await self.get_url.call_zync(f'repos/{owner}/{repo}')
return Repo(
full_name=data['full_name'],
description=data['description'],
stars=data['stargazers_count'],
client=self,
)
@zyncio.zmethod
async def get_languages(self, owner: str, repo: str) -> dict[str, int]:
return await self.get_url.call_zync(f'repos/{owner}/{repo}/languages')
class GitHubClient(BaseGitHubClient, zyncio.SyncMixin):
pass
class AsyncGitHubClient(BaseGitHubClient, zyncio.AsyncMixin):
pass
>>> client = GitHubClient()
>>> repo = client.get_repo('BenjyWiener', 'zyncio')
>>> repo.languages
{'Python': ...}
>>> import asyncio
>>> async_client = AsyncGitHubClient()
>>> async_repo = asyncio.run(async_client.get_repo('BenjyWiener', 'zyncio'))
>>> asyncio.run(async_repo.languages())
{'Python': ...}
Note
Unlike zyncio.zmethod, zyncio.zproperty doesn’t have an equivalent of call_zync.
Instead, you can access and call the zyncio.zproperty object directly via the class:
await type(self).languages(self)
However, if you find yourself using this pattern a lot, you should probably consider using a regular method instead of (or alongside) your property.
Other Decorators¶
In addition to zyncio.zmethod and zyncio.zproperty there’s zyncio.zclassmethod, as well method-versions of
zyncio.zgenerator and zyncio.zcontextmanager, zyncio.zgeneratormethod and zyncio.zcontextmanagermethod. You can
read more about all of these in the API Reference.