modal 0.66.14__py3-none-any.whl → 0.66.39__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 (51) hide show
  1. modal/__init__.py +1 -1
  2. modal/_container_entrypoint.py +27 -358
  3. modal/_runtime/__init__.py +1 -0
  4. modal/{_asgi.py → _runtime/asgi.py} +8 -7
  5. modal/{_container_io_manager.py → _runtime/container_io_manager.py} +18 -27
  6. modal/{execution_context.py → _runtime/execution_context.py} +2 -1
  7. modal/{_telemetry.py → _runtime/telemetry.py} +1 -1
  8. modal/_runtime/user_code_imports.py +361 -0
  9. modal/_serialization.py +1 -1
  10. modal/_utils/function_utils.py +28 -8
  11. modal/app.py +13 -46
  12. modal/cli/import_refs.py +4 -38
  13. modal/client.pyi +2 -2
  14. modal/dict.py +0 -6
  15. modal/dict.pyi +0 -4
  16. modal/experimental.py +1 -4
  17. modal/functions.py +11 -10
  18. modal/functions.pyi +8 -8
  19. modal/gpu.py +8 -6
  20. modal/image.py +93 -6
  21. modal/image.pyi +20 -2
  22. modal/io_streams.py +32 -12
  23. modal/io_streams.pyi +8 -4
  24. modal/mount.py +3 -2
  25. modal/network_file_system.py +0 -28
  26. modal/network_file_system.pyi +0 -14
  27. modal/parallel_map.py +1 -1
  28. modal/partial_function.py +11 -1
  29. modal/queue.py +0 -6
  30. modal/queue.pyi +0 -4
  31. modal/runner.py +1 -1
  32. modal/sandbox.py +1 -1
  33. modal/secret.py +1 -1
  34. modal/volume.py +0 -22
  35. modal/volume.pyi +0 -9
  36. {modal-0.66.14.dist-info → modal-0.66.39.dist-info}/METADATA +1 -2
  37. {modal-0.66.14.dist-info → modal-0.66.39.dist-info}/RECORD +49 -49
  38. modal_proto/api.proto +2 -21
  39. modal_proto/api_grpc.py +0 -16
  40. modal_proto/api_pb2.py +702 -726
  41. modal_proto/api_pb2.pyi +6 -60
  42. modal_proto/api_pb2_grpc.py +0 -33
  43. modal_proto/api_pb2_grpc.pyi +0 -10
  44. modal_proto/modal_api_grpc.py +0 -1
  45. modal_version/_version_generated.py +1 -1
  46. modal/_container_io_manager.pyi +0 -414
  47. modal/execution_context.pyi +0 -22
  48. {modal-0.66.14.dist-info → modal-0.66.39.dist-info}/LICENSE +0 -0
  49. {modal-0.66.14.dist-info → modal-0.66.39.dist-info}/WHEEL +0 -0
  50. {modal-0.66.14.dist-info → modal-0.66.39.dist-info}/entry_points.txt +0 -0
  51. {modal-0.66.14.dist-info → modal-0.66.39.dist-info}/top_level.txt +0 -0
