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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: asynkit
3
- Version: 0.9.0
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 syn it _synchronously_ in the absence
140
- of an event loop. Should the code cause any async features to be triggered, an exception is raised. This helps avoid writing duplicate code.
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 get_processed_data(datagetter):
144
- data = datagetter() # could be an async callback
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
- # will raise SynchronousError if it datagetter to be async
149
+ # raises SynchronousError if datagetter blocks
150
150
  def sync_get_processed_data(datagetter):
151
- return asynkit.coro_sync(combine_stuff(cb1, cb2))
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
- # may return an awaitable
157
- def get_processed_data(datagetter):
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 function
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 = get_processed_data(datagetter)
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 = get_processed_data(datagetter)
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 methods.
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 selectively fail if the logic tries to invoke any truly async operations.
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
- `coro_sync()` can also be applied as a decorator:
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.coro_sync
193
- async def sync_function():
194
- return "look ma, no async!"
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
- return await a
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 syn it _synchronously_ in the absence
116
- of an event loop. Should the code cause any async features to be triggered, an exception is raised. This helps avoid writing duplicate code.
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 get_processed_data(datagetter):
120
- data = datagetter() # could be an async callback
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
- # will raise SynchronousError if it datagetter to be async
125
+ # raises SynchronousError if datagetter blocks
126
126
  def sync_get_processed_data(datagetter):
127
- return asynkit.coro_sync(combine_stuff(cb1, cb2))
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
- # may return an awaitable
133
- def get_processed_data(datagetter):
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 function
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 = get_processed_data(datagetter)
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 = get_processed_data(datagetter)
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 methods.
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 selectively fail if the logic tries to invoke any truly async operations.
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
- `coro_sync()` can also be applied as a decorator:
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.coro_sync
169
- async def sync_function():
170
- return "look ma, no async!"
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
- return await a
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.0"
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
- "CoroutineAbort",
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 CoroutineAbort(BaseException):
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(CoroutineAbort())
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