ez-a-sync 0.22.14__py3-none-any.whl → 0.22.15__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 +4 -3
- a_sync/__init__.py +30 -12
- a_sync/_smart.py +132 -28
- a_sync/_typing.py +56 -12
- a_sync/a_sync/__init__.py +35 -10
- a_sync/a_sync/_descriptor.py +74 -26
- a_sync/a_sync/_flags.py +14 -6
- a_sync/a_sync/_helpers.py +8 -7
- a_sync/a_sync/_kwargs.py +3 -2
- a_sync/a_sync/_meta.py +120 -28
- a_sync/a_sync/abstract.py +102 -28
- a_sync/a_sync/base.py +34 -16
- a_sync/a_sync/config.py +47 -13
- a_sync/a_sync/decorator.py +239 -117
- a_sync/a_sync/function.py +416 -146
- a_sync/a_sync/method.py +197 -59
- a_sync/a_sync/modifiers/__init__.py +47 -5
- a_sync/a_sync/modifiers/cache/__init__.py +46 -17
- a_sync/a_sync/modifiers/cache/memory.py +86 -20
- a_sync/a_sync/modifiers/limiter.py +52 -22
- a_sync/a_sync/modifiers/manager.py +98 -16
- a_sync/a_sync/modifiers/semaphores.py +48 -15
- a_sync/a_sync/property.py +383 -82
- a_sync/a_sync/singleton.py +1 -0
- a_sync/aliases.py +0 -1
- a_sync/asyncio/__init__.py +4 -1
- a_sync/asyncio/as_completed.py +177 -49
- a_sync/asyncio/create_task.py +31 -17
- a_sync/asyncio/gather.py +72 -52
- a_sync/asyncio/utils.py +3 -3
- a_sync/exceptions.py +78 -23
- a_sync/executor.py +118 -71
- a_sync/future.py +575 -158
- a_sync/iter.py +110 -50
- a_sync/primitives/__init__.py +14 -2
- a_sync/primitives/_debug.py +13 -13
- a_sync/primitives/_loggable.py +5 -4
- a_sync/primitives/locks/__init__.py +5 -2
- a_sync/primitives/locks/counter.py +38 -36
- a_sync/primitives/locks/event.py +21 -7
- a_sync/primitives/locks/prio_semaphore.py +182 -62
- a_sync/primitives/locks/semaphore.py +78 -77
- a_sync/primitives/queue.py +560 -58
- a_sync/sphinx/__init__.py +0 -1
- a_sync/sphinx/ext.py +160 -50
- a_sync/task.py +262 -97
- a_sync/utils/__init__.py +12 -6
- a_sync/utils/iterators.py +127 -43
- {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.15.dist-info}/METADATA +1 -1
- ez_a_sync-0.22.15.dist-info/RECORD +74 -0
- {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.15.dist-info}/WHEEL +1 -1
- tests/conftest.py +1 -2
- tests/executor.py +112 -9
- tests/fixtures.py +61 -32
- tests/test_abstract.py +7 -4
- tests/test_as_completed.py +54 -21
- tests/test_base.py +66 -17
- tests/test_cache.py +31 -15
- tests/test_decorator.py +54 -28
- tests/test_executor.py +8 -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 +15 -10
- tests/test_task.py +126 -28
- ez_a_sync-0.22.14.dist-info/RECORD +0 -74
- {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.15.dist-info}/LICENSE.txt +0 -0
- {ez_a_sync-0.22.14.dist-info → ez_a_sync-0.22.15.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,40 @@ 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
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. This dual functionality is particularly useful in codebases that are transitioning between synchronous and asynchronous code, or in libraries that aim to support both synchronous and asynchronous usage patterns without requiring the user to manage different types of iterable objects.
|
|
127
129
|
"""
|
|
130
|
+
|
|
128
131
|
@classmethod
|
|
129
132
|
def wrap(cls, wrapped: AsyncIterable[T]) -> "ASyncIterable[T]":
|
|
130
133
|
"Class method to wrap an AsyncIterable for backward compatibility."
|
|
131
|
-
logger.warning(
|
|
134
|
+
logger.warning(
|
|
135
|
+
"ASyncIterable.wrap will be removed soon. Please replace uses with simple instantiation ie `ASyncIterable(wrapped)`"
|
|
136
|
+
)
|
|
132
137
|
return cls(wrapped)
|
|
138
|
+
|
|
133
139
|
def __init__(self, async_iterable: AsyncIterable[T]):
|
|
134
|
-
"
|
|
140
|
+
"""
|
|
141
|
+
Initializes the ASyncIterable with an async iterable.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
async_iterable: The async iterable to wrap.
|
|
145
|
+
"""
|
|
135
146
|
if not isinstance(async_iterable, AsyncIterable):
|
|
136
|
-
raise TypeError(
|
|
147
|
+
raise TypeError(
|
|
148
|
+
f"`async_iterable` must be an AsyncIterable. You passed {async_iterable}"
|
|
149
|
+
)
|
|
137
150
|
self.__wrapped__ = async_iterable
|
|
138
151
|
"The wrapped async iterable object."
|
|
152
|
+
|
|
139
153
|
def __repr__(self) -> str:
|
|
140
154
|
start = f"<{type(self).__name__}"
|
|
141
155
|
if wrapped := getattr(self, "__wrapped__", None):
|
|
@@ -151,10 +165,13 @@ class ASyncIterable(_AwaitableAsyncIterableMixin[T], Iterable[T]):
|
|
|
151
165
|
def __iter__(self) -> Iterator[T]:
|
|
152
166
|
"Return an iterator that yields :obj:`T` objects from the {cls}."
|
|
153
167
|
yield from ASyncIterator(self.__aiter__())
|
|
154
|
-
|
|
168
|
+
|
|
169
|
+
__slots__ = ("__wrapped__",)
|
|
170
|
+
|
|
155
171
|
|
|
156
172
|
AsyncGenFunc = Callable[P, Union[AsyncGenerator[T, None], AsyncIterator[T]]]
|
|
157
173
|
|
|
174
|
+
|
|
158
175
|
class ASyncIterator(_AwaitableAsyncIterableMixin[T], Iterator[T]):
|
|
159
176
|
"""
|
|
160
177
|
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.
|
|
@@ -163,11 +180,11 @@ class ASyncIterator(_AwaitableAsyncIterableMixin[T], Iterator[T]):
|
|
|
163
180
|
|
|
164
181
|
This class is particularly useful for library developers seeking to provide a consistent iteration interface across synchronous and asynchronous code, reducing the cognitive load on users and promoting code reusability and simplicity.
|
|
165
182
|
"""
|
|
166
|
-
|
|
183
|
+
|
|
167
184
|
def __next__(self) -> T:
|
|
168
185
|
"""
|
|
169
186
|
Synchronously fetch the next item from the {cls}.
|
|
170
|
-
|
|
187
|
+
|
|
171
188
|
Raises:
|
|
172
189
|
:class:`StopIteration`: Once all items have been fetched from the {cls}.
|
|
173
190
|
"""
|
|
@@ -177,34 +194,61 @@ class ASyncIterator(_AwaitableAsyncIterableMixin[T], Iterator[T]):
|
|
|
177
194
|
raise StopIteration from e
|
|
178
195
|
except RuntimeError as e:
|
|
179
196
|
if str(e) == "This event loop is already running":
|
|
180
|
-
raise SyncModeInAsyncContextError(
|
|
197
|
+
raise SyncModeInAsyncContextError(
|
|
198
|
+
"The event loop is already running. Try iterating using `async for` instead of `for`."
|
|
199
|
+
) from e
|
|
181
200
|
raise
|
|
182
201
|
|
|
183
202
|
@overload
|
|
184
|
-
def wrap(cls, aiterator: AsyncIterator[T]) -> "ASyncIterator[T]"
|
|
203
|
+
def wrap(cls, aiterator: AsyncIterator[T]) -> "ASyncIterator[T]":
|
|
204
|
+
"""
|
|
205
|
+
Wraps an AsyncIterator in an ASyncIterator.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
aiterator: The AsyncIterator to wrap.
|
|
209
|
+
"""
|
|
210
|
+
|
|
185
211
|
@overload
|
|
186
|
-
def wrap(cls, async_gen_func: AsyncGenFunc[P, T]) -> "ASyncGeneratorFunction[P, T]"
|
|
212
|
+
def wrap(cls, async_gen_func: AsyncGenFunc[P, T]) -> "ASyncGeneratorFunction[P, T]":
|
|
213
|
+
"""
|
|
214
|
+
Wraps an async generator function in an ASyncGeneratorFunction.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
async_gen_func: The async generator function to wrap.
|
|
218
|
+
"""
|
|
219
|
+
|
|
187
220
|
@classmethod
|
|
188
221
|
def wrap(cls, wrapped):
|
|
189
222
|
"Class method to wrap either an AsyncIterator or an async generator function."
|
|
190
223
|
if isinstance(wrapped, AsyncIterator):
|
|
191
|
-
logger.warning(
|
|
224
|
+
logger.warning(
|
|
225
|
+
"This use case for ASyncIterator.wrap will be removed soon. Please replace uses with simple instantiation ie `ASyncIterator(wrapped)`"
|
|
226
|
+
)
|
|
192
227
|
return cls(wrapped)
|
|
193
228
|
elif inspect.isasyncgenfunction(wrapped):
|
|
194
229
|
return ASyncGeneratorFunction(wrapped)
|
|
195
|
-
raise TypeError(
|
|
230
|
+
raise TypeError(
|
|
231
|
+
f"`wrapped` must be an AsyncIterator or an async generator function. You passed {wrapped}"
|
|
232
|
+
)
|
|
196
233
|
|
|
197
234
|
def __init__(self, async_iterator: AsyncIterator[T]):
|
|
198
|
-
"
|
|
235
|
+
"""
|
|
236
|
+
Initializes the ASyncIterator with an async iterator.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
async_iterator: The async iterator to wrap.
|
|
240
|
+
"""
|
|
199
241
|
if not isinstance(async_iterator, AsyncIterator):
|
|
200
|
-
raise TypeError(
|
|
242
|
+
raise TypeError(
|
|
243
|
+
f"`async_iterator` must be an AsyncIterator. You passed {async_iterator}"
|
|
244
|
+
)
|
|
201
245
|
self.__wrapped__ = async_iterator
|
|
202
246
|
"The wrapped :class:`AsyncIterator`."
|
|
203
247
|
|
|
204
248
|
async def __anext__(self) -> T:
|
|
205
249
|
"""
|
|
206
250
|
Asynchronously fetch the next item from the {cls}.
|
|
207
|
-
|
|
251
|
+
|
|
208
252
|
Raises:
|
|
209
253
|
:class:`StopAsyncIteration`: Once all items have been fetched from the {cls}.
|
|
210
254
|
"""
|
|
@@ -218,6 +262,7 @@ class ASyncIterator(_AwaitableAsyncIterableMixin[T], Iterator[T]):
|
|
|
218
262
|
"Return the {cls} for aiteration."
|
|
219
263
|
return self
|
|
220
264
|
|
|
265
|
+
|
|
221
266
|
class ASyncGeneratorFunction(Generic[P, T]):
|
|
222
267
|
"""
|
|
223
268
|
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.
|
|
@@ -233,7 +278,9 @@ class ASyncGeneratorFunction(Generic[P, T]):
|
|
|
233
278
|
__weakself__: "weakref.ref[object]" = None
|
|
234
279
|
"A weak reference to the instance the function is bound to, if any."
|
|
235
280
|
|
|
236
|
-
def __init__(
|
|
281
|
+
def __init__(
|
|
282
|
+
self, async_gen_func: AsyncGenFunc[P, T], instance: Any = None
|
|
283
|
+
) -> None:
|
|
237
284
|
"""
|
|
238
285
|
Initializes the ASyncGeneratorFunction with the given async generator function and optionally an instance.
|
|
239
286
|
|
|
@@ -241,7 +288,7 @@ class ASyncGeneratorFunction(Generic[P, T]):
|
|
|
241
288
|
async_gen_func: The async generator function to wrap.
|
|
242
289
|
instance (optional): The object to bind to the function, if applicable.
|
|
243
290
|
"""
|
|
244
|
-
|
|
291
|
+
|
|
245
292
|
self.field_name = async_gen_func.__name__
|
|
246
293
|
"The name of the async generator function."
|
|
247
294
|
|
|
@@ -263,7 +310,7 @@ class ASyncGeneratorFunction(Generic[P, T]):
|
|
|
263
310
|
Args:
|
|
264
311
|
*args: Positional arguments for the function.
|
|
265
312
|
**kwargs: Keyword arguments for the function.
|
|
266
|
-
|
|
313
|
+
|
|
267
314
|
Returns:
|
|
268
315
|
An :class:`ASyncIterator` wrapping the :class:`AsyncIterator` returned from the wrapped function call.
|
|
269
316
|
"""
|
|
@@ -296,11 +343,14 @@ class ASyncGeneratorFunction(Generic[P, T]):
|
|
|
296
343
|
|
|
297
344
|
def __get_cache_handle(self, instance: object) -> asyncio.TimerHandle:
|
|
298
345
|
# 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(
|
|
346
|
+
return asyncio.get_event_loop().call_later(
|
|
347
|
+
300, delattr, instance, self.field_name
|
|
348
|
+
)
|
|
300
349
|
|
|
301
350
|
def __cancel_cache_handle(self, instance: object) -> None:
|
|
302
351
|
self._cache_handle.cancel()
|
|
303
352
|
|
|
353
|
+
|
|
304
354
|
class _ASyncView(ASyncIterator[T]):
|
|
305
355
|
"""
|
|
306
356
|
Internal mixin class containing logic for creating specialized views for :class:`~ASyncIterable` objects.
|
|
@@ -313,8 +363,8 @@ class _ASyncView(ASyncIterator[T]):
|
|
|
313
363
|
"""An optional iterator. If None, :attr:`~_ASyncView.__aiterator__` will have a value."""
|
|
314
364
|
|
|
315
365
|
def __init__(
|
|
316
|
-
self,
|
|
317
|
-
function: ViewFn[T],
|
|
366
|
+
self,
|
|
367
|
+
function: ViewFn[T],
|
|
318
368
|
iterable: AnyIterable[T],
|
|
319
369
|
) -> None:
|
|
320
370
|
"""
|
|
@@ -331,18 +381,21 @@ class _ASyncView(ASyncIterator[T]):
|
|
|
331
381
|
elif isinstance(iterable, Iterable):
|
|
332
382
|
self.__iterator__ = iterable.__iter__()
|
|
333
383
|
else:
|
|
334
|
-
raise TypeError(
|
|
384
|
+
raise TypeError(
|
|
385
|
+
f"`iterable` must be AsyncIterable or Iterable, you passed {iterable}"
|
|
386
|
+
)
|
|
387
|
+
|
|
335
388
|
|
|
336
|
-
@final
|
|
389
|
+
@final
|
|
337
390
|
class ASyncFilter(_ASyncView[T]):
|
|
338
391
|
"""
|
|
339
|
-
An async filter class that filters items of an async iterable based on a provided function.
|
|
340
|
-
|
|
392
|
+
An async filter class that filters items of an async iterable based on a provided function.
|
|
393
|
+
|
|
341
394
|
This class inherits from :class:`~_ASyncView` and provides the functionality to asynchronously
|
|
342
395
|
iterate over items, applying the filter function to each item to determine if it should be
|
|
343
396
|
included in the result.
|
|
344
397
|
"""
|
|
345
|
-
|
|
398
|
+
|
|
346
399
|
def __repr__(self) -> str:
|
|
347
400
|
return f"<ASyncFilter for iterator={self.__wrapped__} function={self._function.__name__} at {hex(id(self))}>"
|
|
348
401
|
|
|
@@ -382,28 +435,27 @@ def _key_if_no_key(obj: T) -> T:
|
|
|
382
435
|
|
|
383
436
|
Args:
|
|
384
437
|
obj: The object to return.
|
|
385
|
-
|
|
386
|
-
Returns:
|
|
387
|
-
The object itself.
|
|
388
438
|
"""
|
|
389
439
|
return obj
|
|
390
440
|
|
|
441
|
+
|
|
391
442
|
@final
|
|
392
443
|
class ASyncSorter(_ASyncView[T]):
|
|
393
444
|
"""
|
|
394
|
-
An async sorter class that sorts items of an async iterable based on a provided key function.
|
|
395
|
-
|
|
445
|
+
An async sorter class that sorts items of an async iterable based on a provided key function.
|
|
446
|
+
|
|
396
447
|
This class inherits from :class:`~_ASyncView` and provides the functionality to asynchronously
|
|
397
448
|
iterate over items, applying the key function to each item for sorting.
|
|
398
449
|
"""
|
|
450
|
+
|
|
399
451
|
reversed: bool = False
|
|
400
452
|
_consumed: bool = False
|
|
401
453
|
|
|
402
454
|
def __init__(
|
|
403
|
-
self,
|
|
455
|
+
self,
|
|
404
456
|
iterable: AsyncIterable[T],
|
|
405
457
|
*,
|
|
406
|
-
key: SortKey[T] = None,
|
|
458
|
+
key: SortKey[T] = None,
|
|
407
459
|
reverse: bool = False,
|
|
408
460
|
) -> None:
|
|
409
461
|
"""
|
|
@@ -467,7 +519,9 @@ class ASyncSorter(_ASyncView[T]):
|
|
|
467
519
|
for obj in self.__iterator__:
|
|
468
520
|
items.append(obj)
|
|
469
521
|
sort_tasks.append(asyncio.create_task(self._function(obj)))
|
|
470
|
-
for sort_value, obj in sorted(
|
|
522
|
+
for sort_value, obj in sorted(
|
|
523
|
+
zip(await asyncio.gather(*sort_tasks), items), reverse=reverse
|
|
524
|
+
):
|
|
471
525
|
yield obj
|
|
472
526
|
else:
|
|
473
527
|
if self.__aiterator__:
|
|
@@ -480,4 +534,10 @@ class ASyncSorter(_ASyncView[T]):
|
|
|
480
534
|
self._consumed = True
|
|
481
535
|
|
|
482
536
|
|
|
483
|
-
__all__ = [
|
|
537
|
+
__all__ = [
|
|
538
|
+
"ASyncIterable",
|
|
539
|
+
"ASyncIterator",
|
|
540
|
+
"ASyncFilter",
|
|
541
|
+
"ASyncSorter",
|
|
542
|
+
"ASyncGeneratorFunction",
|
|
543
|
+
]
|
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
|
@@ -14,17 +14,17 @@ from a_sync.primitives._loggable import _LoggerMixin
|
|
|
14
14
|
class _DebugDaemonMixin(_LoggerMixin, metaclass=abc.ABCMeta):
|
|
15
15
|
"""
|
|
16
16
|
A mixin class that provides debugging capabilities using a daemon task.
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
This mixin ensures that rich debug logs are automagically emitted from subclass instances whenever debug logging is enabled.
|
|
19
19
|
"""
|
|
20
|
-
|
|
21
|
-
__slots__ = "_daemon",
|
|
20
|
+
|
|
21
|
+
__slots__ = ("_daemon",)
|
|
22
22
|
|
|
23
23
|
@abc.abstractmethod
|
|
24
24
|
async def _debug_daemon(self, fut: asyncio.Future, fn, *args, **kwargs) -> None:
|
|
25
25
|
"""
|
|
26
26
|
Abstract method to define the debug daemon's behavior.
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
Args:
|
|
29
29
|
fut: The future associated with the daemon.
|
|
30
30
|
fn: The function to be debugged.
|
|
@@ -35,13 +35,13 @@ class _DebugDaemonMixin(_LoggerMixin, metaclass=abc.ABCMeta):
|
|
|
35
35
|
def _start_debug_daemon(self, *args, **kwargs) -> "asyncio.Future[None]":
|
|
36
36
|
"""
|
|
37
37
|
Starts the debug daemon task if debug logging is enabled and the event loop is running.
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
Args:
|
|
40
40
|
*args: Positional arguments for the debug daemon.
|
|
41
41
|
**kwargs: Keyword arguments for the debug daemon.
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
Returns:
|
|
44
|
-
The debug daemon task, or a dummy future if debug logs are not enabled or if the daemon cannot be created.
|
|
44
|
+
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.
|
|
45
45
|
"""
|
|
46
46
|
if self.debug_logs_enabled and asyncio.get_event_loop().is_running():
|
|
47
47
|
return asyncio.create_task(self._debug_daemon(*args, **kwargs))
|
|
@@ -51,17 +51,17 @@ class _DebugDaemonMixin(_LoggerMixin, metaclass=abc.ABCMeta):
|
|
|
51
51
|
def _ensure_debug_daemon(self, *args, **kwargs) -> "asyncio.Future[None]":
|
|
52
52
|
"""
|
|
53
53
|
Ensures that the debug daemon task is running.
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
Args:
|
|
56
56
|
*args: Positional arguments for the debug daemon.
|
|
57
57
|
**kwargs: Keyword arguments for the debug daemon.
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
Returns:
|
|
60
60
|
Either the debug daemon task or a dummy future if debug logging is not enabled.
|
|
61
61
|
"""
|
|
62
62
|
if not self.debug_logs_enabled:
|
|
63
63
|
self._daemon = asyncio.get_event_loop().create_future()
|
|
64
|
-
if not hasattr(self,
|
|
64
|
+
if not hasattr(self, "_daemon") or self._daemon is None:
|
|
65
65
|
self._daemon = self._start_debug_daemon(*args, **kwargs)
|
|
66
66
|
self._daemon.add_done_callback(self._stop_debug_daemon)
|
|
67
67
|
return self._daemon
|
|
@@ -69,10 +69,10 @@ class _DebugDaemonMixin(_LoggerMixin, metaclass=abc.ABCMeta):
|
|
|
69
69
|
def _stop_debug_daemon(self, t: Optional[asyncio.Task] = None) -> None:
|
|
70
70
|
"""
|
|
71
71
|
Stops the debug daemon task.
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
Args:
|
|
74
|
-
t (optional): The task to be stopped, if any.
|
|
75
|
-
|
|
74
|
+
t (optional): The task to be stopped, if any.
|
|
75
|
+
|
|
76
76
|
Raises:
|
|
77
77
|
ValueError: If `t` is not the current daemon.
|
|
78
78
|
"""
|
a_sync/primitives/_loggable.py
CHANGED
|
@@ -9,22 +9,23 @@ from logging import Logger, getLogger, DEBUG
|
|
|
9
9
|
class _LoggerMixin:
|
|
10
10
|
"""
|
|
11
11
|
A mixin class that adds logging capabilities to other classes.
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
This mixin provides a cached property for accessing a logger instance and a property to check if debug logging is enabled.
|
|
14
14
|
"""
|
|
15
|
+
|
|
15
16
|
@cached_property
|
|
16
17
|
def logger(self) -> Logger:
|
|
17
18
|
"""
|
|
18
19
|
Returns a logger instance specific to the class using this mixin.
|
|
19
|
-
|
|
20
|
+
|
|
20
21
|
The logger ID is constructed from the module and class name, and optionally includes an instance name if available.
|
|
21
22
|
|
|
22
23
|
Returns:
|
|
23
24
|
Logger: A logger instance for the class.
|
|
24
25
|
"""
|
|
25
26
|
logger_id = type(self).__qualname__
|
|
26
|
-
if hasattr(self,
|
|
27
|
-
logger_id += f
|
|
27
|
+
if hasattr(self, "_name") and self._name:
|
|
28
|
+
logger_id += f".{self._name}"
|
|
28
29
|
return getLogger(logger_id)
|
|
29
30
|
|
|
30
31
|
@property
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
2
1
|
from a_sync.primitives.locks.counter import CounterLock
|
|
3
2
|
from a_sync.primitives.locks.event import Event
|
|
4
|
-
from a_sync.primitives.locks.semaphore import
|
|
3
|
+
from a_sync.primitives.locks.semaphore import (
|
|
4
|
+
DummySemaphore,
|
|
5
|
+
Semaphore,
|
|
6
|
+
ThreadsafeSemaphore,
|
|
7
|
+
)
|
|
5
8
|
from a_sync.primitives.locks.prio_semaphore import PrioritySemaphore
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
This module provides two specialized async flow management classes, CounterLock and CounterLockCluster.
|
|
3
3
|
|
|
4
|
-
These primitives
|
|
4
|
+
These primitives manage :class:`asyncio.Task` objects that must wait for an internal counter to reach a specific value.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
@@ -15,23 +15,24 @@ from a_sync.primitives.locks.event import Event
|
|
|
15
15
|
|
|
16
16
|
class CounterLock(_DebugDaemonMixin):
|
|
17
17
|
"""
|
|
18
|
-
An async primitive that
|
|
19
|
-
|
|
20
|
-
A coroutine can `await counter.wait_for(3)` and it will
|
|
21
|
-
If some other task executes `counter.value = 5` or `counter.set(5)`, the first coroutine will
|
|
22
|
-
|
|
23
|
-
The internal counter can only
|
|
18
|
+
An async primitive that uses an internal counter to manage task synchronization.
|
|
19
|
+
|
|
20
|
+
A coroutine can `await counter.wait_for(3)` and it will wait until the internal counter >= 3.
|
|
21
|
+
If some other task executes `counter.value = 5` or `counter.set(5)`, the first coroutine will proceed as 5 >= 3.
|
|
22
|
+
|
|
23
|
+
The internal counter can only be set to a value greater than the current value.
|
|
24
24
|
"""
|
|
25
|
+
|
|
25
26
|
__slots__ = "is_ready", "_name", "_value", "_events"
|
|
27
|
+
|
|
26
28
|
def __init__(self, start_value: int = 0, name: Optional[str] = None):
|
|
27
29
|
"""
|
|
28
30
|
Initializes the CounterLock with a starting value and an optional name.
|
|
29
31
|
|
|
30
32
|
Args:
|
|
31
33
|
start_value: The initial value of the counter.
|
|
32
|
-
name
|
|
34
|
+
name: An optional name for the counter, used in debug logs.
|
|
33
35
|
"""
|
|
34
|
-
|
|
35
36
|
self._name = name
|
|
36
37
|
"""An optional name for the counter, used in debug logs."""
|
|
37
38
|
|
|
@@ -43,48 +44,42 @@ class CounterLock(_DebugDaemonMixin):
|
|
|
43
44
|
|
|
44
45
|
self.is_ready = lambda v: self._value >= v
|
|
45
46
|
"""A lambda function that indicates whether a given value has already been surpassed."""
|
|
46
|
-
|
|
47
|
+
|
|
47
48
|
async def wait_for(self, value: int) -> bool:
|
|
48
49
|
"""
|
|
49
50
|
Waits until the counter reaches or exceeds the specified value.
|
|
50
51
|
|
|
51
52
|
Args:
|
|
52
53
|
value: The value to wait for.
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
True when the counter reaches or exceeds the specified value.
|
|
56
54
|
"""
|
|
57
55
|
if not self.is_ready(value):
|
|
58
56
|
self._ensure_debug_daemon()
|
|
59
57
|
await self._events[value].wait()
|
|
60
58
|
return True
|
|
61
|
-
|
|
59
|
+
|
|
62
60
|
def set(self, value: int) -> None:
|
|
63
61
|
"""
|
|
64
62
|
Sets the counter to the specified value.
|
|
65
63
|
|
|
66
64
|
Args:
|
|
67
|
-
value: The value to set the counter to. Must be
|
|
68
|
-
|
|
65
|
+
value: The value to set the counter to. Must be strictly greater than the current value.
|
|
66
|
+
|
|
69
67
|
Raises:
|
|
70
|
-
ValueError: If the new value is less than the current value.
|
|
68
|
+
ValueError: If the new value is less than or equal to the current value.
|
|
71
69
|
"""
|
|
72
70
|
self.value = value
|
|
73
|
-
|
|
71
|
+
|
|
74
72
|
def __repr__(self) -> str:
|
|
75
73
|
waiters = {v: len(self._events[v]._waiters) for v in sorted(self._events)}
|
|
76
74
|
return f"<CounterLock name={self._name} value={self._value} waiters={waiters}>"
|
|
77
|
-
|
|
75
|
+
|
|
78
76
|
@property
|
|
79
77
|
def value(self) -> int:
|
|
80
78
|
"""
|
|
81
79
|
Gets the current value of the counter.
|
|
82
|
-
|
|
83
|
-
Returns:
|
|
84
|
-
The current value of the counter.
|
|
85
80
|
"""
|
|
86
81
|
return self._value
|
|
87
|
-
|
|
82
|
+
|
|
88
83
|
@value.setter
|
|
89
84
|
def value(self, value: int) -> None:
|
|
90
85
|
"""
|
|
@@ -98,28 +93,37 @@ class CounterLock(_DebugDaemonMixin):
|
|
|
98
93
|
"""
|
|
99
94
|
if value > self._value:
|
|
100
95
|
self._value = value
|
|
101
|
-
ready = [
|
|
96
|
+
ready = [
|
|
97
|
+
self._events.pop(key)
|
|
98
|
+
for key in list(self._events.keys())
|
|
99
|
+
if key <= self._value
|
|
100
|
+
]
|
|
102
101
|
for event in ready:
|
|
103
102
|
event.set()
|
|
104
103
|
elif value < self._value:
|
|
105
104
|
raise ValueError("You cannot decrease the value.")
|
|
106
|
-
|
|
105
|
+
|
|
107
106
|
async def _debug_daemon(self) -> None:
|
|
108
107
|
"""
|
|
109
108
|
Periodically logs debug information about the counter state and waiters.
|
|
110
109
|
"""
|
|
111
110
|
start = time()
|
|
112
111
|
while self._events:
|
|
113
|
-
self.logger.debug(
|
|
112
|
+
self.logger.debug(
|
|
113
|
+
"%s is still locked after %sm", self, round(time() - start / 60, 2)
|
|
114
|
+
)
|
|
114
115
|
await asyncio.sleep(300)
|
|
115
116
|
|
|
117
|
+
|
|
116
118
|
class CounterLockCluster:
|
|
117
119
|
"""
|
|
118
|
-
An asyncio primitive that represents
|
|
119
|
-
|
|
120
|
-
`wait_for(i)` will
|
|
120
|
+
An asyncio primitive that represents a collection of CounterLock objects.
|
|
121
|
+
|
|
122
|
+
`wait_for(i)` will wait until the value of all CounterLock objects is >= i.
|
|
121
123
|
"""
|
|
122
|
-
|
|
124
|
+
|
|
125
|
+
__slots__ = ("locks",)
|
|
126
|
+
|
|
123
127
|
def __init__(self, counter_locks: Iterable[CounterLock]) -> None:
|
|
124
128
|
"""
|
|
125
129
|
Initializes the CounterLockCluster with a collection of CounterLock objects.
|
|
@@ -128,17 +132,15 @@ class CounterLockCluster:
|
|
|
128
132
|
counter_locks: The CounterLock objects to manage.
|
|
129
133
|
"""
|
|
130
134
|
self.locks = list(counter_locks)
|
|
131
|
-
|
|
135
|
+
|
|
132
136
|
async def wait_for(self, value: int) -> bool:
|
|
133
137
|
"""
|
|
134
138
|
Waits until the value of all CounterLock objects in the cluster reaches or exceeds the specified value.
|
|
135
139
|
|
|
136
140
|
Args:
|
|
137
141
|
value: The value to wait for.
|
|
138
|
-
|
|
139
|
-
Returns:
|
|
140
|
-
True when the value of all CounterLock objects reach or exceed the specified value.
|
|
141
142
|
"""
|
|
142
|
-
await asyncio.gather(
|
|
143
|
+
await asyncio.gather(
|
|
144
|
+
*[counter_lock.wait_for(value) for counter_lock in self.locks]
|
|
145
|
+
)
|
|
143
146
|
return True
|
|
144
|
-
|