ez-a-sync 0.32.29__cp310-cp310-win_amd64.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 +42 -0
- a_sync/__init__.pxd +2 -0
- a_sync/__init__.py +145 -0
- a_sync/_smart.c +22803 -0
- a_sync/_smart.cp310-win_amd64.pyd +0 -0
- a_sync/_smart.pxd +2 -0
- a_sync/_smart.pyi +202 -0
- a_sync/_smart.pyx +674 -0
- a_sync/_typing.py +258 -0
- a_sync/a_sync/__init__.py +60 -0
- a_sync/a_sync/_descriptor.c +20528 -0
- a_sync/a_sync/_descriptor.cp310-win_amd64.pyd +0 -0
- a_sync/a_sync/_descriptor.pyi +33 -0
- a_sync/a_sync/_descriptor.pyx +422 -0
- a_sync/a_sync/_flags.c +6074 -0
- a_sync/a_sync/_flags.cp310-win_amd64.pyd +0 -0
- a_sync/a_sync/_flags.pxd +3 -0
- a_sync/a_sync/_flags.pyx +92 -0
- a_sync/a_sync/_helpers.c +14521 -0
- a_sync/a_sync/_helpers.cp310-win_amd64.pyd +0 -0
- a_sync/a_sync/_helpers.pxd +3 -0
- a_sync/a_sync/_helpers.pyi +10 -0
- a_sync/a_sync/_helpers.pyx +167 -0
- a_sync/a_sync/_kwargs.c +12194 -0
- a_sync/a_sync/_kwargs.cp310-win_amd64.pyd +0 -0
- a_sync/a_sync/_kwargs.pxd +2 -0
- a_sync/a_sync/_kwargs.pyx +64 -0
- a_sync/a_sync/_meta.py +210 -0
- a_sync/a_sync/abstract.c +12411 -0
- a_sync/a_sync/abstract.cp310-win_amd64.pyd +0 -0
- a_sync/a_sync/abstract.pyi +141 -0
- a_sync/a_sync/abstract.pyx +221 -0
- a_sync/a_sync/base.c +14932 -0
- a_sync/a_sync/base.cp310-win_amd64.pyd +0 -0
- a_sync/a_sync/base.pyi +60 -0
- a_sync/a_sync/base.pyx +271 -0
- a_sync/a_sync/config.py +168 -0
- a_sync/a_sync/decorator.py +651 -0
- a_sync/a_sync/flags.c +5272 -0
- a_sync/a_sync/flags.cp310-win_amd64.pyd +0 -0
- a_sync/a_sync/flags.pxd +72 -0
- a_sync/a_sync/flags.pyi +74 -0
- a_sync/a_sync/flags.pyx +72 -0
- a_sync/a_sync/function.c +37846 -0
- a_sync/a_sync/function.cp310-win_amd64.pyd +0 -0
- a_sync/a_sync/function.pxd +28 -0
- a_sync/a_sync/function.pyi +571 -0
- a_sync/a_sync/function.pyx +1381 -0
- a_sync/a_sync/method.c +29774 -0
- a_sync/a_sync/method.cp310-win_amd64.pyd +0 -0
- a_sync/a_sync/method.pxd +9 -0
- a_sync/a_sync/method.pyi +525 -0
- a_sync/a_sync/method.pyx +1023 -0
- a_sync/a_sync/modifiers/__init__.pxd +1 -0
- a_sync/a_sync/modifiers/__init__.py +101 -0
- a_sync/a_sync/modifiers/cache/__init__.py +160 -0
- a_sync/a_sync/modifiers/cache/memory.py +165 -0
- a_sync/a_sync/modifiers/limiter.py +132 -0
- a_sync/a_sync/modifiers/manager.c +16149 -0
- a_sync/a_sync/modifiers/manager.cp310-win_amd64.pyd +0 -0
- a_sync/a_sync/modifiers/manager.pxd +5 -0
- a_sync/a_sync/modifiers/manager.pyi +219 -0
- a_sync/a_sync/modifiers/manager.pyx +299 -0
- a_sync/a_sync/modifiers/semaphores.py +173 -0
- a_sync/a_sync/property.c +27260 -0
- a_sync/a_sync/property.cp310-win_amd64.pyd +0 -0
- a_sync/a_sync/property.pyi +376 -0
- a_sync/a_sync/property.pyx +819 -0
- a_sync/a_sync/singleton.py +63 -0
- a_sync/aliases.py +3 -0
- a_sync/async_property/__init__.pxd +1 -0
- a_sync/async_property/__init__.py +1 -0
- a_sync/async_property/cached.c +20386 -0
- a_sync/async_property/cached.cp310-win_amd64.pyd +0 -0
- a_sync/async_property/cached.pxd +10 -0
- a_sync/async_property/cached.pyi +45 -0
- a_sync/async_property/cached.pyx +178 -0
- a_sync/async_property/proxy.c +34654 -0
- a_sync/async_property/proxy.cp310-win_amd64.pyd +0 -0
- a_sync/async_property/proxy.pxd +2 -0
- a_sync/async_property/proxy.pyi +124 -0
- a_sync/async_property/proxy.pyx +474 -0
- a_sync/asyncio/__init__.pxd +6 -0
- a_sync/asyncio/__init__.py +164 -0
- a_sync/asyncio/as_completed.c +18841 -0
- a_sync/asyncio/as_completed.cp310-win_amd64.pyd +0 -0
- a_sync/asyncio/as_completed.pxd +8 -0
- a_sync/asyncio/as_completed.pyi +109 -0
- a_sync/asyncio/as_completed.pyx +269 -0
- a_sync/asyncio/create_task.c +15902 -0
- a_sync/asyncio/create_task.cp310-win_amd64.pyd +0 -0
- a_sync/asyncio/create_task.pxd +2 -0
- a_sync/asyncio/create_task.pyi +51 -0
- a_sync/asyncio/create_task.pyx +271 -0
- a_sync/asyncio/gather.c +16679 -0
- a_sync/asyncio/gather.cp310-win_amd64.pyd +0 -0
- a_sync/asyncio/gather.pyi +107 -0
- a_sync/asyncio/gather.pyx +218 -0
- a_sync/asyncio/igather.c +12676 -0
- a_sync/asyncio/igather.cp310-win_amd64.pyd +0 -0
- a_sync/asyncio/igather.pxd +1 -0
- a_sync/asyncio/igather.pyi +7 -0
- a_sync/asyncio/igather.pyx +182 -0
- a_sync/asyncio/sleep.c +9593 -0
- a_sync/asyncio/sleep.cp310-win_amd64.pyd +0 -0
- a_sync/asyncio/sleep.pyi +14 -0
- a_sync/asyncio/sleep.pyx +49 -0
- a_sync/debugging.c +15362 -0
- a_sync/debugging.cp310-win_amd64.pyd +0 -0
- a_sync/debugging.pyi +76 -0
- a_sync/debugging.pyx +107 -0
- a_sync/exceptions.c +13312 -0
- a_sync/exceptions.cp310-win_amd64.pyd +0 -0
- a_sync/exceptions.pyi +376 -0
- a_sync/exceptions.pyx +446 -0
- a_sync/executor.py +619 -0
- a_sync/functools.c +12738 -0
- a_sync/functools.cp310-win_amd64.pyd +0 -0
- a_sync/functools.pxd +7 -0
- a_sync/functools.pyi +33 -0
- a_sync/functools.pyx +139 -0
- a_sync/future.py +1497 -0
- a_sync/iter.c +37271 -0
- a_sync/iter.cp310-win_amd64.pyd +0 -0
- a_sync/iter.pxd +11 -0
- a_sync/iter.pyi +370 -0
- a_sync/iter.pyx +981 -0
- a_sync/primitives/__init__.pxd +1 -0
- a_sync/primitives/__init__.py +53 -0
- a_sync/primitives/_debug.c +15757 -0
- a_sync/primitives/_debug.cp310-win_amd64.pyd +0 -0
- a_sync/primitives/_debug.pxd +12 -0
- a_sync/primitives/_debug.pyi +52 -0
- a_sync/primitives/_debug.pyx +223 -0
- a_sync/primitives/_loggable.c +11529 -0
- a_sync/primitives/_loggable.cp310-win_amd64.pyd +0 -0
- a_sync/primitives/_loggable.pxd +4 -0
- a_sync/primitives/_loggable.pyi +66 -0
- a_sync/primitives/_loggable.pyx +102 -0
- a_sync/primitives/locks/__init__.pxd +8 -0
- a_sync/primitives/locks/__init__.py +17 -0
- a_sync/primitives/locks/counter.c +17679 -0
- a_sync/primitives/locks/counter.cp310-win_amd64.pyd +0 -0
- a_sync/primitives/locks/counter.pxd +12 -0
- a_sync/primitives/locks/counter.pyi +151 -0
- a_sync/primitives/locks/counter.pyx +260 -0
- a_sync/primitives/locks/event.c +17063 -0
- a_sync/primitives/locks/event.cp310-win_amd64.pyd +0 -0
- a_sync/primitives/locks/event.pxd +22 -0
- a_sync/primitives/locks/event.pyi +43 -0
- a_sync/primitives/locks/event.pyx +185 -0
- a_sync/primitives/locks/prio_semaphore.c +25590 -0
- a_sync/primitives/locks/prio_semaphore.cp310-win_amd64.pyd +0 -0
- a_sync/primitives/locks/prio_semaphore.pxd +25 -0
- a_sync/primitives/locks/prio_semaphore.pyi +217 -0
- a_sync/primitives/locks/prio_semaphore.pyx +597 -0
- a_sync/primitives/locks/semaphore.c +26509 -0
- a_sync/primitives/locks/semaphore.cp310-win_amd64.pyd +0 -0
- a_sync/primitives/locks/semaphore.pxd +21 -0
- a_sync/primitives/locks/semaphore.pyi +197 -0
- a_sync/primitives/locks/semaphore.pyx +454 -0
- a_sync/primitives/queue.py +1022 -0
- a_sync/py.typed +0 -0
- a_sync/sphinx/__init__.py +3 -0
- a_sync/sphinx/ext.py +289 -0
- a_sync/task.py +932 -0
- a_sync/utils/__init__.py +105 -0
- a_sync/utils/iterators.py +297 -0
- a_sync/utils/repr.c +15799 -0
- a_sync/utils/repr.cp310-win_amd64.pyd +0 -0
- a_sync/utils/repr.pyi +2 -0
- a_sync/utils/repr.pyx +73 -0
- ez_a_sync-0.32.29.dist-info/METADATA +367 -0
- ez_a_sync-0.32.29.dist-info/RECORD +177 -0
- ez_a_sync-0.32.29.dist-info/WHEEL +5 -0
- ez_a_sync-0.32.29.dist-info/licenses/LICENSE.txt +17 -0
- ez_a_sync-0.32.29.dist-info/top_level.txt +1 -0
a_sync/iter.pyx
ADDED
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
# cython: boundscheck=False
|
|
2
|
+
import asyncio
|
|
3
|
+
import copy
|
|
4
|
+
import inspect
|
|
5
|
+
import sys
|
|
6
|
+
import types
|
|
7
|
+
import typing
|
|
8
|
+
import weakref
|
|
9
|
+
from functools import lru_cache
|
|
10
|
+
from logging import getLogger
|
|
11
|
+
|
|
12
|
+
from cpython.object cimport PyObject, PyObject_GetIter
|
|
13
|
+
from cython cimport final
|
|
14
|
+
from typing_extensions import Self
|
|
15
|
+
|
|
16
|
+
from a_sync._typing import AnyFn, AnyIterable, P, T, SyncFn, V
|
|
17
|
+
from a_sync.a_sync._helpers cimport _await
|
|
18
|
+
from a_sync.async_property import async_cached_property
|
|
19
|
+
from a_sync.async_property.cached cimport AsyncCachedPropertyInstanceState
|
|
20
|
+
from a_sync.asyncio cimport cigather, ccreate_task_simple
|
|
21
|
+
from a_sync.exceptions import SyncModeInAsyncContextError
|
|
22
|
+
from a_sync.functools cimport update_wrapper
|
|
23
|
+
|
|
24
|
+
cdef extern from "pythoncapi_compat.h":
|
|
25
|
+
int PyWeakref_GetRef(PyObject*, PyObject**)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# cdef asyncio
|
|
29
|
+
cdef object get_event_loop = asyncio.get_event_loop
|
|
30
|
+
cdef object iscoroutinefunction = asyncio.iscoroutinefunction
|
|
31
|
+
cdef object TimerHandle = asyncio.TimerHandle
|
|
32
|
+
cdef object cancel_handle = TimerHandle.cancel
|
|
33
|
+
del asyncio
|
|
34
|
+
|
|
35
|
+
# cdef copy
|
|
36
|
+
cdef object deepcopy = copy.deepcopy
|
|
37
|
+
del copy
|
|
38
|
+
|
|
39
|
+
# cdef inspect
|
|
40
|
+
cdef object isasyncgenfunction = inspect.isasyncgenfunction
|
|
41
|
+
cdef object isawaitable = inspect.isawaitable
|
|
42
|
+
del inspect
|
|
43
|
+
|
|
44
|
+
# cdef logging
|
|
45
|
+
cdef public object logger = getLogger(__name__)
|
|
46
|
+
del getLogger
|
|
47
|
+
|
|
48
|
+
# cdef types
|
|
49
|
+
cdef object FunctionType = types.FunctionType
|
|
50
|
+
del types
|
|
51
|
+
|
|
52
|
+
# cdef typing
|
|
53
|
+
cdef object get_args = typing.get_args
|
|
54
|
+
cdef object _GenericAlias = typing._GenericAlias
|
|
55
|
+
cdef object Any = typing.Any
|
|
56
|
+
cdef object AsyncIterable = typing.AsyncIterable
|
|
57
|
+
cdef object AsyncIterator = typing.AsyncIterator
|
|
58
|
+
cdef object AsyncGenerator = typing.AsyncGenerator
|
|
59
|
+
cdef object Callable = typing.Callable
|
|
60
|
+
cdef object Coroutine = typing.Coroutine
|
|
61
|
+
cdef object Generator = typing.Generator
|
|
62
|
+
cdef object Generic = typing.Generic
|
|
63
|
+
cdef object Iterable = typing.Iterable
|
|
64
|
+
cdef object Iterator = typing.Iterator
|
|
65
|
+
cdef object List = typing.List
|
|
66
|
+
cdef object Optional = typing.Optional
|
|
67
|
+
cdef object Type = typing.Type
|
|
68
|
+
cdef object TypeVar = typing.TypeVar
|
|
69
|
+
cdef object Union = typing.Union
|
|
70
|
+
cdef object overload = typing.overload
|
|
71
|
+
del typing
|
|
72
|
+
|
|
73
|
+
# cdef weakref
|
|
74
|
+
cdef object ref = weakref.ref
|
|
75
|
+
del weakref
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
cdef object SortKey, ViewFn
|
|
79
|
+
if sys.version_info < (3, 10):
|
|
80
|
+
SortKey = SyncFn[T, bool]
|
|
81
|
+
ViewFn = AnyFn[T, bool]
|
|
82
|
+
else:
|
|
83
|
+
SortKey = SyncFn[[T], bool]
|
|
84
|
+
ViewFn = AnyFn[[T], bool]
|
|
85
|
+
del sys
|
|
86
|
+
|
|
87
|
+
cdef object AsyncGenFunc = Callable[P, Union[AsyncGenerator[T, None], AsyncIterator[T]]]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
cdef tuple[str, str] _FORMAT_PATTERNS = ("{cls}", "{obj}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
cdef class _AwaitableAsyncIterableMixin:
|
|
94
|
+
"""
|
|
95
|
+
A mixin class defining logic for making an AsyncIterable awaitable.
|
|
96
|
+
|
|
97
|
+
When awaited, a list of all elements will be returned.
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
You must subclass this mixin class and define your own `__aiter__` method as shown below.
|
|
101
|
+
|
|
102
|
+
>>> class MyAwaitableAIterable(_AwaitableAsyncIterableMixin):
|
|
103
|
+
... async def __aiter__(self):
|
|
104
|
+
... for i in range(4):
|
|
105
|
+
... yield i
|
|
106
|
+
|
|
107
|
+
>>> aiterable = MyAwaitableAIterable()
|
|
108
|
+
>>> await aiterable
|
|
109
|
+
[0, 1, 2, 3]
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
cdef readonly object __wrapped__
|
|
113
|
+
cdef readonly AsyncCachedPropertyInstanceState __async_property__
|
|
114
|
+
|
|
115
|
+
def __cinit__(self) -> None:
|
|
116
|
+
self.__async_property__ = AsyncCachedPropertyInstanceState()
|
|
117
|
+
|
|
118
|
+
def __aiter__(self) -> AsyncIterator[T]:
|
|
119
|
+
raise NotImplementedError
|
|
120
|
+
|
|
121
|
+
def __await__(self) -> Generator[Any, Any, List[T]]:
|
|
122
|
+
"""
|
|
123
|
+
Asynchronously iterate through the {cls} and return all {obj}.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
A list of the {obj} yielded by the {cls}.
|
|
127
|
+
"""
|
|
128
|
+
return self._materialized.__await__()
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def materialized(self) -> List[T]:
|
|
132
|
+
"""
|
|
133
|
+
Synchronously iterate through the {cls} and return all {obj}.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
A list of the {obj} yielded by the {cls}.
|
|
137
|
+
"""
|
|
138
|
+
return _await(self._materialized)
|
|
139
|
+
|
|
140
|
+
cpdef object sort(self, key: SortKey[T] = None, reverse: bool = False):
|
|
141
|
+
"""
|
|
142
|
+
Sort the {obj} yielded by the {cls}.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
key (optional): A function of one argument that is used to extract a comparison key from each list element. If None, the elements themselves will be sorted. Defaults to None.
|
|
146
|
+
reverse (optional): If True, the yielded elements will be sorted in reverse order. Defaults to False.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
An instance of :class:`~ASyncSorter` that will yield the {obj} yielded from this {cls}, but sorted.
|
|
150
|
+
"""
|
|
151
|
+
return ASyncSorter(self, key=key, reverse=reverse)
|
|
152
|
+
|
|
153
|
+
cpdef object filter(self, function: ViewFn[T]):
|
|
154
|
+
"""
|
|
155
|
+
Filters the {obj} yielded by the {cls} based on a function.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
function: A function that returns a boolean that indicates if an item should be included in the filtered result. Can be sync or async.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
An instance of :class:`~ASyncFilter` that yields the filtered {obj} from the {cls}.
|
|
162
|
+
"""
|
|
163
|
+
return ASyncFilter(function, self)
|
|
164
|
+
|
|
165
|
+
@async_cached_property
|
|
166
|
+
async def _materialized(self) -> List[T]:
|
|
167
|
+
"""
|
|
168
|
+
Asynchronously iterate through the {cls} and return all {obj}.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
A list of the {obj} yielded by the {cls}.
|
|
172
|
+
"""
|
|
173
|
+
return [obj async for obj in self]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
cdef class _ASyncIterable(_AwaitableAsyncIterableMixin):
|
|
177
|
+
"""
|
|
178
|
+
A hybrid Iterable/AsyncIterable implementation designed to offer
|
|
179
|
+
dual compatibility with both synchronous and asynchronous
|
|
180
|
+
iteration protocols.
|
|
181
|
+
|
|
182
|
+
This class allows objects to be iterated over using either a
|
|
183
|
+
standard `for` loop or an `async for` loop, making it versatile
|
|
184
|
+
in scenarios where the mode of iteration (synchronous or asynchronous)
|
|
185
|
+
needs to be flexible or is determined at runtime.
|
|
186
|
+
|
|
187
|
+
The class achieves this by implementing both `__iter__` and `__aiter__`
|
|
188
|
+
methods, enabling it to return appropriate iterator objects that can
|
|
189
|
+
handle synchronous and asynchronous iteration, respectively. However,
|
|
190
|
+
note that synchronous iteration relies on the :class:`ASyncIterator`
|
|
191
|
+
class, which uses `asyncio.get_event_loop().run_until_complete` to
|
|
192
|
+
fetch items. This can raise a `RuntimeError` if the event loop is
|
|
193
|
+
already running, and in such cases, a :class:`~a_sync.exceptions.SyncModeInAsyncContextError`
|
|
194
|
+
is raised from the `RuntimeError`.
|
|
195
|
+
|
|
196
|
+
Example:
|
|
197
|
+
>>> async_iterable = ASyncIterable(some_async_iterable)
|
|
198
|
+
>>> async for item in async_iterable:
|
|
199
|
+
... print(item)
|
|
200
|
+
>>> for item in async_iterable:
|
|
201
|
+
... print(item)
|
|
202
|
+
|
|
203
|
+
See Also:
|
|
204
|
+
- :class:`ASyncIterator`
|
|
205
|
+
- :class:`ASyncFilter`
|
|
206
|
+
- :class:`ASyncSorter`
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def wrap(cls, wrapped: AsyncIterable[T]) -> "ASyncIterable[T]":
|
|
211
|
+
"Class method to wrap an AsyncIterable for backward compatibility."
|
|
212
|
+
logger.warning(
|
|
213
|
+
"ASyncIterable.wrap will be removed soon. Please replace uses with simple instantiation ie `ASyncIterable(wrapped)`"
|
|
214
|
+
)
|
|
215
|
+
return cls(wrapped)
|
|
216
|
+
|
|
217
|
+
def __init__(self, async_iterable: AsyncIterable[T]):
|
|
218
|
+
"""
|
|
219
|
+
Initializes the ASyncIterable with an async iterable.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
async_iterable: The async iterable to wrap.
|
|
223
|
+
"""
|
|
224
|
+
if not isinstance(async_iterable, AsyncIterable):
|
|
225
|
+
raise TypeError(
|
|
226
|
+
"`async_iterable` must be an AsyncIterable. You passed {}".format(async_iterable)
|
|
227
|
+
)
|
|
228
|
+
self.__wrapped__ = async_iterable
|
|
229
|
+
"The wrapped async iterable object."
|
|
230
|
+
|
|
231
|
+
def __repr__(self) -> str:
|
|
232
|
+
start = "<{}".format(type(self).__name__)
|
|
233
|
+
if wrapped := getattr(self, "__wrapped__", None):
|
|
234
|
+
start += " for {}".format(wrapped)
|
|
235
|
+
return "{} at {}>".format(start, hex(id(self)))
|
|
236
|
+
|
|
237
|
+
def __aiter__(self) -> AsyncIterator[T]:
|
|
238
|
+
"""
|
|
239
|
+
Return an async iterator that yields {obj} from the {cls}.
|
|
240
|
+
"""
|
|
241
|
+
return self.__wrapped__.__aiter__()
|
|
242
|
+
|
|
243
|
+
def __iter__(self) -> ASyncIterator[T]:
|
|
244
|
+
"""
|
|
245
|
+
Return an iterator that yields {obj} from the {cls}.
|
|
246
|
+
|
|
247
|
+
Note:
|
|
248
|
+
Synchronous iteration leverages :class:`ASyncIterator`, which uses :meth:`asyncio.BaseEventLoop.run_until_complete` to fetch items.
|
|
249
|
+
:meth:`ASyncIterator.__next__` raises a :class:`~a_sync.exceptions.SyncModeInAsyncContextError` if the event loop is already running.
|
|
250
|
+
|
|
251
|
+
If you encounter a :class:`~a_sync.exceptions.SyncModeInAsyncContextError`, you are likely working in an async codebase
|
|
252
|
+
and should consider asynchronous iteration using :meth:`__aiter__` and :meth:`__anext__` instead.
|
|
253
|
+
"""
|
|
254
|
+
return ASyncIterator(self.__wrapped__.__aiter__())
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class ASyncIterable(_ASyncIterable):
|
|
258
|
+
def __init_subclass__(cls, **kwargs) -> None:
|
|
259
|
+
_init_subclass(cls, kwargs)
|
|
260
|
+
def __class_getitem__(cls, arg_or_args, **kwargs) -> Type["ASyncIterable[T]"]:
|
|
261
|
+
"""
|
|
262
|
+
This helper passes type information from subclasses to the subclass object.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
arg_or_args: Either a single type argument or a tuple of the type arguments used.
|
|
266
|
+
"""
|
|
267
|
+
if cls is ASyncIterable:
|
|
268
|
+
if kwargs:
|
|
269
|
+
raise RuntimeError("Cannot pass kwargs")
|
|
270
|
+
if isinstance(arg_or_args, tuple):
|
|
271
|
+
args = arg_or_args
|
|
272
|
+
else:
|
|
273
|
+
args = (arg_or_args,)
|
|
274
|
+
return _class_getitem(cls, args)
|
|
275
|
+
|
|
276
|
+
if hasattr(cls, "__parameters__"):
|
|
277
|
+
return super().__class_getitem__(arg_or_args, **kwargs)
|
|
278
|
+
else:
|
|
279
|
+
return cls
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
cdef class _ASyncIterator(_AwaitableAsyncIterableMixin):
|
|
283
|
+
"""
|
|
284
|
+
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.
|
|
285
|
+
|
|
286
|
+
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.
|
|
287
|
+
|
|
288
|
+
Note:
|
|
289
|
+
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:`~a_sync.exceptions.SyncModeInAsyncContextError` is raised from the `RuntimeError`, indicating that synchronous iteration is not possible in an already running event loop.
|
|
290
|
+
|
|
291
|
+
Example:
|
|
292
|
+
>>> async_iterator = ASyncIterator(some_async_iterator)
|
|
293
|
+
>>> async for item in async_iterator:
|
|
294
|
+
... print(item)
|
|
295
|
+
>>> for item in async_iterator:
|
|
296
|
+
... print(item)
|
|
297
|
+
|
|
298
|
+
See Also:
|
|
299
|
+
- :class:`ASyncIterable`
|
|
300
|
+
- :class:`ASyncFilter`
|
|
301
|
+
- :class:`ASyncSorter`
|
|
302
|
+
"""
|
|
303
|
+
cdef readonly object _anext
|
|
304
|
+
cdef object _loop
|
|
305
|
+
|
|
306
|
+
def __next__(self) -> T:
|
|
307
|
+
"""
|
|
308
|
+
Synchronously fetch the next item from the {cls}.
|
|
309
|
+
|
|
310
|
+
Note:
|
|
311
|
+
This method uses :meth:`asyncio.BaseEventLoop.run_until_complete` to fetch {obj}.
|
|
312
|
+
This raises a :class:`RuntimeError` if the event loop is already running.
|
|
313
|
+
This RuntimeError will be caught and a more descriptive :class:`~a_sync.exceptions.SyncModeInAsyncContextError` will be raised in its place.
|
|
314
|
+
|
|
315
|
+
If you encounter a :class:`~a_sync.exceptions.SyncModeInAsyncContextError`, you are likely working in an async codebase
|
|
316
|
+
and should consider asynchronous iteration using :meth:`__aiter__` and :meth:`__anext__` instead.
|
|
317
|
+
|
|
318
|
+
Raises:
|
|
319
|
+
StopIteration: Once all {obj} have been fetched from the {cls}.
|
|
320
|
+
SyncModeInAsyncContextError: If the event loop is already running.
|
|
321
|
+
|
|
322
|
+
"""
|
|
323
|
+
# If this is the first time this instance has been used synchronously, we
|
|
324
|
+
# cache `loop.run_until_complete` to use it more quickly for subsequent nexts
|
|
325
|
+
cdef object run_loop = self._run_loop
|
|
326
|
+
if run_loop is None:
|
|
327
|
+
run_loop = self._run_loop = get_event_loop().run_until_complete
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
return run_loop(self._anext())
|
|
331
|
+
except StopAsyncIteration as e:
|
|
332
|
+
raise StopIteration from e
|
|
333
|
+
except RuntimeError as e:
|
|
334
|
+
if str(e) == "This event loop is already running":
|
|
335
|
+
raise SyncModeInAsyncContextError(
|
|
336
|
+
"The event loop is already running. Try iterating using `async for` instead of `for`."
|
|
337
|
+
) from e
|
|
338
|
+
raise
|
|
339
|
+
|
|
340
|
+
@overload
|
|
341
|
+
def wrap(cls, aiterator: AsyncIterator[T]) -> "ASyncIterator[T]":
|
|
342
|
+
"""
|
|
343
|
+
Wraps an AsyncIterator in an ASyncIterator.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
aiterator: The AsyncIterator to wrap.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
@overload
|
|
350
|
+
def wrap(cls, async_gen_func: AsyncGenFunc[P, T]) -> "ASyncGeneratorFunction[P, T]":
|
|
351
|
+
"""
|
|
352
|
+
Wraps an async generator function in an ASyncGeneratorFunction.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
async_gen_func: The async generator function to wrap.
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
@classmethod
|
|
359
|
+
def wrap(cls, wrapped):
|
|
360
|
+
"""Class method to wrap either an AsyncIterator or an async generator function."""
|
|
361
|
+
if isinstance(wrapped, AsyncIterator):
|
|
362
|
+
logger.warning(
|
|
363
|
+
"This use case for ASyncIterator.wrap will be removed soon. "
|
|
364
|
+
"Please replace uses with simple instantiation ie `ASyncIterator(wrapped)`"
|
|
365
|
+
)
|
|
366
|
+
return cls(wrapped)
|
|
367
|
+
|
|
368
|
+
# We're going to assume that a dev writing cython knows what they're doing.
|
|
369
|
+
# Plus, we need it for this lib's internals to work properly.
|
|
370
|
+
elif isasyncgenfunction(wrapped) or type(wrapped).__name__ == "cython_function_or_method":
|
|
371
|
+
return ASyncGeneratorFunction(wrapped)
|
|
372
|
+
|
|
373
|
+
raise TypeError(
|
|
374
|
+
"`wrapped` must be an AsyncIterator or an async generator function. "
|
|
375
|
+
"You passed {} of type {}".format(wrapped, type(wrapped))
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def __init__(self, async_iterator: AsyncIterator[T]) -> None:
|
|
379
|
+
"""
|
|
380
|
+
Initializes the ASyncIterator with an async iterator.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
async_iterator: The async iterator to wrap.
|
|
384
|
+
"""
|
|
385
|
+
if not isinstance(async_iterator, AsyncIterator):
|
|
386
|
+
raise TypeError(
|
|
387
|
+
"`async_iterator` must be an AsyncIterator. You passed {}".format(async_iterator)
|
|
388
|
+
)
|
|
389
|
+
self.__wrapped__ = async_iterator
|
|
390
|
+
"The wrapped :class:`AsyncIterator`."
|
|
391
|
+
|
|
392
|
+
self._anext = async_iterator.__anext__
|
|
393
|
+
self._run_loop = None
|
|
394
|
+
|
|
395
|
+
def __anext__(self) -> Coroutine[Any, Any, T]:
|
|
396
|
+
"""
|
|
397
|
+
Asynchronously fetch the next item from the {cls}.
|
|
398
|
+
|
|
399
|
+
Raises:
|
|
400
|
+
:class:`StopAsyncIteration`: Once all {obj} have been fetched from the {cls}.
|
|
401
|
+
"""
|
|
402
|
+
return self._anext()
|
|
403
|
+
|
|
404
|
+
def __iter__(self) -> Self:
|
|
405
|
+
"""
|
|
406
|
+
Return the {cls} for iteration.
|
|
407
|
+
|
|
408
|
+
Note:
|
|
409
|
+
Synchronous iteration uses :meth:`asyncio.BaseEventLoop.run_until_complete` to fetch {obj}.
|
|
410
|
+
This raises a :class:`RuntimeError` if the event loop is already running.
|
|
411
|
+
This RuntimeError will be caught and a more descriptive :class:`~a_sync.exceptions.SyncModeInAsyncContextError` will be raised in its place.
|
|
412
|
+
|
|
413
|
+
If you encounter a :class:`~a_sync.exceptions.SyncModeInAsyncContextError`, you are likely working in an async codebase
|
|
414
|
+
and should consider asynchronous iteration using :meth:`__aiter__` and :meth:`__anext__` instead.
|
|
415
|
+
"""
|
|
416
|
+
return self
|
|
417
|
+
|
|
418
|
+
def __aiter__(self) -> Self:
|
|
419
|
+
"Return the {cls} for aiteration."
|
|
420
|
+
return self
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class ASyncIterator(_ASyncIterator):
|
|
424
|
+
def __init_subclass__(cls, **kwargs) -> None:
|
|
425
|
+
_init_subclass(cls, kwargs)
|
|
426
|
+
def __class_getitem__(cls, arg_or_args, **kwargs) -> Type["ASyncIterator[T]"]:
|
|
427
|
+
"""
|
|
428
|
+
This helper passes type information from subclasses to the subclass object.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
arg_or_args: Either a single type argument or a tuple of the type arguments used.
|
|
432
|
+
"""
|
|
433
|
+
if cls is ASyncIterator:
|
|
434
|
+
if kwargs:
|
|
435
|
+
raise RuntimeError("Cannot pass kwargs")
|
|
436
|
+
if isinstance(arg_or_args, tuple):
|
|
437
|
+
args = arg_or_args
|
|
438
|
+
else:
|
|
439
|
+
args = (arg_or_args,)
|
|
440
|
+
return _class_getitem(cls, args)
|
|
441
|
+
|
|
442
|
+
if hasattr(cls, "__parameters__"):
|
|
443
|
+
return super().__class_getitem__(arg_or_args, **kwargs)
|
|
444
|
+
else:
|
|
445
|
+
return cls
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
cdef class _ASyncGeneratorFunction:
|
|
450
|
+
"""
|
|
451
|
+
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.
|
|
452
|
+
|
|
453
|
+
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.
|
|
454
|
+
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.
|
|
455
|
+
Example:
|
|
456
|
+
>>> async def my_async_gen():
|
|
457
|
+
... yield 1
|
|
458
|
+
... yield 2
|
|
459
|
+
>>> async_gen_func = ASyncGeneratorFunction(my_async_gen)
|
|
460
|
+
>>> for item in async_gen_func():
|
|
461
|
+
... print(item)
|
|
462
|
+
See Also:
|
|
463
|
+
- :class:`ASyncIterator`
|
|
464
|
+
- :class:`ASyncIterable`
|
|
465
|
+
"""
|
|
466
|
+
|
|
467
|
+
def __init__(
|
|
468
|
+
self, async_gen_func: AsyncGenFunc[P, T], instance: Any = None
|
|
469
|
+
) -> None:
|
|
470
|
+
"""
|
|
471
|
+
Initializes the ASyncGeneratorFunction with the given async generator function and optionally an instance.
|
|
472
|
+
Args:
|
|
473
|
+
async_gen_func: The async generator function to wrap.
|
|
474
|
+
instance (optional): The object to bind to the function, if applicable.
|
|
475
|
+
"""
|
|
476
|
+
self.field_name = async_gen_func.__name__
|
|
477
|
+
"The name of the async generator function."
|
|
478
|
+
|
|
479
|
+
self.__wrapped__ = async_gen_func
|
|
480
|
+
"The actual async generator function."
|
|
481
|
+
|
|
482
|
+
if instance is None:
|
|
483
|
+
self.__weakself__ = None
|
|
484
|
+
self.__weakself_ptr = NULL
|
|
485
|
+
self._cache_handle = None
|
|
486
|
+
else:
|
|
487
|
+
weakself = ref(instance, _ASyncGeneratorFunction.__cancel_cache_handle)
|
|
488
|
+
self.__weakself__ = weakself
|
|
489
|
+
self.__weakself_ptr = <PyObject*>weakself
|
|
490
|
+
self._cache_handle = self._get_cache_handle(instance)
|
|
491
|
+
|
|
492
|
+
def __repr__(self) -> str:
|
|
493
|
+
return "<{} for {} at {}>".format(
|
|
494
|
+
type(self).__name__,
|
|
495
|
+
self.__wrapped__,
|
|
496
|
+
hex(id(self))
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> ASyncIterator[T]:
|
|
500
|
+
"""
|
|
501
|
+
Calls the wrapped async generator function with the given arguments and keyword arguments, returning an :class:`ASyncIterator`.
|
|
502
|
+
Args:
|
|
503
|
+
*args: Positional arguments for the function.
|
|
504
|
+
**kwargs: Keyword arguments for the function.
|
|
505
|
+
"""
|
|
506
|
+
if self.__weakself__ is None:
|
|
507
|
+
return ASyncIterator(self.__wrapped__(*args, **kwargs))
|
|
508
|
+
return ASyncIterator(self.__wrapped__(self.__self__, *args, **kwargs))
|
|
509
|
+
|
|
510
|
+
def __get__(self, instance: V, owner: Type[V]) -> "ASyncGeneratorFunction[P, T]":
|
|
511
|
+
"Descriptor method to make the function act like a non-data descriptor."
|
|
512
|
+
cdef _ASyncGeneratorFunction gen_func
|
|
513
|
+
cdef dict instance_dict
|
|
514
|
+
|
|
515
|
+
if instance is None:
|
|
516
|
+
return self
|
|
517
|
+
|
|
518
|
+
instance_dict = instance.__dict__
|
|
519
|
+
|
|
520
|
+
try:
|
|
521
|
+
gen_func = instance_dict[self.field_name]
|
|
522
|
+
except KeyError:
|
|
523
|
+
gen_func = ASyncGeneratorFunction(self.__wrapped__, instance)
|
|
524
|
+
instance_dict[self.field_name] = gen_func
|
|
525
|
+
|
|
526
|
+
gen_func._set_cache_handle(self._get_cache_handle(instance))
|
|
527
|
+
return gen_func
|
|
528
|
+
|
|
529
|
+
@property
|
|
530
|
+
def __self__(self) -> object:
|
|
531
|
+
cdef PyObject *weakself_ptr = self.__weakself_ptr
|
|
532
|
+
cdef PyObject *instance_ptr
|
|
533
|
+
|
|
534
|
+
if weakself_ptr is NULL:
|
|
535
|
+
raise AttributeError("{} has no attribute '__self__'".format(self))
|
|
536
|
+
elif PyWeakref_GetRef(weakself_ptr, &instance_ptr) == 1:
|
|
537
|
+
# 1 is success
|
|
538
|
+
return <object>instance_ptr
|
|
539
|
+
raise ReferenceError(self)
|
|
540
|
+
|
|
541
|
+
cdef inline void _set_cache_handle(self, object handle):
|
|
542
|
+
self.__cancel_cache_handle()
|
|
543
|
+
self._cache_handle = handle
|
|
544
|
+
|
|
545
|
+
cdef inline object _get_cache_handle(self, object instance):
|
|
546
|
+
# NOTE: we create a strong reference to instance here. I'm not sure if this is good or not but its necessary for now.
|
|
547
|
+
return get_event_loop().call_later(
|
|
548
|
+
300, delattr, instance, self.field_name
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
cdef void __cancel_cache_handle(self):
|
|
552
|
+
cancel_handle(self._cache_handle)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class ASyncGeneratorFunction(_ASyncGeneratorFunction, Generic[P, T]):
|
|
556
|
+
def __init__(
|
|
557
|
+
self, async_gen_func: AsyncGenFunc[P, T], instance: Any = None
|
|
558
|
+
) -> None:
|
|
559
|
+
_ASyncGeneratorFunction.__init__(self, async_gen_func, instance)
|
|
560
|
+
update_wrapper(self, self.__wrapped__)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
cdef class _ASyncView(_ASyncIterator):
|
|
565
|
+
"""
|
|
566
|
+
Internal mixin class containing logic for creating specialized views for :class:`~ASyncIterable` objects.
|
|
567
|
+
"""
|
|
568
|
+
cdef readonly object __aiterator__
|
|
569
|
+
cdef readonly object __iterator__
|
|
570
|
+
cdef readonly object _function
|
|
571
|
+
|
|
572
|
+
def __init__(
|
|
573
|
+
self,
|
|
574
|
+
function: ViewFn[T],
|
|
575
|
+
iterable: AnyIterable[T],
|
|
576
|
+
) -> None:
|
|
577
|
+
"""
|
|
578
|
+
Initializes the {cls} with a function and an iterable.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
function: A function to apply to the items in the iterable.
|
|
582
|
+
iterable: An iterable or an async iterable yielding objects to which `function` will be applied.
|
|
583
|
+
"""
|
|
584
|
+
self._function = function
|
|
585
|
+
self.__wrapped__ = iterable
|
|
586
|
+
if isinstance(iterable, AsyncIterable):
|
|
587
|
+
self.__iterator__ = None
|
|
588
|
+
self.__aiterator__ = iterable.__aiter__()
|
|
589
|
+
elif isinstance(iterable, Iterable):
|
|
590
|
+
self.__iterator__ = <object>PyObject_GetIter(iterable)
|
|
591
|
+
self.__aiterator__ = None
|
|
592
|
+
else:
|
|
593
|
+
raise TypeError(
|
|
594
|
+
"`iterable` must be AsyncIterable or Iterable, you passed {}".format(iterable)
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
cdef class _ASyncFilter(_ASyncView):
|
|
599
|
+
"""
|
|
600
|
+
An async filter class that filters items of an async iterable based on a provided function.
|
|
601
|
+
|
|
602
|
+
This class inherits from :class:`~_ASyncView` and provides the functionality to asynchronously
|
|
603
|
+
iterate over items, applying the filter function to each item to determine if it should be
|
|
604
|
+
included in the result. The filter function can be either synchronous or asynchronous.
|
|
605
|
+
|
|
606
|
+
Example:
|
|
607
|
+
>>> async def is_even(x):
|
|
608
|
+
... return x % 2 == 0
|
|
609
|
+
>>> filtered_iterable = ASyncFilter(is_even, some_async_iterable)
|
|
610
|
+
>>> async for item in filtered_iterable:
|
|
611
|
+
... print(item)
|
|
612
|
+
|
|
613
|
+
See Also:
|
|
614
|
+
- :class:`ASyncIterable`
|
|
615
|
+
- :class:`ASyncIterator`
|
|
616
|
+
- :class:`ASyncSorter`
|
|
617
|
+
"""
|
|
618
|
+
|
|
619
|
+
@final
|
|
620
|
+
def __repr__(self) -> str:
|
|
621
|
+
return "<{type(self).__name__} for iterator={} function={} at {}>".format(
|
|
622
|
+
self.__wrapped__, self._function.__name__, hex(id(self))
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
@final
|
|
626
|
+
async def __anext__(self) -> T:
|
|
627
|
+
cdef object obj
|
|
628
|
+
if self.__aiterator__:
|
|
629
|
+
async for obj in self.__aiterator__:
|
|
630
|
+
if await self._check(obj):
|
|
631
|
+
return obj
|
|
632
|
+
elif self.__iterator__:
|
|
633
|
+
try:
|
|
634
|
+
for obj in self.__iterator__:
|
|
635
|
+
if await self._check(obj):
|
|
636
|
+
return obj
|
|
637
|
+
except StopIteration:
|
|
638
|
+
pass
|
|
639
|
+
else:
|
|
640
|
+
raise TypeError(self.__wrapped__)
|
|
641
|
+
raise StopAsyncIteration from None
|
|
642
|
+
|
|
643
|
+
@final
|
|
644
|
+
async def _check(self, obj: T) -> bool:
|
|
645
|
+
"""
|
|
646
|
+
Checks if an object passes the filter function.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
obj: The object to check.
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
True if the object passes the filter, False otherwise.
|
|
653
|
+
"""
|
|
654
|
+
cdef object checked = self._function(obj)
|
|
655
|
+
return bool(await checked) if isawaitable(checked) else bool(checked)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
class ASyncFilter(_ASyncFilter):
|
|
659
|
+
def __init_subclass__(cls, **kwargs) -> None:
|
|
660
|
+
_init_subclass(cls, kwargs)
|
|
661
|
+
def __class_getitem__(cls, arg_or_args, **kwargs) -> Type["ASyncFilter[T]"]:
|
|
662
|
+
"""
|
|
663
|
+
This helper passes type information from subclasses to the subclass object.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
arg_or_args: Either a single type argument or a tuple of the type arguments used.
|
|
667
|
+
"""
|
|
668
|
+
if cls is ASyncFilter:
|
|
669
|
+
if kwargs:
|
|
670
|
+
raise RuntimeError("Cannot pass kwargs")
|
|
671
|
+
if isinstance(arg_or_args, tuple):
|
|
672
|
+
args = arg_or_args
|
|
673
|
+
else:
|
|
674
|
+
args = (arg_or_args,)
|
|
675
|
+
return _class_getitem(cls, args)
|
|
676
|
+
return super().__class_getitem__(arg_or_args, **kwargs)
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
cdef object _key_if_no_key(object obj):
|
|
680
|
+
"""
|
|
681
|
+
Default key function that returns the object itself if no key is provided.
|
|
682
|
+
|
|
683
|
+
Args:
|
|
684
|
+
obj: The object to return.
|
|
685
|
+
"""
|
|
686
|
+
return obj
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
cdef class _ASyncSorter(_ASyncView):
|
|
690
|
+
"""
|
|
691
|
+
An async sorter class that sorts items of an async iterable based on a provided key function.
|
|
692
|
+
|
|
693
|
+
This class inherits from :class:`~_ASyncView` and provides the functionality to asynchronously
|
|
694
|
+
iterate over items, applying the key function to each item for sorting. The key function can be
|
|
695
|
+
either synchronous or asynchronous. Note that the ASyncSorter instance can only be consumed once.
|
|
696
|
+
|
|
697
|
+
Example:
|
|
698
|
+
>>> sorted_iterable = ASyncSorter(some_async_iterable, key=lambda x: x.value)
|
|
699
|
+
>>> async for item in sorted_iterable:
|
|
700
|
+
... print(item)
|
|
701
|
+
|
|
702
|
+
See Also:
|
|
703
|
+
- :class:`ASyncIterable`
|
|
704
|
+
- :class:`ASyncIterator`
|
|
705
|
+
- :class:`ASyncFilter`
|
|
706
|
+
"""
|
|
707
|
+
|
|
708
|
+
cdef readonly bint reversed
|
|
709
|
+
cdef readonly bint _consumed
|
|
710
|
+
cdef readonly object __internal
|
|
711
|
+
|
|
712
|
+
def __cinit__(self):
|
|
713
|
+
self.reversed = False
|
|
714
|
+
self._consumed = False
|
|
715
|
+
|
|
716
|
+
@final
|
|
717
|
+
def __init__(
|
|
718
|
+
self,
|
|
719
|
+
iterable: AsyncIterable[T],
|
|
720
|
+
*,
|
|
721
|
+
key: SortKey[T] = None,
|
|
722
|
+
reverse: bool = False,
|
|
723
|
+
) -> None:
|
|
724
|
+
"""
|
|
725
|
+
Initializes the ASyncSorter with an iterable and an optional sorting configuration (key function, and reverse flag).
|
|
726
|
+
|
|
727
|
+
Args:
|
|
728
|
+
iterable: The async iterable to sort.
|
|
729
|
+
key (optional): A function of one argument that is used to extract a comparison key from each list element. If none is provided, elements themselves will be sorted. Defaults to None.
|
|
730
|
+
reverse (optional): If True, the list elements will be sorted in reverse order. Defaults to False.
|
|
731
|
+
"""
|
|
732
|
+
_ASyncView.__init__(self, _key_if_no_key if key is None else key, iterable)
|
|
733
|
+
internal_aiterator = self.__sort(reverse=reverse).__aiter__()
|
|
734
|
+
self.__internal = internal_aiterator
|
|
735
|
+
self._anext = internal_aiterator.__anext__
|
|
736
|
+
if reverse:
|
|
737
|
+
self.reversed = True
|
|
738
|
+
|
|
739
|
+
@final
|
|
740
|
+
def __aiter__(self):
|
|
741
|
+
"""
|
|
742
|
+
Return an async iterator for the {cls}.
|
|
743
|
+
|
|
744
|
+
Raises:
|
|
745
|
+
RuntimeError: If the ASyncSorter instance has already been consumed.
|
|
746
|
+
|
|
747
|
+
Returns:
|
|
748
|
+
An async iterator that will yield the sorted {obj}.
|
|
749
|
+
"""
|
|
750
|
+
if self._consumed:
|
|
751
|
+
raise RuntimeError("{} has already been consumed".format(self))
|
|
752
|
+
return self
|
|
753
|
+
|
|
754
|
+
@final
|
|
755
|
+
def __repr__(self) -> str:
|
|
756
|
+
cdef str rep = f"<{type(self).__name__}"
|
|
757
|
+
if self.reversed:
|
|
758
|
+
rep += " reversed"
|
|
759
|
+
rep += " for iterator={}".format(self.__wrapped__)
|
|
760
|
+
if self._function is not _key_if_no_key:
|
|
761
|
+
rep += " key={}".format(self._function.__name__)
|
|
762
|
+
rep += " at {}>".format(hex(id(self)))
|
|
763
|
+
return rep
|
|
764
|
+
|
|
765
|
+
@final
|
|
766
|
+
def __anext__(self):
|
|
767
|
+
return self._anext()
|
|
768
|
+
|
|
769
|
+
@final
|
|
770
|
+
async def __sort(self, bint reverse):
|
|
771
|
+
"""
|
|
772
|
+
This method is internal so the original iterator can only ever be consumed once.
|
|
773
|
+
|
|
774
|
+
Args:
|
|
775
|
+
reverse: If True, the list elements will be sorted in reverse order.
|
|
776
|
+
|
|
777
|
+
Returns:
|
|
778
|
+
An async iterator that will yield the sorted items.
|
|
779
|
+
"""
|
|
780
|
+
cdef list items, sort_tasks
|
|
781
|
+
cdef object obj
|
|
782
|
+
|
|
783
|
+
if iscoroutinefunction(self._function):
|
|
784
|
+
items = []
|
|
785
|
+
sort_tasks = []
|
|
786
|
+
if self.__aiterator__:
|
|
787
|
+
async for obj in self.__aiterator__:
|
|
788
|
+
items.append(obj)
|
|
789
|
+
sort_tasks.append(
|
|
790
|
+
ccreate_task_simple(self._function(obj))
|
|
791
|
+
)
|
|
792
|
+
elif self.__iterator__:
|
|
793
|
+
for obj in self.__iterator__:
|
|
794
|
+
items.append(obj)
|
|
795
|
+
sort_tasks.append(
|
|
796
|
+
ccreate_task_simple(self._function(obj))
|
|
797
|
+
)
|
|
798
|
+
for sort_value, obj in sorted(
|
|
799
|
+
zip(await cigather(sort_tasks), items),
|
|
800
|
+
reverse=reverse,
|
|
801
|
+
):
|
|
802
|
+
yield obj
|
|
803
|
+
else:
|
|
804
|
+
if self.__aiterator__:
|
|
805
|
+
items = [obj async for obj in self.__aiterator__]
|
|
806
|
+
else:
|
|
807
|
+
items = list(self.__iterator__)
|
|
808
|
+
items.sort(key=self._function, reverse=reverse)
|
|
809
|
+
for obj in items:
|
|
810
|
+
yield obj
|
|
811
|
+
self._consumed = True
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
class ASyncSorter(_ASyncSorter):
|
|
815
|
+
def __init_subclass__(cls, **kwargs) -> None:
|
|
816
|
+
_init_subclass(cls, kwargs)
|
|
817
|
+
def __class_getitem__(cls, arg_or_args, **kwargs) -> Type["ASyncSorter[T]"]:
|
|
818
|
+
"""This helper passes type information from subclasses to the subclass object"""
|
|
819
|
+
if cls is ASyncSorter:
|
|
820
|
+
if kwargs:
|
|
821
|
+
raise RuntimeError("Cannot pass kwargs")
|
|
822
|
+
if isinstance(arg_or_args, tuple):
|
|
823
|
+
args = arg_or_args
|
|
824
|
+
else:
|
|
825
|
+
args = (arg_or_args,)
|
|
826
|
+
return _class_getitem(cls, args)
|
|
827
|
+
|
|
828
|
+
if hasattr(cls, "__parameters__"):
|
|
829
|
+
return super().__class_getitem__(arg_or_args, **kwargs)
|
|
830
|
+
else:
|
|
831
|
+
return cls
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
@lru_cache(maxsize=None)
|
|
835
|
+
def __class_getitem(untyped_cls: Type, tuple type_args):
|
|
836
|
+
args_string = ", ".join(
|
|
837
|
+
getattr(arg, "__name__", None) or repr(arg)
|
|
838
|
+
for arg in type_args
|
|
839
|
+
)
|
|
840
|
+
typed_cls_name = f"{untyped_cls.__name__}[{args_string}]"
|
|
841
|
+
typed_cls_dict = typed_class_dict = {
|
|
842
|
+
"__args__": type_args,
|
|
843
|
+
"__module__": untyped_cls.__module__,
|
|
844
|
+
"__qualname__": f"{untyped_cls.__qualname__}[{args_string}]",
|
|
845
|
+
"__origin__": untyped_cls,
|
|
846
|
+
}
|
|
847
|
+
if untyped_cls.__doc__ is not None:
|
|
848
|
+
typed_cls_dict["__doc__"] = str(untyped_cls.__doc__)
|
|
849
|
+
if hasattr(untyped_cls, "__annotations__"):
|
|
850
|
+
typed_cls_dict["__annotations__"] = untyped_cls.__annotations__
|
|
851
|
+
typed_cls = type(typed_cls_name, (untyped_cls, ), typed_cls_dict)
|
|
852
|
+
return typed_cls
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
cdef object _class_getitem = __class_getitem
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
cdef void _init_subclass(cls, dict kwargs):
|
|
859
|
+
# Determine the type used for T in the subclass
|
|
860
|
+
cdef object type_argument = T # Default value
|
|
861
|
+
cdef str type_string = ":obj:`T` objects"
|
|
862
|
+
|
|
863
|
+
cdef object base
|
|
864
|
+
cdef tuple args
|
|
865
|
+
cdef str module, qualname, name
|
|
866
|
+
for base in getattr(cls, "__orig_bases__", []):
|
|
867
|
+
if not hasattr(base, "__args__"):
|
|
868
|
+
continue
|
|
869
|
+
|
|
870
|
+
args = get_args(base)
|
|
871
|
+
if base in (ASyncIterable, ASyncIterator, ASyncFilter, ASyncSorter):
|
|
872
|
+
raise Exception(base, args)
|
|
873
|
+
|
|
874
|
+
if args and not isinstance(type_argument := args[0], TypeVar):
|
|
875
|
+
module = getattr(type_argument, "__module__", "")
|
|
876
|
+
qualname = getattr(type_argument, "__qualname__", "")
|
|
877
|
+
name = getattr(type_argument, "__name__", "")
|
|
878
|
+
|
|
879
|
+
if module and qualname:
|
|
880
|
+
type_string = ":class:`~{}.{}`".format(module, qualname)
|
|
881
|
+
elif module and name:
|
|
882
|
+
type_string = (":class:`~{}.{}`".format(module, name))
|
|
883
|
+
elif qualname:
|
|
884
|
+
type_string = ":class:`{}`".format(qualname)
|
|
885
|
+
elif name:
|
|
886
|
+
type_string = ":class:`{}`".format(name)
|
|
887
|
+
else:
|
|
888
|
+
type_string = str(type_argument)
|
|
889
|
+
|
|
890
|
+
# modify the class docstring
|
|
891
|
+
cdef str new_chunk = (
|
|
892
|
+
"When awaited, a list of all {} will be returned.\n".format(type_string) +
|
|
893
|
+
"\n"
|
|
894
|
+
"Example:\n"
|
|
895
|
+
" >>> my_object = {}(...)\n".format(cls.__name__) +
|
|
896
|
+
" >>> all_contents = await my_object\n"
|
|
897
|
+
" >>> isinstance(all_contents, list)\n"
|
|
898
|
+
" True\n"
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
cdef str example_text = (
|
|
902
|
+
type_argument._name
|
|
903
|
+
if isinstance(type_argument, _GenericAlias)
|
|
904
|
+
else getattr(type_argument, "__name__", "")
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
if example_text:
|
|
908
|
+
new_chunk += (
|
|
909
|
+
" >>> isinstance(all_contents[0], {})\n".format(example_text) +
|
|
910
|
+
" True\n"
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
if cls.__doc__ is None:
|
|
914
|
+
cls.__doc__ = new_chunk
|
|
915
|
+
elif not cls.__doc__ or cls.__doc__.endswith("\n\n"):
|
|
916
|
+
cls.__doc__ += new_chunk
|
|
917
|
+
elif cls.__doc__.endswith("\n"):
|
|
918
|
+
cls.__doc__ += "\n{}".format(new_chunk)
|
|
919
|
+
else:
|
|
920
|
+
cls.__doc__ += "\n\n{}".format(new_chunk)
|
|
921
|
+
|
|
922
|
+
# Update method docstrings by redefining methods
|
|
923
|
+
# This is necessary because, by default, subclasses inherit methods from their bases
|
|
924
|
+
# which means if we just update the docstring we might edit docs for unrelated objects
|
|
925
|
+
is_function = lambda obj: isinstance(obj, (FunctionType, property)) or "cython_function_or_method" in type(obj).__name__
|
|
926
|
+
|
|
927
|
+
cdef dict functions_to_redefine = {
|
|
928
|
+
attr_name: attr_value
|
|
929
|
+
for attr_name in dir(cls)
|
|
930
|
+
if (attr_value := getattr(cls, attr_name, None))
|
|
931
|
+
and is_function(attr_value)
|
|
932
|
+
and attr_value.__doc__
|
|
933
|
+
and any(pattern in attr_value.__doc__ for pattern in _FORMAT_PATTERNS)
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
cdef str function_name
|
|
937
|
+
cdef object function_obj
|
|
938
|
+
for function_name, function_obj in functions_to_redefine.items():
|
|
939
|
+
# Create a new function object with the docstring formatted appropriately for this class
|
|
940
|
+
#redefined_function_obj = FunctionType(
|
|
941
|
+
# function_obj.__code__,
|
|
942
|
+
# function_obj.__globals__,
|
|
943
|
+
# name=function_obj.__name__,
|
|
944
|
+
# argdefs=function_obj.__defaults__,
|
|
945
|
+
# closure=function_obj.__closure__,
|
|
946
|
+
#)
|
|
947
|
+
redefined_function_obj = None
|
|
948
|
+
if hasattr(_AwaitableAsyncIterableMixin, function_name):
|
|
949
|
+
base_definition = getattr(_AwaitableAsyncIterableMixin, function_name)
|
|
950
|
+
if function_obj.__doc__ == base_definition.__doc__:
|
|
951
|
+
redefined_function_obj = deepcopy(base_definition)
|
|
952
|
+
elif cls.__name__ != "ASyncIterable" and hasattr(ASyncIterable, function_name):
|
|
953
|
+
base_definition = getattr(ASyncIterable, function_name)
|
|
954
|
+
if function_obj.__doc__ == base_definition.__doc__:
|
|
955
|
+
redefined_function_obj = deepcopy(base_definition)
|
|
956
|
+
elif cls.__name__ not in ("ASyncIterable", "ASyncIterator") and hasattr(ASyncIterator, function_name):
|
|
957
|
+
base_definition = getattr(ASyncIterator, function_name)
|
|
958
|
+
if function_obj.__doc__ == base_definition.__doc__:
|
|
959
|
+
redefined_function_obj = deepcopy(base_definition)
|
|
960
|
+
|
|
961
|
+
if redefined_function_obj is None:
|
|
962
|
+
redefined_function_obj = deepcopy(function_obj)
|
|
963
|
+
|
|
964
|
+
redefined_function_obj.__doc__ = function_obj.__doc__.format(
|
|
965
|
+
cls=cls.__name__,
|
|
966
|
+
obj=type_string,
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
if "{cls}" in redefined_function_obj.__doc__:
|
|
970
|
+
raise ValueError(redefined_function_obj.__doc__)
|
|
971
|
+
|
|
972
|
+
setattr(cls, function_name, redefined_function_obj)
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
__all__ = [
|
|
976
|
+
"ASyncIterable",
|
|
977
|
+
"ASyncIterator",
|
|
978
|
+
"ASyncFilter",
|
|
979
|
+
"ASyncSorter",
|
|
980
|
+
"ASyncGeneratorFunction",
|
|
981
|
+
]
|