ez-a-sync 0.22.14__py3-none-any.whl → 0.22.16__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.
Potentially problematic release.
This version of ez-a-sync might be problematic. Click here for more details.
- a_sync/ENVIRONMENT_VARIABLES.py +37 -5
- a_sync/__init__.py +53 -12
- a_sync/_smart.py +231 -28
- a_sync/_typing.py +112 -15
- a_sync/a_sync/__init__.py +35 -10
- a_sync/a_sync/_descriptor.py +248 -38
- a_sync/a_sync/_flags.py +78 -9
- a_sync/a_sync/_helpers.py +46 -13
- a_sync/a_sync/_kwargs.py +33 -8
- a_sync/a_sync/_meta.py +149 -28
- a_sync/a_sync/abstract.py +150 -28
- a_sync/a_sync/base.py +34 -16
- a_sync/a_sync/config.py +85 -14
- a_sync/a_sync/decorator.py +441 -139
- a_sync/a_sync/function.py +709 -147
- a_sync/a_sync/method.py +437 -110
- a_sync/a_sync/modifiers/__init__.py +85 -5
- a_sync/a_sync/modifiers/cache/__init__.py +116 -17
- a_sync/a_sync/modifiers/cache/memory.py +130 -20
- a_sync/a_sync/modifiers/limiter.py +101 -22
- a_sync/a_sync/modifiers/manager.py +142 -16
- a_sync/a_sync/modifiers/semaphores.py +121 -15
- a_sync/a_sync/property.py +383 -82
- a_sync/a_sync/singleton.py +44 -19
- a_sync/aliases.py +0 -1
- a_sync/asyncio/__init__.py +140 -1
- a_sync/asyncio/as_completed.py +213 -79
- a_sync/asyncio/create_task.py +70 -20
- a_sync/asyncio/gather.py +125 -58
- a_sync/asyncio/utils.py +3 -3
- a_sync/exceptions.py +248 -26
- a_sync/executor.py +164 -69
- a_sync/future.py +1227 -168
- a_sync/iter.py +173 -56
- a_sync/primitives/__init__.py +14 -2
- a_sync/primitives/_debug.py +72 -18
- a_sync/primitives/_loggable.py +41 -10
- a_sync/primitives/locks/__init__.py +5 -2
- a_sync/primitives/locks/counter.py +107 -38
- a_sync/primitives/locks/event.py +21 -7
- a_sync/primitives/locks/prio_semaphore.py +262 -63
- a_sync/primitives/locks/semaphore.py +138 -89
- a_sync/primitives/queue.py +601 -60
- a_sync/sphinx/__init__.py +0 -1
- a_sync/sphinx/ext.py +160 -50
- a_sync/task.py +313 -112
- a_sync/utils/__init__.py +12 -6
- a_sync/utils/iterators.py +170 -50
- {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.16.dist-info}/METADATA +1 -1
- ez_a_sync-0.22.16.dist-info/RECORD +74 -0
- {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.16.dist-info}/WHEEL +1 -1
- tests/conftest.py +1 -2
- tests/executor.py +250 -9
- tests/fixtures.py +61 -32
- tests/test_abstract.py +22 -4
- tests/test_as_completed.py +54 -21
- tests/test_base.py +264 -19
- tests/test_cache.py +31 -15
- tests/test_decorator.py +54 -28
- tests/test_executor.py +31 -13
- tests/test_future.py +45 -8
- tests/test_gather.py +8 -2
- tests/test_helpers.py +2 -0
- tests/test_iter.py +55 -13
- tests/test_limiter.py +5 -3
- tests/test_meta.py +23 -9
- tests/test_modified.py +4 -1
- tests/test_semaphore.py +15 -8
- tests/test_singleton.py +28 -11
- tests/test_task.py +162 -36
- ez_a_sync-0.22.14.dist-info/RECORD +0 -74
- {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.16.dist-info}/LICENSE.txt +0 -0
- {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.16.dist-info}/top_level.txt +0 -0
a_sync/iter.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
import asyncio
|
|
3
2
|
import functools
|
|
4
3
|
import inspect
|
|
@@ -22,6 +21,7 @@ else:
|
|
|
22
21
|
SortKey = SyncFn[[T], bool]
|
|
23
22
|
ViewFn = AnyFn[[T], bool]
|
|
24
23
|
|
|
24
|
+
|
|
25
25
|
class _AwaitableAsyncIterableMixin(AsyncIterable[T]):
|
|
26
26
|
"""
|
|
27
27
|
A mixin class defining logic for making an AsyncIterable awaitable.
|
|
@@ -30,24 +30,23 @@ class _AwaitableAsyncIterableMixin(AsyncIterable[T]):
|
|
|
30
30
|
|
|
31
31
|
Example:
|
|
32
32
|
You must subclass this mixin class and define your own `__aiter__` method as shown below.
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
>>> class MyAwaitableAIterable(_AwaitableAsyncIterableMixin):
|
|
35
|
-
... def __aiter__(self):
|
|
35
|
+
... async def __aiter__(self):
|
|
36
36
|
... for i in range(4):
|
|
37
37
|
... yield i
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
>>> aiterable = MyAwaitableAIterable()
|
|
40
40
|
>>> await aiterable
|
|
41
|
-
[0, 1, 2, 3
|
|
42
|
-
|
|
43
|
-
```
|
|
41
|
+
[0, 1, 2, 3]
|
|
44
42
|
"""
|
|
43
|
+
|
|
45
44
|
__wrapped__: AsyncIterable[T]
|
|
46
|
-
|
|
45
|
+
|
|
47
46
|
def __await__(self) -> Generator[Any, Any, List[T]]:
|
|
48
47
|
"""
|
|
49
48
|
Asynchronously iterate through the {cls} and return all objects.
|
|
50
|
-
|
|
49
|
+
|
|
51
50
|
Returns:
|
|
52
51
|
A list of the objects yielded by the {cls}.
|
|
53
52
|
"""
|
|
@@ -57,13 +56,15 @@ class _AwaitableAsyncIterableMixin(AsyncIterable[T]):
|
|
|
57
56
|
def materialized(self) -> List[T]:
|
|
58
57
|
"""
|
|
59
58
|
Synchronously iterate through the {cls} and return all objects.
|
|
60
|
-
|
|
59
|
+
|
|
61
60
|
Returns:
|
|
62
61
|
A list of the objects yielded by the {cls}.
|
|
63
62
|
"""
|
|
64
63
|
return _helpers._await(self._materialized)
|
|
65
64
|
|
|
66
|
-
def sort(
|
|
65
|
+
def sort(
|
|
66
|
+
self, *, key: SortKey[T] = None, reverse: bool = False
|
|
67
|
+
) -> "ASyncSorter[T]":
|
|
67
68
|
"""
|
|
68
69
|
Sort the contents of the {cls}.
|
|
69
70
|
|
|
@@ -92,7 +93,7 @@ class _AwaitableAsyncIterableMixin(AsyncIterable[T]):
|
|
|
92
93
|
async def _materialized(self) -> List[T]:
|
|
93
94
|
"""
|
|
94
95
|
Asynchronously iterate through the {cls} and return all objects.
|
|
95
|
-
|
|
96
|
+
|
|
96
97
|
Returns:
|
|
97
98
|
A list of the objects yielded by the {cls}.
|
|
98
99
|
"""
|
|
@@ -106,7 +107,7 @@ class _AwaitableAsyncIterableMixin(AsyncIterable[T]):
|
|
|
106
107
|
cls.__doc__ = new
|
|
107
108
|
else:
|
|
108
109
|
cls.__doc__ += f"\n\n{new}"
|
|
109
|
-
|
|
110
|
+
|
|
110
111
|
# format the member docstrings
|
|
111
112
|
for attr_name in dir(cls):
|
|
112
113
|
attr = getattr(cls, attr_name, None)
|
|
@@ -115,27 +116,52 @@ class _AwaitableAsyncIterableMixin(AsyncIterable[T]):
|
|
|
115
116
|
|
|
116
117
|
return super().__init_subclass__(**kwargs)
|
|
117
118
|
|
|
118
|
-
__slots__ =
|
|
119
|
-
|
|
119
|
+
__slots__ = ("__async_property__",)
|
|
120
|
+
|
|
121
|
+
|
|
120
122
|
class ASyncIterable(_AwaitableAsyncIterableMixin[T], Iterable[T]):
|
|
121
123
|
"""
|
|
122
124
|
A hybrid Iterable/AsyncIterable implementation designed to offer dual compatibility with both synchronous and asynchronous iteration protocols.
|
|
123
|
-
|
|
125
|
+
|
|
124
126
|
This class allows objects to be iterated over using either a standard `for` loop or an `async for` loop, making it versatile in scenarios where the mode of iteration (synchronous or asynchronous) needs to be flexible or is determined at runtime.
|
|
125
127
|
|
|
126
|
-
The class achieves this by implementing both `__iter__` and `__aiter__` methods, enabling it to return appropriate iterator objects that can handle synchronous and asynchronous iteration, respectively.
|
|
128
|
+
The class achieves this by implementing both `__iter__` and `__aiter__` methods, enabling it to return appropriate iterator objects that can handle synchronous and asynchronous iteration, respectively. However, note that synchronous iteration relies on the :class:`ASyncIterator` class, which uses `asyncio.get_event_loop().run_until_complete` to fetch items. This can raise a `RuntimeError` if the event loop is already running, resulting in a :class:`SyncModeInAsyncContextError`.
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
>>> async_iterable = ASyncIterable(some_async_iterable)
|
|
132
|
+
>>> async for item in async_iterable:
|
|
133
|
+
... print(item)
|
|
134
|
+
>>> for item in async_iterable:
|
|
135
|
+
... print(item)
|
|
136
|
+
|
|
137
|
+
See Also:
|
|
138
|
+
- :class:`ASyncIterator`
|
|
139
|
+
- :class:`ASyncFilter`
|
|
140
|
+
- :class:`ASyncSorter`
|
|
127
141
|
"""
|
|
142
|
+
|
|
128
143
|
@classmethod
|
|
129
144
|
def wrap(cls, wrapped: AsyncIterable[T]) -> "ASyncIterable[T]":
|
|
130
145
|
"Class method to wrap an AsyncIterable for backward compatibility."
|
|
131
|
-
logger.warning(
|
|
146
|
+
logger.warning(
|
|
147
|
+
"ASyncIterable.wrap will be removed soon. Please replace uses with simple instantiation ie `ASyncIterable(wrapped)`"
|
|
148
|
+
)
|
|
132
149
|
return cls(wrapped)
|
|
150
|
+
|
|
133
151
|
def __init__(self, async_iterable: AsyncIterable[T]):
|
|
134
|
-
"
|
|
152
|
+
"""
|
|
153
|
+
Initializes the ASyncIterable with an async iterable.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
async_iterable: The async iterable to wrap.
|
|
157
|
+
"""
|
|
135
158
|
if not isinstance(async_iterable, AsyncIterable):
|
|
136
|
-
raise TypeError(
|
|
159
|
+
raise TypeError(
|
|
160
|
+
f"`async_iterable` must be an AsyncIterable. You passed {async_iterable}"
|
|
161
|
+
)
|
|
137
162
|
self.__wrapped__ = async_iterable
|
|
138
163
|
"The wrapped async iterable object."
|
|
164
|
+
|
|
139
165
|
def __repr__(self) -> str:
|
|
140
166
|
start = f"<{type(self).__name__}"
|
|
141
167
|
if wrapped := getattr(self, "__wrapped__", None):
|
|
@@ -151,23 +177,39 @@ class ASyncIterable(_AwaitableAsyncIterableMixin[T], Iterable[T]):
|
|
|
151
177
|
def __iter__(self) -> Iterator[T]:
|
|
152
178
|
"Return an iterator that yields :obj:`T` objects from the {cls}."
|
|
153
179
|
yield from ASyncIterator(self.__aiter__())
|
|
154
|
-
|
|
180
|
+
|
|
181
|
+
__slots__ = ("__wrapped__",)
|
|
182
|
+
|
|
155
183
|
|
|
156
184
|
AsyncGenFunc = Callable[P, Union[AsyncGenerator[T, None], AsyncIterator[T]]]
|
|
157
185
|
|
|
186
|
+
|
|
158
187
|
class ASyncIterator(_AwaitableAsyncIterableMixin[T], Iterator[T]):
|
|
159
188
|
"""
|
|
160
189
|
A hybrid Iterator/AsyncIterator implementation that bridges the gap between synchronous and asynchronous iteration. This class provides a unified interface for iteration that can seamlessly operate in both synchronous (`for` loop) and asynchronous (`async for` loop) contexts. It allows the wrapping of asynchronous iterable objects or async generator functions, making them usable in synchronous code without explicitly managing event loops or asynchronous context switches.
|
|
161
190
|
|
|
162
191
|
By implementing both `__next__` and `__anext__` methods, ASyncIterator enables objects to be iterated using standard iteration protocols while internally managing the complexities of asynchronous iteration. This design simplifies the use of asynchronous iterables in environments or frameworks that are not inherently asynchronous, such as standard synchronous functions or older codebases being gradually migrated to asynchronous IO.
|
|
163
192
|
|
|
164
|
-
|
|
193
|
+
Note:
|
|
194
|
+
Synchronous iteration with `ASyncIterator` uses `asyncio.get_event_loop().run_until_complete`, which can raise a `RuntimeError` if the event loop is already running. In such cases, a :class:`SyncModeInAsyncContextError` is raised, indicating that synchronous iteration is not possible in an already running event loop.
|
|
195
|
+
|
|
196
|
+
Example:
|
|
197
|
+
>>> async_iterator = ASyncIterator(some_async_iterator)
|
|
198
|
+
>>> async for item in async_iterator:
|
|
199
|
+
... print(item)
|
|
200
|
+
>>> for item in async_iterator:
|
|
201
|
+
... print(item)
|
|
202
|
+
|
|
203
|
+
See Also:
|
|
204
|
+
- :class:`ASyncIterable`
|
|
205
|
+
- :class:`ASyncFilter`
|
|
206
|
+
- :class:`ASyncSorter`
|
|
165
207
|
"""
|
|
166
|
-
|
|
208
|
+
|
|
167
209
|
def __next__(self) -> T:
|
|
168
210
|
"""
|
|
169
211
|
Synchronously fetch the next item from the {cls}.
|
|
170
|
-
|
|
212
|
+
|
|
171
213
|
Raises:
|
|
172
214
|
:class:`StopIteration`: Once all items have been fetched from the {cls}.
|
|
173
215
|
"""
|
|
@@ -177,34 +219,61 @@ class ASyncIterator(_AwaitableAsyncIterableMixin[T], Iterator[T]):
|
|
|
177
219
|
raise StopIteration from e
|
|
178
220
|
except RuntimeError as e:
|
|
179
221
|
if str(e) == "This event loop is already running":
|
|
180
|
-
raise SyncModeInAsyncContextError(
|
|
222
|
+
raise SyncModeInAsyncContextError(
|
|
223
|
+
"The event loop is already running. Try iterating using `async for` instead of `for`."
|
|
224
|
+
) from e
|
|
181
225
|
raise
|
|
182
226
|
|
|
183
227
|
@overload
|
|
184
|
-
def wrap(cls, aiterator: AsyncIterator[T]) -> "ASyncIterator[T]"
|
|
228
|
+
def wrap(cls, aiterator: AsyncIterator[T]) -> "ASyncIterator[T]":
|
|
229
|
+
"""
|
|
230
|
+
Wraps an AsyncIterator in an ASyncIterator.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
aiterator: The AsyncIterator to wrap.
|
|
234
|
+
"""
|
|
235
|
+
|
|
185
236
|
@overload
|
|
186
|
-
def wrap(cls, async_gen_func: AsyncGenFunc[P, T]) -> "ASyncGeneratorFunction[P, T]"
|
|
237
|
+
def wrap(cls, async_gen_func: AsyncGenFunc[P, T]) -> "ASyncGeneratorFunction[P, T]":
|
|
238
|
+
"""
|
|
239
|
+
Wraps an async generator function in an ASyncGeneratorFunction.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
async_gen_func: The async generator function to wrap.
|
|
243
|
+
"""
|
|
244
|
+
|
|
187
245
|
@classmethod
|
|
188
246
|
def wrap(cls, wrapped):
|
|
189
247
|
"Class method to wrap either an AsyncIterator or an async generator function."
|
|
190
248
|
if isinstance(wrapped, AsyncIterator):
|
|
191
|
-
logger.warning(
|
|
249
|
+
logger.warning(
|
|
250
|
+
"This use case for ASyncIterator.wrap will be removed soon. Please replace uses with simple instantiation ie `ASyncIterator(wrapped)`"
|
|
251
|
+
)
|
|
192
252
|
return cls(wrapped)
|
|
193
253
|
elif inspect.isasyncgenfunction(wrapped):
|
|
194
254
|
return ASyncGeneratorFunction(wrapped)
|
|
195
|
-
raise TypeError(
|
|
255
|
+
raise TypeError(
|
|
256
|
+
f"`wrapped` must be an AsyncIterator or an async generator function. You passed {wrapped}"
|
|
257
|
+
)
|
|
196
258
|
|
|
197
259
|
def __init__(self, async_iterator: AsyncIterator[T]):
|
|
198
|
-
"
|
|
260
|
+
"""
|
|
261
|
+
Initializes the ASyncIterator with an async iterator.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
async_iterator: The async iterator to wrap.
|
|
265
|
+
"""
|
|
199
266
|
if not isinstance(async_iterator, AsyncIterator):
|
|
200
|
-
raise TypeError(
|
|
267
|
+
raise TypeError(
|
|
268
|
+
f"`async_iterator` must be an AsyncIterator. You passed {async_iterator}"
|
|
269
|
+
)
|
|
201
270
|
self.__wrapped__ = async_iterator
|
|
202
271
|
"The wrapped :class:`AsyncIterator`."
|
|
203
272
|
|
|
204
273
|
async def __anext__(self) -> T:
|
|
205
274
|
"""
|
|
206
275
|
Asynchronously fetch the next item from the {cls}.
|
|
207
|
-
|
|
276
|
+
|
|
208
277
|
Raises:
|
|
209
278
|
:class:`StopAsyncIteration`: Once all items have been fetched from the {cls}.
|
|
210
279
|
"""
|
|
@@ -218,6 +287,7 @@ class ASyncIterator(_AwaitableAsyncIterableMixin[T], Iterator[T]):
|
|
|
218
287
|
"Return the {cls} for aiteration."
|
|
219
288
|
return self
|
|
220
289
|
|
|
290
|
+
|
|
221
291
|
class ASyncGeneratorFunction(Generic[P, T]):
|
|
222
292
|
"""
|
|
223
293
|
Encapsulates an asynchronous generator function, providing a mechanism to use it as an asynchronous iterator with enhanced capabilities. This class wraps an async generator function, allowing it to be called with parameters and return an :class:`~ASyncIterator` object. It is particularly useful for situations where an async generator function needs to be used in a manner that is consistent with both synchronous and asynchronous execution contexts.
|
|
@@ -225,6 +295,18 @@ class ASyncGeneratorFunction(Generic[P, T]):
|
|
|
225
295
|
The ASyncGeneratorFunction class supports dynamic binding to instances, enabling it to be used as a method on class instances. When accessed as a descriptor, it automatically handles the binding to the instance, thereby allowing the wrapped async generator function to be invoked with instance context ('self') automatically provided. This feature is invaluable for designing classes that need to expose asynchronous generators as part of their interface while maintaining the ease of use and calling semantics similar to regular methods.
|
|
226
296
|
|
|
227
297
|
By providing a unified interface to asynchronous generator functions, this class facilitates the creation of APIs that are flexible and easy to use in a wide range of asynchronous programming scenarios. It abstracts away the complexities involved in managing asynchronous generator lifecycles and invocation semantics, making it easier for developers to integrate asynchronous iteration patterns into their applications.
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
>>> async def my_async_gen():
|
|
301
|
+
... yield 1
|
|
302
|
+
... yield 2
|
|
303
|
+
>>> async_gen_func = ASyncGeneratorFunction(my_async_gen)
|
|
304
|
+
>>> for item in async_gen_func():
|
|
305
|
+
... print(item)
|
|
306
|
+
|
|
307
|
+
See Also:
|
|
308
|
+
- :class:`ASyncIterator`
|
|
309
|
+
- :class:`ASyncIterable`
|
|
228
310
|
"""
|
|
229
311
|
|
|
230
312
|
_cache_handle: asyncio.TimerHandle
|
|
@@ -233,7 +315,9 @@ class ASyncGeneratorFunction(Generic[P, T]):
|
|
|
233
315
|
__weakself__: "weakref.ref[object]" = None
|
|
234
316
|
"A weak reference to the instance the function is bound to, if any."
|
|
235
317
|
|
|
236
|
-
def __init__(
|
|
318
|
+
def __init__(
|
|
319
|
+
self, async_gen_func: AsyncGenFunc[P, T], instance: Any = None
|
|
320
|
+
) -> None:
|
|
237
321
|
"""
|
|
238
322
|
Initializes the ASyncGeneratorFunction with the given async generator function and optionally an instance.
|
|
239
323
|
|
|
@@ -241,7 +325,7 @@ class ASyncGeneratorFunction(Generic[P, T]):
|
|
|
241
325
|
async_gen_func: The async generator function to wrap.
|
|
242
326
|
instance (optional): The object to bind to the function, if applicable.
|
|
243
327
|
"""
|
|
244
|
-
|
|
328
|
+
|
|
245
329
|
self.field_name = async_gen_func.__name__
|
|
246
330
|
"The name of the async generator function."
|
|
247
331
|
|
|
@@ -263,9 +347,6 @@ class ASyncGeneratorFunction(Generic[P, T]):
|
|
|
263
347
|
Args:
|
|
264
348
|
*args: Positional arguments for the function.
|
|
265
349
|
**kwargs: Keyword arguments for the function.
|
|
266
|
-
|
|
267
|
-
Returns:
|
|
268
|
-
An :class:`ASyncIterator` wrapping the :class:`AsyncIterator` returned from the wrapped function call.
|
|
269
350
|
"""
|
|
270
351
|
if self.__weakself__ is None:
|
|
271
352
|
return ASyncIterator(self.__wrapped__(*args, **kwargs))
|
|
@@ -296,11 +377,14 @@ class ASyncGeneratorFunction(Generic[P, T]):
|
|
|
296
377
|
|
|
297
378
|
def __get_cache_handle(self, instance: object) -> asyncio.TimerHandle:
|
|
298
379
|
# NOTE: we create a strong reference to instance here. I'm not sure if this is good or not but its necessary for now.
|
|
299
|
-
return asyncio.get_event_loop().call_later(
|
|
380
|
+
return asyncio.get_event_loop().call_later(
|
|
381
|
+
300, delattr, instance, self.field_name
|
|
382
|
+
)
|
|
300
383
|
|
|
301
384
|
def __cancel_cache_handle(self, instance: object) -> None:
|
|
302
385
|
self._cache_handle.cancel()
|
|
303
386
|
|
|
387
|
+
|
|
304
388
|
class _ASyncView(ASyncIterator[T]):
|
|
305
389
|
"""
|
|
306
390
|
Internal mixin class containing logic for creating specialized views for :class:`~ASyncIterable` objects.
|
|
@@ -313,8 +397,8 @@ class _ASyncView(ASyncIterator[T]):
|
|
|
313
397
|
"""An optional iterator. If None, :attr:`~_ASyncView.__aiterator__` will have a value."""
|
|
314
398
|
|
|
315
399
|
def __init__(
|
|
316
|
-
self,
|
|
317
|
-
function: ViewFn[T],
|
|
400
|
+
self,
|
|
401
|
+
function: ViewFn[T],
|
|
318
402
|
iterable: AnyIterable[T],
|
|
319
403
|
) -> None:
|
|
320
404
|
"""
|
|
@@ -331,18 +415,33 @@ class _ASyncView(ASyncIterator[T]):
|
|
|
331
415
|
elif isinstance(iterable, Iterable):
|
|
332
416
|
self.__iterator__ = iterable.__iter__()
|
|
333
417
|
else:
|
|
334
|
-
raise TypeError(
|
|
418
|
+
raise TypeError(
|
|
419
|
+
f"`iterable` must be AsyncIterable or Iterable, you passed {iterable}"
|
|
420
|
+
)
|
|
335
421
|
|
|
336
|
-
|
|
422
|
+
|
|
423
|
+
@final
|
|
337
424
|
class ASyncFilter(_ASyncView[T]):
|
|
338
425
|
"""
|
|
339
|
-
An async filter class that filters items of an async iterable based on a provided function.
|
|
340
|
-
|
|
426
|
+
An async filter class that filters items of an async iterable based on a provided function.
|
|
427
|
+
|
|
341
428
|
This class inherits from :class:`~_ASyncView` and provides the functionality to asynchronously
|
|
342
429
|
iterate over items, applying the filter function to each item to determine if it should be
|
|
343
|
-
included in the result.
|
|
430
|
+
included in the result. The filter function can be either synchronous or asynchronous.
|
|
431
|
+
|
|
432
|
+
Example:
|
|
433
|
+
>>> async def is_even(x):
|
|
434
|
+
... return x % 2 == 0
|
|
435
|
+
>>> filtered_iterable = ASyncFilter(is_even, some_async_iterable)
|
|
436
|
+
>>> async for item in filtered_iterable:
|
|
437
|
+
... print(item)
|
|
438
|
+
|
|
439
|
+
See Also:
|
|
440
|
+
- :class:`ASyncIterable`
|
|
441
|
+
- :class:`ASyncIterator`
|
|
442
|
+
- :class:`ASyncSorter`
|
|
344
443
|
"""
|
|
345
|
-
|
|
444
|
+
|
|
346
445
|
def __repr__(self) -> str:
|
|
347
446
|
return f"<ASyncFilter for iterator={self.__wrapped__} function={self._function.__name__} at {hex(id(self))}>"
|
|
348
447
|
|
|
@@ -382,28 +481,38 @@ def _key_if_no_key(obj: T) -> T:
|
|
|
382
481
|
|
|
383
482
|
Args:
|
|
384
483
|
obj: The object to return.
|
|
385
|
-
|
|
386
|
-
Returns:
|
|
387
|
-
The object itself.
|
|
388
484
|
"""
|
|
389
485
|
return obj
|
|
390
486
|
|
|
487
|
+
|
|
391
488
|
@final
|
|
392
489
|
class ASyncSorter(_ASyncView[T]):
|
|
393
490
|
"""
|
|
394
|
-
An async sorter class that sorts items of an async iterable based on a provided key function.
|
|
395
|
-
|
|
491
|
+
An async sorter class that sorts items of an async iterable based on a provided key function.
|
|
492
|
+
|
|
396
493
|
This class inherits from :class:`~_ASyncView` and provides the functionality to asynchronously
|
|
397
|
-
iterate over items, applying the key function to each item for sorting.
|
|
494
|
+
iterate over items, applying the key function to each item for sorting. The key function can be
|
|
495
|
+
either synchronous or asynchronous. Note that the ASyncSorter instance can only be consumed once.
|
|
496
|
+
|
|
497
|
+
Example:
|
|
498
|
+
>>> sorted_iterable = ASyncSorter(some_async_iterable, key=lambda x: x.value)
|
|
499
|
+
>>> async for item in sorted_iterable:
|
|
500
|
+
... print(item)
|
|
501
|
+
|
|
502
|
+
See Also:
|
|
503
|
+
- :class:`ASyncIterable`
|
|
504
|
+
- :class:`ASyncIterator`
|
|
505
|
+
- :class:`ASyncFilter`
|
|
398
506
|
"""
|
|
507
|
+
|
|
399
508
|
reversed: bool = False
|
|
400
509
|
_consumed: bool = False
|
|
401
510
|
|
|
402
511
|
def __init__(
|
|
403
|
-
self,
|
|
512
|
+
self,
|
|
404
513
|
iterable: AsyncIterable[T],
|
|
405
514
|
*,
|
|
406
|
-
key: SortKey[T] = None,
|
|
515
|
+
key: SortKey[T] = None,
|
|
407
516
|
reverse: bool = False,
|
|
408
517
|
) -> None:
|
|
409
518
|
"""
|
|
@@ -467,7 +576,9 @@ class ASyncSorter(_ASyncView[T]):
|
|
|
467
576
|
for obj in self.__iterator__:
|
|
468
577
|
items.append(obj)
|
|
469
578
|
sort_tasks.append(asyncio.create_task(self._function(obj)))
|
|
470
|
-
for sort_value, obj in sorted(
|
|
579
|
+
for sort_value, obj in sorted(
|
|
580
|
+
zip(await asyncio.gather(*sort_tasks), items), reverse=reverse
|
|
581
|
+
):
|
|
471
582
|
yield obj
|
|
472
583
|
else:
|
|
473
584
|
if self.__aiterator__:
|
|
@@ -480,4 +591,10 @@ class ASyncSorter(_ASyncView[T]):
|
|
|
480
591
|
self._consumed = True
|
|
481
592
|
|
|
482
593
|
|
|
483
|
-
__all__ = [
|
|
594
|
+
__all__ = [
|
|
595
|
+
"ASyncIterable",
|
|
596
|
+
"ASyncIterator",
|
|
597
|
+
"ASyncFilter",
|
|
598
|
+
"ASyncSorter",
|
|
599
|
+
"ASyncGeneratorFunction",
|
|
600
|
+
]
|
a_sync/primitives/__init__.py
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
|
|
2
1
|
"""
|
|
3
|
-
|
|
2
|
+
This module includes both new primitives and modified versions of standard asyncio primitives.
|
|
3
|
+
|
|
4
|
+
The primitives provided in this module are:
|
|
5
|
+
|
|
6
|
+
- Semaphore
|
|
7
|
+
- ThreadsafeSemaphore
|
|
8
|
+
- PrioritySemaphore
|
|
9
|
+
- CounterLock
|
|
10
|
+
- Event
|
|
11
|
+
- Queue
|
|
12
|
+
- ProcessingQueue
|
|
13
|
+
- SmartProcessingQueue
|
|
14
|
+
|
|
15
|
+
These primitives extend or modify the functionality of standard asyncio primitives to provide additional features or improved performance for specific use cases.
|
|
4
16
|
"""
|
|
5
17
|
|
|
6
18
|
from a_sync.primitives.locks import *
|
a_sync/primitives/_debug.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
This module provides a mixin class used to
|
|
2
|
+
This module provides a mixin class used to facilitate the creation of debugging daemons in subclasses.
|
|
3
3
|
|
|
4
|
-
The mixin
|
|
4
|
+
The mixin provides a framework for managing a debug daemon task, which can be used to emit rich debug logs from subclass instances whenever debug logging is enabled. Subclasses must implement the specific logging behavior.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import abc
|
|
@@ -13,55 +13,96 @@ from a_sync.primitives._loggable import _LoggerMixin
|
|
|
13
13
|
|
|
14
14
|
class _DebugDaemonMixin(_LoggerMixin, metaclass=abc.ABCMeta):
|
|
15
15
|
"""
|
|
16
|
-
A mixin class that provides debugging capabilities using a daemon task.
|
|
17
|
-
|
|
18
|
-
This mixin
|
|
16
|
+
A mixin class that provides a framework for debugging capabilities using a daemon task.
|
|
17
|
+
|
|
18
|
+
This mixin sets up the structure for managing a debug daemon task. Subclasses are responsible for implementing the specific behavior of the daemon, including any logging functionality.
|
|
19
|
+
|
|
20
|
+
See Also:
|
|
21
|
+
:class:`_LoggerMixin` for logging capabilities.
|
|
19
22
|
"""
|
|
20
|
-
|
|
21
|
-
__slots__ = "_daemon",
|
|
23
|
+
|
|
24
|
+
__slots__ = ("_daemon",)
|
|
22
25
|
|
|
23
26
|
@abc.abstractmethod
|
|
24
27
|
async def _debug_daemon(self, fut: asyncio.Future, fn, *args, **kwargs) -> None:
|
|
25
28
|
"""
|
|
26
29
|
Abstract method to define the debug daemon's behavior.
|
|
27
|
-
|
|
30
|
+
|
|
31
|
+
Subclasses must implement this method to specify what the debug daemon should do, including any logging or monitoring tasks.
|
|
32
|
+
|
|
28
33
|
Args:
|
|
29
34
|
fut: The future associated with the daemon.
|
|
30
35
|
fn: The function to be debugged.
|
|
31
36
|
*args: Positional arguments for the function.
|
|
32
37
|
**kwargs: Keyword arguments for the function.
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
Implementing a simple debug daemon in a subclass:
|
|
41
|
+
|
|
42
|
+
.. code-block:: python
|
|
43
|
+
|
|
44
|
+
class MyDebugClass(_DebugDaemonMixin):
|
|
45
|
+
async def _debug_daemon(self, fut, fn, *args, **kwargs):
|
|
46
|
+
while not fut.done():
|
|
47
|
+
self.logger.debug("Debugging...")
|
|
48
|
+
await asyncio.sleep(1)
|
|
33
49
|
"""
|
|
34
50
|
|
|
35
51
|
def _start_debug_daemon(self, *args, **kwargs) -> "asyncio.Future[None]":
|
|
36
52
|
"""
|
|
37
53
|
Starts the debug daemon task if debug logging is enabled and the event loop is running.
|
|
38
|
-
|
|
54
|
+
|
|
55
|
+
This method checks if debug logging is enabled and if the event loop is running. If both conditions are met, it starts the debug daemon task.
|
|
56
|
+
|
|
39
57
|
Args:
|
|
40
58
|
*args: Positional arguments for the debug daemon.
|
|
41
59
|
**kwargs: Keyword arguments for the debug daemon.
|
|
42
|
-
|
|
60
|
+
|
|
43
61
|
Returns:
|
|
44
|
-
The debug daemon task, or a dummy future if debug logs are not enabled or if the daemon cannot be created.
|
|
62
|
+
The debug daemon task as an asyncio.Task, or a dummy future if debug logs are not enabled or if the daemon cannot be created.
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
Starting the debug daemon:
|
|
66
|
+
|
|
67
|
+
.. code-block:: python
|
|
68
|
+
|
|
69
|
+
my_instance = MyDebugClass()
|
|
70
|
+
my_instance._start_debug_daemon()
|
|
71
|
+
|
|
72
|
+
See Also:
|
|
73
|
+
:meth:`_ensure_debug_daemon` for ensuring the daemon is running.
|
|
45
74
|
"""
|
|
46
75
|
if self.debug_logs_enabled and asyncio.get_event_loop().is_running():
|
|
47
76
|
return asyncio.create_task(self._debug_daemon(*args, **kwargs))
|
|
48
|
-
# else we return a blank Future since we shouldn't or can't create the daemon
|
|
49
77
|
return asyncio.get_event_loop().create_future()
|
|
50
78
|
|
|
51
79
|
def _ensure_debug_daemon(self, *args, **kwargs) -> "asyncio.Future[None]":
|
|
52
80
|
"""
|
|
53
81
|
Ensures that the debug daemon task is running.
|
|
54
|
-
|
|
82
|
+
|
|
83
|
+
This method checks if the debug daemon is already running and starts it if necessary. It will only start the daemon if it is not already running.
|
|
84
|
+
|
|
55
85
|
Args:
|
|
56
86
|
*args: Positional arguments for the debug daemon.
|
|
57
87
|
**kwargs: Keyword arguments for the debug daemon.
|
|
58
|
-
|
|
88
|
+
|
|
59
89
|
Returns:
|
|
60
90
|
Either the debug daemon task or a dummy future if debug logging is not enabled.
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
Ensuring the debug daemon is running:
|
|
94
|
+
|
|
95
|
+
.. code-block:: python
|
|
96
|
+
|
|
97
|
+
my_instance = MyDebugClass()
|
|
98
|
+
my_instance._ensure_debug_daemon()
|
|
99
|
+
|
|
100
|
+
See Also:
|
|
101
|
+
:meth:`_start_debug_daemon` for starting the daemon.
|
|
61
102
|
"""
|
|
62
103
|
if not self.debug_logs_enabled:
|
|
63
104
|
self._daemon = asyncio.get_event_loop().create_future()
|
|
64
|
-
if not hasattr(self,
|
|
105
|
+
if not hasattr(self, "_daemon") or self._daemon is None:
|
|
65
106
|
self._daemon = self._start_debug_daemon(*args, **kwargs)
|
|
66
107
|
self._daemon.add_done_callback(self._stop_debug_daemon)
|
|
67
108
|
return self._daemon
|
|
@@ -69,12 +110,25 @@ class _DebugDaemonMixin(_LoggerMixin, metaclass=abc.ABCMeta):
|
|
|
69
110
|
def _stop_debug_daemon(self, t: Optional[asyncio.Task] = None) -> None:
|
|
70
111
|
"""
|
|
71
112
|
Stops the debug daemon task.
|
|
72
|
-
|
|
113
|
+
|
|
114
|
+
This method cancels the debug daemon task if it is running. Raises a ValueError if the task to be stopped is not the current daemon.
|
|
115
|
+
|
|
73
116
|
Args:
|
|
74
|
-
t (optional): The task to be stopped, if any.
|
|
75
|
-
|
|
117
|
+
t (optional): The task to be stopped, if any.
|
|
118
|
+
|
|
76
119
|
Raises:
|
|
77
120
|
ValueError: If `t` is not the current daemon.
|
|
121
|
+
|
|
122
|
+
Examples:
|
|
123
|
+
Stopping the debug daemon:
|
|
124
|
+
|
|
125
|
+
.. code-block:: python
|
|
126
|
+
|
|
127
|
+
my_instance = MyDebugClass()
|
|
128
|
+
my_instance._stop_debug_daemon()
|
|
129
|
+
|
|
130
|
+
See Also:
|
|
131
|
+
:meth:`_ensure_debug_daemon` for ensuring the daemon is running.
|
|
78
132
|
"""
|
|
79
133
|
if t and t != self._daemon:
|
|
80
134
|
raise ValueError(f"{t} is not {self._daemon}")
|