python-iterutils 0.2.10.2__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
iterutils/dynamic.py ADDED
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env python3
2
+ # encoding: utf-8
3
+
4
+ __all__ = ["dynamic_async", "dynamic_async_iter"]
5
+
6
+ from asyncio import run as asyncio_run, to_thread
7
+ from collections.abc import (
8
+ AsyncIterable, AsyncIterator, Awaitable, Callable, Iterable, Iterator,
9
+ )
10
+ from inspect import isawaitable
11
+
12
+ from argtools import has_keyword_arg
13
+ from asynctools import ensure_async, in_async
14
+ from decotools import optional
15
+
16
+
17
+ async def _as_async[T](v: T, /) -> T:
18
+ return v
19
+
20
+
21
+ async def _as_async_iter[T](
22
+ it: Iterable[T],
23
+ /,
24
+ threaded: bool = False,
25
+ ) -> AsyncIterator[T]:
26
+ if threaded:
27
+ call = iter(it).__next__
28
+ try:
29
+ while True:
30
+ yield await to_thread(call)
31
+ except StopIteration:
32
+ pass
33
+ else:
34
+ for e in it:
35
+ yield e
36
+
37
+
38
+ def _as_iter[T](
39
+ it: AsyncIterable[T],
40
+ /,
41
+ run: Callable = asyncio_run,
42
+ ) -> Iterator[T]:
43
+ try:
44
+ call = aiter(it).__anext__
45
+ while True:
46
+ yield run(call())
47
+ except StopAsyncIteration:
48
+ pass
49
+
50
+
51
+ @optional
52
+ def dynamic_async[T](
53
+ func: Callable,
54
+ /,
55
+ run: Callable = asyncio_run,
56
+ threaded: bool = False,
57
+ async_: None | bool = None,
58
+ ):
59
+ has_async_arg = has_keyword_arg(func, "async_")
60
+ def wrapper(*args, async_: None | bool = async_, **kwds):
61
+ if async_ is None:
62
+ async_ = in_async()
63
+ if has_async_arg:
64
+ kwds["async_"] = async_
65
+ if async_:
66
+ if threaded:
67
+ r = ensure_async(func, threaded=threaded)(*args, **kwds)
68
+ else:
69
+ r = func(*args, **kwds)
70
+ if not isawaitable(r):
71
+ r = _as_async(r)
72
+ else:
73
+ r = func(*args, **kwds)
74
+ if isawaitable(r):
75
+ r = run(r)
76
+ return r
77
+ return wrapper
78
+
79
+
80
+ @optional
81
+ def dynamic_async_iter[T](
82
+ func: Callable[..., Iterable] | Callable[..., AsyncIterable],
83
+ /,
84
+ run: Callable = asyncio_run,
85
+ threaded: bool = False,
86
+ async_: None | bool = None,
87
+ ):
88
+ has_async_arg = has_keyword_arg(func, "async_")
89
+ def wrapper(*args, async_: None | bool = async_, **kwds):
90
+ if async_ is None:
91
+ async_ = in_async()
92
+ if has_async_arg:
93
+ kwds["async_"] = async_
94
+ it = func(*args, **kwds)
95
+ if async_:
96
+ if not isinstance(it, AsyncIterable):
97
+ it = _as_async_iter(it, threaded=threaded)
98
+ elif isinstance(it, AsyncIterable):
99
+ it = _as_iter(it, run)
100
+ return it
101
+ return wrapper
102
+
iterutils/gen_step.py ADDED
@@ -0,0 +1,414 @@
1
+ #!/usr/bin/env python3
2
+ # encoding: utf-8
3
+
4
+ __all__ = [
5
+ "iter_gen_step", "run_gen_step", "run_gen_step_iter",
6
+ "as_gen_step", "with_iter_next", "Yield", "YieldFrom",
7
+ ]
8
+
9
+ from asyncio import CancelledError as AsyncCancelledError
10
+ from collections.abc import (
11
+ AsyncGenerator, AsyncIterable, AsyncIterator, Awaitable, Callable,
12
+ Coroutine, Generator, Iterable, Iterator,
13
+ )
14
+ from concurrent.futures import CancelledError
15
+ from contextlib import contextmanager, AbstractContextManager, AbstractAsyncContextManager
16
+ from dataclasses import dataclass
17
+ from inspect import isawaitable
18
+ from sys import exc_info
19
+ from typing import (
20
+ cast, overload, runtime_checkable, Any, ContextManager, Literal, Protocol,
21
+ )
22
+
23
+ from argtools import has_keyword_arg
24
+ from asynctools import in_async
25
+ from decotools import optional
26
+
27
+
28
+ @runtime_checkable
29
+ class SupportsBool(Protocol):
30
+ def __bool__(self, /) -> bool:
31
+ ...
32
+
33
+
34
+ @dataclass(slots=True, frozen=True)
35
+ class Yield:
36
+ """专供 `run_gen_step_iter`,说明值需要 yield 给用户
37
+ """
38
+ value: Any
39
+
40
+
41
+ @dataclass(slots=True, frozen=True)
42
+ class YieldFrom:
43
+ """专供 `run_gen_step_iter`,说明值需要解包后逐个 yield 给用户
44
+ """
45
+ value: Any
46
+
47
+
48
+ def iter_gen_step_sync[Y](
49
+ gen: Generator[Y, None | Y, Y],
50
+ /,
51
+ ) -> Generator[Y, Any, Any]:
52
+ send = gen.send
53
+ try:
54
+ v = None
55
+ while True:
56
+ yield (v := send(v))
57
+ except StopIteration as e:
58
+ yield e.value
59
+ finally:
60
+ gen.close()
61
+
62
+
63
+ async def iter_gen_step_async[Y](
64
+ gen: Generator[Awaitable[Y], None | Y, Awaitable[Y] | Y],
65
+ /,
66
+ ) -> AsyncGenerator[Y, Any]:
67
+ send = gen.send
68
+ throw = gen.throw
69
+ try:
70
+ r = send(None)
71
+ while True:
72
+ try:
73
+ yield (v := await r)
74
+ except BaseException as e:
75
+ r = throw(e)
76
+ else:
77
+ r = send(v)
78
+ raise AsyncCancelledError
79
+ except StopIteration as e:
80
+ v = e.value
81
+ if isawaitable(v):
82
+ v = await v
83
+ yield v
84
+ finally:
85
+ gen.close()
86
+
87
+
88
+ @overload
89
+ def iter_gen_step[Y](
90
+ gen: Generator[Y, None | Y, Y] | Callable[[], Generator[Y, None | Y, Y]],
91
+ /,
92
+ async_: Literal[False] = False,
93
+ ) -> Generator[Y, Any, Any]:
94
+ ...
95
+ @overload
96
+ def iter_gen_step[Y](
97
+ gen: Generator[Awaitable[Y], None | Y, Awaitable[Y] | Y] | Callable[[], Generator[Awaitable[Y], None | Y, Awaitable[Y] | Y]],
98
+ /,
99
+ async_: Literal[True],
100
+ ) -> AsyncGenerator[Y, Any]:
101
+ ...
102
+ def iter_gen_step[Y](
103
+ gen: Generator[Y, None | Y, Y] | Callable[[], Generator[Y, None | Y, Y]] | Generator[Awaitable[Y], None | Y, Awaitable[Y] | Y] | Callable[[], Generator[Awaitable[Y], None | Y, Awaitable[Y] | Y]],
104
+ /,
105
+ async_: Literal[False, True] = False,
106
+ ) -> Generator[Y, Any, Any] | AsyncGenerator[Y, Any]:
107
+ if not isinstance(gen, Generator):
108
+ gen = gen()
109
+ if async_:
110
+ gen = cast(Generator[Awaitable[Y], None | Y, Awaitable[Y] | Y], gen)
111
+ return iter_gen_step_async(gen)
112
+ else:
113
+ gen = cast(Generator[Y, None | Y, Y], gen)
114
+ return iter_gen_step_sync(gen)
115
+
116
+
117
+ def run_gen_step_sync[Y, T](
118
+ gen: Generator[Y, None | Y, T],
119
+ /,
120
+ running: SupportsBool = True,
121
+ ) -> T:
122
+ send = gen.send
123
+ try:
124
+ v = None
125
+ while running:
126
+ v = send(v)
127
+ raise CancelledError
128
+ except StopIteration as e:
129
+ return e.value
130
+ finally:
131
+ gen.close()
132
+
133
+
134
+ async def run_gen_step_async[Y, T](
135
+ gen: Generator[Awaitable[Y], None | Y, Awaitable[T] | T],
136
+ /,
137
+ running: SupportsBool = True,
138
+ ) -> T:
139
+ send = gen.send
140
+ throw = gen.throw
141
+ try:
142
+ r = send(None)
143
+ while running:
144
+ try:
145
+ v = await r
146
+ except BaseException as e:
147
+ r = throw(e)
148
+ else:
149
+ r = send(v)
150
+ raise AsyncCancelledError
151
+ except StopIteration as e:
152
+ v = e.value
153
+ if isawaitable(v):
154
+ v = await v
155
+ return v
156
+ finally:
157
+ gen.close()
158
+
159
+
160
+ @overload
161
+ def run_gen_step[T](
162
+ gen: Generator[Any, Any, T] | Callable[[], Generator[Any, Any, T]],
163
+ /,
164
+ async_: Literal[False] = False,
165
+ running: SupportsBool = True,
166
+ ) -> T:
167
+ ...
168
+ @overload
169
+ def run_gen_step[T](
170
+ gen: Generator[Awaitable, Any, Awaitable[T] | T] | Callable[[], Generator[Awaitable, Any, Awaitable[T] | T]],
171
+ /,
172
+ async_: Literal[True],
173
+ running: SupportsBool = True,
174
+ ) -> Awaitable[T]:
175
+ ...
176
+ def run_gen_step(
177
+ gen,
178
+ /,
179
+ async_: Literal[False, True] = False,
180
+ running: SupportsBool = True,
181
+ ):
182
+ if not isinstance(gen, Generator):
183
+ gen = gen()
184
+ if async_:
185
+ return run_gen_step_async(gen, running)
186
+ else:
187
+ return run_gen_step_sync(gen, running)
188
+
189
+
190
+ def run_gen_step_iter_sync(
191
+ gen: Generator,
192
+ /,
193
+ running: SupportsBool = True,
194
+ ) -> Iterator:
195
+ send = gen.send
196
+ try:
197
+ v: Any = None
198
+ while running:
199
+ v = send(v)
200
+ if isinstance(v, Yield):
201
+ v = v.value
202
+ yield v
203
+ elif isinstance(v, YieldFrom):
204
+ v = yield from v.value
205
+ raise CancelledError
206
+ except StopIteration as e:
207
+ return e.value
208
+ finally:
209
+ gen.close()
210
+
211
+
212
+ async def run_gen_step_iter_async(
213
+ gen: Generator,
214
+ /,
215
+ running: SupportsBool = True,
216
+ ) -> AsyncIterator:
217
+ send = gen.send
218
+ throw = gen.throw
219
+ try:
220
+ v = send(None)
221
+ while running:
222
+ try:
223
+ if isinstance(v, Yield):
224
+ yield_type = 1
225
+ v = v.value
226
+ elif isinstance(v, YieldFrom):
227
+ yield_type = 2
228
+ v = v.value
229
+ else:
230
+ yield_type = 0
231
+ if isawaitable(v):
232
+ v = await v
233
+ match yield_type:
234
+ case 1:
235
+ yield v
236
+ case 2:
237
+ if isinstance(v, AsyncIterable):
238
+ async for e in v:
239
+ yield e
240
+ else:
241
+ for e in v:
242
+ yield e
243
+ except BaseException as e:
244
+ v = throw(e)
245
+ else:
246
+ v = send(v)
247
+ except StopIteration as e:
248
+ v = e.value
249
+ if isawaitable(v):
250
+ v = await v
251
+ #return v
252
+ finally:
253
+ gen.close()
254
+
255
+
256
+ @overload
257
+ def run_gen_step_iter(
258
+ gen: Generator | Callable[[], Generator],
259
+ /,
260
+ async_: Literal[False] = False,
261
+ running: SupportsBool = True,
262
+ ) -> Iterator:
263
+ ...
264
+ @overload
265
+ def run_gen_step_iter(
266
+ gen: Generator | Callable[[], Generator],
267
+ /,
268
+ async_: Literal[True],
269
+ running: SupportsBool = True,
270
+ ) -> AsyncIterator:
271
+ ...
272
+ def run_gen_step_iter(
273
+ gen: Generator | Callable[[], Generator],
274
+ /,
275
+ async_: Literal[False, True] = False,
276
+ running: SupportsBool = True,
277
+ ) -> Iterator | AsyncIterator:
278
+ if not isinstance(gen, Generator):
279
+ gen = gen()
280
+ if async_:
281
+ return run_gen_step_iter_async(gen, running)
282
+ else:
283
+ return run_gen_step_iter_sync(gen, running)
284
+
285
+
286
+ @optional
287
+ def as_gen_step(
288
+ func: Callable[..., Generator],
289
+ /,
290
+ async_: None | bool = None,
291
+ is_iter: bool = False,
292
+ ):
293
+ has_async_arg = has_keyword_arg(func, "async_")
294
+ if is_iter:
295
+ run: Callable = run_gen_step_iter
296
+ else:
297
+ run = run_gen_step
298
+ def wrapper(*args, async_: None | bool = async_, **kwds):
299
+ if async_ is None:
300
+ async_ = in_async()
301
+ if has_async_arg:
302
+ kwds["async_"] = async_
303
+ return run(func(*args, **kwds), async_)
304
+ return wrapper
305
+
306
+
307
+ @overload
308
+ def with_iter_next[T](
309
+ iterable: Iterable[T],
310
+ /,
311
+ ) -> ContextManager[Callable[[], T]]:
312
+ ...
313
+ @overload
314
+ def with_iter_next[T](
315
+ iterable: AsyncIterable[T],
316
+ /,
317
+ ) -> ContextManager[Callable[[], Awaitable[T]]]:
318
+ ...
319
+ @contextmanager
320
+ def with_iter_next[T](
321
+ iterable: Iterable[T] | AsyncIterable[T],
322
+ /,
323
+ ):
324
+ """包装迭代器,以供 `run_gen_step` 和 `run_gen_step_iter` 使用
325
+
326
+ .. code:: python
327
+
328
+ if async_:
329
+ async def process():
330
+ async for e in iterable:
331
+ do_what_you_want(e)
332
+ return process()
333
+ else:
334
+ for e in iterable:
335
+ do_what_you_want(e)
336
+
337
+ 大概相当于
338
+
339
+ .. code:: python
340
+
341
+ def gen_step():
342
+ with with_iter_next(iterable) as do_next:
343
+ while True:
344
+ e = yield do_next()
345
+ do_what_you_want(e)
346
+
347
+ run_gen_step(gen_step, async_)
348
+ """
349
+ if isinstance(iterable, AsyncIterable):
350
+ try:
351
+ yield aiter(iterable).__anext__
352
+ except StopAsyncIteration:
353
+ pass
354
+ else:
355
+ try:
356
+ yield iter(iterable).__next__
357
+ except StopIteration:
358
+ pass
359
+
360
+
361
+ @overload
362
+ def split_cm[T](
363
+ cm: AbstractContextManager[T],
364
+ /,
365
+ ) -> tuple[Callable[[], T], Callable[[], Any]]:
366
+ ...
367
+ @overload
368
+ def split_cm[T](
369
+ cm: AbstractAsyncContextManager[T],
370
+ /,
371
+ ) -> tuple[Callable[[], Coroutine[Any, Any, T]], Callable[[], Coroutine]]:
372
+ ...
373
+ def split_cm[T](
374
+ cm: AbstractContextManager[T] | AbstractAsyncContextManager[T],
375
+ /,
376
+ ) -> (
377
+ tuple[Callable[[], T], Callable[[], Any]] |
378
+ tuple[Callable[[], Coroutine[Any, Any, T]], Callable[[], Coroutine]]
379
+ ):
380
+ """拆分上下文管理器,以供 `run_gen_step` 和 `run_gen_step_iter` 使用
381
+
382
+ .. code:: python
383
+
384
+ if async_:
385
+ async def process():
386
+ async with cm as obj:
387
+ do_what_you_want(obj)
388
+ return process()
389
+ else:
390
+ with cm as obj:
391
+ do_what_you_want(obj)
392
+
393
+ 大概相当于
394
+
395
+ .. code:: python
396
+
397
+ def gen_step():
398
+ enter, exit = split_cm(cm)
399
+ obj = yield enter()
400
+ try:
401
+ do_what_you_want(obj)
402
+ finally:
403
+ yield exit()
404
+
405
+ run_gen_step(gen_step, async_)
406
+ """
407
+ if isinstance(cm, AbstractAsyncContextManager):
408
+ enter: Callable = cm.__aenter__
409
+ exit: Callable = cm.__aexit__
410
+ else:
411
+ enter = cm.__enter__
412
+ exit = cm.__exit__
413
+ return enter, lambda: exit(*exc_info())
414
+