modal 1.1.1.dev41__py3-none-any.whl → 1.1.2__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.

Potentially problematic release.


This version of modal might be problematic. Click here for more details.

Files changed (68) hide show
  1. modal/__main__.py +1 -2
  2. modal/_container_entrypoint.py +18 -7
  3. modal/_functions.py +135 -13
  4. modal/_object.py +13 -2
  5. modal/_partial_function.py +8 -8
  6. modal/_runtime/asgi.py +3 -2
  7. modal/_runtime/container_io_manager.py +20 -14
  8. modal/_runtime/container_io_manager.pyi +38 -13
  9. modal/_runtime/execution_context.py +18 -2
  10. modal/_runtime/execution_context.pyi +4 -1
  11. modal/_runtime/gpu_memory_snapshot.py +158 -54
  12. modal/_utils/blob_utils.py +83 -24
  13. modal/_utils/function_utils.py +4 -3
  14. modal/_utils/time_utils.py +28 -4
  15. modal/app.py +8 -4
  16. modal/app.pyi +8 -8
  17. modal/cli/dict.py +14 -11
  18. modal/cli/entry_point.py +9 -3
  19. modal/cli/launch.py +102 -4
  20. modal/cli/profile.py +1 -0
  21. modal/cli/programs/launch_instance_ssh.py +94 -0
  22. modal/cli/programs/run_marimo.py +95 -0
  23. modal/cli/queues.py +49 -19
  24. modal/cli/secret.py +45 -18
  25. modal/cli/volume.py +14 -16
  26. modal/client.pyi +2 -10
  27. modal/cls.py +12 -2
  28. modal/cls.pyi +9 -1
  29. modal/config.py +7 -7
  30. modal/dict.py +206 -12
  31. modal/dict.pyi +358 -4
  32. modal/experimental/__init__.py +130 -0
  33. modal/file_io.py +1 -1
  34. modal/file_io.pyi +2 -2
  35. modal/file_pattern_matcher.py +25 -16
  36. modal/functions.pyi +111 -11
  37. modal/image.py +9 -3
  38. modal/image.pyi +7 -7
  39. modal/mount.py +20 -13
  40. modal/mount.pyi +16 -3
  41. modal/network_file_system.py +8 -2
  42. modal/object.pyi +3 -0
  43. modal/parallel_map.py +346 -101
  44. modal/parallel_map.pyi +108 -0
  45. modal/proxy.py +2 -1
  46. modal/queue.py +199 -9
  47. modal/queue.pyi +357 -3
  48. modal/sandbox.py +6 -5
  49. modal/sandbox.pyi +17 -14
  50. modal/secret.py +196 -3
  51. modal/secret.pyi +372 -0
  52. modal/volume.py +239 -23
  53. modal/volume.pyi +405 -10
  54. {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/METADATA +2 -2
  55. {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/RECORD +68 -66
  56. modal_docs/mdmd/mdmd.py +11 -1
  57. modal_proto/api.proto +37 -10
  58. modal_proto/api_grpc.py +32 -0
  59. modal_proto/api_pb2.py +627 -597
  60. modal_proto/api_pb2.pyi +107 -19
  61. modal_proto/api_pb2_grpc.py +67 -2
  62. modal_proto/api_pb2_grpc.pyi +24 -8
  63. modal_proto/modal_api_grpc.py +2 -0
  64. modal_version/__init__.py +1 -1
  65. {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/WHEEL +0 -0
  66. {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/entry_points.txt +0 -0
  67. {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/licenses/LICENSE +0 -0
  68. {modal-1.1.1.dev41.dist-info → modal-1.1.2.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  # Copyright Modal Labs 2025
2
2
  import os
3
+ import shlex
3
4
  from dataclasses import dataclass
4
5
  from pathlib import Path
5
6
  from typing import Literal, Optional, Union
@@ -212,6 +213,135 @@ async def raw_registry_image(
212
213
  )
213
214
 
214
215
 
216
+ def _install_cuda_command() -> str:
217
+ """Command to install CUDA Toolkit (nvcc) inside a container."""
218
+ arch = "x86_64" # instruction set architecture for the CPU, all Modal machines are x86_64
219
+ distro = "debian12" # the distribution and version number of our OS (GNU/Linux)
220
+ filename = "cuda-keyring_1.1-1_all.deb" # NVIDIA signing key file
221
+ cuda_keyring_url = f"https://developer.download.nvidia.com/compute/cuda/repos/{distro}/{arch}/{filename}"
222
+
223
+ major, minor = 12, 8
224
+ max_cuda_version = f"{major}-{minor}"
225
+
226
+ return (
227
+ f"wget {cuda_keyring_url} && "
228
+ + f"dpkg -i {filename} && "
229
+ + f"rm -f {filename} && "
230
+ + f"apt-get update && apt-get install -y cuda-nvcc-{max_cuda_version}"
231
+ )
232
+
233
+
234
+ @synchronizer.create_blocking
235
+ async def notebook_base_image(*, python_version: Optional[str] = None, force_build: bool = False) -> _Image:
236
+ """Default image used for Modal notebook kernels, with common libraries.
237
+
238
+ This can be used to bootstrap development workflows quickly. We don't
239
+ recommend using this image for production Modal Functions though, as it may
240
+ change at any time in the future.
241
+ """
242
+ # Include several common packages, as well as kernelshim dependencies (except 'modal').
243
+ # These packages aren't pinned, so they may change over time with builds.
244
+ #
245
+ # We plan to use `--exclude-newer` in the future, with date-specific image builds.
246
+ base_image = _Image.debian_slim(python_version=python_version)
247
+
248
+ environment_packages: list[str] = [
249
+ "accelerate",
250
+ "aiohttp",
251
+ "altair",
252
+ "anthropic",
253
+ "asyncpg",
254
+ "beautifulsoup4",
255
+ "bokeh",
256
+ "boto3[crt]",
257
+ "click",
258
+ "diffusers[torch,flax]",
259
+ "dm-sonnet",
260
+ "flax",
261
+ "ftfy",
262
+ "h5py",
263
+ "urllib3",
264
+ "httpx",
265
+ "huggingface-hub",
266
+ "ipywidgets",
267
+ "jax[cuda12]",
268
+ "keras",
269
+ "matplotlib",
270
+ "nbformat",
271
+ "numba",
272
+ "numpy",
273
+ "openai",
274
+ "optax",
275
+ "pandas",
276
+ "plotly[express]",
277
+ "polars",
278
+ "psycopg2",
279
+ "requests",
280
+ "safetensors",
281
+ "scikit-image",
282
+ "scikit-learn",
283
+ "scipy",
284
+ "seaborn",
285
+ "sentencepiece",
286
+ "sqlalchemy",
287
+ "statsmodels",
288
+ "sympy",
289
+ "tabulate",
290
+ "tensorboard",
291
+ "toml",
292
+ "transformers",
293
+ "triton",
294
+ "typer",
295
+ "vega-datasets",
296
+ "watchfiles",
297
+ "websockets",
298
+ ]
299
+
300
+ # Kernelshim dependencies. (see NOTEBOOK_KERNELSHIM_DEPENDENCIES)
301
+ kernelshim_packages: list[str] = [
302
+ "authlib>=1.3",
303
+ "basedpyright>=1.28",
304
+ "fastapi>=0.100",
305
+ "ipykernel>=6",
306
+ "pydantic>=2",
307
+ "pyzmq>=26",
308
+ "ruff>=0.11",
309
+ "uvicorn>=0.32",
310
+ ]
311
+
312
+ commands: list[str] = [
313
+ "apt-get update",
314
+ "apt-get install -y libpq-dev pkg-config cmake git curl wget unzip zip libsqlite3-dev openssh-server vim",
315
+ _install_cuda_command(),
316
+ # Install uv since it's faster than pip for installing packages.
317
+ "pip install uv",
318
+ # https://github.com/astral-sh/uv/issues/11480
319
+ "pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126",
320
+ f"uv pip install --system {shlex.join(sorted(environment_packages))}",
321
+ f"uv pip install --system {shlex.join(sorted(kernelshim_packages))}",
322
+ ]
323
+
324
+ # TODO: Also install the CUDA Toolkit, so `nvcc` is available.
325
+ # https://github.com/charlesfrye/cuda-modal/blob/7fef8db12402986cf42d9c8cca8c63d1da6d7700/cuda/use_cuda.py#L158-L188
326
+
327
+ def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
328
+ return DockerfileSpec(
329
+ commands=[
330
+ "FROM base",
331
+ *(f"RUN {cmd}" for cmd in commands),
332
+ "ENV PATH=/usr/local/cuda/bin:$PATH",
333
+ ],
334
+ context_files={},
335
+ )
336
+
337
+ return _Image._from_args(
338
+ base_images={"base": base_image},
339
+ dockerfile_function=build_dockerfile,
340
+ force_build=force_build,
341
+ _namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
342
+ )
343
+
344
+
215
345
  @synchronizer.create_blocking
216
346
  async def update_autoscaler(
217
347
  obj: Union[_Function, _Obj],
modal/file_io.py CHANGED
@@ -128,7 +128,7 @@ class _FileIO(Generic[T]):
128
128
 
129
129
  **Usage**
130
130
 
131
- ```python
131
+ ```python notest
132
132
  import modal
133
133
 
134
134
  app = modal.App.lookup("my-app", create_if_missing=True)
modal/file_io.pyi CHANGED
@@ -62,7 +62,7 @@ class _FileIO(typing.Generic[T]):
62
62
 
63
63
  **Usage**
64
64
 
65
- ```python
65
+ ```python notest
66
66
  import modal
67
67
 
68
68
  app = modal.App.lookup("my-app", create_if_missing=True)
@@ -232,7 +232,7 @@ class FileIO(typing.Generic[T]):
232
232
 
233
233
  **Usage**
234
234
 
235
- ```python
235
+ ```python notest
236
236
  import modal
237
237
 
238
238
  app = modal.App.lookup("my-app", create_if_missing=True)
@@ -11,6 +11,7 @@ then asking it whether file paths match any of its patterns.
11
11
 
12
12
  import os
13
13
  from abc import abstractmethod
14
+ from functools import cached_property
14
15
  from pathlib import Path
15
16
  from typing import Callable, Optional, Sequence, Union
16
17
 
@@ -99,11 +100,11 @@ class FilePatternMatcher(_AbstractPatternMatcher):
99
100
  ```
100
101
  """
101
102
 
102
- patterns: list[Pattern]
103
- _delayed_init: Callable[[], None] = None
103
+ _file_path: Optional[Union[str, Path]]
104
+ _pattern_strings: Optional[Sequence[str]]
104
105
 
105
- def _set_patterns(self, patterns: Sequence[str]) -> None:
106
- self.patterns = []
106
+ def _parse_patterns(self, patterns: Sequence[str]) -> list[Pattern]:
107
+ parsed_patterns = []
107
108
  for pattern in list(patterns):
108
109
  pattern = pattern.strip().strip(os.path.sep)
109
110
  if not pattern:
@@ -118,7 +119,8 @@ class FilePatternMatcher(_AbstractPatternMatcher):
118
119
  # In Python, we can proceed without explicit syntax checking
119
120
  new_pattern.cleaned_pattern = pattern
120
121
  new_pattern.dirs = pattern.split(os.path.sep)
121
- self.patterns.append(new_pattern)
122
+ parsed_patterns.append(new_pattern)
123
+ return parsed_patterns
122
124
 
123
125
  def __init__(self, *pattern: str) -> None:
124
126
  """Initialize a new FilePatternMatcher instance.
@@ -129,7 +131,8 @@ class FilePatternMatcher(_AbstractPatternMatcher):
129
131
  Raises:
130
132
  ValueError: If an illegal exclusion pattern is provided.
131
133
  """
132
- self._set_patterns(pattern)
134
+ self._pattern_strings = pattern
135
+ self._file_path = None
133
136
 
134
137
  @classmethod
135
138
  def from_file(cls, file_path: Union[str, Path]) -> "FilePatternMatcher":
@@ -148,14 +151,10 @@ class FilePatternMatcher(_AbstractPatternMatcher):
148
151
  ```
149
152
 
150
153
  """
151
- uninitialized = cls.__new__(cls)
152
-
153
- def _delayed_init():
154
- uninitialized._set_patterns(Path(file_path).read_text("utf8").splitlines())
155
- uninitialized._delayed_init = None
156
-
157
- uninitialized._delayed_init = _delayed_init
158
- return uninitialized
154
+ instance = cls.__new__(cls)
155
+ instance._file_path = file_path
156
+ instance._pattern_strings = None
157
+ return instance
159
158
 
160
159
  def _matches(self, file_path: str) -> bool:
161
160
  """Check if the file path or any of its parent directories match the patterns.
@@ -194,6 +193,18 @@ class FilePatternMatcher(_AbstractPatternMatcher):
194
193
 
195
194
  return matched
196
195
 
196
+ @cached_property
197
+ def patterns(self) -> list[Pattern]:
198
+ """Get the patterns, loading from file if necessary."""
199
+ if self._file_path is not None:
200
+ # Lazy load from file
201
+ pattern_strings = Path(self._file_path).read_text("utf8").splitlines()
202
+ else:
203
+ # Use patterns provided in __init__
204
+ pattern_strings = list(self._pattern_strings)
205
+
206
+ return self._parse_patterns(pattern_strings)
207
+
197
208
  def can_prune_directories(self) -> bool:
198
209
  """
199
210
  Returns True if this pattern matcher allows safe early directory pruning.
@@ -205,8 +216,6 @@ class FilePatternMatcher(_AbstractPatternMatcher):
205
216
  return not any(pattern.exclusion for pattern in self.patterns)
206
217
 
207
218
  def __call__(self, file_path: Path) -> bool:
208
- if self._delayed_init:
209
- self._delayed_init()
210
219
  return self._matches(str(file_path))
211
220
 
212
221
 
modal/functions.pyi CHANGED
@@ -84,7 +84,7 @@ class Function(
84
84
  memory: typing.Union[int, tuple[int, int], None] = None,
85
85
  proxy: typing.Optional[modal.proxy.Proxy] = None,
86
86
  retries: typing.Union[int, modal.retries.Retries, None] = None,
87
- timeout: typing.Optional[int] = None,
87
+ timeout: int = 300,
88
88
  min_containers: typing.Optional[int] = None,
89
89
  max_containers: typing.Optional[int] = None,
90
90
  buffer_containers: typing.Optional[int] = None,
@@ -405,6 +405,12 @@ class Function(
405
405
 
406
406
  _map: ___map_spec[typing_extensions.Self]
407
407
 
408
+ class ___spawn_map_spec(typing_extensions.Protocol[ReturnType_INNER, SUPERSELF]):
409
+ def __call__(self, /, input_queue: modal.parallel_map.SynchronizedQueue) -> FunctionCall[ReturnType_INNER]: ...
410
+ async def aio(self, /, input_queue: modal.parallel_map.SynchronizedQueue) -> FunctionCall[ReturnType_INNER]: ...
411
+
412
+ _spawn_map: ___spawn_map_spec[modal._functions.ReturnType, typing_extensions.Self]
413
+
408
414
  class ___call_function_spec(typing_extensions.Protocol[ReturnType_INNER, SUPERSELF]):
409
415
  def __call__(self, /, args, kwargs) -> ReturnType_INNER: ...
410
416
  async def aio(self, /, args, kwargs) -> ReturnType_INNER: ...
@@ -427,7 +433,7 @@ class Function(
427
433
 
428
434
  _call_generator: ___call_generator_spec[typing_extensions.Self]
429
435
 
430
- class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
436
+ class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
431
437
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
432
438
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
433
439
  ...
@@ -436,7 +442,7 @@ class Function(
436
442
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
437
443
  ...
438
444
 
439
- remote: __remote_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
445
+ remote: __remote_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
440
446
 
441
447
  class __remote_gen_spec(typing_extensions.Protocol[SUPERSELF]):
442
448
  def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
@@ -463,7 +469,7 @@ class Function(
463
469
  """
464
470
  ...
465
471
 
466
- class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
472
+ class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
467
473
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
468
474
  """[Experimental] Calls the function with the given arguments, without waiting for the results.
469
475
 
@@ -487,7 +493,7 @@ class Function(
487
493
  ...
488
494
 
489
495
  _experimental_spawn: ___experimental_spawn_spec[
490
- modal._functions.P, modal._functions.ReturnType, typing_extensions.Self
496
+ modal._functions.ReturnType, modal._functions.P, typing_extensions.Self
491
497
  ]
492
498
 
493
499
  class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER, SUPERSELF]):
@@ -496,7 +502,7 @@ class Function(
496
502
 
497
503
  _spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P, typing_extensions.Self]
498
504
 
499
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
505
+ class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
500
506
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
501
507
  """Calls the function with the given arguments, without waiting for the results.
502
508
 
@@ -517,7 +523,7 @@ class Function(
517
523
  """
518
524
  ...
519
525
 
520
- spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
526
+ spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
521
527
 
522
528
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
523
529
  """Return the inner Python object wrapped by this Modal Function."""
@@ -693,6 +699,34 @@ class Function(
693
699
 
694
700
  spawn_map: __spawn_map_spec[typing_extensions.Self]
695
701
 
702
+ class __experimental_spawn_map_spec(typing_extensions.Protocol[SUPERSELF]):
703
+ def __call__(self, /, *input_iterators, kwargs={}) -> modal._functions._FunctionCall:
704
+ """mdmd:hidden
705
+ Spawn parallel execution over a set of inputs, returning as soon as the inputs are created.
706
+
707
+ Unlike `modal.Function.map`, this method does not block on completion of the remote execution but
708
+ returns a `modal.FunctionCall` object that can be used to poll status and retrieve results later.
709
+
710
+ Takes one iterator argument per argument in the function being mapped over.
711
+
712
+ Example:
713
+ ```python
714
+ @app.function()
715
+ def my_func(a, b):
716
+ return a ** b
717
+
718
+
719
+ @app.local_entrypoint()
720
+ def main():
721
+ fc = my_func.spawn_map([1, 2], [3, 4])
722
+ ```
723
+ """
724
+ ...
725
+
726
+ async def aio(self, /, *input_iterators, kwargs={}) -> modal._functions._FunctionCall: ...
727
+
728
+ experimental_spawn_map: __experimental_spawn_map_spec[typing_extensions.Self]
729
+
696
730
  class FunctionCall(typing.Generic[modal._functions.ReturnType], modal.object.Object):
697
731
  """A reference to an executed function call.
698
732
 
@@ -705,16 +739,31 @@ class FunctionCall(typing.Generic[modal._functions.ReturnType], modal.object.Obj
705
739
  """
706
740
 
707
741
  _is_generator: bool
742
+ _num_inputs: typing.Optional[int]
708
743
 
709
744
  def __init__(self, *args, **kwargs):
710
745
  """mdmd:hidden"""
711
746
  ...
712
747
 
713
748
  def _invocation(self): ...
749
+ def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
750
+
751
+ class __num_inputs_spec(typing_extensions.Protocol[SUPERSELF]):
752
+ def __call__(self, /) -> int:
753
+ """Get the number of inputs in the function call."""
754
+ ...
755
+
756
+ async def aio(self, /) -> int:
757
+ """Get the number of inputs in the function call."""
758
+ ...
759
+
760
+ num_inputs: __num_inputs_spec[typing_extensions.Self]
714
761
 
715
762
  class __get_spec(typing_extensions.Protocol[ReturnType_INNER, SUPERSELF]):
716
- def __call__(self, /, timeout: typing.Optional[float] = None) -> ReturnType_INNER:
717
- """Get the result of the function call.
763
+ def __call__(self, /, timeout: typing.Optional[float] = None, *, index: int = 0) -> ReturnType_INNER:
764
+ """Get the result of the index-th input of the function call.
765
+ `.spawn()` calls have a single output, so only specifying `index=0` is valid.
766
+ A non-zero index is useful when your function has multiple outputs, like via `.spawn_map()`.
718
767
 
719
768
  This function waits indefinitely by default. It takes an optional
720
769
  `timeout` argument that specifies the maximum number of seconds to wait,
@@ -724,8 +773,10 @@ class FunctionCall(typing.Generic[modal._functions.ReturnType], modal.object.Obj
724
773
  """
725
774
  ...
726
775
 
727
- async def aio(self, /, timeout: typing.Optional[float] = None) -> ReturnType_INNER:
728
- """Get the result of the function call.
776
+ async def aio(self, /, timeout: typing.Optional[float] = None, *, index: int = 0) -> ReturnType_INNER:
777
+ """Get the result of the index-th input of the function call.
778
+ `.spawn()` calls have a single output, so only specifying `index=0` is valid.
779
+ A non-zero index is useful when your function has multiple outputs, like via `.spawn_map()`.
729
780
 
730
781
  This function waits indefinitely by default. It takes an optional
731
782
  `timeout` argument that specifies the maximum number of seconds to wait,
@@ -737,6 +788,55 @@ class FunctionCall(typing.Generic[modal._functions.ReturnType], modal.object.Obj
737
788
 
738
789
  get: __get_spec[modal._functions.ReturnType, typing_extensions.Self]
739
790
 
791
+ class __iter_spec(typing_extensions.Protocol[ReturnType_INNER, SUPERSELF]):
792
+ def __call__(self, /, *, start: int = 0, end: typing.Optional[int] = None) -> typing.Iterator[ReturnType_INNER]:
793
+ """Iterate in-order over the results of the function call.
794
+
795
+ Optionally, specify a range [start, end) to iterate over.
796
+
797
+ Example:
798
+ ```python
799
+ @app.function()
800
+ def my_func(a):
801
+ return a ** 2
802
+
803
+
804
+ @app.local_entrypoint()
805
+ def main():
806
+ fc = my_func.spawn_map([1, 2, 3, 4])
807
+ assert list(fc.iter()) == [1, 4, 9, 16]
808
+ assert list(fc.iter(start=1, end=3)) == [4, 9]
809
+ ```
810
+
811
+ If `end` is not provided, it will iterate over all results.
812
+ """
813
+ ...
814
+
815
+ def aio(self, /, *, start: int = 0, end: typing.Optional[int] = None) -> typing.AsyncIterator[ReturnType_INNER]:
816
+ """Iterate in-order over the results of the function call.
817
+
818
+ Optionally, specify a range [start, end) to iterate over.
819
+
820
+ Example:
821
+ ```python
822
+ @app.function()
823
+ def my_func(a):
824
+ return a ** 2
825
+
826
+
827
+ @app.local_entrypoint()
828
+ def main():
829
+ fc = my_func.spawn_map([1, 2, 3, 4])
830
+ assert list(fc.iter()) == [1, 4, 9, 16]
831
+ assert list(fc.iter(start=1, end=3)) == [4, 9]
832
+ ```
833
+
834
+ If `end` is not provided, it will iterate over all results.
835
+ """
836
+ ...
837
+
838
+ iter: __iter_spec[modal._functions.ReturnType, typing_extensions.Self]
839
+
740
840
  class __get_call_graph_spec(typing_extensions.Protocol[SUPERSELF]):
741
841
  def __call__(self, /) -> list[modal.call_graph.InputInfo]:
742
842
  """Returns a structure representing the call graph from a given root
modal/image.py CHANGED
@@ -1393,7 +1393,13 @@ class _Image(_Object, type_prefix="im"):
1393
1393
  # a requirement in `uv.lock`
1394
1394
  return
1395
1395
 
1396
- dependencies = pyproject_toml_content["project"]["dependencies"]
1396
+ try:
1397
+ dependencies = pyproject_toml_content["project"]["dependencies"]
1398
+ except KeyError as e:
1399
+ raise InvalidError(
1400
+ f"Invalid pyproject.toml file: missing key {e} in {pyproject_toml}. "
1401
+ "See https://packaging.python.org/en/latest/guides/writing-pyproject-toml for guidelines."
1402
+ )
1397
1403
 
1398
1404
  for group in groups:
1399
1405
  if (
@@ -1459,7 +1465,7 @@ class _Image(_Object, type_prefix="im"):
1459
1465
  commands.append(f"COPY /.uv.lock {UV_ROOT}/uv.lock")
1460
1466
 
1461
1467
  if frozen:
1462
- # Do not update `uv.lock` when we have one when `frozen=True`. This it ehd efault because this
1468
+ # Do not update `uv.lock` when we have one when `frozen=True`. This is the default because this
1463
1469
  # ensures that the runtime environment matches the local `uv.lock`.
1464
1470
  #
1465
1471
  # If `frozen=False`, then `uv sync` will update the the dependencies in the `uv.lock` file
@@ -2108,7 +2114,7 @@ class _Image(_Object, type_prefix="im"):
2108
2114
  gpu: Union[GPU_T, list[GPU_T]] = None, # Requested GPU or or list of acceptable GPUs( e.g. ["A10", "A100"])
2109
2115
  cpu: Optional[float] = None, # How many CPU cores to request. This is a soft limit.
2110
2116
  memory: Optional[int] = None, # How much memory to request, in MiB. This is a soft limit.
2111
- timeout: Optional[int] = 60 * 60, # Maximum execution time of the function in seconds.
2117
+ timeout: int = 60 * 60, # Maximum execution time of the function in seconds.
2112
2118
  cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
2113
2119
  region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
2114
2120
  force_build: bool = False, # Ignore cached builds, similar to 'docker build --no-cache'
modal/image.pyi CHANGED
@@ -91,7 +91,7 @@ def _create_context_mount_function(
91
91
  dockerfile_cmds: list[str] = [],
92
92
  dockerfile_path: typing.Optional[pathlib.Path] = None,
93
93
  context_mount: typing.Optional[modal.mount._Mount] = None,
94
- context_dir: typing.Union[pathlib.Path, str, None] = None,
94
+ context_dir: typing.Union[str, pathlib.Path, None] = None,
95
95
  ): ...
96
96
 
97
97
  class _ImageRegistryConfig:
@@ -537,7 +537,7 @@ class _Image(modal._object._Object):
537
537
  secrets: collections.abc.Sequence[modal.secret._Secret] = [],
538
538
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
539
539
  context_mount: typing.Optional[modal.mount._Mount] = None,
540
- context_dir: typing.Union[pathlib.Path, str, None] = None,
540
+ context_dir: typing.Union[str, pathlib.Path, None] = None,
541
541
  force_build: bool = False,
542
542
  ignore: typing.Union[
543
543
  collections.abc.Sequence[str], collections.abc.Callable[[pathlib.Path], bool]
@@ -748,7 +748,7 @@ class _Image(modal._object._Object):
748
748
  *,
749
749
  context_mount: typing.Optional[modal.mount._Mount] = None,
750
750
  force_build: bool = False,
751
- context_dir: typing.Union[pathlib.Path, str, None] = None,
751
+ context_dir: typing.Union[str, pathlib.Path, None] = None,
752
752
  secrets: collections.abc.Sequence[modal.secret._Secret] = [],
753
753
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
754
754
  add_python: typing.Optional[str] = None,
@@ -845,7 +845,7 @@ class _Image(modal._object._Object):
845
845
  gpu: typing.Union[None, str, modal.gpu._GPUConfig, list[typing.Union[None, str, modal.gpu._GPUConfig]]] = None,
846
846
  cpu: typing.Optional[float] = None,
847
847
  memory: typing.Optional[int] = None,
848
- timeout: typing.Optional[int] = 3600,
848
+ timeout: int = 3600,
849
849
  cloud: typing.Optional[str] = None,
850
850
  region: typing.Union[str, collections.abc.Sequence[str], None] = None,
851
851
  force_build: bool = False,
@@ -1381,7 +1381,7 @@ class Image(modal.object.Object):
1381
1381
  secrets: collections.abc.Sequence[modal.secret.Secret] = [],
1382
1382
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
1383
1383
  context_mount: typing.Optional[modal.mount.Mount] = None,
1384
- context_dir: typing.Union[pathlib.Path, str, None] = None,
1384
+ context_dir: typing.Union[str, pathlib.Path, None] = None,
1385
1385
  force_build: bool = False,
1386
1386
  ignore: typing.Union[
1387
1387
  collections.abc.Sequence[str], collections.abc.Callable[[pathlib.Path], bool]
@@ -1592,7 +1592,7 @@ class Image(modal.object.Object):
1592
1592
  *,
1593
1593
  context_mount: typing.Optional[modal.mount.Mount] = None,
1594
1594
  force_build: bool = False,
1595
- context_dir: typing.Union[pathlib.Path, str, None] = None,
1595
+ context_dir: typing.Union[str, pathlib.Path, None] = None,
1596
1596
  secrets: collections.abc.Sequence[modal.secret.Secret] = [],
1597
1597
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
1598
1598
  add_python: typing.Optional[str] = None,
@@ -1689,7 +1689,7 @@ class Image(modal.object.Object):
1689
1689
  gpu: typing.Union[None, str, modal.gpu._GPUConfig, list[typing.Union[None, str, modal.gpu._GPUConfig]]] = None,
1690
1690
  cpu: typing.Optional[float] = None,
1691
1691
  memory: typing.Optional[int] = None,
1692
- timeout: typing.Optional[int] = 3600,
1692
+ timeout: int = 3600,
1693
1693
  cloud: typing.Optional[str] = None,
1694
1694
  region: typing.Union[str, collections.abc.Sequence[str], None] = None,
1695
1695
  force_build: bool = False,
modal/mount.py CHANGED
@@ -13,6 +13,7 @@ from pathlib import Path, PurePosixPath
13
13
  from typing import Callable, Optional, Sequence, Union
14
14
 
15
15
  from google.protobuf.message import Message
16
+ from grpclib import GRPCError
16
17
 
17
18
  import modal.exception
18
19
  import modal.file_pattern_matcher
@@ -929,19 +930,23 @@ async def _create_single_client_dependency_mount(
929
930
  remote_path=REMOTE_SITECUSTOMIZE_PATH,
930
931
  )
931
932
 
932
- await python_mount._deploy.aio(
933
- mount_name,
934
- api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
935
- environment_name=profile_environment,
936
- allow_overwrite=allow_overwrite,
937
- client=client,
938
- )
939
- print(f"✅ Deployed mount {mount_name} to global namespace.")
933
+ try:
934
+ await python_mount._deploy.aio(
935
+ mount_name,
936
+ api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
937
+ environment_name=profile_environment,
938
+ allow_overwrite=allow_overwrite,
939
+ client=client,
940
+ )
941
+ print(f"✅ Deployed mount {mount_name} to global namespace.")
942
+ except GRPCError as e:
943
+ print(f"⚠️ Mount creation failed with {e.status}: {e.message}")
940
944
 
941
945
 
942
946
  async def _create_client_dependency_mounts(
943
947
  client=None,
944
948
  python_versions: list[str] = list(PYTHON_STANDALONE_VERSIONS),
949
+ builder_versions: list[str] = ["2025.06"], # Reenable "PREVIEW" during testing
945
950
  check_if_exists=True,
946
951
  ):
947
952
  arch = "x86_64"
@@ -950,8 +955,8 @@ async def _create_client_dependency_mounts(
950
955
  ("musllinux_1_2", f"{arch}-unknown-linux-musl"), # musl >= 1.2
951
956
  ]
952
957
  coros = []
953
- for builder_version in ["2025.06", "PREVIEW"]:
954
- for python_version in python_versions:
958
+ for python_version in python_versions:
959
+ for builder_version in builder_versions:
955
960
  for platform, uv_python_platform in platform_tags:
956
961
  coros.append(
957
962
  _create_single_client_dependency_mount(
@@ -961,9 +966,11 @@ async def _create_client_dependency_mounts(
961
966
  arch,
962
967
  platform,
963
968
  uv_python_platform,
964
- # Re-enable mount overwriting for PREVIEW version while under development
965
- # check_if_exists=builder_version != "PREVIEW",
966
- # allow_overwrite=builder_version == "PREVIEW",
969
+ # This check_if_exists / allow_overwrite parameterization is very awkward
970
+ # Also it doesn't provide a hook for overwriting a non-preview version, which
971
+ # in theory we may need to do at some point (hopefully not, but...)
972
+ check_if_exists=check_if_exists and builder_version != "PREVIEW",
973
+ allow_overwrite=builder_version == "PREVIEW",
967
974
  )
968
975
  )
969
976
  await TaskContext.gather(*coros)
modal/mount.pyi CHANGED
@@ -570,15 +570,28 @@ async def _create_single_client_dependency_mount(
570
570
  allow_overwrite: bool = False,
571
571
  ): ...
572
572
  async def _create_client_dependency_mounts(
573
- client=None, python_versions: list[str] = ["3.9", "3.10", "3.11", "3.12", "3.13"], check_if_exists=True
573
+ client=None,
574
+ python_versions: list[str] = ["3.9", "3.10", "3.11", "3.12", "3.13"],
575
+ builder_versions: list[str] = ["2025.06"],
576
+ check_if_exists=True,
574
577
  ): ...
575
578
 
576
579
  class __create_client_dependency_mounts_spec(typing_extensions.Protocol):
577
580
  def __call__(
578
- self, /, client=None, python_versions: list[str] = ["3.9", "3.10", "3.11", "3.12", "3.13"], check_if_exists=True
581
+ self,
582
+ /,
583
+ client=None,
584
+ python_versions: list[str] = ["3.9", "3.10", "3.11", "3.12", "3.13"],
585
+ builder_versions: list[str] = ["2025.06"],
586
+ check_if_exists=True,
579
587
  ): ...
580
588
  async def aio(
581
- self, /, client=None, python_versions: list[str] = ["3.9", "3.10", "3.11", "3.12", "3.13"], check_if_exists=True
589
+ self,
590
+ /,
591
+ client=None,
592
+ python_versions: list[str] = ["3.9", "3.10", "3.11", "3.12", "3.13"],
593
+ builder_versions: list[str] = ["2025.06"],
594
+ check_if_exists=True,
582
595
  ): ...
583
596
 
584
597
  create_client_dependency_mounts: __create_client_dependency_mounts_spec