ez-a-sync 0.33.4__cp313-cp313-musllinux_1_2_i686.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.
Files changed (177) hide show
  1. a_sync/ENVIRONMENT_VARIABLES.py +42 -0
  2. a_sync/__init__.pxd +2 -0
  3. a_sync/__init__.py +145 -0
  4. a_sync/_smart.c +22830 -0
  5. a_sync/_smart.cpython-313-i386-linux-musl.so +0 -0
  6. a_sync/_smart.pxd +2 -0
  7. a_sync/_smart.pyi +202 -0
  8. a_sync/_smart.pyx +674 -0
  9. a_sync/_typing.py +258 -0
  10. a_sync/a_sync/__init__.py +60 -0
  11. a_sync/a_sync/_descriptor.c +20537 -0
  12. a_sync/a_sync/_descriptor.cpython-313-i386-linux-musl.so +0 -0
  13. a_sync/a_sync/_descriptor.pyi +33 -0
  14. a_sync/a_sync/_descriptor.pyx +422 -0
  15. a_sync/a_sync/_flags.c +6082 -0
  16. a_sync/a_sync/_flags.cpython-313-i386-linux-musl.so +0 -0
  17. a_sync/a_sync/_flags.pxd +3 -0
  18. a_sync/a_sync/_flags.pyx +92 -0
  19. a_sync/a_sync/_helpers.c +14529 -0
  20. a_sync/a_sync/_helpers.cpython-313-i386-linux-musl.so +0 -0
  21. a_sync/a_sync/_helpers.pxd +3 -0
  22. a_sync/a_sync/_helpers.pyi +10 -0
  23. a_sync/a_sync/_helpers.pyx +167 -0
  24. a_sync/a_sync/_kwargs.c +12202 -0
  25. a_sync/a_sync/_kwargs.cpython-313-i386-linux-musl.so +0 -0
  26. a_sync/a_sync/_kwargs.pxd +2 -0
  27. a_sync/a_sync/_kwargs.pyx +64 -0
  28. a_sync/a_sync/_meta.py +210 -0
  29. a_sync/a_sync/abstract.c +12420 -0
  30. a_sync/a_sync/abstract.cpython-313-i386-linux-musl.so +0 -0
  31. a_sync/a_sync/abstract.pyi +141 -0
  32. a_sync/a_sync/abstract.pyx +221 -0
  33. a_sync/a_sync/base.c +14940 -0
  34. a_sync/a_sync/base.cpython-313-i386-linux-musl.so +0 -0
  35. a_sync/a_sync/base.pyi +60 -0
  36. a_sync/a_sync/base.pyx +271 -0
  37. a_sync/a_sync/config.py +168 -0
  38. a_sync/a_sync/decorator.py +651 -0
  39. a_sync/a_sync/flags.c +5272 -0
  40. a_sync/a_sync/flags.cpython-313-i386-linux-musl.so +0 -0
  41. a_sync/a_sync/flags.pxd +72 -0
  42. a_sync/a_sync/flags.pyi +74 -0
  43. a_sync/a_sync/flags.pyx +72 -0
  44. a_sync/a_sync/function.c +37856 -0
  45. a_sync/a_sync/function.cpython-313-i386-linux-musl.so +0 -0
  46. a_sync/a_sync/function.pxd +28 -0
  47. a_sync/a_sync/function.pyi +571 -0
  48. a_sync/a_sync/function.pyx +1381 -0
  49. a_sync/a_sync/method.c +29662 -0
  50. a_sync/a_sync/method.cpython-313-i386-linux-musl.so +0 -0
  51. a_sync/a_sync/method.pxd +9 -0
  52. a_sync/a_sync/method.pyi +523 -0
  53. a_sync/a_sync/method.pyx +1023 -0
  54. a_sync/a_sync/modifiers/__init__.pxd +1 -0
  55. a_sync/a_sync/modifiers/__init__.py +101 -0
  56. a_sync/a_sync/modifiers/cache/__init__.py +160 -0
  57. a_sync/a_sync/modifiers/cache/memory.py +165 -0
  58. a_sync/a_sync/modifiers/limiter.py +132 -0
  59. a_sync/a_sync/modifiers/manager.c +16157 -0
  60. a_sync/a_sync/modifiers/manager.cpython-313-i386-linux-musl.so +0 -0
  61. a_sync/a_sync/modifiers/manager.pxd +5 -0
  62. a_sync/a_sync/modifiers/manager.pyi +219 -0
  63. a_sync/a_sync/modifiers/manager.pyx +299 -0
  64. a_sync/a_sync/modifiers/semaphores.py +173 -0
  65. a_sync/a_sync/property.c +27268 -0
  66. a_sync/a_sync/property.cpython-313-i386-linux-musl.so +0 -0
  67. a_sync/a_sync/property.pyi +376 -0
  68. a_sync/a_sync/property.pyx +819 -0
  69. a_sync/a_sync/singleton.py +63 -0
  70. a_sync/aliases.py +3 -0
  71. a_sync/async_property/__init__.pxd +1 -0
  72. a_sync/async_property/__init__.py +1 -0
  73. a_sync/async_property/cached.c +20397 -0
  74. a_sync/async_property/cached.cpython-313-i386-linux-musl.so +0 -0
  75. a_sync/async_property/cached.pxd +10 -0
  76. a_sync/async_property/cached.pyi +45 -0
  77. a_sync/async_property/cached.pyx +178 -0
  78. a_sync/async_property/proxy.c +34662 -0
  79. a_sync/async_property/proxy.cpython-313-i386-linux-musl.so +0 -0
  80. a_sync/async_property/proxy.pxd +2 -0
  81. a_sync/async_property/proxy.pyi +124 -0
  82. a_sync/async_property/proxy.pyx +474 -0
  83. a_sync/asyncio/__init__.pxd +6 -0
  84. a_sync/asyncio/__init__.py +164 -0
  85. a_sync/asyncio/as_completed.c +18849 -0
  86. a_sync/asyncio/as_completed.cpython-313-i386-linux-musl.so +0 -0
  87. a_sync/asyncio/as_completed.pxd +8 -0
  88. a_sync/asyncio/as_completed.pyi +109 -0
  89. a_sync/asyncio/as_completed.pyx +269 -0
  90. a_sync/asyncio/create_task.c +15912 -0
  91. a_sync/asyncio/create_task.cpython-313-i386-linux-musl.so +0 -0
  92. a_sync/asyncio/create_task.pxd +2 -0
  93. a_sync/asyncio/create_task.pyi +51 -0
  94. a_sync/asyncio/create_task.pyx +271 -0
  95. a_sync/asyncio/gather.c +16687 -0
  96. a_sync/asyncio/gather.cpython-313-i386-linux-musl.so +0 -0
  97. a_sync/asyncio/gather.pyi +107 -0
  98. a_sync/asyncio/gather.pyx +218 -0
  99. a_sync/asyncio/igather.c +13080 -0
  100. a_sync/asyncio/igather.cpython-313-i386-linux-musl.so +0 -0
  101. a_sync/asyncio/igather.pxd +1 -0
  102. a_sync/asyncio/igather.pyi +8 -0
  103. a_sync/asyncio/igather.pyx +183 -0
  104. a_sync/asyncio/sleep.c +9601 -0
  105. a_sync/asyncio/sleep.cpython-313-i386-linux-musl.so +0 -0
  106. a_sync/asyncio/sleep.pyi +14 -0
  107. a_sync/asyncio/sleep.pyx +49 -0
  108. a_sync/debugging.c +15370 -0
  109. a_sync/debugging.cpython-313-i386-linux-musl.so +0 -0
  110. a_sync/debugging.pyi +76 -0
  111. a_sync/debugging.pyx +107 -0
  112. a_sync/exceptions.c +13320 -0
  113. a_sync/exceptions.cpython-313-i386-linux-musl.so +0 -0
  114. a_sync/exceptions.pyi +376 -0
  115. a_sync/exceptions.pyx +446 -0
  116. a_sync/executor.py +619 -0
  117. a_sync/functools.c +12746 -0
  118. a_sync/functools.cpython-313-i386-linux-musl.so +0 -0
  119. a_sync/functools.pxd +7 -0
  120. a_sync/functools.pyi +33 -0
  121. a_sync/functools.pyx +139 -0
  122. a_sync/future.py +1497 -0
  123. a_sync/iter.c +37279 -0
  124. a_sync/iter.cpython-313-i386-linux-musl.so +0 -0
  125. a_sync/iter.pxd +11 -0
  126. a_sync/iter.pyi +370 -0
  127. a_sync/iter.pyx +981 -0
  128. a_sync/primitives/__init__.pxd +1 -0
  129. a_sync/primitives/__init__.py +53 -0
  130. a_sync/primitives/_debug.c +15765 -0
  131. a_sync/primitives/_debug.cpython-313-i386-linux-musl.so +0 -0
  132. a_sync/primitives/_debug.pxd +12 -0
  133. a_sync/primitives/_debug.pyi +52 -0
  134. a_sync/primitives/_debug.pyx +223 -0
  135. a_sync/primitives/_loggable.c +11538 -0
  136. a_sync/primitives/_loggable.cpython-313-i386-linux-musl.so +0 -0
  137. a_sync/primitives/_loggable.pxd +4 -0
  138. a_sync/primitives/_loggable.pyi +66 -0
  139. a_sync/primitives/_loggable.pyx +102 -0
  140. a_sync/primitives/locks/__init__.pxd +8 -0
  141. a_sync/primitives/locks/__init__.py +17 -0
  142. a_sync/primitives/locks/counter.c +17938 -0
  143. a_sync/primitives/locks/counter.cpython-313-i386-linux-musl.so +0 -0
  144. a_sync/primitives/locks/counter.pxd +12 -0
  145. a_sync/primitives/locks/counter.pyi +151 -0
  146. a_sync/primitives/locks/counter.pyx +267 -0
  147. a_sync/primitives/locks/event.c +17072 -0
  148. a_sync/primitives/locks/event.cpython-313-i386-linux-musl.so +0 -0
  149. a_sync/primitives/locks/event.pxd +22 -0
  150. a_sync/primitives/locks/event.pyi +43 -0
  151. a_sync/primitives/locks/event.pyx +185 -0
  152. a_sync/primitives/locks/prio_semaphore.c +25635 -0
  153. a_sync/primitives/locks/prio_semaphore.cpython-313-i386-linux-musl.so +0 -0
  154. a_sync/primitives/locks/prio_semaphore.pxd +25 -0
  155. a_sync/primitives/locks/prio_semaphore.pyi +217 -0
  156. a_sync/primitives/locks/prio_semaphore.pyx +597 -0
  157. a_sync/primitives/locks/semaphore.c +26553 -0
  158. a_sync/primitives/locks/semaphore.cpython-313-i386-linux-musl.so +0 -0
  159. a_sync/primitives/locks/semaphore.pxd +21 -0
  160. a_sync/primitives/locks/semaphore.pyi +197 -0
  161. a_sync/primitives/locks/semaphore.pyx +454 -0
  162. a_sync/primitives/queue.py +1026 -0
  163. a_sync/py.typed +0 -0
  164. a_sync/sphinx/__init__.py +3 -0
  165. a_sync/sphinx/ext.py +289 -0
  166. a_sync/task.py +934 -0
  167. a_sync/utils/__init__.py +105 -0
  168. a_sync/utils/iterators.py +297 -0
  169. a_sync/utils/repr.c +15866 -0
  170. a_sync/utils/repr.cpython-313-i386-linux-musl.so +0 -0
  171. a_sync/utils/repr.pyi +2 -0
  172. a_sync/utils/repr.pyx +73 -0
  173. ez_a_sync-0.33.4.dist-info/METADATA +368 -0
  174. ez_a_sync-0.33.4.dist-info/RECORD +177 -0
  175. ez_a_sync-0.33.4.dist-info/WHEEL +5 -0
  176. ez_a_sync-0.33.4.dist-info/licenses/LICENSE.txt +17 -0
  177. ez_a_sync-0.33.4.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
+ ]