zyncio 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zyncio-0.1.0/.github/workflows/release.yml +55 -0
- zyncio-0.1.0/.github/workflows/static-analyses.yml +43 -0
- zyncio-0.1.0/.github/workflows/test.yml +75 -0
- zyncio-0.1.0/.gitignore +1 -0
- zyncio-0.1.0/LICENSE +21 -0
- zyncio-0.1.0/PKG-INFO +169 -0
- zyncio-0.1.0/README.md +142 -0
- zyncio-0.1.0/pyproject.toml +75 -0
- zyncio-0.1.0/src/zyncio/__init__.py +243 -0
- zyncio-0.1.0/src/zyncio/py.typed +0 -0
- zyncio-0.1.0/tests/__init__.py +1 -0
- zyncio-0.1.0/tests/client.py +56 -0
- zyncio-0.1.0/tests/test_zyncio.py +142 -0
- zyncio-0.1.0/tests/typing_tests.py +33 -0
- zyncio-0.1.0/tests/utils.py +21 -0
- zyncio-0.1.0/uv.lock +296 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Make release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
name: Build source distribution and wheel
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- name: Check out repository
|
|
15
|
+
uses: actions/checkout@v6
|
|
16
|
+
- name: Install Python 3.x
|
|
17
|
+
uses: actions/setup-python@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: 3.x
|
|
20
|
+
- name: Install build
|
|
21
|
+
run: pip install build
|
|
22
|
+
- name: Build sdist & wheel
|
|
23
|
+
run: python -m build
|
|
24
|
+
- name: Save sdist & wheel
|
|
25
|
+
uses: actions/upload-artifact@v5
|
|
26
|
+
with:
|
|
27
|
+
name: dist
|
|
28
|
+
path: |
|
|
29
|
+
dist/*.tar.gz
|
|
30
|
+
dist/*.whl
|
|
31
|
+
|
|
32
|
+
upload:
|
|
33
|
+
name: Upload
|
|
34
|
+
needs:
|
|
35
|
+
- build
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
# Don't release when running the workflow manually from GitHub's UI.
|
|
38
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
|
39
|
+
permissions:
|
|
40
|
+
id-token: write
|
|
41
|
+
attestations: write
|
|
42
|
+
contents: write
|
|
43
|
+
steps:
|
|
44
|
+
- name: Download artifacts
|
|
45
|
+
uses: actions/download-artifact@v4
|
|
46
|
+
with:
|
|
47
|
+
pattern: dist
|
|
48
|
+
merge-multiple: true
|
|
49
|
+
path: dist
|
|
50
|
+
- name: Attest provenance
|
|
51
|
+
uses: actions/attest-build-provenance@v2
|
|
52
|
+
with:
|
|
53
|
+
subject-path: dist/*
|
|
54
|
+
- name: Upload to PyPI
|
|
55
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Run static analyses
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
lint:
|
|
9
|
+
name: Lint source with Ruff
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Check out repository
|
|
13
|
+
uses: actions/checkout@v6
|
|
14
|
+
- name: Run ruff check
|
|
15
|
+
uses: astral-sh/ruff-action@v3
|
|
16
|
+
with:
|
|
17
|
+
version: latest
|
|
18
|
+
- name: Run ruff format
|
|
19
|
+
run: ruff format --diff
|
|
20
|
+
|
|
21
|
+
type-check:
|
|
22
|
+
name: Type-check with Pyright
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
strategy:
|
|
25
|
+
fail-fast: false
|
|
26
|
+
matrix:
|
|
27
|
+
python-version:
|
|
28
|
+
- "3.10"
|
|
29
|
+
- "3.11"
|
|
30
|
+
- "3.12"
|
|
31
|
+
- "3.13"
|
|
32
|
+
- "3.14"
|
|
33
|
+
steps:
|
|
34
|
+
- name: Check out repository
|
|
35
|
+
uses: actions/checkout@v6
|
|
36
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
37
|
+
uses: actions/setup-python@v6
|
|
38
|
+
with:
|
|
39
|
+
python-version: ${{ matrix.python-version }}
|
|
40
|
+
- name: Install dependencies
|
|
41
|
+
run: pip install -e .[dev]
|
|
42
|
+
- name: Run Pyright
|
|
43
|
+
uses: jakebailey/pyright-action@v1
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: Run tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
python-version:
|
|
14
|
+
- "3.10"
|
|
15
|
+
- "3.11"
|
|
16
|
+
- "3.12"
|
|
17
|
+
- "3.13"
|
|
18
|
+
- "3.14"
|
|
19
|
+
steps:
|
|
20
|
+
- name: Check out repository
|
|
21
|
+
uses: actions/checkout@v6
|
|
22
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
23
|
+
uses: actions/setup-python@v6
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python-version }}
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: pip install -e .[dev]
|
|
28
|
+
- name: Run tests
|
|
29
|
+
run: pytest --cov --cov-report=
|
|
30
|
+
env:
|
|
31
|
+
COVERAGE_FILE: coverage/coverage-${{ matrix.python-version }}
|
|
32
|
+
- name: Upload coverage files
|
|
33
|
+
uses: actions/upload-artifact@v5
|
|
34
|
+
with:
|
|
35
|
+
name: coverage-${{ matrix.python-version }}
|
|
36
|
+
path: coverage
|
|
37
|
+
|
|
38
|
+
coverage-merge:
|
|
39
|
+
needs: [test]
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
permissions:
|
|
42
|
+
statuses: write
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v6
|
|
45
|
+
- name: Set up Python 3.14
|
|
46
|
+
uses: actions/setup-python@v6
|
|
47
|
+
with:
|
|
48
|
+
python-version: "3.14"
|
|
49
|
+
- name: Install dependencies
|
|
50
|
+
run: pip install coverage
|
|
51
|
+
- name: Get coverage files
|
|
52
|
+
uses: actions/download-artifact@v6
|
|
53
|
+
with:
|
|
54
|
+
pattern: coverage-*
|
|
55
|
+
path: coverage
|
|
56
|
+
merge-multiple: true
|
|
57
|
+
- run: coverage combine coverage/*
|
|
58
|
+
- name: Create coverage status
|
|
59
|
+
run: |
|
|
60
|
+
coverage="$(coverage report --format=total)"
|
|
61
|
+
state="$([[ "$coverage" = "100.00" ]] && echo "success" || echo "failure")"
|
|
62
|
+
|
|
63
|
+
curl -sS --fail-with-body -X POST \
|
|
64
|
+
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
|
65
|
+
-H "Accept: application/vnd.github+json" \
|
|
66
|
+
https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }} \
|
|
67
|
+
-d @- <<EOF
|
|
68
|
+
{
|
|
69
|
+
"state": "${state}",
|
|
70
|
+
"context": "coverage",
|
|
71
|
+
"description": "Coverage ${coverage}%",
|
|
72
|
+
"target_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
73
|
+
}
|
|
74
|
+
EOF
|
|
75
|
+
- run: coverage report --fail-under=100
|
zyncio-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__pycache__/
|
zyncio-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benjy Wiener
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
zyncio-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zyncio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Write dual sync/async interfaces with minimal duplication.
|
|
5
|
+
Project-URL: Documentation, https://github.com/BenjyWiener/zyncio#readme
|
|
6
|
+
Project-URL: Issues, https://github.com/BenjyWiener/zyncio/issues
|
|
7
|
+
Project-URL: Source, https://github.com/BenjyWiener/zyncio
|
|
8
|
+
Author-email: Benjy Wiener <benjywiener@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: typing-extensions~=4.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: coverage; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# `zyncio`
|
|
29
|
+
|
|
30
|
+
## Write dual sync/async interfaces with minimal duplication.
|
|
31
|
+
|
|
32
|
+
> If I had a nickel for every almost identical interface I had to write,
|
|
33
|
+
> I'd have two nickels... which isn't a lot, but it's weird that I had to
|
|
34
|
+
> write it twice.
|
|
35
|
+
>
|
|
36
|
+
> – Dr. Doofenshmirtz, before discovering zyncio.
|
|
37
|
+
|
|
38
|
+
# What is `zyncio`?
|
|
39
|
+
|
|
40
|
+
`zyncio` allows you to write interfaces that can be used synchronously and asynchronously,
|
|
41
|
+
while avoiding the code duplication this usually entails.
|
|
42
|
+
|
|
43
|
+
# How does it work?
|
|
44
|
+
|
|
45
|
+
`zyncio` works due to the fact that in Python you can actually run a coroutine **without an event loop**,
|
|
46
|
+
as long as your chain of `await`s consists exclusively of other coroutines (i.e. no `Future`s or `Task`s):
|
|
47
|
+
|
|
48
|
+
> The behavior of `await coroutine` is effectively the same as invoking a regular, synchronous Python function.
|
|
49
|
+
>
|
|
50
|
+
> – [A Conceptual Overview of `asyncio`](https://docs.python.org/3/howto/a-conceptual-overview-of-asyncio.html#await)
|
|
51
|
+
|
|
52
|
+
To run such a coroutine, we simply call `send(None)`, catch the `StopIteration`, and extract its `value`:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
coro = pure_coroutine_func()
|
|
56
|
+
try:
|
|
57
|
+
coro.send(None)
|
|
58
|
+
except StopIteration as e:
|
|
59
|
+
ret = e.value
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This means that a single `async def` function can be made to run in both synchronous and asynchronous
|
|
63
|
+
contexts, as long as we have a way to determine which mode we're currently using:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
async def zync_sleep(zync_mode: zyncio.Mode, secs: float) -> None:
|
|
67
|
+
if zync_mode is zyncio.SYNC:
|
|
68
|
+
time.sleep(secs)
|
|
69
|
+
else:
|
|
70
|
+
await asyncio.sleep(secs)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
But this isn't very convenient; you need to pass an additional parameter, and running in
|
|
74
|
+
sync mode is pretty clunky. That's where `zyncio.zfunc` comes in:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
@zyncio.zfunc
|
|
78
|
+
async def zync_sleep(zync_mode: zyncio.Mode, secs: float) -> None:
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
zync_sleep.run_sync(3)
|
|
82
|
+
asyncio.run(zync_sleep.run_async(3))
|
|
83
|
+
|
|
84
|
+
@zyncio.zfunc
|
|
85
|
+
async def sleep_3(zync_mode: zyncio.Mode) -> None:
|
|
86
|
+
await zync_sleep.run_zync(zync_mode, 3)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## The real magic: `SyncMixin`/`AsyncMixin`, `zyncio.zmethod`, and `zyncio.zproperty`
|
|
90
|
+
|
|
91
|
+
The real power of `zyncio` comes out when implementing client interfaces:
|
|
92
|
+
|
|
93
|
+
1. Implement a single base client, using the `zyncio.zmethod` and `zyncio.zproperty`
|
|
94
|
+
decorators.
|
|
95
|
+
|
|
96
|
+
2. Create two subclasses a sync client and an async client, adding the `zyncio.SyncMixin`
|
|
97
|
+
and `zyncio.AsyncMixin` mixins respectively.
|
|
98
|
+
|
|
99
|
+
3. All of your `zyncio.zmethod`s magically become sync methods on the sync client and async
|
|
100
|
+
methods on the async client.
|
|
101
|
+
|
|
102
|
+
All of the `zyncio.zproperty`s magically become properties on the sync client, and async
|
|
103
|
+
methods on the async client.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
class BaseClient:
|
|
107
|
+
def __init__(self, sock: socket.socket) -> None:
|
|
108
|
+
self.sock: socket.socket = sock
|
|
109
|
+
|
|
110
|
+
@zyncio.zmethod
|
|
111
|
+
async def send_msg(self, zync_mode: zyncio.Mode, data: bytes) -> None:
|
|
112
|
+
if zync_mode is zyncio.SYNC:
|
|
113
|
+
self.sock.sendall(data)
|
|
114
|
+
else:
|
|
115
|
+
loop = asyncio.get_running_loop()
|
|
116
|
+
await loop.sock_sendall(self.sock, data)
|
|
117
|
+
|
|
118
|
+
@zyncio.zmethod
|
|
119
|
+
async def recv_msg(self, zync_mode: zyncio.Mode, n: int) -> bytes:
|
|
120
|
+
buf = b''
|
|
121
|
+
if zync_mode is zyncio.SYNC:
|
|
122
|
+
while len(buf) < n:
|
|
123
|
+
buf += self.sock.recv(n)
|
|
124
|
+
else:
|
|
125
|
+
loop = asyncio.get_running_loop()
|
|
126
|
+
while len(buf) < n:
|
|
127
|
+
buf += await loop.sock_recv(self.sock, n)
|
|
128
|
+
return buf
|
|
129
|
+
|
|
130
|
+
@zyncio.zmethod
|
|
131
|
+
async def do_handshake(self, zync_mode: zyncio.Mode) -> None:
|
|
132
|
+
await self.send_msg.run_zync(zync_mode, HANDSHAKE_REQ)
|
|
133
|
+
response = await self.recv_msg.run_zync(zync_mode, len(HANDSHAKE_RESP))
|
|
134
|
+
if response != HANDSHAKE_RESP:
|
|
135
|
+
raise RuntimeError('Handshake failed')
|
|
136
|
+
|
|
137
|
+
@zyncio.zproperty
|
|
138
|
+
async def status(self, zync_mode: zyncio.Mode) -> str:
|
|
139
|
+
await self.send_msg.run_zync(zync_mode, STATUS_REQ)
|
|
140
|
+
return (await self.recv_msg.run_zync(zync_mode, STATUS_RESP_LEN)).decode()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class SyncClient(BaseClient, zyncio.SyncMixin):
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class AsyncClient(BaseClient, zyncio.AsyncMixin):
|
|
148
|
+
def __init__(self, sock: socket.socket) -> None:
|
|
149
|
+
super().__init__(sock)
|
|
150
|
+
self.sock.setblocking(False)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
sync_client = SyncClient(sock)
|
|
154
|
+
sync_client.do_handshake() # Magically sync!
|
|
155
|
+
print('Status:', sync_client.status) # Sync property
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def use_async_client():
|
|
159
|
+
async_client = AsyncClient(sock)
|
|
160
|
+
await async_client.do_handshake() # Magically async!
|
|
161
|
+
print('Status:', await sync_client.status()) # Async func
|
|
162
|
+
|
|
163
|
+
asyncio.run(use_async_client())
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
# Typing
|
|
167
|
+
|
|
168
|
+
`zyncio` is fully typed, and built specifically for typed projects. If you're getting
|
|
169
|
+
unexepcted type checking errors, please [open an issue](https://github.com/BenjyWiener/zyncio/issues).
|
zyncio-0.1.0/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# `zyncio`
|
|
2
|
+
|
|
3
|
+
## Write dual sync/async interfaces with minimal duplication.
|
|
4
|
+
|
|
5
|
+
> If I had a nickel for every almost identical interface I had to write,
|
|
6
|
+
> I'd have two nickels... which isn't a lot, but it's weird that I had to
|
|
7
|
+
> write it twice.
|
|
8
|
+
>
|
|
9
|
+
> – Dr. Doofenshmirtz, before discovering zyncio.
|
|
10
|
+
|
|
11
|
+
# What is `zyncio`?
|
|
12
|
+
|
|
13
|
+
`zyncio` allows you to write interfaces that can be used synchronously and asynchronously,
|
|
14
|
+
while avoiding the code duplication this usually entails.
|
|
15
|
+
|
|
16
|
+
# How does it work?
|
|
17
|
+
|
|
18
|
+
`zyncio` works due to the fact that in Python you can actually run a coroutine **without an event loop**,
|
|
19
|
+
as long as your chain of `await`s consists exclusively of other coroutines (i.e. no `Future`s or `Task`s):
|
|
20
|
+
|
|
21
|
+
> The behavior of `await coroutine` is effectively the same as invoking a regular, synchronous Python function.
|
|
22
|
+
>
|
|
23
|
+
> – [A Conceptual Overview of `asyncio`](https://docs.python.org/3/howto/a-conceptual-overview-of-asyncio.html#await)
|
|
24
|
+
|
|
25
|
+
To run such a coroutine, we simply call `send(None)`, catch the `StopIteration`, and extract its `value`:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
coro = pure_coroutine_func()
|
|
29
|
+
try:
|
|
30
|
+
coro.send(None)
|
|
31
|
+
except StopIteration as e:
|
|
32
|
+
ret = e.value
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This means that a single `async def` function can be made to run in both synchronous and asynchronous
|
|
36
|
+
contexts, as long as we have a way to determine which mode we're currently using:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
async def zync_sleep(zync_mode: zyncio.Mode, secs: float) -> None:
|
|
40
|
+
if zync_mode is zyncio.SYNC:
|
|
41
|
+
time.sleep(secs)
|
|
42
|
+
else:
|
|
43
|
+
await asyncio.sleep(secs)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
But this isn't very convenient; you need to pass an additional parameter, and running in
|
|
47
|
+
sync mode is pretty clunky. That's where `zyncio.zfunc` comes in:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
@zyncio.zfunc
|
|
51
|
+
async def zync_sleep(zync_mode: zyncio.Mode, secs: float) -> None:
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
zync_sleep.run_sync(3)
|
|
55
|
+
asyncio.run(zync_sleep.run_async(3))
|
|
56
|
+
|
|
57
|
+
@zyncio.zfunc
|
|
58
|
+
async def sleep_3(zync_mode: zyncio.Mode) -> None:
|
|
59
|
+
await zync_sleep.run_zync(zync_mode, 3)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## The real magic: `SyncMixin`/`AsyncMixin`, `zyncio.zmethod`, and `zyncio.zproperty`
|
|
63
|
+
|
|
64
|
+
The real power of `zyncio` comes out when implementing client interfaces:
|
|
65
|
+
|
|
66
|
+
1. Implement a single base client, using the `zyncio.zmethod` and `zyncio.zproperty`
|
|
67
|
+
decorators.
|
|
68
|
+
|
|
69
|
+
2. Create two subclasses a sync client and an async client, adding the `zyncio.SyncMixin`
|
|
70
|
+
and `zyncio.AsyncMixin` mixins respectively.
|
|
71
|
+
|
|
72
|
+
3. All of your `zyncio.zmethod`s magically become sync methods on the sync client and async
|
|
73
|
+
methods on the async client.
|
|
74
|
+
|
|
75
|
+
All of the `zyncio.zproperty`s magically become properties on the sync client, and async
|
|
76
|
+
methods on the async client.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
class BaseClient:
|
|
80
|
+
def __init__(self, sock: socket.socket) -> None:
|
|
81
|
+
self.sock: socket.socket = sock
|
|
82
|
+
|
|
83
|
+
@zyncio.zmethod
|
|
84
|
+
async def send_msg(self, zync_mode: zyncio.Mode, data: bytes) -> None:
|
|
85
|
+
if zync_mode is zyncio.SYNC:
|
|
86
|
+
self.sock.sendall(data)
|
|
87
|
+
else:
|
|
88
|
+
loop = asyncio.get_running_loop()
|
|
89
|
+
await loop.sock_sendall(self.sock, data)
|
|
90
|
+
|
|
91
|
+
@zyncio.zmethod
|
|
92
|
+
async def recv_msg(self, zync_mode: zyncio.Mode, n: int) -> bytes:
|
|
93
|
+
buf = b''
|
|
94
|
+
if zync_mode is zyncio.SYNC:
|
|
95
|
+
while len(buf) < n:
|
|
96
|
+
buf += self.sock.recv(n)
|
|
97
|
+
else:
|
|
98
|
+
loop = asyncio.get_running_loop()
|
|
99
|
+
while len(buf) < n:
|
|
100
|
+
buf += await loop.sock_recv(self.sock, n)
|
|
101
|
+
return buf
|
|
102
|
+
|
|
103
|
+
@zyncio.zmethod
|
|
104
|
+
async def do_handshake(self, zync_mode: zyncio.Mode) -> None:
|
|
105
|
+
await self.send_msg.run_zync(zync_mode, HANDSHAKE_REQ)
|
|
106
|
+
response = await self.recv_msg.run_zync(zync_mode, len(HANDSHAKE_RESP))
|
|
107
|
+
if response != HANDSHAKE_RESP:
|
|
108
|
+
raise RuntimeError('Handshake failed')
|
|
109
|
+
|
|
110
|
+
@zyncio.zproperty
|
|
111
|
+
async def status(self, zync_mode: zyncio.Mode) -> str:
|
|
112
|
+
await self.send_msg.run_zync(zync_mode, STATUS_REQ)
|
|
113
|
+
return (await self.recv_msg.run_zync(zync_mode, STATUS_RESP_LEN)).decode()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class SyncClient(BaseClient, zyncio.SyncMixin):
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class AsyncClient(BaseClient, zyncio.AsyncMixin):
|
|
121
|
+
def __init__(self, sock: socket.socket) -> None:
|
|
122
|
+
super().__init__(sock)
|
|
123
|
+
self.sock.setblocking(False)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
sync_client = SyncClient(sock)
|
|
127
|
+
sync_client.do_handshake() # Magically sync!
|
|
128
|
+
print('Status:', sync_client.status) # Sync property
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def use_async_client():
|
|
132
|
+
async_client = AsyncClient(sock)
|
|
133
|
+
await async_client.do_handshake() # Magically async!
|
|
134
|
+
print('Status:', await sync_client.status()) # Async func
|
|
135
|
+
|
|
136
|
+
asyncio.run(use_async_client())
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
# Typing
|
|
140
|
+
|
|
141
|
+
`zyncio` is fully typed, and built specifically for typed projects. If you're getting
|
|
142
|
+
unexepcted type checking errors, please [open an issue](https://github.com/BenjyWiener/zyncio/issues).
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "zyncio"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Write dual sync/async interfaces with minimal duplication."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
keywords = []
|
|
9
|
+
authors = [{ name = "Benjy Wiener", email = "benjywiener@gmail.com" }]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Programming Language :: Python",
|
|
13
|
+
"Programming Language :: Python :: 3.10",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Programming Language :: Python :: 3.14",
|
|
18
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
19
|
+
"Programming Language :: Python :: Implementation :: PyPy",
|
|
20
|
+
]
|
|
21
|
+
dependencies = ["typing_extensions~=4.0"]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = ["coverage", "pytest", "pytest-cov"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Documentation = "https://github.com/BenjyWiener/zyncio#readme"
|
|
28
|
+
Issues = "https://github.com/BenjyWiener/zyncio/issues"
|
|
29
|
+
Source = "https://github.com/BenjyWiener/zyncio"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
33
|
+
build-backend = "hatchling.build"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.version]
|
|
36
|
+
source = "vcs"
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
target-version = "py310"
|
|
40
|
+
line-length = 150
|
|
41
|
+
|
|
42
|
+
[tool.ruff.format]
|
|
43
|
+
quote-style = "single"
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
extend-select = ["D", "I"]
|
|
47
|
+
ignore = [
|
|
48
|
+
"D105", # undocumented-magic-method
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.ruff.lint.pydocstyle]
|
|
52
|
+
convention = "pep257"
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint.isort]
|
|
55
|
+
force-sort-within-sections = true
|
|
56
|
+
lines-after-imports = 2
|
|
57
|
+
extra-standard-library = ["typing_extensions"]
|
|
58
|
+
|
|
59
|
+
[tool.pyright]
|
|
60
|
+
reportUnnecessaryTypeIgnoreComment = "error"
|
|
61
|
+
|
|
62
|
+
pythonVersion = "3.10"
|
|
63
|
+
[tool.coverage.run]
|
|
64
|
+
source_pkgs = ["zyncio", "tests"]
|
|
65
|
+
branch = true
|
|
66
|
+
|
|
67
|
+
[tool.coverage.paths]
|
|
68
|
+
zyncio = ["src/zyncio"]
|
|
69
|
+
tests = ["tests"]
|
|
70
|
+
|
|
71
|
+
[tool.coverage.report]
|
|
72
|
+
precision = 2
|
|
73
|
+
exclude_lines = ["# pragma: no cover", "def __repr__", "@overload"]
|
|
74
|
+
omit = ["tests/typing_tests.py"]
|
|
75
|
+
show_missing = true
|