asyncer 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.
asyncer-0/LICENSE ADDED
@@ -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.
asyncer-0/PKG-INFO ADDED
@@ -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
+
asyncer-0/README.md ADDED
@@ -0,0 +1,125 @@
1
+ <p align="center">
2
+ <a href="https://asyncer.tiangolo.com"><img src="https://asyncer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" alt="Asyncer"></a>
3
+ </p>
4
+ <p align="center">
5
+ <em>Asyncer, async and await, focused on developer experience.</em>
6
+ </p>
7
+ <p align="center">
8
+ <a href="https://github.com/tiangolo/asyncer/actions?query=workflow%3ATest" target="_blank">
9
+ <img src="https://github.com/tiangolo/asyncer/workflows/Test/badge.svg" alt="Test">
10
+ </a>
11
+ <a href="https://github.com/tiangolo/asyncer/actions?query=workflow%3APublish" target="_blank">
12
+ <img src="https://github.com/tiangolo/asyncer/workflows/Publish/badge.svg" alt="Publish">
13
+ </a>
14
+ <a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/tiangolo/asyncer" target="_blank">
15
+ <img src="https://coverage-badge.samuelcolvin.workers.dev/tiangolo/asyncer.svg" alt="Coverage">
16
+ <a href="https://pypi.org/project/asyncer" target="_blank">
17
+ <img src="https://img.shields.io/pypi/v/asyncer?color=%2334D058&label=pypi%20package" alt="Package version">
18
+ </a>
19
+ </p>
20
+
21
+ ---
22
+
23
+ **Documentation**: <a href="https://asyncer.tiangolo.com" target="_blank">https://asyncer.tiangolo.com</a>
24
+
25
+ **Source Code**: <a href="https://github.com/tiangolo/asyncer" target="_blank">https://github.com/tiangolo/asyncer</a>
26
+
27
+ ---
28
+
29
+ **Asyncer** is a small library built on top of <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a>.
30
+
31
+ **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.
32
+
33
+ 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**.
34
+
35
+ **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.
36
+
37
+ ## 🚨 Warning
38
+
39
+ This small library only exists to be able to use these **utility functions** until (and if) they are integrated into **AnyIO**.
40
+
41
+ It will probably take some time for that to happen (or to be decided if it will be included or not).
42
+
43
+ So I made this to be able to use these ideas right now. 🤓
44
+
45
+ ## Can I Use It?
46
+
47
+ Yes 🎉 (but continue reading).
48
+
49
+ 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).
50
+
51
+ But still, consider this lab material, expect it to change a bit. 🧪
52
+
53
+ If you use it, **pin the exact Asyncer version** for your project, to make sure it all works.
54
+
55
+ Have **tests** for your project (as you should, anyway). And **upgrade the version** once you know that the new version continues to work correctly.
56
+
57
+ 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.
58
+
59
+ 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).
60
+
61
+ ## Requirements
62
+
63
+ As **Asyncer** is based on **AnyIO** it will be also installed automatically when you install **Asyncer**.
64
+
65
+ ## Installation
66
+
67
+ <div class="termy">
68
+
69
+ ```console
70
+ $ pip install asyncer
71
+ ---> 100%
72
+ Successfully installed asyncer anyio
73
+ ```
74
+
75
+ </div>
76
+
77
+ ## How to Use
78
+
79
+ 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>.
80
+
81
+ As a sneak preview of one of the utilities, you can **call sync code from async code** using `asyncify()`:
82
+
83
+ ```Python
84
+ import time
85
+
86
+ import anyio
87
+ from asyncer import asyncify
88
+
89
+
90
+ def do_sync_work(name: str):
91
+ time.sleep(1)
92
+ return f"Hello, {name}"
93
+
94
+
95
+ async def main():
96
+ message = await asyncify(do_sync_work)(name="World")
97
+ print(message)
98
+
99
+
100
+ anyio.run(main)
101
+ ```
102
+
103
+ **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**.
104
+
105
+ ### Editor Support
106
+
107
+ Everything in **Asyncer** is designed to get the best **developer experience** possible, with the best editor support.
108
+
109
+ * **Autocompletion** for function arguments:
110
+
111
+ <img class="shadow" src="https://asyncer.tiangolo.com/img/tutorial/asyncify/image01.png">
112
+
113
+ * **Autocompletion** for return values:
114
+
115
+ <img class="shadow" src="https://asyncer.tiangolo.com/img/tutorial/asyncify/image02.png">
116
+
117
+ * **Inline errors** in editor:
118
+
119
+ <img class="shadow" src="https://asyncer.tiangolo.com/img/tutorial/soonify/image02.png">
120
+
121
+ * Support for tools like **mypy**, that can help you verify that your **code is correct**, and prevent many bugs.
122
+
123
+ ## License
124
+
125
+ This project is licensed under the terms of the [MIT license](https://github.com/tiangolo/asyncer/blob/main/LICENSE).
@@ -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
@@ -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
File without changes
@@ -0,0 +1,101 @@
1
+ [tool.poetry]
2
+ name = "asyncer"
3
+ version = "0"
4
+ description = "Asyncer, async and await, focused on developer experience."
5
+ authors = ["Sebastián Ramírez <tiangolo@gmail.com>"]
6
+ readme = "README.md"
7
+ homepage = "https://github.com/tiangolo/asyncer"
8
+ documentation = "https://asyncer.tiangolo.com"
9
+ repository = "https://github.com/tiangolo/asyncer"
10
+ license = "MIT"
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Framework :: AsyncIO",
14
+ "Framework :: Trio",
15
+ "Intended Audience :: Developers",
16
+ "Intended Audience :: Education",
17
+ "Intended Audience :: Information Technology",
18
+ "Intended Audience :: Science/Research",
19
+ "Intended Audience :: System Administrators",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.8",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Topic :: Internet",
28
+ "Typing :: Typed",
29
+ ]
30
+
31
+ [tool.poetry.dependencies]
32
+ python = "^3.8"
33
+ anyio = ">=3.4.0,<5.0"
34
+ typing_extensions = { version = "^4.8.0", python = "<3.10" }
35
+
36
+ [tool.poetry.dev-dependencies]
37
+ pytest = "^7.0.1"
38
+ mypy = "1.4.1"
39
+ mkdocs-material = "9.2.7"
40
+ pillow = "^9.3.0"
41
+ cairosvg = "^2.5.2"
42
+ mdx-include = "^1.4.1"
43
+ coverage = {extras = ["toml"], version = ">=6.2,<8.0"}
44
+ ruff = "^0.1.2"
45
+
46
+ [build-system]
47
+ requires = ["poetry-core"]
48
+ build-backend = "poetry.core.masonry.api"
49
+
50
+ [tool.poetry-version-plugin]
51
+ source = "init"
52
+
53
+ [tool.coverage.run]
54
+ parallel = true
55
+ source = [
56
+ "docs_src",
57
+ "tests",
58
+ "asyncer"
59
+ ]
60
+ context = '${CONTEXT}'
61
+
62
+ [tool.coverage.report]
63
+ omit = [
64
+ "docs_src/tutorial/soonify_return/tutorial002.py",
65
+ ]
66
+
67
+ [tool.mypy]
68
+ strict = true
69
+
70
+ [[tool.mypy.overrides]]
71
+ module = "docs_src.*"
72
+ disallow_incomplete_defs = false
73
+ disallow_untyped_defs = false
74
+ disallow_untyped_calls = false
75
+ warn_no_return = false
76
+
77
+ [tool.ruff]
78
+ select = [
79
+ "E", # pycodestyle errors
80
+ "W", # pycodestyle warnings
81
+ "F", # pyflakes
82
+ "I", # isort
83
+ "B", # flake8-bugbear
84
+ "C4", # flake8-comprehensions
85
+ "UP", # pyupgrade
86
+ ]
87
+ ignore = [
88
+ "E501", # line too long, handled by black
89
+ "B008", # do not perform function calls in argument defaults
90
+ "W191", # indentation contains tabs
91
+ ]
92
+
93
+ [tool.ruff.per-file-ignores]
94
+ "__init__.py" = ["F401"]
95
+
96
+ [tool.ruff.isort]
97
+ known-third-party = ["asyncer", "anyio"]
98
+
99
+ [tool.ruff.pyupgrade]
100
+ # Preserve types, even if a file imports `from __future__ import annotations`.
101
+ keep-runtime-typing = true