modal 1.0.4.dev12__py3-none-any.whl → 1.0.5__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.
Files changed (67) hide show
  1. modal/_clustered_functions.pyi +13 -3
  2. modal/_functions.py +84 -46
  3. modal/_partial_function.py +1 -1
  4. modal/_runtime/container_io_manager.pyi +222 -40
  5. modal/_runtime/execution_context.pyi +60 -6
  6. modal/_serialization.py +25 -2
  7. modal/_tunnel.pyi +380 -12
  8. modal/_utils/async_utils.py +1 -1
  9. modal/_utils/blob_utils.py +56 -19
  10. modal/_utils/function_utils.py +33 -7
  11. modal/_utils/grpc_utils.py +11 -4
  12. modal/app.py +5 -5
  13. modal/app.pyi +658 -48
  14. modal/cli/run.py +2 -1
  15. modal/client.pyi +224 -36
  16. modal/cloud_bucket_mount.pyi +192 -4
  17. modal/cls.py +7 -7
  18. modal/cls.pyi +442 -35
  19. modal/container_process.pyi +103 -14
  20. modal/dict.py +4 -4
  21. modal/dict.pyi +453 -51
  22. modal/environments.pyi +41 -9
  23. modal/exception.py +6 -2
  24. modal/experimental/__init__.py +90 -0
  25. modal/experimental/ipython.py +11 -7
  26. modal/file_io.pyi +236 -45
  27. modal/functions.pyi +573 -65
  28. modal/gpu.py +1 -1
  29. modal/image.py +1 -1
  30. modal/image.pyi +1256 -74
  31. modal/io_streams.py +8 -4
  32. modal/io_streams.pyi +348 -38
  33. modal/mount.pyi +261 -31
  34. modal/network_file_system.py +3 -3
  35. modal/network_file_system.pyi +307 -26
  36. modal/object.pyi +48 -9
  37. modal/parallel_map.py +93 -19
  38. modal/parallel_map.pyi +160 -15
  39. modal/partial_function.pyi +255 -14
  40. modal/proxy.py +1 -1
  41. modal/proxy.pyi +28 -3
  42. modal/queue.py +4 -4
  43. modal/queue.pyi +447 -30
  44. modal/runner.pyi +160 -22
  45. modal/sandbox.py +8 -7
  46. modal/sandbox.pyi +310 -50
  47. modal/schedule.py +1 -1
  48. modal/secret.py +2 -2
  49. modal/secret.pyi +164 -15
  50. modal/snapshot.pyi +25 -4
  51. modal/token_flow.pyi +28 -8
  52. modal/volume.py +41 -4
  53. modal/volume.pyi +693 -59
  54. {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/METADATA +3 -3
  55. {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/RECORD +67 -67
  56. modal_proto/api.proto +56 -0
  57. modal_proto/api_grpc.py +48 -0
  58. modal_proto/api_pb2.py +874 -780
  59. modal_proto/api_pb2.pyi +194 -8
  60. modal_proto/api_pb2_grpc.py +100 -0
  61. modal_proto/api_pb2_grpc.pyi +32 -0
  62. modal_proto/modal_api_grpc.py +3 -0
  63. modal_version/__init__.py +1 -1
  64. {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/WHEEL +0 -0
  65. {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/entry_points.txt +0 -0
  66. {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/licenses/LICENSE +0 -0
  67. {modal-1.0.4.dev12.dist-info → modal-1.0.5.dist-info}/top_level.txt +0 -0
modal/parallel_map.py CHANGED
@@ -1,6 +1,7 @@
1
1
  # Copyright Modal Labs 2024
2
2
  import asyncio
3
3
  import enum
4
+ import inspect
4
5
  import time
5
6
  import typing
6
7
  from asyncio import FIRST_COMPLETED
@@ -9,6 +10,7 @@ from typing import Any, Callable, Optional
9
10
 
10
11
  from grpclib import Status
11
12
 
13
+ import modal.exception
12
14
  from modal._runtime.execution_context import current_input_id
13
15
  from modal._utils.async_utils import (
14
16
  AsyncOrSyncIterable,
@@ -26,6 +28,7 @@ from modal._utils.async_utils import (
26
28
  warn_if_generator_is_not_consumed,
27
29
  )
28
30
  from modal._utils.blob_utils import BLOB_MAX_PARALLELISM
31
+ from modal._utils.deprecation import deprecation_warning
29
32
  from modal._utils.function_utils import (
30
33
  ATTEMPT_TIMEOUT_GRACE_PERIOD,
31
34
  OUTPUTS_TIMEOUT,
@@ -89,6 +92,7 @@ async def _map_invocation(
89
92
  client: "modal.client._Client",
90
93
  order_outputs: bool,
91
94
  return_exceptions: bool,
95
+ wrap_returned_exceptions: bool,
92
96
  count_update_callback: Optional[Callable[[int, int], None]],
93
97
  function_call_invocation_type: "api_pb2.FunctionCallInvocationType.ValueType",
94
98
  ):
@@ -278,17 +282,19 @@ async def _map_invocation(
278
282
  )
279
283
  )
280
284
  map_done_task = asyncio.create_task(map_done_event.wait())
281
- done, pending = await asyncio.wait([get_response_task, map_done_task], return_when=FIRST_COMPLETED)
282
- if get_response_task in done:
283
- map_done_task.cancel()
284
- response = get_response_task.result()
285
- else:
286
- assert map_done_event.is_set()
287
- # map is done, cancel the pending call
285
+ try:
286
+ done, pending = await asyncio.wait([get_response_task, map_done_task], return_when=FIRST_COMPLETED)
287
+ if get_response_task in done:
288
+ map_done_task.cancel()
289
+ response = get_response_task.result()
290
+ else:
291
+ assert map_done_event.is_set()
292
+ # map is done - no more outputs, so return early
293
+ return
294
+ finally:
295
+ # clean up tasks, in case of cancellations etc.
288
296
  get_response_task.cancel()
289
- # not strictly necessary - don't leave dangling task
290
- await asyncio.gather(get_response_task, return_exceptions=True)
291
- return
297
+ map_done_task.cancel()
292
298
 
293
299
  last_entry_id = response.last_entry_id
294
300
  now_seconds = int(time.time())
@@ -339,7 +345,13 @@ async def _map_invocation(
339
345
  output = await _process_result(item.result, item.data_format, client.stub, client)
340
346
  except Exception as e:
341
347
  if return_exceptions:
342
- output = e
348
+ if wrap_returned_exceptions:
349
+ # Prior to client 1.0.4 there was a bug where return_exceptions would wrap
350
+ # any returned exceptions in a synchronicity.UserCodeException. This adds
351
+ # deprecated non-breaking compatibility bandaid for migrating away from that:
352
+ output = modal.exception.UserCodeException(e)
353
+ else:
354
+ output = e
343
355
  else:
344
356
  raise e
345
357
  return (item.idx, output)
@@ -399,7 +411,7 @@ async def _map_invocation(
399
411
  async_merge(drain_input_generator(), pump_inputs(), poll_outputs(), retry_inputs())
400
412
  ) as streamer:
401
413
  async for response in streamer:
402
- if response is not None:
414
+ if response is not None: # type: ignore[unreachable]
403
415
  yield response.value
404
416
  log_debug_stats_task.cancel()
405
417
  await log_debug_stats_task
@@ -411,6 +423,7 @@ async def _map_helper(
411
423
  kwargs={}, # any extra keyword arguments for the function
412
424
  order_outputs: bool = True, # return outputs in order
413
425
  return_exceptions: bool = False, # propagate exceptions (False) or aggregate them in the results list (True)
426
+ wrap_returned_exceptions: bool = True,
414
427
  ) -> typing.AsyncGenerator[Any, None]:
415
428
  """Core implementation that supports `_map_async()`, `_starmap_async()` and `_for_each_async()`.
416
429
 
@@ -423,7 +436,6 @@ async def _map_helper(
423
436
  We could make this explicit as an improvement or even let users decide what they
424
437
  prefer: throughput (prioritize queueing inputs) or latency (prioritize yielding results)
425
438
  """
426
-
427
439
  raw_input_queue: Any = SynchronizedQueue() # type: ignore
428
440
  await raw_input_queue.init.aio()
429
441
 
@@ -441,12 +453,41 @@ async def _map_helper(
441
453
  # synchronicity-wrapped, since they accept executable code in the form of iterators that we don't want to run inside
442
454
  # the synchronicity thread. Instead, we delegate to `._map()` with a safer Queue as input.
443
455
  async with aclosing(
444
- async_merge(self._map.aio(raw_input_queue, order_outputs, return_exceptions), feed_queue())
456
+ async_merge(
457
+ self._map.aio(raw_input_queue, order_outputs, return_exceptions, wrap_returned_exceptions), feed_queue()
458
+ )
445
459
  ) as map_output_stream:
446
460
  async for output in map_output_stream:
447
461
  yield output
448
462
 
449
463
 
464
+ def _maybe_warn_about_exceptions(func_name: str, return_exceptions: bool, wrap_returned_exceptions: bool):
465
+ if return_exceptions and wrap_returned_exceptions:
466
+ deprecation_warning(
467
+ (2025, 6, 27),
468
+ (
469
+ f"Function.{func_name} currently leaks an internal exception wrapping type "
470
+ "(modal.exceptions.UserCodeException) when `return_exceptions=True` is set. "
471
+ "In the future, this will change, and the underlying exception will be returned directly.\n"
472
+ "To opt into the future behavior and silence this warning, add `wrap_returned_exceptions=False`:\n\n"
473
+ f" f.{func_name}(..., return_exceptions=True, wrap_returned_exceptions=False)"
474
+ ),
475
+ )
476
+
477
+
478
+ def _invoked_from_sync_wrapper() -> bool:
479
+ """Check whether the calling function was called from a sync wrapper."""
480
+ # This is temporary: we only need it to avoind double-firing the wrap_returned_exceptions warning.
481
+ # (We don't want to push the warning lower in the stack beacuse then we can't attribute to the user's code.)
482
+ try:
483
+ frame = inspect.currentframe()
484
+ caller_function_name = frame.f_back.f_back.f_code.co_name
485
+ # Embeds some assumptions about how the current calling stack works, but this is just temporary.
486
+ return caller_function_name == "asend"
487
+ except Exception:
488
+ return False
489
+
490
+
450
491
  @warn_if_generator_is_not_consumed(function_name="Function.map.aio")
451
492
  async def _map_async(
452
493
  self: "modal.functions.Function",
@@ -456,10 +497,18 @@ async def _map_async(
456
497
  kwargs={}, # any extra keyword arguments for the function
457
498
  order_outputs: bool = True, # return outputs in order
458
499
  return_exceptions: bool = False, # propagate exceptions (False) or aggregate them in the results list (True)
500
+ wrap_returned_exceptions: bool = True, # wrap returned exceptions in modal.exception.UserCodeException
459
501
  ) -> typing.AsyncGenerator[Any, None]:
502
+ if not _invoked_from_sync_wrapper():
503
+ _maybe_warn_about_exceptions("map.aio", return_exceptions, wrap_returned_exceptions)
460
504
  async_input_gen = async_zip(*[sync_or_async_iter(it) for it in input_iterators])
461
505
  async for output in _map_helper(
462
- self, async_input_gen, kwargs=kwargs, order_outputs=order_outputs, return_exceptions=return_exceptions
506
+ self,
507
+ async_input_gen,
508
+ kwargs=kwargs,
509
+ order_outputs=order_outputs,
510
+ return_exceptions=return_exceptions,
511
+ wrap_returned_exceptions=wrap_returned_exceptions,
463
512
  ):
464
513
  yield output
465
514
 
@@ -472,13 +521,17 @@ async def _starmap_async(
472
521
  kwargs={},
473
522
  order_outputs: bool = True,
474
523
  return_exceptions: bool = False,
524
+ wrap_returned_exceptions: bool = True,
475
525
  ) -> typing.AsyncIterable[Any]:
526
+ if not _invoked_from_sync_wrapper():
527
+ _maybe_warn_about_exceptions("starmap.aio", return_exceptions, wrap_returned_exceptions)
476
528
  async for output in _map_helper(
477
529
  self,
478
530
  sync_or_async_iter(input_iterator),
479
531
  kwargs=kwargs,
480
532
  order_outputs=order_outputs,
481
533
  return_exceptions=return_exceptions,
534
+ wrap_returned_exceptions=wrap_returned_exceptions,
482
535
  ):
483
536
  yield output
484
537
 
@@ -488,7 +541,12 @@ async def _for_each_async(self, *input_iterators, kwargs={}, ignore_exceptions:
488
541
  # rather than iterating over the result
489
542
  async_input_gen = async_zip(*[sync_or_async_iter(it) for it in input_iterators])
490
543
  async for _ in _map_helper(
491
- self, async_input_gen, kwargs=kwargs, order_outputs=False, return_exceptions=ignore_exceptions
544
+ self,
545
+ async_input_gen,
546
+ kwargs=kwargs,
547
+ order_outputs=False,
548
+ return_exceptions=ignore_exceptions,
549
+ wrap_returned_exceptions=False,
492
550
  ):
493
551
  pass
494
552
 
@@ -500,6 +558,7 @@ def _map_sync(
500
558
  kwargs={}, # any extra keyword arguments for the function
501
559
  order_outputs: bool = True, # return outputs in order
502
560
  return_exceptions: bool = False, # propagate exceptions (False) or aggregate them in the results list (True)
561
+ wrap_returned_exceptions: bool = True,
503
562
  ) -> AsyncOrSyncIterable:
504
563
  """Parallel map over a set of inputs.
505
564
 
@@ -537,10 +596,16 @@ def _map_sync(
537
596
  print(list(my_func.map(range(3), return_exceptions=True)))
538
597
  ```
539
598
  """
599
+ _maybe_warn_about_exceptions("map", return_exceptions, wrap_returned_exceptions)
540
600
 
541
601
  return AsyncOrSyncIterable(
542
602
  _map_async(
543
- self, *input_iterators, kwargs=kwargs, order_outputs=order_outputs, return_exceptions=return_exceptions
603
+ self,
604
+ *input_iterators,
605
+ kwargs=kwargs,
606
+ order_outputs=order_outputs,
607
+ return_exceptions=return_exceptions,
608
+ wrap_returned_exceptions=wrap_returned_exceptions,
544
609
  ),
545
610
  nested_async_message=(
546
611
  "You can't iter(Function.map()) from an async function. Use async for ... in Function.map.aio() instead."
@@ -620,6 +685,7 @@ def _starmap_sync(
620
685
  kwargs={},
621
686
  order_outputs: bool = True,
622
687
  return_exceptions: bool = False,
688
+ wrap_returned_exceptions: bool = True,
623
689
  ) -> AsyncOrSyncIterable:
624
690
  """Like `map`, but spreads arguments over multiple function arguments.
625
691
 
@@ -637,9 +703,15 @@ def _starmap_sync(
637
703
  assert list(my_func.starmap([(1, 2), (3, 4)])) == [3, 7]
638
704
  ```
639
705
  """
706
+ _maybe_warn_about_exceptions("starmap", return_exceptions, wrap_returned_exceptions)
640
707
  return AsyncOrSyncIterable(
641
708
  _starmap_async(
642
- self, input_iterator, kwargs=kwargs, order_outputs=order_outputs, return_exceptions=return_exceptions
709
+ self,
710
+ input_iterator,
711
+ kwargs=kwargs,
712
+ order_outputs=order_outputs,
713
+ return_exceptions=return_exceptions,
714
+ wrap_returned_exceptions=wrap_returned_exceptions,
643
715
  ),
644
716
  nested_async_message=(
645
717
  "You can't `iter(Function.starmap())` from an async function. "
@@ -716,6 +788,7 @@ class _MapItemContext:
716
788
  Return True if input state was changed to COMPLETE, otherwise False.
717
789
  """
718
790
  # If the item is already complete, this is a duplicate output and can be ignored.
791
+
719
792
  if self.state == _MapItemState.COMPLETE:
720
793
  logger.debug(
721
794
  f"Received output for input marked as complete. Must be duplicate, so ignoring. "
@@ -761,11 +834,12 @@ class _MapItemContext:
761
834
  delay_ms = 0
762
835
 
763
836
  # None means the maximum number of retries has been reached, so output the error
764
- if delay_ms is None:
837
+ if delay_ms is None or item.result.status == api_pb2.GenericResult.GENERIC_STATUS_TERMINATED:
765
838
  self.state = _MapItemState.COMPLETE
766
839
  return _OutputType.FAILED_COMPLETION
767
840
 
768
841
  self.state = _MapItemState.WAITING_TO_RETRY
842
+
769
843
  await retry_queue.put(now_seconds + (delay_ms / 1000), item.idx)
770
844
 
771
845
  return _OutputType.RETRYING
modal/parallel_map.pyi CHANGED
@@ -12,6 +12,7 @@ import typing
12
12
  import typing_extensions
13
13
 
14
14
  class _SynchronizedQueue:
15
+ """mdmd:hidden"""
15
16
  async def init(self): ...
16
17
  async def put(self, item): ...
17
18
  async def get(self): ...
@@ -19,7 +20,10 @@ class _SynchronizedQueue:
19
20
  SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
20
21
 
21
22
  class SynchronizedQueue:
22
- def __init__(self, /, *args, **kwargs): ...
23
+ """mdmd:hidden"""
24
+ def __init__(self, /, *args, **kwargs):
25
+ """Initialize self. See help(type(self)) for accurate signature."""
26
+ ...
23
27
 
24
28
  class __init_spec(typing_extensions.Protocol[SUPERSELF]):
25
29
  def __call__(self, /): ...
@@ -40,11 +44,21 @@ class SynchronizedQueue:
40
44
  get: __get_spec[typing_extensions.Self]
41
45
 
42
46
  class _OutputValue:
47
+ """_OutputValue(value: Any)"""
48
+
43
49
  value: typing.Any
44
50
 
45
- def __init__(self, value: typing.Any) -> None: ...
46
- def __repr__(self): ...
47
- def __eq__(self, other): ...
51
+ def __init__(self, value: typing.Any) -> None:
52
+ """Initialize self. See help(type(self)) for accurate signature."""
53
+ ...
54
+
55
+ def __repr__(self):
56
+ """Return repr(self)."""
57
+ ...
58
+
59
+ def __eq__(self, other):
60
+ """Return self==value."""
61
+ ...
48
62
 
49
63
  def _map_invocation(
50
64
  function: modal._functions._Function,
@@ -52,6 +66,7 @@ def _map_invocation(
52
66
  client: modal.client._Client,
53
67
  order_outputs: bool,
54
68
  return_exceptions: bool,
69
+ wrap_returned_exceptions: bool,
55
70
  count_update_callback: typing.Optional[collections.abc.Callable[[int, int], None]],
56
71
  function_call_invocation_type: int,
57
72
  ): ...
@@ -61,13 +76,33 @@ def _map_helper(
61
76
  kwargs={},
62
77
  order_outputs: bool = True,
63
78
  return_exceptions: bool = False,
64
- ) -> typing.AsyncGenerator[typing.Any, None]: ...
79
+ wrap_returned_exceptions: bool = True,
80
+ ) -> typing.AsyncGenerator[typing.Any, None]:
81
+ """Core implementation that supports `_map_async()`, `_starmap_async()` and `_for_each_async()`.
82
+
83
+ Runs in an event loop on the main thread. Concurrently feeds new input to the input queue and yields available
84
+ outputs to the caller.
85
+
86
+ Note that since the iterator(s) can block, it's a bit opaque how often the event
87
+ loop decides to get a new input vs how often it will emit a new output.
88
+
89
+ We could make this explicit as an improvement or even let users decide what they
90
+ prefer: throughput (prioritize queueing inputs) or latency (prioritize yielding results)
91
+ """
92
+ ...
93
+
94
+ def _maybe_warn_about_exceptions(func_name: str, return_exceptions: bool, wrap_returned_exceptions: bool): ...
95
+ def _invoked_from_sync_wrapper() -> bool:
96
+ """Check whether the calling function was called from a sync wrapper."""
97
+ ...
98
+
65
99
  def _map_async(
66
100
  self: modal.functions.Function,
67
101
  *input_iterators: typing.Union[typing.Iterable[typing.Any], typing.AsyncIterable[typing.Any]],
68
102
  kwargs={},
69
103
  order_outputs: bool = True,
70
104
  return_exceptions: bool = False,
105
+ wrap_returned_exceptions: bool = True,
71
106
  ) -> typing.AsyncGenerator[typing.Any, None]: ...
72
107
  def _starmap_async(
73
108
  self,
@@ -78,14 +113,91 @@ def _starmap_async(
78
113
  kwargs={},
79
114
  order_outputs: bool = True,
80
115
  return_exceptions: bool = False,
116
+ wrap_returned_exceptions: bool = True,
81
117
  ) -> typing.AsyncIterable[typing.Any]: ...
82
118
  async def _for_each_async(self, *input_iterators, kwargs={}, ignore_exceptions: bool = False) -> None: ...
83
119
  def _map_sync(
84
- self, *input_iterators, kwargs={}, order_outputs: bool = True, return_exceptions: bool = False
85
- ) -> modal._utils.async_utils.AsyncOrSyncIterable: ...
86
- async def _spawn_map_async(self, *input_iterators, kwargs={}) -> None: ...
87
- def _spawn_map_sync(self, *input_iterators, kwargs={}) -> None: ...
88
- def _for_each_sync(self, *input_iterators, kwargs={}, ignore_exceptions: bool = False): ...
120
+ self,
121
+ *input_iterators,
122
+ kwargs={},
123
+ order_outputs: bool = True,
124
+ return_exceptions: bool = False,
125
+ wrap_returned_exceptions: bool = True,
126
+ ) -> modal._utils.async_utils.AsyncOrSyncIterable:
127
+ """Parallel map over a set of inputs.
128
+
129
+ Takes one iterator argument per argument in the function being mapped over.
130
+
131
+ Example:
132
+ ```python
133
+ @app.function()
134
+ def my_func(a):
135
+ return a ** 2
136
+
137
+
138
+ @app.local_entrypoint()
139
+ def main():
140
+ assert list(my_func.map([1, 2, 3, 4])) == [1, 4, 9, 16]
141
+ ```
142
+
143
+ If applied to a `app.function`, `map()` returns one result per input and the output order
144
+ is guaranteed to be the same as the input order. Set `order_outputs=False` to return results
145
+ in the order that they are completed instead.
146
+
147
+ `return_exceptions` can be used to treat exceptions as successful results:
148
+
149
+ ```python
150
+ @app.function()
151
+ def my_func(a):
152
+ if a == 2:
153
+ raise Exception("ohno")
154
+ return a ** 2
155
+
156
+
157
+ @app.local_entrypoint()
158
+ def main():
159
+ # [0, 1, UserCodeException(Exception('ohno'))]
160
+ print(list(my_func.map(range(3), return_exceptions=True)))
161
+ ```
162
+ """
163
+ ...
164
+
165
+ async def _spawn_map_async(self, *input_iterators, kwargs={}) -> None:
166
+ """This runs in an event loop on the main thread. It consumes inputs from the input iterators and creates async
167
+ function calls for each.
168
+ """
169
+ ...
170
+
171
+ def _spawn_map_sync(self, *input_iterators, kwargs={}) -> None:
172
+ """Spawn parallel execution over a set of inputs, exiting as soon as the inputs are created (without waiting
173
+ for the map to complete).
174
+
175
+ Takes one iterator argument per argument in the function being mapped over.
176
+
177
+ Example:
178
+ ```python
179
+ @app.function()
180
+ def my_func(a):
181
+ return a ** 2
182
+
183
+
184
+ @app.local_entrypoint()
185
+ def main():
186
+ my_func.spawn_map([1, 2, 3, 4])
187
+ ```
188
+
189
+ Programmatic retrieval of results will be supported in a future update.
190
+ """
191
+ ...
192
+
193
+ def _for_each_sync(self, *input_iterators, kwargs={}, ignore_exceptions: bool = False):
194
+ """Execute function for all inputs, ignoring outputs. Waits for completion of the inputs.
195
+
196
+ Convenient alias for `.map()` in cases where the function just needs to be called.
197
+ as the caller doesn't have to consume the generator to process the inputs.
198
+ """
199
+ ...
200
+
89
201
  def _starmap_sync(
90
202
  self,
91
203
  input_iterator: typing.Iterable[typing.Sequence[typing.Any]],
@@ -93,7 +205,25 @@ def _starmap_sync(
93
205
  kwargs={},
94
206
  order_outputs: bool = True,
95
207
  return_exceptions: bool = False,
96
- ) -> modal._utils.async_utils.AsyncOrSyncIterable: ...
208
+ wrap_returned_exceptions: bool = True,
209
+ ) -> modal._utils.async_utils.AsyncOrSyncIterable:
210
+ """Like `map`, but spreads arguments over multiple function arguments.
211
+
212
+ Assumes every input is a sequence (e.g. a tuple).
213
+
214
+ Example:
215
+ ```python
216
+ @app.function()
217
+ def my_func(a, b):
218
+ return a + b
219
+
220
+
221
+ @app.local_entrypoint()
222
+ def main():
223
+ assert list(my_func.starmap([(1, 2), (3, 4)])) == [3, 7]
224
+ ```
225
+ """
226
+ ...
97
227
 
98
228
  class _MapItemState(enum.Enum):
99
229
  # The input is being sent the server with a PutInputs request, but the response has not been received yet.
@@ -130,7 +260,10 @@ class _MapItemContext:
130
260
  input: modal_proto.api_pb2.FunctionInput,
131
261
  retry_manager: modal.retries.RetryManager,
132
262
  sync_client_retries_enabled: bool,
133
- ): ...
263
+ ):
264
+ """Initialize self. See help(type(self)) for accurate signature."""
265
+ ...
266
+
134
267
  def handle_put_inputs_response(self, item: modal_proto.api_pb2.FunctionPutInputsResponseItem): ...
135
268
  async def handle_get_outputs_response(
136
269
  self,
@@ -138,7 +271,13 @@ class _MapItemContext:
138
271
  now_seconds: int,
139
272
  function_call_invocation_type: int,
140
273
  retry_queue: modal._utils.async_utils.TimestampPriorityQueue,
141
- ) -> _OutputType: ...
274
+ ) -> _OutputType:
275
+ """Processes the output, and determines if it is complete or needs to be retried.
276
+
277
+ Return True if input state was changed to COMPLETE, otherwise False.
278
+ """
279
+ ...
280
+
142
281
  async def prepare_item_for_retry(self) -> modal_proto.api_pb2.FunctionRetryInputsItem: ...
143
282
  def handle_retry_response(self, input_jwt: str): ...
144
283
 
@@ -150,12 +289,18 @@ class _MapItemsManager:
150
289
  retry_queue: modal._utils.async_utils.TimestampPriorityQueue,
151
290
  sync_client_retries_enabled: bool,
152
291
  max_inputs_outstanding: int,
153
- ): ...
292
+ ):
293
+ """Initialize self. See help(type(self)) for accurate signature."""
294
+ ...
295
+
154
296
  async def add_items(self, items: list[modal_proto.api_pb2.FunctionPutInputsItem]): ...
155
297
  async def prepare_items_for_retry(
156
298
  self, retriable_idxs: list[int]
157
299
  ) -> list[modal_proto.api_pb2.FunctionRetryInputsItem]: ...
158
- def get_input_jwts_waiting_for_output(self) -> list[str]: ...
300
+ def get_input_jwts_waiting_for_output(self) -> list[str]:
301
+ """Returns a list of input_jwts for inputs that are waiting for output."""
302
+ ...
303
+
159
304
  def _remove_item(self, item_idx: int): ...
160
305
  def get_item_context(self, item_idx: int) -> _MapItemContext: ...
161
306
  def handle_put_inputs_response(self, items: list[modal_proto.api_pb2.FunctionPutInputsResponseItem]): ...