fluentmap 0.0.1__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.
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.1
2
+ Name: fluentmap
3
+ Version: 0.0.1
4
+ Summary: Drop-in replacement for Python map with concurrency support
5
+ Author-Email: Leavers <leavers930@gmail.com>
6
+ License: MIT
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+
10
+ # Fluentmap
11
+
12
+ Drop-in replacement for Python map with concurrency support.
13
+
@@ -0,0 +1,5 @@
1
+ fluentmap-0.0.1.dist-info/METADATA,sha256=5mT2k8qnbb8ii4zbFUdvRzPd7goW_2z4aLd-O9MsgEU,319
2
+ fluentmap-0.0.1.dist-info/WHEEL,sha256=rSwsxJWe3vzyR5HCwjWXQruDgschpei4h_giTm0dJVE,90
3
+ fluentmap-0.0.1.dist-info/licenses/LICENSE,sha256=Mf4F5_85PdEl7wjMtKbOBIdvBy1oeGe2FmsPMZHdCyQ,1064
4
+ fluentmap.py,sha256=bljIxF-dB6LceAswmRi1CmFIhC2p0hGq67RKNMS3sg4,27616
5
+ fluentmap-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: pdm-backend (2.3.3)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 leavers
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
fluentmap.py ADDED
@@ -0,0 +1,766 @@
1
+ """
2
+ Fluentmap is a drop-in replacement for Python map with concurrency support.
3
+
4
+ Copyright (c) 2020-2024, Leavers.
5
+ License: MIT
6
+ """
7
+
8
+ from queue import Empty, Queue
9
+ from threading import Event, Thread
10
+ from typing import (
11
+ Any,
12
+ Callable,
13
+ Dict,
14
+ Iterable,
15
+ Iterator,
16
+ List,
17
+ Optional,
18
+ Protocol,
19
+ Set,
20
+ Tuple,
21
+ TypeVar,
22
+ Union,
23
+ cast,
24
+ overload,
25
+ runtime_checkable,
26
+ )
27
+
28
+ __version__ = "0.0.1"
29
+ __all__ = ("Arguments", "map")
30
+
31
+
32
+ T = TypeVar("T")
33
+ T_co = TypeVar("T_co", covariant=True)
34
+ R = TypeVar("R")
35
+
36
+
37
+ @runtime_checkable
38
+ class FutureLike(Protocol[T_co]):
39
+ def done(self) -> bool: ...
40
+
41
+ def result(self, timeout: Any = None) -> T_co: ...
42
+
43
+
44
+ @runtime_checkable
45
+ class SupportsSubmit(Protocol):
46
+ def submit(*args: Any, **kwargs: Any) -> FutureLike: ...
47
+
48
+
49
+ class Arguments:
50
+ def __init__(self, *args, **kwargs) -> None:
51
+ self.args = args
52
+ self.kwargs = kwargs
53
+
54
+ def __eq__(self, obj: object) -> bool:
55
+ if not isinstance(obj, Arguments):
56
+ return False
57
+ return self.args == obj.args and self.kwargs == obj.kwargs
58
+
59
+ def __repr__(self) -> str:
60
+ return f"{self.__class__.__name__}(args={self.args}, kwargs={self.kwargs})"
61
+
62
+
63
+ def _simple_map(
64
+ fn: Callable[..., T],
65
+ args: Iterable[Any],
66
+ *more_args: Iterable[Any],
67
+ batch_size: int = 0,
68
+ num_prepare: int = 0,
69
+ on_return: Optional[Callable[[T], R]] = None,
70
+ ) -> Iterator[Union[T, R]]:
71
+ is_batch_args = True if batch_size > 1 else False
72
+
73
+ if more_args:
74
+ iterable_args = cast(Iterable[Any], zip(args, *more_args))
75
+ extract_args = True
76
+ else:
77
+ iterable_args = args
78
+ extract_args = False
79
+
80
+ def submit(fn_args: Union[Arguments, Iterable[Any], Any]) -> Union[T, R]:
81
+ if is_batch_args:
82
+ res = fn(fn_args)
83
+ else:
84
+ if isinstance(fn_args, Arguments):
85
+ res = fn(*fn_args.args, **fn_args.kwargs)
86
+ elif isinstance(fn_args, Iterable) and extract_args:
87
+ res = fn(*fn_args)
88
+ else:
89
+ res = fn(fn_args)
90
+ if on_return is None:
91
+ return res
92
+ return on_return(res)
93
+
94
+ if num_prepare == 0:
95
+ batch: List[Any] = []
96
+ for args in iterable_args:
97
+ if is_batch_args:
98
+ batch.append(args)
99
+ if len(batch) < batch_size:
100
+ continue
101
+ yield submit(batch)
102
+ batch = []
103
+ else:
104
+ yield submit(args)
105
+ if batch:
106
+ yield submit(batch)
107
+ else:
108
+ undefined = object()
109
+ queue: Queue = Queue()
110
+ exc_event = Event()
111
+
112
+ def worker():
113
+ batch: List[Any] = []
114
+ for args in iterable_args:
115
+ if exc_event.is_set():
116
+ break
117
+ if is_batch_args:
118
+ batch.append(args)
119
+ if len(batch) < batch_size:
120
+ continue
121
+ queue.put(batch)
122
+ batch = []
123
+ else:
124
+ queue.put(args)
125
+ del args
126
+ while queue.qsize() >= num_prepare:
127
+ if exc_event.is_set():
128
+ break
129
+ exc_event.wait(0.05)
130
+ if not exc_event.is_set() and batch:
131
+ queue.put(batch)
132
+ queue.put(undefined)
133
+
134
+ worker_thread = Thread(target=worker, daemon=True)
135
+ worker_thread.start()
136
+ while True:
137
+ try:
138
+ data = queue.get(timeout=0.05)
139
+ except Empty:
140
+ continue
141
+ if data is undefined:
142
+ break
143
+ try:
144
+ yield submit(data)
145
+ except Exception as exc:
146
+ exc_event.set()
147
+ worker_thread.join()
148
+ raise exc
149
+ finally:
150
+ del data
151
+ worker_thread.join()
152
+
153
+
154
+ def _process_chunk(
155
+ fn: Callable,
156
+ fn_args_chunk: List[Union[Arguments, Iterable[Any], Any]],
157
+ *,
158
+ extract_args: bool,
159
+ is_batch_args: bool,
160
+ ) -> List[Tuple[Any, bool]]:
161
+ result: List[Tuple[Any, bool]] = []
162
+ if is_batch_args:
163
+ for fn_args in fn_args_chunk:
164
+ try:
165
+ ret = fn(fn_args)
166
+ except BaseException as exc:
167
+ result.append((exc, True))
168
+ else:
169
+ result.append((ret, False))
170
+ else:
171
+ for fn_args in fn_args_chunk:
172
+ try:
173
+ if isinstance(fn_args, Arguments):
174
+ ret = fn(*fn_args.args, **fn_args.kwargs)
175
+ elif isinstance(fn_args, Iterable) and extract_args:
176
+ ret = fn(*fn_args)
177
+ else:
178
+ ret = fn(fn_args)
179
+ except BaseException as exc:
180
+ result.append((exc, True))
181
+ else:
182
+ result.append((ret, False))
183
+ return result
184
+
185
+
186
+ def _concurrent_chunk_map(
187
+ fn: Callable[..., T],
188
+ args: Iterable[Any],
189
+ *more_args: Iterable[Any],
190
+ executor: SupportsSubmit,
191
+ chunk_size: int,
192
+ batch_size: int = 0,
193
+ num_prepare: int = 0,
194
+ sort_by_completion: bool = False,
195
+ on_return: Optional[Callable[[T], R]] = None,
196
+ ) -> Iterator[Union[T, R]]:
197
+ assert chunk_size > 1
198
+ is_batch_args = True if batch_size > 1 else False
199
+
200
+ args_iter: Iterator[Any]
201
+ if more_args:
202
+ args_iter = iter(zip(args, *more_args))
203
+
204
+ # Whether to deconstruct iterable args in submit, this helps sumbit identify
205
+ # whether an iterable fn_args is a batch generated by _concurrent_map itself
206
+ # (which should be deconstructed) or a item in args given by users (which
207
+ # should not be deconstructed)
208
+ extract_args = True
209
+ else:
210
+ args_iter = iter(args)
211
+ extract_args = False
212
+
213
+ # Indicates the chunk currently being assembled
214
+ chunk: List[Any] = []
215
+
216
+ # The chunk sequence number which is auto-incremental and unique.
217
+ chunk_cursor = 0
218
+
219
+ # Indicates the batch currently being assembled. If batch_size is 0, the length
220
+ # of batch will only be 1 and will be deconstructed before being added to
221
+ # pending_batches.
222
+ batch: List[Any] = []
223
+
224
+ # Batch cursors which have been submitted but not yielded yet.
225
+ task_indices: Set[int] = set()
226
+
227
+ # Futures which have not been yielded.
228
+ futures: Dict[int, FutureLike[List[Tuple[Union[T, BaseException], bool]]]] = {}
229
+
230
+ # Store results when sort_by_completion is False to make sure results are returned
231
+ # in the order of submission.
232
+ result_cache: Dict[int, List[Tuple[Union[T, BaseException], bool]]] = {}
233
+
234
+ # Used to reorder results and communicate with handle_result.
235
+ result_queue: Queue = Queue()
236
+
237
+ # Indicates the maximum index of result that has been processed by handle_result
238
+ # when sort_by_completion is False
239
+ result_cursor = 0
240
+
241
+ # Indicates the maximum index of result that has been returned when
242
+ # sort_by_completion is False
243
+ result_cursor_bound = 0
244
+
245
+ def submit(
246
+ fn_args_chunk: List[Union[Arguments, Iterable[Any], Any]],
247
+ ) -> FutureLike[List[Tuple[Union[T, BaseException], bool]]]:
248
+ return executor.submit(
249
+ _process_chunk,
250
+ fn,
251
+ fn_args_chunk,
252
+ extract_args=extract_args,
253
+ is_batch_args=is_batch_args,
254
+ )
255
+
256
+ if on_return is None:
257
+
258
+ def handle_result_chunk(
259
+ i: int,
260
+ result_chunk: List[Tuple[Union[T, BaseException], bool]],
261
+ ) -> None:
262
+ nonlocal result_cursor, result_cursor_bound
263
+
264
+ if sort_by_completion:
265
+ # yield result in the order of completion
266
+ result_queue.put((i, result_chunk))
267
+ else:
268
+ # yield result in the order of submission
269
+ result_cursor_bound = max(result_cursor_bound, i)
270
+ result_cache[i] = result_chunk
271
+ while result_cursor <= result_cursor_bound:
272
+ if result_cursor not in result_cache:
273
+ return
274
+ result_chunk = result_cache.pop(result_cursor)
275
+ result_queue.put((result_cursor, result_chunk))
276
+ result_cursor += 1
277
+
278
+ def stop_result_handler() -> None: ...
279
+
280
+ else:
281
+
282
+ def handle_result_concurrently(inqueue: Queue, outqueue: Queue) -> None:
283
+ while True:
284
+ try:
285
+ pair: Optional[
286
+ Tuple[int, List[Tuple[Union[T, BaseException], bool]]]
287
+ ] = inqueue.get(timeout=0.01)
288
+ except Empty:
289
+ continue
290
+
291
+ if pair is None:
292
+ break
293
+
294
+ i, raw_result_chunk = pair
295
+ results: List[Tuple[Union[T, BaseException], bool]] = []
296
+ for raw_result, is_exc in raw_result_chunk:
297
+ if is_exc:
298
+ results.append((raw_result, True))
299
+ continue
300
+ try:
301
+ new_result = on_return(raw_result) # type: ignore[arg-type]
302
+ results.append((new_result, False)) # type: ignore[arg-type]
303
+ except BaseException as exc:
304
+ results.append((exc, True))
305
+ del exc
306
+ finally:
307
+ del raw_result
308
+ outqueue.put((i, results))
309
+ del pair
310
+
311
+ raw_result_queue: Queue = Queue()
312
+ result_handler: Optional[Thread] = None
313
+
314
+ def stop_result_handler() -> None:
315
+ nonlocal result_handler
316
+ if result_handler is None or not result_handler.is_alive():
317
+ return
318
+ raw_result_queue.put(None)
319
+ result_handler.join()
320
+ result_handler = None
321
+
322
+ def handle_result_chunk(
323
+ i: int,
324
+ result_chunk: List[Tuple[Union[T, BaseException], bool]],
325
+ ) -> None:
326
+ nonlocal result_handler, result_cache
327
+ nonlocal result_cursor, result_cursor_bound
328
+
329
+ if result_handler is None:
330
+ result_handler = Thread(
331
+ target=handle_result_concurrently,
332
+ args=(raw_result_queue, result_queue),
333
+ daemon=True,
334
+ )
335
+ result_handler.start()
336
+
337
+ if sort_by_completion:
338
+ # yield result in the order of completion
339
+ raw_result_queue.put((i, result_chunk))
340
+ else:
341
+ # yield result in the order of submission
342
+ result_cursor_bound = max(result_cursor_bound, i)
343
+ result_cache[i] = result_chunk
344
+ while result_cursor <= result_cursor_bound:
345
+ if result_cursor not in result_cache:
346
+ return
347
+ result = result_cache.pop(result_cursor)
348
+ raw_result_queue.put((result_cursor, result))
349
+ result_cursor += 1
350
+
351
+ try:
352
+ # Indicates whether there are pending data in args_iter. At least one
353
+ # iteration should be invoked so the initial value is True and it will be set
354
+ # as False on StopIteration.
355
+ has_args = True
356
+
357
+ # Conditions to stop iteration (must meet all requirements):
358
+ # 1. No more data in args_iter (has_args is False)
359
+ # 2. The last batch has been submitted (batch is empty)
360
+ # 3. All results have been yielded (task_indices is empty)
361
+ while has_args or chunk or batch or task_indices:
362
+ if has_args and (num_prepare == 0 or len(futures) < num_prepare):
363
+ try:
364
+ fn_args = next(args_iter)
365
+ if is_batch_args:
366
+ batch.append(fn_args)
367
+ if len(batch) >= batch_size:
368
+ chunk.append(batch)
369
+ batch = []
370
+ if len(chunk) >= chunk_size:
371
+ futures[chunk_cursor] = submit(chunk)
372
+ task_indices.add(chunk_cursor)
373
+ chunk_cursor += 1
374
+ chunk = []
375
+ else:
376
+ chunk.append(fn_args)
377
+ if len(chunk) >= chunk_size:
378
+ futures[chunk_cursor] = submit(chunk)
379
+ task_indices.add(chunk_cursor)
380
+ chunk_cursor += 1
381
+ chunk = []
382
+ except StopIteration:
383
+ if is_batch_args and batch:
384
+ chunk.append(batch)
385
+ batch = []
386
+ if chunk:
387
+ futures[chunk_cursor] = submit(chunk)
388
+ task_indices.add(chunk_cursor)
389
+ chunk_cursor += 1
390
+ chunk = []
391
+ has_args = False
392
+
393
+ if futures:
394
+ finished_indices: List[int] = []
395
+ for i, future in futures.items():
396
+ if not future.done():
397
+ continue
398
+ finished_indices.append(i)
399
+ handle_result_chunk(i, future.result())
400
+ for i in finished_indices:
401
+ futures.pop(i)
402
+
403
+ while not result_queue.empty():
404
+ i, results = result_queue.get()
405
+ task_indices.remove(i)
406
+ for result, is_exc in results:
407
+ if is_exc:
408
+ stop_result_handler()
409
+ raise cast(BaseException, result)
410
+ else:
411
+ yield result
412
+ finally:
413
+ stop_result_handler()
414
+
415
+
416
+ def _concurrent_map(
417
+ fn: Callable[..., T],
418
+ args: Iterable[Any],
419
+ *more_args: Iterable[Any],
420
+ executor: SupportsSubmit,
421
+ batch_size: int = 0,
422
+ num_prepare: int = 0,
423
+ sort_by_completion: bool = False,
424
+ on_return: Optional[Callable[[T], R]] = None,
425
+ ) -> Iterator[Union[T, R]]:
426
+ is_batch_args = True if batch_size > 1 else False
427
+
428
+ args_iter: Iterator[Any]
429
+ if more_args:
430
+ args_iter = iter(zip(args, *more_args))
431
+
432
+ # Whether to deconstruct iterable args in submit, this helps sumbit identify
433
+ # whether an iterable fn_args is a batch generated by _concurrent_map itself
434
+ # (which should be deconstructed) or a item in args given by users (which
435
+ # should not be deconstructed)
436
+ extract_args = True
437
+ else:
438
+ args_iter = iter(args)
439
+ extract_args = False
440
+
441
+ # Indicates the batch currently being assembled. If batch_size is 0, the length
442
+ # of batch will only be 1 and will be deconstructed before being added to
443
+ # pending_batches.
444
+ batch: List[Any] = []
445
+
446
+ # The batch sequence number which is auto-incremental and unique.
447
+ batch_cursor = 0
448
+
449
+ # Batch cursors which have been submitted but not yielded yet.
450
+ task_indices: Set[int] = set()
451
+
452
+ # Futures which have not been yielded.
453
+ futures: Dict[int, FutureLike[T]] = {}
454
+
455
+ # Store results when sort_by_completion is False to make sure results are returned
456
+ # in the order of submission.
457
+ result_cache: Dict[int, T] = {}
458
+
459
+ # Used to reorder results and communicate with handle_result.
460
+ result_queue: Queue = Queue()
461
+
462
+ # Indicates the maximum index of result that has been processed by handle_result
463
+ # when sort_by_completion is False
464
+ result_cursor = 0
465
+
466
+ # Indicates the maximum index of result that has been returned when
467
+ # sort_by_completion is False
468
+ result_cursor_bound = 0
469
+
470
+ def submit(
471
+ fn_args: Union[Arguments, Iterable[Any], Any],
472
+ ) -> FutureLike[T]:
473
+ if is_batch_args:
474
+ future = executor.submit(fn, fn_args)
475
+ else:
476
+ if isinstance(fn_args, Arguments):
477
+ future = executor.submit(fn, *fn_args.args, **fn_args.kwargs)
478
+ elif isinstance(fn_args, Iterable) and extract_args:
479
+ future = executor.submit(fn, *fn_args)
480
+ else:
481
+ future = executor.submit(fn, fn_args)
482
+ return future
483
+
484
+ if on_return is None:
485
+
486
+ def handle_result(i: int, result: T) -> None:
487
+ nonlocal result_cursor, result_cursor_bound
488
+
489
+ if sort_by_completion:
490
+ # yield result in the order of completion
491
+ result_queue.put((i, result, False))
492
+ else:
493
+ # yield result in the order of submission
494
+ result_cursor_bound = max(result_cursor_bound, i)
495
+ result_cache[i] = result
496
+ while result_cursor <= result_cursor_bound:
497
+ if result_cursor not in result_cache:
498
+ return
499
+ result = result_cache.pop(result_cursor)
500
+ result_queue.put((result_cursor, result, False))
501
+ result_cursor += 1
502
+
503
+ def stop_result_handler() -> None: ...
504
+
505
+ else:
506
+
507
+ def handle_result_concurrently(inqueue: Queue, outqueue: Queue) -> None:
508
+ while True:
509
+ try:
510
+ pair: Optional[Tuple[int, T]] = inqueue.get(timeout=0.01)
511
+ except Empty:
512
+ continue
513
+
514
+ if pair is None:
515
+ break
516
+
517
+ i, raw_result = pair
518
+ try:
519
+ result = on_return(raw_result) # type: ignore[misc]
520
+ outqueue.put((i, result, False))
521
+ except BaseException as exc:
522
+ outqueue.put((i, exc, True))
523
+ del exc
524
+ finally:
525
+ del raw_result, pair
526
+
527
+ raw_result_queue: Queue = Queue()
528
+ result_handler: Optional[Thread] = None
529
+
530
+ def stop_result_handler() -> None:
531
+ nonlocal result_handler
532
+ if result_handler is None or not result_handler.is_alive():
533
+ return
534
+ raw_result_queue.put(None)
535
+ result_handler.join()
536
+ result_handler = None
537
+
538
+ def handle_result(i: int, result: T) -> None:
539
+ nonlocal result_handler, result_cache
540
+ nonlocal result_cursor, result_cursor_bound
541
+
542
+ if result_handler is None:
543
+ result_handler = Thread(
544
+ target=handle_result_concurrently,
545
+ args=(raw_result_queue, result_queue),
546
+ daemon=True,
547
+ )
548
+ result_handler.start()
549
+
550
+ if sort_by_completion:
551
+ # yield result in the order of completion
552
+ raw_result_queue.put((i, result))
553
+ else:
554
+ # yield result in the order of submission
555
+ result_cursor_bound = max(result_cursor_bound, i)
556
+ result_cache[i] = result
557
+ while result_cursor <= result_cursor_bound:
558
+ if result_cursor not in result_cache:
559
+ return
560
+ result = result_cache.pop(result_cursor)
561
+ raw_result_queue.put((result_cursor, result))
562
+ result_cursor += 1
563
+
564
+ try:
565
+ # Indicates whether there are pending data in args_iter. At least one
566
+ # iteration should be invoked so the initial value is True and it will be set
567
+ # as False on StopIteration.
568
+ has_args = True
569
+
570
+ # Conditions to stop iteration (must meet all requirements):
571
+ # 1. No more data in args_iter (has_args is False)
572
+ # 2. The last batch has been submitted (batch is empty)
573
+ # 3. All results have been yielded (task_indices is empty)
574
+ while has_args or batch or task_indices:
575
+ if has_args and (num_prepare == 0 or len(futures) < num_prepare):
576
+ try:
577
+ fn_args = next(args_iter)
578
+ batch.append(fn_args)
579
+ if len(batch) >= batch_size:
580
+ futures[batch_cursor] = submit(
581
+ batch if is_batch_args else batch[0]
582
+ )
583
+ task_indices.add(batch_cursor)
584
+ batch_cursor += 1
585
+ batch = []
586
+ except StopIteration:
587
+ if batch:
588
+ futures[batch_cursor] = submit(
589
+ batch if is_batch_args else batch[0]
590
+ )
591
+ task_indices.add(batch_cursor)
592
+ batch_cursor += 1
593
+ batch = []
594
+ has_args = False
595
+
596
+ if futures:
597
+ finished_indices: List[int] = []
598
+ for i, future in futures.items():
599
+ if not future.done():
600
+ continue
601
+ finished_indices.append(i)
602
+ handle_result(i, future.result())
603
+ for i in finished_indices:
604
+ futures.pop(i)
605
+
606
+ while not result_queue.empty():
607
+ i, result, is_exc = result_queue.get()
608
+ task_indices.remove(i)
609
+ if is_exc:
610
+ stop_result_handler()
611
+ raise cast(BaseException, result)
612
+ else:
613
+ yield result
614
+ finally:
615
+ stop_result_handler()
616
+
617
+
618
+ @overload
619
+ def map(
620
+ fn: Callable[..., T],
621
+ args: Iterable[Any],
622
+ *more_args: Iterable[Any],
623
+ executor: Optional[SupportsSubmit] = None,
624
+ batch_size: Optional[int] = None,
625
+ num_prepare: Optional[int] = None,
626
+ sort_by_completion: bool = False,
627
+ ) -> Iterator[T]: ...
628
+
629
+
630
+ @overload
631
+ def map(
632
+ fn: Callable[..., T],
633
+ args: Iterable[Any],
634
+ *more_args: Iterable[Any],
635
+ executor: Optional[SupportsSubmit] = None,
636
+ batch_size: Optional[int] = None,
637
+ num_prepare: Optional[int] = None,
638
+ sort_by_completion: bool = False,
639
+ on_return: Callable[[T], R],
640
+ ) -> Iterator[R]: ...
641
+
642
+
643
+ def map(
644
+ fn: Callable[..., T],
645
+ args: Iterable[Any],
646
+ *more_args: Iterable[Any],
647
+ executor: Optional[SupportsSubmit] = None,
648
+ batch_size: Optional[int] = None,
649
+ chunk_size: Optional[int] = None,
650
+ num_prepare: Optional[int] = None,
651
+ sort_by_completion: bool = False,
652
+ on_return: Optional[Callable[[T], R]] = None,
653
+ ) -> Iterator[Union[T, R]]:
654
+ """Drop-in replacement of builtin ``map`` but adds an argument ``executor`` for
655
+ executing function in an executor.
656
+
657
+ :type fn: Callable[..., T]
658
+ :param fn: The function to be mapped.
659
+
660
+ :type args: Iterable[Any]
661
+ :param args: The arguments to be mapped into ``fn``.
662
+
663
+ :type executor: SupportsSubmit, optional, default to ``None``
664
+ :param executor: The executor to run function ``fn``. It supports not only an
665
+ implementation of ``concurrent.futures.Executor`` but also any instances that
666
+ have a method ``submit(fn, *args, **kwargs) -> Future``. The default value is
667
+ ``None``, which means not to use an executor and this function will behave
668
+ as the builtin ``map`` does.
669
+
670
+ :type batch_size: int, optional, default to ``None``
671
+ :param batch_size: If batch size is given, then items in args will be aranged into
672
+ batches (a list of items) firstly before sent to ``fn``. For example,
673
+ ``map(fn, (1, 2, 3, 4, 5), batch_size=2)`` means invoking 3 functions:
674
+ ``fn([1, 2])``, ``fn([3, 4])`` and ``fn([5])``. Batch will be disabled if
675
+ ``batch_size`` is set as ``None`` or a number which is <= 1.
676
+
677
+ :type chunk_size: int, optional, default to ``None``
678
+ :param chunk_size: If ``chunk_size`` is given, multiple arguments will be submitted
679
+ to the executor in chunks and will be executed sequentially in one executor
680
+ worker. ``chunk_size`` may be useful to reduce the time spent on worker
681
+ initialization.
682
+
683
+ Here is an example to illustrate the difference between ``batch_size`` and
684
+ ``chunk_size``. Considering
685
+ ``map(fn, (1, 2, 3, 4, 5), executor=some_executor)``:
686
+
687
+ *. If set ``batch_size=2``, ``map`` will send 3 batched invocations to executor,
688
+ ``fn([1, 2])``, ``fn([3, 4])`` and ``fn([5])``, so ``fn`` needs to accept
689
+ list of integers as its arguments instead of single integer.
690
+ *. If set ``chunk_size=2``, ``map`` will send 3 chunked invocations to executor,
691
+ it firstly send ``1`` and ``2`` to a worker, the worker will invoke
692
+ ``fn(1)`` and ``fn(2)`` sequentially; then ``3`` and ``4`` to another
693
+ worker; then finally ``5``. This keeps ``fn`` not to be refactored to accept
694
+ batch input.
695
+ *. ``batch_size`` and ``chunk_size`` can be used simultaneously. If set
696
+ ``batch_size=2`` and ``chunk_size=3``, then ``[1, 2]``, ``[3, 4]`` and
697
+ ``[5]`` will be sent to a worker in on chunk.
698
+
699
+ Note that ``chunk_size`` will be ignored if ``executor`` is ``None``.
700
+
701
+ :type num_prepare: int, optional, default to ``None``
702
+ :param num_prepare: Number of items or batches (if ``batch_size`` is enabled) to
703
+ be submitted to executor. This can be useful to prevent get too many data
704
+ from ``args`` and occupy huge memory. A typical usecase is setting
705
+ ``num_prepare`` to a rational value if ``args`` is a iterable line reader
706
+ of a super huge text file. If executor is ``None``, and ``num_prepare`` is
707
+ larger than zero, a thread for yielding data will be started to non-blockingly
708
+ fetch the specified amount of items or batches.
709
+
710
+ :type sort_by_completion: bool, default to ``False``
711
+ :param sort_by_completion: If set to ``True``, then the result will be returned in
712
+ the order of their completion time. Otherwise returned in the order of
713
+ submission, which is the same as builtin ``map``.
714
+
715
+ :type on_return: Callable[[T], U], optional, default to ``None``
716
+ :param on_return: An optional callback to be invoked on each result being returned.
717
+ It accept each result as its sole argument and its return will be treated as
718
+ as the final return of each invocation of ``fn``.
719
+
720
+ :rtype: Iterator
721
+ :return: An iterator which yield all results of ``fn``.
722
+ """
723
+
724
+ for fn_args in (args, *more_args):
725
+ if not isinstance(fn_args, Iterable):
726
+ raise TypeError(
727
+ "Positional arguments after function should be iterable, "
728
+ f"got {fn_args} ({type(fn_args)}) instead."
729
+ )
730
+ if batch_size is None or batch_size <= 1:
731
+ batch_size = 0
732
+ if num_prepare is None or num_prepare <= 0:
733
+ num_prepare = 0
734
+
735
+ if executor is None:
736
+ return _simple_map(
737
+ fn,
738
+ args,
739
+ *more_args,
740
+ batch_size=batch_size,
741
+ num_prepare=num_prepare,
742
+ on_return=on_return, # type: ignore[arg-type]
743
+ )
744
+
745
+ if chunk_size is None or chunk_size <= 1:
746
+ return _concurrent_map(
747
+ fn,
748
+ args,
749
+ *more_args,
750
+ executor=executor,
751
+ batch_size=batch_size,
752
+ num_prepare=num_prepare,
753
+ sort_by_completion=sort_by_completion,
754
+ on_return=on_return, # type: ignore[arg-type]
755
+ )
756
+ return _concurrent_chunk_map(
757
+ fn,
758
+ args,
759
+ *more_args,
760
+ executor=executor,
761
+ batch_size=batch_size,
762
+ chunk_size=chunk_size,
763
+ num_prepare=num_prepare,
764
+ sort_by_completion=sort_by_completion,
765
+ on_return=on_return, # type: ignore[arg-type]
766
+ )