modal/experimental.py CHANGED
@@ -7,7 +7,7 @@ from typing import (
7
7
  import modal._clustered_functions
8
8
  from modal.functions import _Function
9
9
 
10
- from ._container_io_manager import _ContainerIOManager
10
+ from ._runtime.container_io_manager import _ContainerIOManager
11
11
  from .exception import (
12
12
  InvalidError,
13
13
  )
@@ -17,7 +17,6 @@ from .partial_function import _PartialFunction, _PartialFunctionFlags
17
17
  def stop_fetching_inputs():
18
18
  """Don't fetch any more inputs from the server, after the current one.
19
19
  The container will exit gracefully after the current input is processed."""
20
-
21
20
  _ContainerIOManager.stop_fetching_inputs()
22
21
 
23
22
 
@@ -25,7 +24,6 @@ def get_local_input_concurrency():
25
24
  """Get the container's local input concurrency.
26
25
  If recently reduced to particular value, it can return a larger number than
27
26
  set due to in-progress inputs."""
28
-
29
27
  return _ContainerIOManager.get_input_concurrency()
30
28
 
31
29
 
@@ -33,7 +31,6 @@ def set_local_input_concurrency(concurrency: int):
33
31
  """Set the container's local input concurrency. Dynamic concurrency will be disabled.
34
32
  When setting to a smaller value, this method will not interrupt in-progress inputs.
35
33
  """
36
-
37
34
  _ContainerIOManager.set_input_concurrency(concurrency)
38
35
 
39
36
 
modal/functions.py CHANGED
@@ -35,6 +35,7 @@ from ._location import parse_cloud_provider
35
35
  from ._pty import get_pty_info
36
36
  from ._resolver import Resolver
37
37
  from ._resources import convert_fn_config_to_resources_config
38
+ from ._runtime.execution_context import current_input_id, is_local
38
39
  from ._serialization import serialize, serialize_proto_params
39
40
  from ._utils.async_utils import (
40
41
  TaskContext,
@@ -68,7 +69,6 @@ from .exception import (
68
69
  OutputExpiredError,
69
70
  deprecation_warning,
70
71
  )
71
- from .execution_context import current_input_id, is_local
72
72
  from .gpu import GPU_T, parse_gpu_config
73
73
  from .image import _Image
74
74
  from .mount import _get_client_mount, _Mount, get_auto_mounts
@@ -299,13 +299,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
299
299
  """Functions are the basic units of serverless execution on Modal.
300
300
 
301
301
  Generally, you will not construct a `Function` directly. Instead, use the
302
- `@app.function()` decorator on the `App` object (formerly called "Stub")
303
- for your application.
302
+ `App.function()` decorator to register your Python functions with your App.
304
303
  """
305
304
 
306
305
  # TODO: more type annotations
307
306
  _info: Optional[FunctionInfo]
308
- _used_local_mounts: typing.FrozenSet[_Mount] # set at load time, only by loader
307
+ _serve_mounts: typing.FrozenSet[_Mount] # set at load time, only by loader
309
308
  _app: Optional["modal.app._App"] = None
310
309
  _obj: Optional["modal.cls._Obj"] = None # only set for InstanceServiceFunctions and bound instance methods
311
310
  _web_url: Optional[str]
@@ -580,6 +579,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
580
579
 
581
580
  if is_local():
582
581
  entrypoint_mounts = info.get_entrypoint_mount()
582
+
583
583
  all_mounts = [
584
584
  _get_client_mount(),
585
585
  *explicit_mounts,
@@ -612,6 +612,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
612
612
  if proxy:
613
613
  # HACK: remove this once we stop using ssh tunnels for this.
614
614
  if image:
615
+ # TODO(elias): this will cause an error if users use prior `.add_local_*` commands without copy=True
615
616
  image = image.apt_install("autossh")
616
617
 
617
618
  function_spec = _FunctionSpec(
@@ -828,7 +829,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
828
829
  )
829
830
  for path, volume in validated_volumes
830
831
  ]
831
- loaded_mount_ids = {m.object_id for m in all_mounts}
832
+ loaded_mount_ids = {m.object_id for m in all_mounts} | {m.object_id for m in image._mount_layers}
832
833
 
833
834
  # Get object dependencies
834
835
  object_dependencies = []
@@ -970,9 +971,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
970
971
  raise InvalidError(f"Function {info.function_name} is too large to deploy.")
971
972
  raise
972
973
  function_creation_status.set_response(response)
973
- local_mounts = set(m for m in all_mounts if m.is_local()) # needed for modal.serve file watching
974
- local_mounts |= image._used_local_mounts
975
- obj._used_local_mounts = frozenset(local_mounts)
974
+ serve_mounts = set(m for m in all_mounts if m.is_local()) # needed for modal.serve file watching
975
+ serve_mounts |= image._serve_mounts
976
+ obj._serve_mounts = frozenset(serve_mounts)
976
977
  self._hydrate(response.function_id, resolver.client, response.handle_metadata)
977
978
 
978
979
  rep = f"Function({tag})"
@@ -1028,7 +1029,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1028
1029
  identity = "class service function for a parameterized class"
1029
1030
  if not self._parent.is_hydrated:
1030
1031
  if self._parent.app._running_app is None:
1031
- reason = ", because the App it is defined on is not running."
1032
+ reason = ", because the App it is defined on is not running"
1032
1033
  else:
1033
1034
  reason = ""
1034
1035
  raise ExecutionError(
@@ -1223,7 +1224,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1223
1224
  self._function_name = None
1224
1225
  self._info = None
1225
1226
  self._use_function_id = ""
1226
- self._used_local_mounts = frozenset()
1227
+ self._serve_mounts = frozenset()
1227
1228
 
1228
1229
  def _hydrate_metadata(self, metadata: Optional[Message]):
1229
1230
  # Overridden concrete implementation of base class method
modal/functions.pyi CHANGED
@@ -110,7 +110,7 @@ OriginalReturnType = typing.TypeVar("OriginalReturnType", covariant=True)
110
110
 
111
111
  class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object._Object):
112
112
  _info: typing.Optional[modal._utils.function_utils.FunctionInfo]
113
- _used_local_mounts: typing.FrozenSet[modal.mount._Mount]
113
+ _serve_mounts: typing.FrozenSet[modal.mount._Mount]
114
114
  _app: typing.Optional[modal.app._App]
115
115
  _obj: typing.Optional[modal.cls._Obj]
116
116
  _web_url: typing.Optional[str]
@@ -286,7 +286,7 @@ P_INNER = typing_extensions.ParamSpec("P_INNER")
286
286
 
287
287
  class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.Object):
288
288
  _info: typing.Optional[modal._utils.function_utils.FunctionInfo]
289
- _used_local_mounts: typing.FrozenSet[modal.mount.Mount]
289
+ _serve_mounts: typing.FrozenSet[modal.mount.Mount]
290
290
  _app: typing.Optional[modal.app.App]
291
291
  _obj: typing.Optional[modal.cls.Obj]
292
292
  _web_url: typing.Optional[str]
@@ -450,11 +450,11 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
450
450
 
451
451
  _call_generator_nowait: ___call_generator_nowait_spec
452
452
 
453
- class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
453
+ class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
454
454
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
455
455
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
456
456
 
457
- remote: __remote_spec[P, ReturnType]
457
+ remote: __remote_spec[ReturnType, P]
458
458
 
459
459
  class __remote_gen_spec(typing_extensions.Protocol):
460
460
  def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
@@ -466,17 +466,17 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
466
466
  def _get_obj(self) -> typing.Optional[modal.cls.Obj]: ...
467
467
  def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
468
468
 
469
- class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
469
+ class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
470
470
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
471
471
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
472
472
 
473
- _experimental_spawn: ___experimental_spawn_spec[P, ReturnType]
473
+ _experimental_spawn: ___experimental_spawn_spec[ReturnType, P]
474
474
 
475
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
475
+ class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
476
476
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
477
477
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
478
478
 
479
- spawn: __spawn_spec[P, ReturnType]
479
+ spawn: __spawn_spec[ReturnType, P]
480
480
 
481
481
  def get_raw_f(self) -> typing.Callable[..., typing.Any]: ...
482
482
 
modal/gpu.py CHANGED
@@ -1,6 +1,6 @@
1
1
  # Copyright Modal Labs 2022
2
2
  from dataclasses import dataclass
3
- from typing import Optional, Union
3
+ from typing import Callable, Optional, Union
4
4
 
5
5
  from modal_proto import api_pb2
6
6
 
@@ -147,25 +147,27 @@ class Any(_GPUConfig):
147
147
  return f"GPU(Any, count={self.count})"
148
148
 
149
149
 
150
- STRING_TO_GPU_CONFIG = {
150
+ STRING_TO_GPU_CONFIG: dict[str, Callable] = {
151
151
  "t4": T4,
152
152
  "l4": L4,
153
153
  "a100": A100,
154
+ "a100-80gb": lambda: A100(size="80GB"),
154
155
  "h100": H100,
155
156
  "a10g": A10G,
156
157
  "any": Any,
157
158
  }
158
- display_string_to_config = "\n".join(
159
- f'- "{key}" → `{cls()}`' for key, cls in STRING_TO_GPU_CONFIG.items() if key != "inf2"
160
- )
159
+ display_string_to_config = "\n".join(f'- "{key}" → `{c()}`' for key, c in STRING_TO_GPU_CONFIG.items() if key != "inf2")
161
160
  __doc__ = f"""
162
161
  **GPU configuration shortcodes**
163
162
 
164
163
  The following are the valid `str` values for the `gpu` parameter of
165
- [`@app.function`](/docs/reference/modal.Stub#function).
164
+ [`@app.function`](/docs/reference/modal.App#function).
166
165
 
167
166
  {display_string_to_config}
168
167
 
168
+ The shortcodes also support specifying count by suffixing `:N` to acquire `N` GPUs.
169
+ For example, `a10g:4` will provision 4 A10G GPUs.
170
+
169
171
  Other configurations can be created using the constructors documented below.
170
172
  """
171
173
 
modal/image.py CHANGED
@@ -273,12 +273,24 @@ class _Image(_Object, type_prefix="im"):
273
273
 
274
274
  force_build: bool
275
275
  inside_exceptions: List[Exception]
276
- _used_local_mounts: typing.FrozenSet[_Mount] # used for mounts watching
276
+ _serve_mounts: typing.FrozenSet[_Mount] # used for mounts watching in `modal serve`
277
+ _deferred_mounts: Sequence[
278
+ _Mount
279
+ ] # added as mounts on any container referencing the Image, see `def _mount_layers`
277
280
  _metadata: Optional[api_pb2.ImageMetadata] = None # set on hydration, private for now
278
281
 
279
282
  def _initialize_from_empty(self):
280
283
  self.inside_exceptions = []
281
- self._used_local_mounts = frozenset()
284
+ self._serve_mounts = frozenset()
285
+ self._deferred_mounts = ()
286
+ self.force_build = False
287
+
288
+ def _initialize_from_other(self, other: "_Image"):
289
+ # used by .clone()
290
+ self.inside_exceptions = other.inside_exceptions
291
+ self.force_build = other.force_build
292
+ self._serve_mounts = other._serve_mounts
293
+ self._deferred_mounts = other._deferred_mounts
282
294
 
283
295
  def _hydrate_metadata(self, message: Optional[Message]):
284
296
  env_image_id = config.get("image_id") # set as an env var in containers
@@ -292,6 +304,51 @@ class _Image(_Object, type_prefix="im"):
292
304
  assert isinstance(message, api_pb2.ImageMetadata)
293
305
  self._metadata = message
294
306
 
307
+ def _add_mount_layer_or_copy(self, mount: _Mount, copy: bool = False):
308
+ if copy:
309
+ return self.copy_mount(mount, remote_path="/")
310
+
311
+ base_image = self
312
+
313
+ async def _load(self2: "_Image", resolver: Resolver, existing_object_id: Optional[str]):
314
+ self2._hydrate_from_other(base_image) # same image id as base image as long as it's lazy
315
+ self2._deferred_mounts = tuple(base_image._deferred_mounts) + (mount,)
316
+ self2._serve_mounts = base_image._serve_mounts | ({mount} if mount.is_local() else set())
317
+
318
+ return _Image._from_loader(_load, "Image(local files)", deps=lambda: [base_image, mount])
319
+
320
+ @property
321
+ def _mount_layers(self) -> typing.Tuple[_Mount]:
322
+ """Non-evaluated mount layers on the image
323
+
324
+ When the image is used by a Modal container, these mounts need to be attached as well to
325
+ represent the full image content, as they haven't yet been represented as a layer in the
326
+ image.
327
+
328
+ When the image is used as a base image for a new layer (that is not itself a mount layer)
329
+ these mounts need to first be inserted as a copy operation (.copy_mount) into the image.
330
+ """
331
+ return self._deferred_mounts
332
+
333
+ def _assert_no_mount_layers(self):
334
+ if self._mount_layers:
335
+ raise InvalidError(
336
+ "An image tried to run a build step after using `image.add_local_*` to include local files.\n"
337
+ "\n"
338
+ "Run `image.add_local_*` commands last in your image build to avoid rebuilding images with every local "
339
+ "file change. Modal will then add these files to containers on startup instead, saving build time.\n"
340
+ "If you need to run other build steps after adding local files, set `copy=True` to copy the files"
341
+ "directly into the image, at the expense of some added build time.\n"
342
+ "\n"
343
+ "Example:\n"
344
+ "\n"
345
+ "my_image = (\n"
346
+ " Image.debian_slim()\n"
347
+ ' .add_local_python_packages("mypak", copy=True)\n'
348
+ ' .run_commands("python -m mypak") # this now works!\n'
349
+ ")\n"
350
+ )
351
+
295
352
  @staticmethod
296
353
  def _from_args(
297
354
  *,
@@ -306,9 +363,11 @@ class _Image(_Object, type_prefix="im"):
306
363
  force_build: bool = False,
307
364
  # For internal use only.
308
365
  _namespace: int = api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
366
+ _do_assert_no_mount_layers: bool = True,
309
367
  ):
310
368
  if base_images is None:
311
369
  base_images = {}
370
+
312
371
  if secrets is None:
313
372
  secrets = []
314
373
  if gpu_config is None:
@@ -334,6 +393,11 @@ class _Image(_Object, type_prefix="im"):
334
393
  return deps
335
394
 
336
395
  async def _load(self: _Image, resolver: Resolver, existing_object_id: Optional[str]):
396
+ if _do_assert_no_mount_layers:
397
+ for image in base_images.values():
398
+ # base images can't have
399
+ image._assert_no_mount_layers()
400
+
337
401
  environment = await _get_environment_cached(resolver.environment_name or "", resolver.client)
338
402
  # A bit hacky,but assume that the environment provides a valid builder version
339
403
  image_builder_version = cast(ImageBuilderVersion, environment._settings.image_builder_version)
@@ -349,7 +413,9 @@ class _Image(_Object, type_prefix="im"):
349
413
  "No commands were provided for the image — have you tried using modal.Image.debian_slim()?"
350
414
  )
351
415
  if dockerfile.commands and build_function:
352
- raise InvalidError("Cannot provide both a build function and Dockerfile commands!")
416
+ raise InvalidError(
417
+ "Cannot provide both build function and Dockerfile commands in the same image layer!"
418
+ )
353
419
 
354
420
  base_images_pb2s = [
355
421
  api_pb2.BaseImage(
@@ -482,12 +548,12 @@ class _Image(_Object, type_prefix="im"):
482
548
  self._hydrate(image_id, resolver.client, result_response.metadata)
483
549
  local_mounts = set()
484
550
  for base in base_images.values():
485
- local_mounts |= base._used_local_mounts
551
+ local_mounts |= base._serve_mounts
486
552
  if context_mount and context_mount.is_local():
487
553
  local_mounts.add(context_mount)
488
- self._used_local_mounts = frozenset(local_mounts)
554
+ self._serve_mounts = frozenset(local_mounts)
489
555
 
490
- rep = "Image()"
556
+ rep = f"Image({dockerfile_function})"
491
557
  obj = _Image._from_loader(_load, rep, deps=_deps)
492
558
  obj.force_build = force_build
493
559
  return obj
@@ -553,6 +619,27 @@ class _Image(_Object, type_prefix="im"):
553
619
  context_mount=mount,
554
620
  )
555
621
 
622
+ def _add_local_python_packages(self, *packages: Union[str, Path], copy: bool = False) -> "_Image":
623
+ """Adds Python package files to containers
624
+
625
+ Adds all files from the specified Python packages to containers running the Image.
626
+
627
+ Packages are added to the `/root` directory of containers, which is on the `PYTHONPATH`
628
+ of any executed Modal Functions.
629
+
630
+ By default (`copy=False`), the files are added to containers on startup and are not built into the actual Image,
631
+ which speeds up deployment.
632
+
633
+ Set `copy=True` to copy the files into an Image layer at build time instead. This can slow down iteration since
634
+ it requires a rebuild of the Image and any subsequent build steps whenever the included files change, but it is
635
+ required if you want to run additional build steps after this one.
636
+
637
+ **Note:** This excludes all dot-prefixed subdirectories or files and all `.pyc`/`__pycache__` files.
638
+ To add full directories with finer control, use `.add_local_dir()` instead.
639
+ """
640
+ mount = _Mount.from_local_python_packages(*packages)
641
+ return self._add_mount_layer_or_copy(mount, copy=copy)
642
+
556
643
  def copy_local_dir(self, local_path: Union[str, Path], remote_path: Union[str, Path] = ".") -> "_Image":
557
644
  """Copy a directory into the image as a part of building the image.
558
645
 
modal/image.pyi CHANGED
@@ -58,11 +58,17 @@ class DockerfileSpec:
58
58
  class _Image(modal.object._Object):
59
59
  force_build: bool
60
60
  inside_exceptions: typing.List[Exception]
61
- _used_local_mounts: typing.FrozenSet[modal.mount._Mount]
61
+ _serve_mounts: typing.FrozenSet[modal.mount._Mount]
62
+ _deferred_mounts: typing.Sequence[modal.mount._Mount]
62
63
  _metadata: typing.Optional[modal_proto.api_pb2.ImageMetadata]
63
64
 
64
65
  def _initialize_from_empty(self): ...
66
+ def _initialize_from_other(self, other: _Image): ...
65
67
  def _hydrate_metadata(self, message: typing.Optional[google.protobuf.message.Message]): ...
68
+ def _add_mount_layer_or_copy(self, mount: modal.mount._Mount, copy: bool = False): ...
69
+ @property
70
+ def _mount_layers(self) -> typing.Tuple[modal.mount._Mount]: ...
71
+ def _assert_no_mount_layers(self): ...
66
72
  @staticmethod
67
73
  def _from_args(
68
74
  *,
@@ -78,6 +84,7 @@ class _Image(modal.object._Object):
78
84
  context_mount: typing.Optional[modal.mount._Mount] = None,
79
85
  force_build: bool = False,
80
86
  _namespace: int = 1,
87
+ _do_assert_no_mount_layers: bool = True,
81
88
  ): ...
82
89
  def extend(
83
90
  self,
@@ -90,11 +97,13 @@ class _Image(modal.object._Object):
90
97
  context_mount: typing.Optional[modal.mount._Mount] = None,
91
98
  force_build: bool = False,
92
99
  _namespace: int = 1,
100
+ _do_assert_no_mount_layers: bool = True,
93
101
  ) -> _Image: ...
94
102
  def copy_mount(self, mount: modal.mount._Mount, remote_path: typing.Union[str, pathlib.Path] = ".") -> _Image: ...
95
103
  def copy_local_file(
96
104
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
97
105
  ) -> _Image: ...
106
+ def _add_local_python_packages(self, *module_names, copy: bool = False) -> _Image: ...
98
107
  def copy_local_dir(
99
108
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "."
100
109
  ) -> _Image: ...
@@ -299,12 +308,18 @@ class _Image(modal.object._Object):
299
308
  class Image(modal.object.Object):
300
309
  force_build: bool
301
310
  inside_exceptions: typing.List[Exception]
302
- _used_local_mounts: typing.FrozenSet[modal.mount.Mount]
311
+ _serve_mounts: typing.FrozenSet[modal.mount.Mount]
312
+ _deferred_mounts: typing.Sequence[modal.mount.Mount]
303
313
  _metadata: typing.Optional[modal_proto.api_pb2.ImageMetadata]
304
314
 
305
315
  def __init__(self, *args, **kwargs): ...
306
316
  def _initialize_from_empty(self): ...
317
+ def _initialize_from_other(self, other: Image): ...
307
318
  def _hydrate_metadata(self, message: typing.Optional[google.protobuf.message.Message]): ...
319
+ def _add_mount_layer_or_copy(self, mount: modal.mount.Mount, copy: bool = False): ...
320
+ @property
321
+ def _mount_layers(self) -> typing.Tuple[modal.mount.Mount]: ...
322
+ def _assert_no_mount_layers(self): ...
308
323
  @staticmethod
309
324
  def _from_args(
310
325
  *,
@@ -320,6 +335,7 @@ class Image(modal.object.Object):
320
335
  context_mount: typing.Optional[modal.mount.Mount] = None,
321
336
  force_build: bool = False,
322
337
  _namespace: int = 1,
338
+ _do_assert_no_mount_layers: bool = True,
323
339
  ): ...
324
340
  def extend(
325
341
  self,
@@ -332,11 +348,13 @@ class Image(modal.object.Object):
332
348
  context_mount: typing.Optional[modal.mount.Mount] = None,
333
349
  force_build: bool = False,
334
350
  _namespace: int = 1,
351
+ _do_assert_no_mount_layers: bool = True,
335
352
  ) -> Image: ...
336
353
  def copy_mount(self, mount: modal.mount.Mount, remote_path: typing.Union[str, pathlib.Path] = ".") -> Image: ...
337
354
  def copy_local_file(
338
355
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
339
356
  ) -> Image: ...
357
+ def _add_local_python_packages(self, *module_names, copy: bool = False) -> Image: ...
340
358
  def copy_local_dir(
341
359
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "."
342
360
  ) -> Image: ...
modal/io_streams.py CHANGED
@@ -1,6 +1,18 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import asyncio
3
- from typing import TYPE_CHECKING, AsyncGenerator, Generic, List, Literal, Optional, Tuple, TypeVar, Union
3
+ from typing import (
4
+ TYPE_CHECKING,
5
+ AsyncGenerator,
6
+ AsyncIterator,
7
+ Generic,
8
+ List,
9
+ Literal,
10
+ Optional,
11
+ Tuple,
12
+ TypeVar,
13
+ Union,
14
+ cast,
15
+ )
4
16
 
5
17
  from grpclib import Status
6
18
  from grpclib.exceptions import GRPCError, StreamTerminatedError
@@ -18,7 +30,7 @@ if TYPE_CHECKING:
18
30
 
19
31
 
20
32
  async def _sandbox_logs_iterator(
21
- sandbox_id: str, file_descriptor: int, last_entry_id: Optional[str], client: _Client
33
+ sandbox_id: str, file_descriptor: "api_pb2.FileDescriptor.ValueType", last_entry_id: str, client: _Client
22
34
  ) -> AsyncGenerator[Tuple[Optional[bytes], str], None]:
23
35
  req = api_pb2.SandboxGetLogsRequest(
24
36
  sandbox_id=sandbox_id,
@@ -37,7 +49,7 @@ async def _sandbox_logs_iterator(
37
49
 
38
50
 
39
51
  async def _container_process_logs_iterator(
40
- process_id: str, file_descriptor: int, client: _Client
52
+ process_id: str, file_descriptor: "api_pb2.FileDescriptor.ValueType", client: _Client
41
53
  ) -> AsyncGenerator[Optional[bytes], None]:
42
54
  req = api_pb2.ContainerExecGetOutputRequest(
43
55
  exec_id=process_id, timeout=55, file_descriptor=file_descriptor, get_raw_bytes=True
@@ -74,9 +86,11 @@ class _StreamReader(Generic[T]):
74
86
  ```
75
87
  """
76
88
 
89
+ _stream: Optional[AsyncGenerator[Optional[bytes], None]]
90
+
77
91
  def __init__(
78
92
  self,
79
- file_descriptor: int,
93
+ file_descriptor: "api_pb2.FileDescriptor.ValueType",
80
94
  object_id: str,
81
95
  object_type: Literal["sandbox", "container_process"],
82
96
  client: _Client,
@@ -90,7 +104,7 @@ class _StreamReader(Generic[T]):
90
104
  self._object_id = object_id
91
105
  self._client = client
92
106
  self._stream = None
93
- self._last_entry_id: Optional[str] = None
107
+ self._last_entry_id: str = ""
94
108
  self._line_buffer = b""
95
109
 
96
110
  # Sandbox logs are streamed to the client as strings, so StreamReaders reading
@@ -145,15 +159,20 @@ class _StreamReader(Generic[T]):
145
159
  ```
146
160
 
147
161
  """
148
- data = "" if self._text else b""
162
+ data_str = ""
163
+ data_bytes = b""
149
164
  async for message in self._get_logs():
150
165
  if message is None:
151
166
  break
152
167
  if self._text:
153
- data += message.decode("utf-8")
168
+ data_str += message.decode("utf-8")
154
169
  else:
155
- data += message
156
- return data
170
+ data_bytes += message
171
+
172
+ if self._text:
173
+ return cast(T, data_str)
174
+ else:
175
+ return cast(T, data_bytes)
157
176
 
158
177
  async def _consume_container_process_stream(self):
159
178
  """
@@ -275,7 +294,7 @@ class _StreamReader(Generic[T]):
275
294
  line, self._line_buffer = self._line_buffer.split(b"\n", 1)
276
295
  yield line + b"\n"
277
296
 
278
- def __aiter__(self) -> AsyncGenerator[T, None]:
297
+ def __aiter__(self) -> AsyncIterator[T]:
279
298
  """mdmd:hidden"""
280
299
  if not self._stream:
281
300
  if self._by_line:
@@ -287,6 +306,7 @@ class _StreamReader(Generic[T]):
287
306
  async def __anext__(self) -> T:
288
307
  """mdmd:hidden"""
289
308
  assert self._stream is not None
309
+
290
310
  value = await self._stream.__anext__()
291
311
 
292
312
  # The stream yields None if it receives an EOF batch.
@@ -294,9 +314,9 @@ class _StreamReader(Generic[T]):
294
314
  raise StopAsyncIteration
295
315
 
296
316
  if self._text:
297
- return value.decode("utf-8")
317
+ return cast(T, value.decode("utf-8"))
298
318
  else:
299
- return value
319
+ return cast(T, value)
300
320
 
301
321
 
302
322
  MAX_BUFFER_SIZE = 2 * 1024 * 1024
modal/io_streams.pyi CHANGED
@@ -4,7 +4,7 @@ import typing
4
4
  import typing_extensions
5
5
 
6
6
  def _sandbox_logs_iterator(
7
- sandbox_id: str, file_descriptor: int, last_entry_id: typing.Optional[str], client: modal.client._Client
7
+ sandbox_id: str, file_descriptor: int, last_entry_id: str, client: modal.client._Client
8
8
  ) -> typing.AsyncGenerator[typing.Tuple[typing.Optional[bytes], str], None]: ...
9
9
  def _container_process_logs_iterator(
10
10
  process_id: str, file_descriptor: int, client: modal.client._Client
@@ -13,6 +13,8 @@ def _container_process_logs_iterator(
13
13
  T = typing.TypeVar("T")
14
14
 
15
15
  class _StreamReader(typing.Generic[T]):
16
+ _stream: typing.Optional[typing.AsyncGenerator[typing.Optional[bytes], None]]
17
+
16
18
  def __init__(
17
19
  self,
18
20
  file_descriptor: int,
@@ -30,7 +32,7 @@ class _StreamReader(typing.Generic[T]):
30
32
  def _stream_container_process(self) -> typing.AsyncGenerator[typing.Tuple[typing.Optional[bytes], str], None]: ...
31
33
  def _get_logs(self) -> typing.AsyncGenerator[typing.Optional[bytes], None]: ...
32
34
  def _get_logs_by_line(self) -> typing.AsyncGenerator[typing.Optional[bytes], None]: ...
33
- def __aiter__(self) -> typing.AsyncGenerator[T, None]: ...
35
+ def __aiter__(self) -> typing.AsyncIterator[T]: ...
34
36
  async def __anext__(self) -> T: ...
35
37
 
36
38
  class _StreamWriter:
@@ -45,6 +47,8 @@ class _StreamWriter:
45
47
  T_INNER = typing.TypeVar("T_INNER", covariant=True)
46
48
 
47
49
  class StreamReader(typing.Generic[T]):
50
+ _stream: typing.Optional[typing.AsyncGenerator[typing.Optional[bytes], None]]
51
+
48
52
  def __init__(
49
53
  self,
50
54
  file_descriptor: int,
@@ -88,8 +92,8 @@ class StreamReader(typing.Generic[T]):
88
92
 
89
93
  _get_logs_by_line: ___get_logs_by_line_spec
90
94
 
91
- def __iter__(self) -> typing.Generator[T, None, None]: ...
92
- def __aiter__(self) -> typing.AsyncGenerator[T, None]: ...
95
+ def __iter__(self) -> typing.Iterator[T]: ...
96
+ def __aiter__(self) -> typing.AsyncIterator[T]: ...
93
97
  def __next__(self) -> T: ...
94
98
  async def __anext__(self) -> T: ...
95
99
 
modal/mount.py CHANGED
@@ -571,7 +571,7 @@ class _Mount(_Object, type_prefix="mo"):
571
571
  # Don't re-run inside container.
572
572
 
573
573
  mount = _Mount._new()
574
- from .execution_context import is_local
574
+ from ._runtime.execution_context import is_local
575
575
 
576
576
  if not is_local():
577
577
  return mount # empty/non-mountable mount in case it's used from within a container
@@ -622,12 +622,13 @@ class _Mount(_Object, type_prefix="mo"):
622
622
  client: Optional[_Client] = None,
623
623
  ) -> None:
624
624
  check_object_name(deployment_name, "Mount")
625
+ environment_name = _get_environment_name(environment_name, resolver=None)
625
626
  self._deployment_name = deployment_name
626
627
  self._namespace = namespace
627
628
  self._environment_name = environment_name
628
629
  if client is None:
629
630
  client = await _Client.from_env()
630
- resolver = Resolver(client=client)
631
+ resolver = Resolver(client=client, environment_name=environment_name)
631
632
  await resolver.load(self)
632
633
 
633
634
  def _get_metadata(self) -> api_pb2.MountHandleMetadata:
@@ -171,34 +171,6 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
171
171
  tc.infinite_loop(lambda: client.stub.SharedVolumeHeartbeat(request), sleep=_heartbeat_sleep)
172
172
  yield cls._new_hydrated(response.shared_volume_id, client, None, is_another_app=True)
173
173
 
174
- @staticmethod
175
- def persisted(
176
- label: str,
177
- namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
178
- environment_name: Optional[str] = None,
179
- cloud: Optional[str] = None,
180
- ):
181
- """mdmd:hidden"""
182
- message = (
183
- "`NetworkFileSystem.persisted` is deprecated."
184
- " Please use `NetworkFileSystem.from_name(name, create_if_missing=True)` instead."
185
- )
186
- deprecation_error((2024, 3, 1), message)
187
-
188
- def persist(
189
- self,
190
- label: str,
191
- namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
192
- environment_name: Optional[str] = None,
193
- cloud: Optional[str] = None,
194
- ):
195
- """mdmd:hidden"""
196
- message = (
197
- "`NetworkFileSystem().persist('my-volume')` is deprecated."
198
- " Please use `NetworkFileSystem.from_name('my-volume', create_if_missing=True)` instead."
199
- )
200
- deprecation_error((2024, 2, 29), message)
201
-
202
174
  @staticmethod
203
175
  async def lookup(
204
176
  label: str,