asyncer 0__py3-none-any.whl
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.
- asyncer/__init__.py +9 -0
- asyncer/_main.py +362 -0
- asyncer/py.typed +0 -0
- asyncer-0.dist-info/LICENSE +21 -0
- asyncer-0.dist-info/METADATA +159 -0
- asyncer-0.dist-info/RECORD +7 -0
- asyncer-0.dist-info/WHEEL +4 -0
asyncer/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
__version__ = "0.0.4"
|
|
2
|
+
|
|
3
|
+
from ._main import PendingValueException as PendingValueException
|
|
4
|
+
from ._main import SoonValue as SoonValue
|
|
5
|
+
from ._main import TaskGroup as TaskGroup
|
|
6
|
+
from ._main import asyncify as asyncify
|
|
7
|
+
from ._main import create_task_group as create_task_group
|
|
8
|
+
from ._main import runnify as runnify
|
|
9
|
+
from ._main import syncify as syncify
|
asyncer/_main.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import sys
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
Awaitable,
|
|
7
|
+
Callable,
|
|
8
|
+
Coroutine,
|
|
9
|
+
Dict,
|
|
10
|
+
Generic,
|
|
11
|
+
Optional,
|
|
12
|
+
TypeVar,
|
|
13
|
+
Union,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if sys.version_info >= (3, 10):
|
|
17
|
+
from typing import ParamSpec
|
|
18
|
+
else:
|
|
19
|
+
from typing_extensions import ParamSpec
|
|
20
|
+
|
|
21
|
+
import anyio
|
|
22
|
+
import sniffio
|
|
23
|
+
from anyio._core._eventloop import threadlocals
|
|
24
|
+
from anyio.abc import TaskGroup as _TaskGroup
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# This was obtained with: from anyio._core._eventloop import get_asynclib
|
|
28
|
+
# Removed in https://github.com/agronholm/anyio/pull/429
|
|
29
|
+
# First release (not released yet): 4.0-dev
|
|
30
|
+
def get_asynclib(asynclib_name: Union[str, None] = None) -> Any:
|
|
31
|
+
if asynclib_name is None:
|
|
32
|
+
asynclib_name = sniffio.current_async_library()
|
|
33
|
+
|
|
34
|
+
modulename = "anyio._backends._" + asynclib_name
|
|
35
|
+
try:
|
|
36
|
+
return sys.modules[modulename]
|
|
37
|
+
except KeyError: # pragma: no cover
|
|
38
|
+
return import_module(modulename)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
T_Retval = TypeVar("T_Retval")
|
|
42
|
+
T_ParamSpec = ParamSpec("T_ParamSpec")
|
|
43
|
+
T = TypeVar("T")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PendingType:
|
|
47
|
+
def __repr__(self) -> str:
|
|
48
|
+
return "AsyncerPending"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
Pending = PendingType()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PendingValueException(Exception):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SoonValue(Generic[T]):
|
|
59
|
+
def __init__(self) -> None:
|
|
60
|
+
self._stored_value: Union[T, PendingType] = Pending
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def value(self) -> T:
|
|
64
|
+
if isinstance(self._stored_value, PendingType):
|
|
65
|
+
raise PendingValueException(
|
|
66
|
+
"The return value of this task is still pending. Maybe you forgot to "
|
|
67
|
+
"access it after the async with asyncer.create_task_group() block. "
|
|
68
|
+
"If you need to access values of async tasks inside the same task "
|
|
69
|
+
"group, you probably need a different approach, for example with "
|
|
70
|
+
"AnyIO Streams."
|
|
71
|
+
)
|
|
72
|
+
return self._stored_value
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def ready(self) -> bool:
|
|
76
|
+
return not isinstance(self._stored_value, PendingType)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TaskGroup(_TaskGroup):
|
|
80
|
+
def soonify(
|
|
81
|
+
self, async_function: Callable[T_ParamSpec, Awaitable[T]], name: object = None
|
|
82
|
+
) -> Callable[T_ParamSpec, SoonValue[T]]:
|
|
83
|
+
"""
|
|
84
|
+
Create and return a function that when called will start a new task in this
|
|
85
|
+
task group.
|
|
86
|
+
|
|
87
|
+
Internally it uses the same `task_group.start_soon()` method. But
|
|
88
|
+
`task_group.soonify()` supports keyword arguments additional to positional
|
|
89
|
+
arguments and it adds better support for autocompletion and inline errors
|
|
90
|
+
for the arguments of the function called.
|
|
91
|
+
|
|
92
|
+
Use it like this:
|
|
93
|
+
|
|
94
|
+
```Python
|
|
95
|
+
async with asyncer.create_task_group() as task_group:
|
|
96
|
+
async def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str:
|
|
97
|
+
# Do work
|
|
98
|
+
return "Some result value"
|
|
99
|
+
|
|
100
|
+
result = task_group.soonify(do_work)("spam", "ham", kwarg1="a", kwarg2="b")
|
|
101
|
+
|
|
102
|
+
print(result.value)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The return value from that function (`result` in the example) is an object
|
|
106
|
+
`SoonValue`.
|
|
107
|
+
|
|
108
|
+
This `SoonValue` object has an attribute `soon_value.value` that will hold the
|
|
109
|
+
return value of the original `async_function` *after* the `async with` block.
|
|
110
|
+
|
|
111
|
+
If you try to access the `soon_value.value` inside the `async with` block,
|
|
112
|
+
before it has the actual return value, it will raise a an exception
|
|
113
|
+
`asyncer.PendingValueException`.
|
|
114
|
+
|
|
115
|
+
If you think you need to access the return values inside the `async with` block,
|
|
116
|
+
there's a high chance that you really need a different approach, for example
|
|
117
|
+
using an AnyIO Stream.
|
|
118
|
+
|
|
119
|
+
But either way, if you have checkpoints inside the `async with` block (you have
|
|
120
|
+
some `await` there), one or more of the `SoonValue` objects you might have
|
|
121
|
+
could end up having the result value ready before ending the `async with` block.
|
|
122
|
+
You can check that with `soon_value.pending`. For example:
|
|
123
|
+
|
|
124
|
+
```Python
|
|
125
|
+
async def do_work(name: str) -> str:
|
|
126
|
+
return f"Hello {name}"
|
|
127
|
+
|
|
128
|
+
async with asyncer.create_task_group() as task_group:
|
|
129
|
+
result1 = task_group.soonify(do_work)(name="task 1")
|
|
130
|
+
result2 = task_group.soonify(do_work)(name="task 2")
|
|
131
|
+
await anyio.sleep(0)
|
|
132
|
+
if not result1.pending:
|
|
133
|
+
print(result1.value)
|
|
134
|
+
if not result2.pending:
|
|
135
|
+
print(result2.value)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
## Arguments
|
|
140
|
+
|
|
141
|
+
`async_function`: an async function to call soon
|
|
142
|
+
`name`: name of the task, for the purposes of introspection and debugging
|
|
143
|
+
|
|
144
|
+
## Return
|
|
145
|
+
|
|
146
|
+
A function that takes positional and keyword arguments and when called
|
|
147
|
+
uses `task_group.start_soon()` to start the task in this task group.
|
|
148
|
+
|
|
149
|
+
That function returns a `SoonValue` object holding the return value of the
|
|
150
|
+
original function in `soon_value.value`.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
@functools.wraps(async_function)
|
|
154
|
+
def wrapper(
|
|
155
|
+
*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
|
|
156
|
+
) -> SoonValue[T]:
|
|
157
|
+
partial_f = functools.partial(async_function, *args, **kwargs)
|
|
158
|
+
soon_value: SoonValue[T] = SoonValue()
|
|
159
|
+
|
|
160
|
+
@functools.wraps(partial_f)
|
|
161
|
+
async def value_wrapper(*args: Any) -> None:
|
|
162
|
+
value = await partial_f()
|
|
163
|
+
soon_value._stored_value = value
|
|
164
|
+
|
|
165
|
+
self.start_soon(value_wrapper, name=name)
|
|
166
|
+
return soon_value
|
|
167
|
+
|
|
168
|
+
return wrapper
|
|
169
|
+
|
|
170
|
+
# This is only for the return type annotation, but it won't really be called
|
|
171
|
+
async def __aenter__(self) -> "TaskGroup": # pragma: nocover
|
|
172
|
+
"""Enter the task group context and allow starting new tasks."""
|
|
173
|
+
return await super().__aenter__() # type: ignore
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def create_task_group() -> "TaskGroup":
|
|
177
|
+
"""
|
|
178
|
+
Create a task group used to start multiple concurrent tasks with async functions.
|
|
179
|
+
|
|
180
|
+
`asyncer.create_task_group()` is different from `anyio.create_task_group()` in that
|
|
181
|
+
it creates an extended `TaskGroup` object that includes the `task_group.soonify()`
|
|
182
|
+
method.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
LibTaskGroup = get_asynclib().TaskGroup
|
|
186
|
+
|
|
187
|
+
class ExtendedTaskGroup(LibTaskGroup, TaskGroup): # type: ignore
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
return ExtendedTaskGroup()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def runnify(
|
|
194
|
+
async_function: Callable[T_ParamSpec, Coroutine[Any, Any, T_Retval]],
|
|
195
|
+
backend: str = "asyncio",
|
|
196
|
+
backend_options: Optional[Dict[str, Any]] = None,
|
|
197
|
+
) -> Callable[T_ParamSpec, T_Retval]:
|
|
198
|
+
"""
|
|
199
|
+
Take an async function and create a regular (blocking) function that receives the
|
|
200
|
+
same keyword and positional arguments for the original async function, and that when
|
|
201
|
+
called will create an event loop and use it to run the original `async_function`
|
|
202
|
+
with those arguments.
|
|
203
|
+
|
|
204
|
+
That function returns the return value from the original `async_function`.
|
|
205
|
+
|
|
206
|
+
The current thread must not be already running an event loop.
|
|
207
|
+
|
|
208
|
+
This calls `anyio.run()` underneath.
|
|
209
|
+
|
|
210
|
+
Use it like this:
|
|
211
|
+
|
|
212
|
+
```Python
|
|
213
|
+
async def program(name: str) -> str:
|
|
214
|
+
return f"Hello {name}"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
result = asyncer.runnify(program)(name="World")
|
|
218
|
+
print(result)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Arguments
|
|
222
|
+
|
|
223
|
+
`async_function`: an async function to call
|
|
224
|
+
`backend`: name of the asynchronous event loop implementation - currently either
|
|
225
|
+
`asyncio` or `trio`
|
|
226
|
+
`backend_options` keyword arguments to call the backend `run()` implementation with
|
|
227
|
+
|
|
228
|
+
## Return
|
|
229
|
+
|
|
230
|
+
The return value of the async function
|
|
231
|
+
|
|
232
|
+
## Raises
|
|
233
|
+
|
|
234
|
+
`RuntimeError`: if an asynchronous event loop is already running in this thread
|
|
235
|
+
`LookupError`: if the named backend is not found
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
@functools.wraps(async_function)
|
|
239
|
+
def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval:
|
|
240
|
+
partial_f = functools.partial(async_function, *args, **kwargs)
|
|
241
|
+
|
|
242
|
+
return anyio.run(partial_f, backend=backend, backend_options=backend_options)
|
|
243
|
+
|
|
244
|
+
return wrapper
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def syncify(
|
|
248
|
+
async_function: Callable[T_ParamSpec, Coroutine[Any, Any, T_Retval]],
|
|
249
|
+
raise_sync_error: bool = True,
|
|
250
|
+
) -> Callable[T_ParamSpec, T_Retval]:
|
|
251
|
+
"""
|
|
252
|
+
Take an async function and create a regular one that receives the same keyword and
|
|
253
|
+
positional arguments, and that when called, calls the original async function in
|
|
254
|
+
the main async loop from the worker thread using `anyio.to_thread.run()`.
|
|
255
|
+
|
|
256
|
+
By default this is expected to be used from a worker thread. For example inside
|
|
257
|
+
some function passed to `asyncify()`.
|
|
258
|
+
|
|
259
|
+
But if you set `raise_sync_error` to `False`, you can also use this function
|
|
260
|
+
in a non-async context: without an async event loop. For example, from a
|
|
261
|
+
blocking/regular function called at the top level of a Python file. In that case,
|
|
262
|
+
if it is not being called from inside a worker thread started from an async context
|
|
263
|
+
(e.g. this is not called from a function that was called with `asyncify()`) it will
|
|
264
|
+
run `async_function` in a new async event loop with `anyio.run()`.
|
|
265
|
+
|
|
266
|
+
This functionality with `raise_sync_error` is there only to allow using
|
|
267
|
+
`syncify()` in codebases that are used by async code in some cases and by blocking
|
|
268
|
+
code in others. For example, during migrations from blocking code to async code.
|
|
269
|
+
|
|
270
|
+
Internally, `asyncer.syncify()` uses the same `anyio.from_thread.run()`, but it
|
|
271
|
+
supports keyword arguments additional to positional arguments and it adds better
|
|
272
|
+
support for tooling (e.g. editor autocompletion and inline errors) for the
|
|
273
|
+
arguments and return value of the function.
|
|
274
|
+
|
|
275
|
+
Use it like this:
|
|
276
|
+
|
|
277
|
+
```Python
|
|
278
|
+
async def do_work(arg1, arg2, kwarg1="", kwarg2=""):
|
|
279
|
+
# Do work
|
|
280
|
+
|
|
281
|
+
result = from_thread.syncify(do_work)("spam", "ham", kwarg1="a", kwarg2="b")
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Arguments
|
|
285
|
+
|
|
286
|
+
`async_function`: an async function to be called in the main thread, in the async
|
|
287
|
+
event loop
|
|
288
|
+
`raise_sync_error`: If set to `False`, when used in a non-async context (without
|
|
289
|
+
an async event loop), it will run `async_function` in a new async event loop,
|
|
290
|
+
instead of raising an exception.
|
|
291
|
+
|
|
292
|
+
## Return
|
|
293
|
+
|
|
294
|
+
A regular blocking function that takes the same positional and keyword arguments
|
|
295
|
+
as the original async one, that when called runs the same original function in
|
|
296
|
+
the main async loop when called from a worker thread and returns the result.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
@functools.wraps(async_function)
|
|
300
|
+
def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval:
|
|
301
|
+
current_async_module = getattr(threadlocals, "current_async_module", None)
|
|
302
|
+
partial_f = functools.partial(async_function, *args, **kwargs)
|
|
303
|
+
if current_async_module is None and raise_sync_error is False:
|
|
304
|
+
return anyio.run(partial_f)
|
|
305
|
+
return anyio.from_thread.run(partial_f)
|
|
306
|
+
|
|
307
|
+
return wrapper
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def asyncify(
|
|
311
|
+
function: Callable[T_ParamSpec, T_Retval],
|
|
312
|
+
*,
|
|
313
|
+
cancellable: bool = False,
|
|
314
|
+
limiter: Optional[anyio.CapacityLimiter] = None,
|
|
315
|
+
) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
|
|
316
|
+
"""
|
|
317
|
+
Take a blocking function and create an async one that receives the same
|
|
318
|
+
positional and keyword arguments, and that when called, calls the original function
|
|
319
|
+
in a worker thread using `anyio.to_thread.run_sync()`. Internally,
|
|
320
|
+
`asyncer.asyncify()` uses the same `anyio.to_thread.run_sync()`, but it supports
|
|
321
|
+
keyword arguments additional to positional arguments and it adds better support for
|
|
322
|
+
autocompletion and inline errors for the arguments of the function called and the
|
|
323
|
+
return value.
|
|
324
|
+
|
|
325
|
+
If the `cancellable` option is enabled and the task waiting for its completion is
|
|
326
|
+
cancelled, the thread will still run its course but its return value (or any raised
|
|
327
|
+
exception) will be ignored.
|
|
328
|
+
|
|
329
|
+
Use it like this:
|
|
330
|
+
|
|
331
|
+
```Python
|
|
332
|
+
def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str:
|
|
333
|
+
# Do work
|
|
334
|
+
return "Some result"
|
|
335
|
+
|
|
336
|
+
result = await to_thread.asyncify(do_work)("spam", "ham", kwarg1="a", kwarg2="b")
|
|
337
|
+
print(result)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Arguments
|
|
341
|
+
|
|
342
|
+
`function`: a blocking regular callable (e.g. a function)
|
|
343
|
+
`cancellable`: `True` to allow cancellation of the operation
|
|
344
|
+
`limiter`: capacity limiter to use to limit the total amount of threads running
|
|
345
|
+
(if omitted, the default limiter is used)
|
|
346
|
+
|
|
347
|
+
## Return
|
|
348
|
+
|
|
349
|
+
An async function that takes the same positional and keyword arguments as the
|
|
350
|
+
original one, that when called runs the same original function in a thread worker
|
|
351
|
+
and returns the result.
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
async def wrapper(
|
|
355
|
+
*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
|
|
356
|
+
) -> T_Retval:
|
|
357
|
+
partial_f = functools.partial(function, *args, **kwargs)
|
|
358
|
+
return await anyio.to_thread.run_sync(
|
|
359
|
+
partial_f, cancellable=cancellable, limiter=limiter
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
return wrapper
|
asyncer/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Sebastián Ramírez
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: asyncer
|
|
3
|
+
Version: 0
|
|
4
|
+
Summary: Asyncer, async and await, focused on developer experience.
|
|
5
|
+
Home-page: https://github.com/tiangolo/asyncer
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: Sebastián Ramírez
|
|
8
|
+
Author-email: tiangolo@gmail.com
|
|
9
|
+
Requires-Python: >=3.8,<4.0
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Framework :: Trio
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Education
|
|
15
|
+
Classifier: Intended Audience :: Information Technology
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Intended Audience :: System Administrators
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
26
|
+
Classifier: Topic :: Internet
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Dist: anyio (>=3.4.0,<5.0)
|
|
29
|
+
Requires-Dist: typing_extensions (>=4.8.0,<5.0.0) ; python_version < "3.10"
|
|
30
|
+
Project-URL: Documentation, https://asyncer.tiangolo.com
|
|
31
|
+
Project-URL: Repository, https://github.com/tiangolo/asyncer
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<a href="https://asyncer.tiangolo.com"><img src="https://asyncer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" alt="Asyncer"></a>
|
|
36
|
+
</p>
|
|
37
|
+
<p align="center">
|
|
38
|
+
<em>Asyncer, async and await, focused on developer experience.</em>
|
|
39
|
+
</p>
|
|
40
|
+
<p align="center">
|
|
41
|
+
<a href="https://github.com/tiangolo/asyncer/actions?query=workflow%3ATest" target="_blank">
|
|
42
|
+
<img src="https://github.com/tiangolo/asyncer/workflows/Test/badge.svg" alt="Test">
|
|
43
|
+
</a>
|
|
44
|
+
<a href="https://github.com/tiangolo/asyncer/actions?query=workflow%3APublish" target="_blank">
|
|
45
|
+
<img src="https://github.com/tiangolo/asyncer/workflows/Publish/badge.svg" alt="Publish">
|
|
46
|
+
</a>
|
|
47
|
+
<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/tiangolo/asyncer" target="_blank">
|
|
48
|
+
<img src="https://coverage-badge.samuelcolvin.workers.dev/tiangolo/asyncer.svg" alt="Coverage">
|
|
49
|
+
<a href="https://pypi.org/project/asyncer" target="_blank">
|
|
50
|
+
<img src="https://img.shields.io/pypi/v/asyncer?color=%2334D058&label=pypi%20package" alt="Package version">
|
|
51
|
+
</a>
|
|
52
|
+
</p>
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
**Documentation**: <a href="https://asyncer.tiangolo.com" target="_blank">https://asyncer.tiangolo.com</a>
|
|
57
|
+
|
|
58
|
+
**Source Code**: <a href="https://github.com/tiangolo/asyncer" target="_blank">https://github.com/tiangolo/asyncer</a>
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
**Asyncer** is a small library built on top of <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a>.
|
|
63
|
+
|
|
64
|
+
**Asyncer** has a small number of utility functions that allow working with `async`, `await`, and concurrent code in a more convenient way under my (<a href="https://twitter.com/tiangolo" class="external-link" target="_blank">@tiangolo - Sebastián Ramírez</a>) very opinionated and subjective point of view.
|
|
65
|
+
|
|
66
|
+
The main goal of **Asyncer** is to improve **developer experience** by providing better support for **autocompletion** and **inline errors** in the editor, and **more certainty** that the code is **bug-free** by providing better support for type checking tools like **mypy**.
|
|
67
|
+
|
|
68
|
+
**Asyncer** also tries to improve **convenience** and simplicity when working with **async** code **mixed** with regular <abbr title="synchronous code, code that is not async">**blocking code**</abbr>, allowing to use them together in a simpler way... again, under my very **subjective** point of view.
|
|
69
|
+
|
|
70
|
+
## 🚨 Warning
|
|
71
|
+
|
|
72
|
+
This small library only exists to be able to use these **utility functions** until (and if) they are integrated into **AnyIO**.
|
|
73
|
+
|
|
74
|
+
It will probably take some time for that to happen (or to be decided if it will be included or not).
|
|
75
|
+
|
|
76
|
+
So I made this to be able to use these ideas right now. 🤓
|
|
77
|
+
|
|
78
|
+
## Can I Use It?
|
|
79
|
+
|
|
80
|
+
Yes 🎉 (but continue reading).
|
|
81
|
+
|
|
82
|
+
You can use this and evaluate the **library API design** I'm proposing. It will probably be useful to know if it works and is useful for you (I hope so).
|
|
83
|
+
|
|
84
|
+
But still, consider this lab material, expect it to change a bit. 🧪
|
|
85
|
+
|
|
86
|
+
If you use it, **pin the exact Asyncer version** for your project, to make sure it all works.
|
|
87
|
+
|
|
88
|
+
Have **tests** for your project (as you should, anyway). And **upgrade the version** once you know that the new version continues to work correctly.
|
|
89
|
+
|
|
90
|
+
Still, it's **just 4 functions**, so there's not much to change, if you had to refactor your code to update something it would not be much.
|
|
91
|
+
|
|
92
|
+
And if you don't want to add `asyncer` as a dependency to your project, you can also just copy the main file and try out those functions, it's quite small (but in that case you won't get updates easily).
|
|
93
|
+
|
|
94
|
+
## Requirements
|
|
95
|
+
|
|
96
|
+
As **Asyncer** is based on **AnyIO** it will be also installed automatically when you install **Asyncer**.
|
|
97
|
+
|
|
98
|
+
## Installation
|
|
99
|
+
|
|
100
|
+
<div class="termy">
|
|
101
|
+
|
|
102
|
+
```console
|
|
103
|
+
$ pip install asyncer
|
|
104
|
+
---> 100%
|
|
105
|
+
Successfully installed asyncer anyio
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
## How to Use
|
|
111
|
+
|
|
112
|
+
You can read more about each of the use cases and utility functions in **Asyncer** in the <a href="https://asyncer.tiangolo.com/tutorial/" class="external-link" target="_blank">tutorial</a>.
|
|
113
|
+
|
|
114
|
+
As a sneak preview of one of the utilities, you can **call sync code from async code** using `asyncify()`:
|
|
115
|
+
|
|
116
|
+
```Python
|
|
117
|
+
import time
|
|
118
|
+
|
|
119
|
+
import anyio
|
|
120
|
+
from asyncer import asyncify
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def do_sync_work(name: str):
|
|
124
|
+
time.sleep(1)
|
|
125
|
+
return f"Hello, {name}"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def main():
|
|
129
|
+
message = await asyncify(do_sync_work)(name="World")
|
|
130
|
+
print(message)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
anyio.run(main)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Asyncer**'s `asyncify()` will use AnyIO underneath to do *the smart thing*, avoid blocking the main **async** event loop, and run the **sync**/blocking function in a **worker thread**.
|
|
137
|
+
|
|
138
|
+
### Editor Support
|
|
139
|
+
|
|
140
|
+
Everything in **Asyncer** is designed to get the best **developer experience** possible, with the best editor support.
|
|
141
|
+
|
|
142
|
+
* **Autocompletion** for function arguments:
|
|
143
|
+
|
|
144
|
+
<img class="shadow" src="https://asyncer.tiangolo.com/img/tutorial/asyncify/image01.png">
|
|
145
|
+
|
|
146
|
+
* **Autocompletion** for return values:
|
|
147
|
+
|
|
148
|
+
<img class="shadow" src="https://asyncer.tiangolo.com/img/tutorial/asyncify/image02.png">
|
|
149
|
+
|
|
150
|
+
* **Inline errors** in editor:
|
|
151
|
+
|
|
152
|
+
<img class="shadow" src="https://asyncer.tiangolo.com/img/tutorial/soonify/image02.png">
|
|
153
|
+
|
|
154
|
+
* Support for tools like **mypy**, that can help you verify that your **code is correct**, and prevent many bugs.
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
This project is licensed under the terms of the [MIT license](https://github.com/tiangolo/asyncer/blob/main/LICENSE).
|
|
159
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
asyncer/__init__.py,sha256=zwXbjrX3oFTeVasflK20r9_5ilCNF9mlJ8xkvDqpLr8,347
|
|
2
|
+
asyncer/_main.py,sha256=GdSwgl6O8oEA-qVch4DOCKIHUCVbU62czQY9pniWKZI,12826
|
|
3
|
+
asyncer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
asyncer-0.dist-info/LICENSE,sha256=S1EnT02PpzNeD8_rIBcSbkGC_9aw6MWKW4myxIlzFIc,1086
|
|
5
|
+
asyncer-0.dist-info/METADATA,sha256=v90P5qxiI0dtjoGUW-f9XmMuC3WM0hGBsVEwbcS5ZfQ,6653
|
|
6
|
+
asyncer-0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
7
|
+
asyncer-0.dist-info/RECORD,,
|