asynkit 0.9.0__tar.gz → 0.9.2__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.
- {asynkit-0.9.0 → asynkit-0.9.2}/PKG-INFO +45 -22
- {asynkit-0.9.0 → asynkit-0.9.2}/README.md +44 -21
- {asynkit-0.9.0 → asynkit-0.9.2}/pyproject.toml +2 -1
- {asynkit-0.9.0 → asynkit-0.9.2}/src/asynkit/coroutine.py +29 -30
- {asynkit-0.9.0 → asynkit-0.9.2}/LICENSE +0 -0
- {asynkit-0.9.0 → asynkit-0.9.2}/src/asynkit/__init__.py +0 -0
- {asynkit-0.9.0 → asynkit-0.9.2}/src/asynkit/compat.py +0 -0
- {asynkit-0.9.0 → asynkit-0.9.2}/src/asynkit/experimental/__init__.py +0 -0
- {asynkit-0.9.0 → asynkit-0.9.2}/src/asynkit/experimental/anyio.py +0 -0
- {asynkit-0.9.0 → asynkit-0.9.2}/src/asynkit/loop/__init__.py +0 -0
- {asynkit-0.9.0 → asynkit-0.9.2}/src/asynkit/loop/default.py +0 -0
- {asynkit-0.9.0 → asynkit-0.9.2}/src/asynkit/loop/eventloop.py +0 -0
- {asynkit-0.9.0 → asynkit-0.9.2}/src/asynkit/loop/extensions.py +0 -0
- {asynkit-0.9.0 → asynkit-0.9.2}/src/asynkit/loop/schedulingloop.py +0 -0
- {asynkit-0.9.0 → asynkit-0.9.2}/src/asynkit/loop/types.py +0 -0
- {asynkit-0.9.0 → asynkit-0.9.2}/src/asynkit/monitor.py +0 -0
- {asynkit-0.9.0 → asynkit-0.9.2}/src/asynkit/py.typed +0 -0
- {asynkit-0.9.0 → asynkit-0.9.2}/src/asynkit/scheduling.py +0 -0
- {asynkit-0.9.0 → asynkit-0.9.2}/src/asynkit/tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: asynkit
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: Async toolkit for advanced scheduling
|
|
5
5
|
Home-page: https://github.com/kristjanvalur/py-asynkit
|
|
6
6
|
License: MIT
|
|
@@ -136,28 +136,29 @@ just as would happen if it were directly turned into a `Task`.
|
|
|
136
136
|
## `coro_sync()` - Running coroutines synchronously
|
|
137
137
|
|
|
138
138
|
If you are writing code which should work both synchronously and asynchronously,
|
|
139
|
-
you can now write the code fully _async_ and then
|
|
140
|
-
of an event loop.
|
|
139
|
+
you can now write the code fully _async_ and then run it _synchronously_ in the absence
|
|
140
|
+
of an event loop. As long as the code doesn't _block_ (await unfinished _futures_) and doesn't try to access the event loop, it can successfully be executed. This helps avoid writing duplicate code.
|
|
141
141
|
|
|
142
142
|
```python
|
|
143
|
-
async def
|
|
144
|
-
data = datagetter() #
|
|
143
|
+
async def async_get_processed_data(datagetter):
|
|
144
|
+
data = datagetter() # an optionally async callback
|
|
145
145
|
data = await data if isawaitable(data) else data
|
|
146
146
|
return process_data(data)
|
|
147
147
|
|
|
148
148
|
|
|
149
|
-
#
|
|
149
|
+
# raises SynchronousError if datagetter blocks
|
|
150
150
|
def sync_get_processed_data(datagetter):
|
|
151
|
-
return asynkit.coro_sync(
|
|
151
|
+
return asynkit.coro_sync(async_get_processed_data(datagetter))
|
|
152
152
|
```
|
|
153
153
|
|
|
154
154
|
This sort of code might previously have been written thus:
|
|
155
|
+
|
|
155
156
|
```python
|
|
156
|
-
#
|
|
157
|
-
def
|
|
157
|
+
# A hybrid function, _may_ return an _awaitable_
|
|
158
|
+
def hybrid_get_processed_data(datagetter):
|
|
158
159
|
data = datagetter()
|
|
159
160
|
if isawaitable(data):
|
|
160
|
-
# return an awaitable helper
|
|
161
|
+
# return an awaitable helper closure
|
|
161
162
|
async def helper():
|
|
162
163
|
data = await data
|
|
163
164
|
return process_data(data)
|
|
@@ -167,12 +168,12 @@ def get_processed_data(datagetter):
|
|
|
167
168
|
|
|
168
169
|
|
|
169
170
|
async def async_get_processed_data(datagetter):
|
|
170
|
-
r =
|
|
171
|
+
r = hybrid_get_processed_data(datagetter)
|
|
171
172
|
return await r if isawaitable(r) else r
|
|
172
173
|
|
|
173
174
|
|
|
174
175
|
def sync_get_processed_data(datagetter):
|
|
175
|
-
r =
|
|
176
|
+
r = hybrid_get_processed_data(datagetter)
|
|
176
177
|
if isawaitable(r):
|
|
177
178
|
raise RuntimeError("callbacks failed to run synchronously")
|
|
178
179
|
return r
|
|
@@ -181,22 +182,42 @@ def sync_get_processed_data(datagetter):
|
|
|
181
182
|
The above pattern, writing async methods as sync and returning async helpers,
|
|
182
183
|
is common in library code which needs to work both in synchronous and asynchronous
|
|
183
184
|
context. Needless to say, it is very convoluted, hard to debug and contains a lot
|
|
184
|
-
of code duplication where the same logic is repeated inside async helper
|
|
185
|
+
of code duplication where the same logic is repeated inside async helper closures.
|
|
185
186
|
|
|
186
187
|
Using `coro_sync()` it is possible to write the entire logic as `async` methods and
|
|
187
|
-
then
|
|
188
|
+
then simply fail if the code tries to invoke any truly async operations.
|
|
189
|
+
If the invoked coroutine blocks, a `SynchronousError` is raised _from_ a `SynchronousAbort` exception which
|
|
190
|
+
contains a traceback. This makes it easy to pinpoint the location in the code where the
|
|
191
|
+
async code blocked. If the code tries to access the event loop, e.g. by creating a `Task`, a `RuntimeError` will be raised.
|
|
192
|
+
|
|
193
|
+
The `syncfunction()` decorator can be used to automatically wrap an async function
|
|
194
|
+
so that it is executed using `coro_sync()`:
|
|
195
|
+
|
|
196
|
+
```pycon
|
|
197
|
+
>>> @asynkit.syncfunction
|
|
198
|
+
... async def sync_function():
|
|
199
|
+
... async def async_function():
|
|
200
|
+
... return "look, no async!"
|
|
201
|
+
... return await async_function()
|
|
202
|
+
...
|
|
203
|
+
>>> sync_function()
|
|
204
|
+
'look, no async!'
|
|
205
|
+
>>>
|
|
206
|
+
```
|
|
188
207
|
|
|
189
|
-
`
|
|
208
|
+
the `asyncfunction()` utility can be used when passing synchronous callbacks to async
|
|
209
|
+
code, to make them async. This, along with `syncfunction()` and `coro_sync()`,
|
|
210
|
+
can be used to integrate synchronous code with async middleware:
|
|
190
211
|
|
|
191
212
|
```python
|
|
192
|
-
@asynkit.
|
|
193
|
-
async def
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
assert sync_function().contains("look")
|
|
213
|
+
@asynkit.syncfunction
|
|
214
|
+
async def sync_client(sync_callback):
|
|
215
|
+
middleware = AsyncMiddleware(asynkit.asyncfunction(sync_callback))
|
|
216
|
+
return await middleware.run()
|
|
198
217
|
```
|
|
199
218
|
|
|
219
|
+
Using this pattern, one can write the middleware completely async, make it also work
|
|
220
|
+
for synchronous code, while avoiding the hybrid function _antipattern._
|
|
200
221
|
|
|
201
222
|
## `CoroStart`
|
|
202
223
|
|
|
@@ -319,6 +340,7 @@ async def runner():
|
|
|
319
340
|
while True:
|
|
320
341
|
try:
|
|
321
342
|
print(await m.aawait(c))
|
|
343
|
+
break
|
|
322
344
|
except OOBData as oob:
|
|
323
345
|
print(oob.data)
|
|
324
346
|
```
|
|
@@ -345,7 +367,8 @@ m = Monitor()
|
|
|
345
367
|
a = m.awaitable(coro(m))
|
|
346
368
|
while True:
|
|
347
369
|
try:
|
|
348
|
-
|
|
370
|
+
await a
|
|
371
|
+
break
|
|
349
372
|
except OOBData as oob:
|
|
350
373
|
handle_oob(oob.data)
|
|
351
374
|
```
|
|
@@ -112,28 +112,29 @@ just as would happen if it were directly turned into a `Task`.
|
|
|
112
112
|
## `coro_sync()` - Running coroutines synchronously
|
|
113
113
|
|
|
114
114
|
If you are writing code which should work both synchronously and asynchronously,
|
|
115
|
-
you can now write the code fully _async_ and then
|
|
116
|
-
of an event loop.
|
|
115
|
+
you can now write the code fully _async_ and then run it _synchronously_ in the absence
|
|
116
|
+
of an event loop. As long as the code doesn't _block_ (await unfinished _futures_) and doesn't try to access the event loop, it can successfully be executed. This helps avoid writing duplicate code.
|
|
117
117
|
|
|
118
118
|
```python
|
|
119
|
-
async def
|
|
120
|
-
data = datagetter() #
|
|
119
|
+
async def async_get_processed_data(datagetter):
|
|
120
|
+
data = datagetter() # an optionally async callback
|
|
121
121
|
data = await data if isawaitable(data) else data
|
|
122
122
|
return process_data(data)
|
|
123
123
|
|
|
124
124
|
|
|
125
|
-
#
|
|
125
|
+
# raises SynchronousError if datagetter blocks
|
|
126
126
|
def sync_get_processed_data(datagetter):
|
|
127
|
-
return asynkit.coro_sync(
|
|
127
|
+
return asynkit.coro_sync(async_get_processed_data(datagetter))
|
|
128
128
|
```
|
|
129
129
|
|
|
130
130
|
This sort of code might previously have been written thus:
|
|
131
|
+
|
|
131
132
|
```python
|
|
132
|
-
#
|
|
133
|
-
def
|
|
133
|
+
# A hybrid function, _may_ return an _awaitable_
|
|
134
|
+
def hybrid_get_processed_data(datagetter):
|
|
134
135
|
data = datagetter()
|
|
135
136
|
if isawaitable(data):
|
|
136
|
-
# return an awaitable helper
|
|
137
|
+
# return an awaitable helper closure
|
|
137
138
|
async def helper():
|
|
138
139
|
data = await data
|
|
139
140
|
return process_data(data)
|
|
@@ -143,12 +144,12 @@ def get_processed_data(datagetter):
|
|
|
143
144
|
|
|
144
145
|
|
|
145
146
|
async def async_get_processed_data(datagetter):
|
|
146
|
-
r =
|
|
147
|
+
r = hybrid_get_processed_data(datagetter)
|
|
147
148
|
return await r if isawaitable(r) else r
|
|
148
149
|
|
|
149
150
|
|
|
150
151
|
def sync_get_processed_data(datagetter):
|
|
151
|
-
r =
|
|
152
|
+
r = hybrid_get_processed_data(datagetter)
|
|
152
153
|
if isawaitable(r):
|
|
153
154
|
raise RuntimeError("callbacks failed to run synchronously")
|
|
154
155
|
return r
|
|
@@ -157,22 +158,42 @@ def sync_get_processed_data(datagetter):
|
|
|
157
158
|
The above pattern, writing async methods as sync and returning async helpers,
|
|
158
159
|
is common in library code which needs to work both in synchronous and asynchronous
|
|
159
160
|
context. Needless to say, it is very convoluted, hard to debug and contains a lot
|
|
160
|
-
of code duplication where the same logic is repeated inside async helper
|
|
161
|
+
of code duplication where the same logic is repeated inside async helper closures.
|
|
161
162
|
|
|
162
163
|
Using `coro_sync()` it is possible to write the entire logic as `async` methods and
|
|
163
|
-
then
|
|
164
|
+
then simply fail if the code tries to invoke any truly async operations.
|
|
165
|
+
If the invoked coroutine blocks, a `SynchronousError` is raised _from_ a `SynchronousAbort` exception which
|
|
166
|
+
contains a traceback. This makes it easy to pinpoint the location in the code where the
|
|
167
|
+
async code blocked. If the code tries to access the event loop, e.g. by creating a `Task`, a `RuntimeError` will be raised.
|
|
168
|
+
|
|
169
|
+
The `syncfunction()` decorator can be used to automatically wrap an async function
|
|
170
|
+
so that it is executed using `coro_sync()`:
|
|
171
|
+
|
|
172
|
+
```pycon
|
|
173
|
+
>>> @asynkit.syncfunction
|
|
174
|
+
... async def sync_function():
|
|
175
|
+
... async def async_function():
|
|
176
|
+
... return "look, no async!"
|
|
177
|
+
... return await async_function()
|
|
178
|
+
...
|
|
179
|
+
>>> sync_function()
|
|
180
|
+
'look, no async!'
|
|
181
|
+
>>>
|
|
182
|
+
```
|
|
164
183
|
|
|
165
|
-
`
|
|
184
|
+
the `asyncfunction()` utility can be used when passing synchronous callbacks to async
|
|
185
|
+
code, to make them async. This, along with `syncfunction()` and `coro_sync()`,
|
|
186
|
+
can be used to integrate synchronous code with async middleware:
|
|
166
187
|
|
|
167
188
|
```python
|
|
168
|
-
@asynkit.
|
|
169
|
-
async def
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
assert sync_function().contains("look")
|
|
189
|
+
@asynkit.syncfunction
|
|
190
|
+
async def sync_client(sync_callback):
|
|
191
|
+
middleware = AsyncMiddleware(asynkit.asyncfunction(sync_callback))
|
|
192
|
+
return await middleware.run()
|
|
174
193
|
```
|
|
175
194
|
|
|
195
|
+
Using this pattern, one can write the middleware completely async, make it also work
|
|
196
|
+
for synchronous code, while avoiding the hybrid function _antipattern._
|
|
176
197
|
|
|
177
198
|
## `CoroStart`
|
|
178
199
|
|
|
@@ -295,6 +316,7 @@ async def runner():
|
|
|
295
316
|
while True:
|
|
296
317
|
try:
|
|
297
318
|
print(await m.aawait(c))
|
|
319
|
+
break
|
|
298
320
|
except OOBData as oob:
|
|
299
321
|
print(oob.data)
|
|
300
322
|
```
|
|
@@ -321,7 +343,8 @@ m = Monitor()
|
|
|
321
343
|
a = m.awaitable(coro(m))
|
|
322
344
|
while True:
|
|
323
345
|
try:
|
|
324
|
-
|
|
346
|
+
await a
|
|
347
|
+
break
|
|
325
348
|
except OOBData as oob:
|
|
326
349
|
handle_oob(oob.data)
|
|
327
350
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "asynkit"
|
|
3
|
-
version = "0.9.
|
|
3
|
+
version = "0.9.2"
|
|
4
4
|
description = "Async toolkit for advanced scheduling"
|
|
5
5
|
authors = ["Kristján Valur Jónsson <sweskman@gmail.com>"]
|
|
6
6
|
repository = "https://github.com/kristjanvalur/py-asynkit"
|
|
@@ -75,6 +75,7 @@ module = [
|
|
|
75
75
|
"asynkit.loop.eventloop",
|
|
76
76
|
"asynkit.compat",
|
|
77
77
|
"asynkit.tools",
|
|
78
|
+
"tests.test_coro",
|
|
78
79
|
]
|
|
79
80
|
warn_unused_ignores=false
|
|
80
81
|
|
|
@@ -4,7 +4,6 @@ import inspect
|
|
|
4
4
|
import sys
|
|
5
5
|
import types
|
|
6
6
|
from contextvars import Context, copy_context
|
|
7
|
-
from inspect import iscoroutinefunction
|
|
8
7
|
from types import FrameType
|
|
9
8
|
from typing import (
|
|
10
9
|
Any,
|
|
@@ -41,7 +40,9 @@ __all__ = [
|
|
|
41
40
|
"coro_iter",
|
|
42
41
|
"coro_sync",
|
|
43
42
|
"SynchronousError",
|
|
44
|
-
"
|
|
43
|
+
"SynchronousAbort",
|
|
44
|
+
"asyncfunction",
|
|
45
|
+
"syncfunction",
|
|
45
46
|
]
|
|
46
47
|
|
|
47
48
|
PYTHON_37 = sys.version_info.major == 3 and sys.version_info.minor == 7
|
|
@@ -64,7 +65,7 @@ class SynchronousError(RuntimeError):
|
|
|
64
65
|
"""
|
|
65
66
|
|
|
66
67
|
|
|
67
|
-
class
|
|
68
|
+
class SynchronousAbort(BaseException):
|
|
68
69
|
"""
|
|
69
70
|
Exception thrown into coroutine to abort it.
|
|
70
71
|
"""
|
|
@@ -504,36 +505,10 @@ def awaitmethod(
|
|
|
504
505
|
return wrapper
|
|
505
506
|
|
|
506
507
|
|
|
507
|
-
@overload
|
|
508
508
|
def coro_sync(coro: Coroutine[Any, Any, T]) -> T:
|
|
509
|
-
...
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
@overload
|
|
513
|
-
def coro_sync(coro: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]:
|
|
514
|
-
...
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
def coro_sync(
|
|
518
|
-
coro: Union[Coroutine[Any, Any, T], Callable[P, Coroutine[Any, Any, T]]]
|
|
519
|
-
) -> Union[T, Callable[P, T]]:
|
|
520
|
-
|
|
521
509
|
"""Runs a corouting synchronlously. If the coroutine blocks, a
|
|
522
510
|
SynchronousError is raised.
|
|
523
|
-
|
|
524
|
-
Can also be used as a decorator for an async function.
|
|
525
511
|
"""
|
|
526
|
-
if iscoroutinefunction(coro):
|
|
527
|
-
# we are a decorator
|
|
528
|
-
coro2 = cast(Callable[..., Coroutine[Any, Any, T]], coro)
|
|
529
|
-
|
|
530
|
-
@functools.wraps(coro)
|
|
531
|
-
def helper(*args: Any, **kwargs: Any) -> T:
|
|
532
|
-
return coro_sync(coro2(*args, **kwargs))
|
|
533
|
-
|
|
534
|
-
return helper
|
|
535
|
-
coro = cast(Coroutine[Any, Any, T], coro)
|
|
536
|
-
# We are running a coroutine synchronously
|
|
537
512
|
start = CoroStart[T](coro)
|
|
538
513
|
if start.done():
|
|
539
514
|
return start.result()
|
|
@@ -541,7 +516,7 @@ def coro_sync(
|
|
|
541
516
|
try:
|
|
542
517
|
# we can't use GeneratorExit because that gets special handling and
|
|
543
518
|
# a traceback is not collected.
|
|
544
|
-
start.throw(
|
|
519
|
+
start.throw(SynchronousAbort())
|
|
545
520
|
except BaseException as err:
|
|
546
521
|
raise SynchronousError("coroutine failed to complete synchronously") from err
|
|
547
522
|
else:
|
|
@@ -553,3 +528,27 @@ def coro_sync(
|
|
|
553
528
|
start.close()
|
|
554
529
|
except RuntimeError:
|
|
555
530
|
pass
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def syncfunction(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]:
|
|
534
|
+
"""Make an async function synchronous, by invoking
|
|
535
|
+
`coro_sync()` on its coroutine. Useful as a decorator.
|
|
536
|
+
"""
|
|
537
|
+
|
|
538
|
+
@functools.wraps(func)
|
|
539
|
+
def helper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
540
|
+
return coro_sync(func(*args, **kwargs))
|
|
541
|
+
|
|
542
|
+
return helper
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def asyncfunction(func: Callable[P, T]) -> Callable[P, Coroutine[Any, Any, T]]:
|
|
546
|
+
"""Make a regular function async, so that its result needs to be awaited.
|
|
547
|
+
Useful when providing synchronous callbacks to async code.
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
@functools.wraps(func)
|
|
551
|
+
async def helper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
552
|
+
return func(*args, **kwargs)
|
|
553
|
+
|
|
554
|
+
return helper
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|