modal 0.66.17__py3-none-any.whl → 0.66.44__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 (43) hide show
  1. modal/_container_entrypoint.py +5 -342
  2. modal/_runtime/container_io_manager.py +6 -14
  3. modal/_runtime/user_code_imports.py +361 -0
  4. modal/_utils/function_utils.py +28 -8
  5. modal/_utils/grpc_testing.py +33 -26
  6. modal/app.py +13 -46
  7. modal/cli/import_refs.py +4 -38
  8. modal/client.pyi +2 -2
  9. modal/cls.py +26 -19
  10. modal/cls.pyi +4 -4
  11. modal/dict.py +0 -6
  12. modal/dict.pyi +0 -4
  13. modal/experimental.py +0 -3
  14. modal/functions.py +42 -38
  15. modal/functions.pyi +9 -13
  16. modal/gpu.py +8 -6
  17. modal/image.py +141 -7
  18. modal/image.pyi +34 -4
  19. modal/io_streams.py +40 -33
  20. modal/io_streams.pyi +13 -13
  21. modal/mount.py +5 -2
  22. modal/network_file_system.py +0 -28
  23. modal/network_file_system.pyi +0 -14
  24. modal/partial_function.py +12 -2
  25. modal/queue.py +0 -6
  26. modal/queue.pyi +0 -4
  27. modal/sandbox.py +1 -1
  28. modal/volume.py +0 -22
  29. modal/volume.pyi +0 -9
  30. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/METADATA +1 -2
  31. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/RECORD +43 -42
  32. modal_proto/api.proto +3 -20
  33. modal_proto/api_grpc.py +0 -16
  34. modal_proto/api_pb2.py +389 -413
  35. modal_proto/api_pb2.pyi +12 -58
  36. modal_proto/api_pb2_grpc.py +0 -33
  37. modal_proto/api_pb2_grpc.pyi +0 -10
  38. modal_proto/modal_api_grpc.py +0 -1
  39. modal_version/_version_generated.py +1 -1
  40. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/LICENSE +0 -0
  41. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/WHEEL +0 -0
  42. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/entry_points.txt +0 -0
  43. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/top_level.txt +0 -0
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_file("data.json", 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
@@ -535,12 +601,59 @@ class _Image(_Object, type_prefix="im"):
535
601
  context_mount=mount,
536
602
  )
537
603
 
604
+ def add_local_file(self, local_path: Union[str, Path], remote_path: str, *, copy: bool = False) -> "_Image":
605
+ """Adds a local file to the image at `remote_path` within the container
606
+
607
+ By default (`copy=False`), the files are added to containers on startup and are not built into the actual Image,
608
+ which speeds up deployment.
609
+
610
+ Set `copy=True` to copy the files into an Image layer at build time instead, similar to how
611
+ [`COPY`](https://docs.docker.com/engine/reference/builder/#copy) works in a `Dockerfile`.
612
+
613
+ copy=True can slow down iteration since it requires a rebuild of the Image and any subsequent
614
+ build steps whenever the included files change, but it is required if you want to run additional
615
+ build steps after this one.
616
+ """
617
+ if not PurePosixPath(remote_path).is_absolute():
618
+ # TODO(elias): implement relative to absolute resolution using image workdir metadata
619
+ # + make default remote_path="./"
620
+ # This requires deferring the Mount creation until after "self" (the base image) has been resolved
621
+ # so we know the workdir of the operation.
622
+ raise InvalidError("image.add_local_file() currently only supports absolute remote_path values")
623
+
624
+ if remote_path.endswith("/"):
625
+ remote_path = remote_path + Path(local_path).name
626
+
627
+ mount = _Mount.from_local_file(local_path, remote_path)
628
+ return self._add_mount_layer_or_copy(mount, copy=copy)
629
+
630
+ def add_local_dir(self, local_path: Union[str, Path], remote_path: str, *, copy: bool = False) -> "_Image":
631
+ """Adds a local directory's content to the image at `remote_path` within the container
632
+
633
+ By default (`copy=False`), the files are added to containers on startup and are not built into the actual Image,
634
+ which speeds up deployment.
635
+
636
+ Set `copy=True` to copy the files into an Image layer at build time instead, similar to how
637
+ [`COPY`](https://docs.docker.com/engine/reference/builder/#copy) works in a `Dockerfile`.
638
+
639
+ copy=True can slow down iteration since it requires a rebuild of the Image and any subsequent
640
+ build steps whenever the included files change, but it is required if you want to run additional
641
+ build steps after this one.
642
+ """
643
+ if not PurePosixPath(remote_path).is_absolute():
644
+ # TODO(elias): implement relative to absolute resolution using image workdir metadata
645
+ # + make default remote_path="./"
646
+ raise InvalidError("image.add_local_dir() currently only supports absolute remote_path values")
647
+ mount = _Mount.from_local_dir(local_path, remote_path=remote_path)
648
+ return self._add_mount_layer_or_copy(mount, copy=copy)
649
+
538
650
  def copy_local_file(self, local_path: Union[str, Path], remote_path: Union[str, Path] = "./") -> "_Image":
539
651
  """Copy a file into the image as a part of building it.
540
652
 
541
653
  This works in a similar way to [`COPY`](https://docs.docker.com/engine/reference/builder/#copy)
542
654
  works in a `Dockerfile`.
543
655
  """
656
+ # TODO(elias): add pending deprecation with suggestion to use add_* instead
544
657
  basename = str(Path(local_path).name)
545
658
  mount = _Mount.from_local_file(local_path, remote_path=f"/{basename}")
546
659
 
@@ -553,6 +666,27 @@ class _Image(_Object, type_prefix="im"):
553
666
  context_mount=mount,
554
667
  )
555
668
 
669
+ def _add_local_python_packages(self, *packages: Union[str, Path], copy: bool = False) -> "_Image":
670
+ """Adds Python package files to containers
671
+
672
+ Adds all files from the specified Python packages to containers running the Image.
673
+
674
+ Packages are added to the `/root` directory of containers, which is on the `PYTHONPATH`
675
+ of any executed Modal Functions.
676
+
677
+ By default (`copy=False`), the files are added to containers on startup and are not built into the actual Image,
678
+ which speeds up deployment.
679
+
680
+ Set `copy=True` to copy the files into an Image layer at build time instead. This can slow down iteration since
681
+ it requires a rebuild of the Image and any subsequent build steps whenever the included files change, but it is
682
+ required if you want to run additional build steps after this one.
683
+
684
+ **Note:** This excludes all dot-prefixed subdirectories or files and all `.pyc`/`__pycache__` files.
685
+ To add full directories with finer control, use `.add_local_dir()` instead.
686
+ """
687
+ mount = _Mount.from_local_python_packages(*packages)
688
+ return self._add_mount_layer_or_copy(mount, copy=copy)
689
+
556
690
  def copy_local_dir(self, local_path: Union[str, Path], remote_path: Union[str, Path] = ".") -> "_Image":
557
691
  """Copy a directory into the image as a part of building the image.
558
692
 
@@ -1550,7 +1684,7 @@ class _Image(_Object, type_prefix="im"):
1550
1684
  dockerfile_function=build_dockerfile,
1551
1685
  )
1552
1686
 
1553
- def workdir(self, path: str) -> "_Image":
1687
+ def workdir(self, path: Union[str, PurePosixPath]) -> "_Image":
1554
1688
  """Set the working directory for subsequent image build steps and function execution.
1555
1689
 
1556
1690
  **Example**
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,19 @@ 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: ...
103
+ def add_local_file(
104
+ self, local_path: typing.Union[str, pathlib.Path], remote_path: str, *, copy: bool = False
105
+ ) -> _Image: ...
106
+ def add_local_dir(
107
+ self, local_path: typing.Union[str, pathlib.Path], remote_path: str, *, copy: bool = False
108
+ ) -> _Image: ...
95
109
  def copy_local_file(
96
110
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
97
111
  ) -> _Image: ...
112
+ def _add_local_python_packages(self, *module_names, copy: bool = False) -> _Image: ...
98
113
  def copy_local_dir(
99
114
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "."
100
115
  ) -> _Image: ...
@@ -292,19 +307,25 @@ class _Image(modal.object._Object):
292
307
  kwargs: typing.Dict[str, typing.Any] = {},
293
308
  ) -> _Image: ...
294
309
  def env(self, vars: typing.Dict[str, str]) -> _Image: ...
295
- def workdir(self, path: str) -> _Image: ...
310
+ def workdir(self, path: typing.Union[str, pathlib.PurePosixPath]) -> _Image: ...
296
311
  def imports(self): ...
297
312
  def _logs(self) -> typing.AsyncGenerator[str, None]: ...
298
313
 
299
314
  class Image(modal.object.Object):
300
315
  force_build: bool
301
316
  inside_exceptions: typing.List[Exception]
302
- _used_local_mounts: typing.FrozenSet[modal.mount.Mount]
317
+ _serve_mounts: typing.FrozenSet[modal.mount.Mount]
318
+ _deferred_mounts: typing.Sequence[modal.mount.Mount]
303
319
  _metadata: typing.Optional[modal_proto.api_pb2.ImageMetadata]
304
320
 
305
321
  def __init__(self, *args, **kwargs): ...
306
322
  def _initialize_from_empty(self): ...
323
+ def _initialize_from_other(self, other: Image): ...
307
324
  def _hydrate_metadata(self, message: typing.Optional[google.protobuf.message.Message]): ...
325
+ def _add_mount_layer_or_copy(self, mount: modal.mount.Mount, copy: bool = False): ...
326
+ @property
327
+ def _mount_layers(self) -> typing.Tuple[modal.mount.Mount]: ...
328
+ def _assert_no_mount_layers(self): ...
308
329
  @staticmethod
309
330
  def _from_args(
310
331
  *,
@@ -320,6 +341,7 @@ class Image(modal.object.Object):
320
341
  context_mount: typing.Optional[modal.mount.Mount] = None,
321
342
  force_build: bool = False,
322
343
  _namespace: int = 1,
344
+ _do_assert_no_mount_layers: bool = True,
323
345
  ): ...
324
346
  def extend(
325
347
  self,
@@ -332,11 +354,19 @@ class Image(modal.object.Object):
332
354
  context_mount: typing.Optional[modal.mount.Mount] = None,
333
355
  force_build: bool = False,
334
356
  _namespace: int = 1,
357
+ _do_assert_no_mount_layers: bool = True,
335
358
  ) -> Image: ...
336
359
  def copy_mount(self, mount: modal.mount.Mount, remote_path: typing.Union[str, pathlib.Path] = ".") -> Image: ...
360
+ def add_local_file(
361
+ self, local_path: typing.Union[str, pathlib.Path], remote_path: str, *, copy: bool = False
362
+ ) -> Image: ...
363
+ def add_local_dir(
364
+ self, local_path: typing.Union[str, pathlib.Path], remote_path: str, *, copy: bool = False
365
+ ) -> Image: ...
337
366
  def copy_local_file(
338
367
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
339
368
  ) -> Image: ...
369
+ def _add_local_python_packages(self, *module_names, copy: bool = False) -> Image: ...
340
370
  def copy_local_dir(
341
371
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "."
342
372
  ) -> Image: ...
@@ -534,7 +564,7 @@ class Image(modal.object.Object):
534
564
  kwargs: typing.Dict[str, typing.Any] = {},
535
565
  ) -> Image: ...
536
566
  def env(self, vars: typing.Dict[str, str]) -> Image: ...
537
- def workdir(self, path: str) -> Image: ...
567
+ def workdir(self, path: typing.Union[str, pathlib.PurePosixPath]) -> Image: ...
538
568
  def imports(self): ...
539
569
 
540
570
  class ___logs_spec(typing_extensions.Protocol):
modal/io_streams.py CHANGED
@@ -66,9 +66,10 @@ T = TypeVar("T", str, bytes)
66
66
 
67
67
 
68
68
  class _StreamReader(Generic[T]):
69
- """Provides an interface to buffer and fetch logs from a stream (`stdout` or `stderr`).
69
+ """Retrieve logs from a stream (`stdout` or `stderr`).
70
70
 
71
- As an asynchronous iterable, the object supports the async for statement.
71
+ As an asynchronous iterable, the object supports the `for` and `async for`
72
+ statements. Just loop over the object to read in chunks.
72
73
 
73
74
  **Usage**
74
75
 
@@ -140,12 +141,12 @@ class _StreamReader(Generic[T]):
140
141
  self._consume_container_process_task = asyncio.create_task(self._consume_container_process_stream())
141
142
 
142
143
  @property
143
- def file_descriptor(self):
144
+ def file_descriptor(self) -> int:
145
+ """Possible values are `1` for stdout and `2` for stderr."""
144
146
  return self._file_descriptor
145
147
 
146
148
  async def read(self) -> T:
147
- """Fetch and return contents of the entire stream. If EOF was received,
148
- return an empty string.
149
+ """Fetch the entire contents of the stream until EOF.
149
150
 
150
151
  **Usage**
151
152
 
@@ -157,7 +158,6 @@ class _StreamReader(Generic[T]):
157
158
 
158
159
  print(sandbox.stdout.read())
159
160
  ```
160
-
161
161
  """
162
162
  data_str = ""
163
163
  data_bytes = b""
@@ -175,9 +175,7 @@ class _StreamReader(Generic[T]):
175
175
  return cast(T, data_bytes)
176
176
 
177
177
  async def _consume_container_process_stream(self):
178
- """
179
- Consumes the container process stream and stores the messages in the buffer.
180
- """
178
+ """Consume the container process stream and store messages in the buffer."""
181
179
  if self._stream_type == StreamType.DEVNULL:
182
180
  return
183
181
 
@@ -211,9 +209,7 @@ class _StreamReader(Generic[T]):
211
209
  raise exc
212
210
 
213
211
  async def _stream_container_process(self) -> AsyncGenerator[Tuple[Optional[bytes], str], None]:
214
- """mdmd:hidden
215
- Streams the container process buffer to the reader.
216
- """
212
+ """Streams the container process buffer to the reader."""
217
213
  entry_id = 0
218
214
  if self._last_entry_id:
219
215
  entry_id = int(self._last_entry_id) + 1
@@ -232,8 +228,7 @@ class _StreamReader(Generic[T]):
232
228
  entry_id += 1
233
229
 
234
230
  async def _get_logs(self) -> AsyncGenerator[Optional[bytes], None]:
235
- """mdmd:hidden
236
- Streams sandbox or process logs from the server to the reader.
231
+ """Streams sandbox or process logs from the server to the reader.
237
232
 
238
233
  Logs returned by this method may contain partial or multiple lines at a time.
239
234
 
@@ -278,9 +273,7 @@ class _StreamReader(Generic[T]):
278
273
  raise
279
274
 
280
275
  async def _get_logs_by_line(self) -> AsyncGenerator[Optional[bytes], None]:
281
- """mdmd:hidden
282
- Processes logs from the server and yields complete lines only.
283
- """
276
+ """Process logs from the server and yield complete lines only."""
284
277
  async for message in self._get_logs():
285
278
  if message is None:
286
279
  if self._line_buffer:
@@ -325,7 +318,8 @@ MAX_BUFFER_SIZE = 2 * 1024 * 1024
325
318
  class _StreamWriter:
326
319
  """Provides an interface to buffer and write logs to a sandbox or container process stream (`stdin`)."""
327
320
 
328
- def __init__(self, object_id: str, object_type: Literal["sandbox", "container_process"], client: _Client):
321
+ def __init__(self, object_id: str, object_type: Literal["sandbox", "container_process"], client: _Client) -> None:
322
+ """mdmd:hidden"""
329
323
  self._index = 1
330
324
  self._object_id = object_id
331
325
  self._object_type = object_type
@@ -333,17 +327,16 @@ class _StreamWriter:
333
327
  self._is_closed = False
334
328
  self._buffer = bytearray()
335
329
 
336
- def get_next_index(self):
337
- """mdmd:hidden"""
330
+ def _get_next_index(self) -> int:
338
331
  index = self._index
339
332
  self._index += 1
340
333
  return index
341
334
 
342
- def write(self, data: Union[bytes, bytearray, memoryview, str]):
343
- """
344
- Writes data to stream's internal buffer, but does not drain/flush the write.
335
+ def write(self, data: Union[bytes, bytearray, memoryview, str]) -> None:
336
+ """Write data to the stream but does not send it immediately.
345
337
 
346
- This method needs to be used along with the `drain()` method which flushes the buffer.
338
+ This is non-blocking and queues the data to an internal buffer. Must be
339
+ used along with the `drain()` method, which flushes the buffer.
347
340
 
348
341
  **Usage**
349
342
 
@@ -375,22 +368,36 @@ class _StreamWriter:
375
368
  else:
376
369
  raise TypeError(f"data argument must be a bytes-like object, not {type(data).__name__}")
377
370
 
378
- def write_eof(self):
379
- """
380
- Closes the write end of the stream after the buffered write data is drained.
381
- If the process was blocked on input, it will become unblocked after `write_eof()`.
371
+ def write_eof(self) -> None:
372
+ """Close the write end of the stream after the buffered data is drained.
382
373
 
383
- This method needs to be used along with the `drain()` method which flushes the EOF to the process.
374
+ If the process was blocked on input, it will become unblocked after
375
+ `write_eof()`. This method needs to be used along with the `drain()`
376
+ method, which flushes the EOF to the process.
384
377
  """
385
378
  self._is_closed = True
386
379
 
387
- async def drain(self):
388
- """
389
- Flushes the write buffer to the running process. Flushes the EOF if the writer is closed.
380
+ async def drain(self) -> None:
381
+ """Flush the write buffer and send data to the running process.
382
+
383
+ This is a flow control method that blocks until data is sent. It returns
384
+ when it is appropriate to continue writing data to the stream.
385
+
386
+ **Usage**
387
+
388
+ ```python
389
+ # Synchronous
390
+ writer.write(data)
391
+ writer.drain()
392
+
393
+ # Async
394
+ writer.write(data)
395
+ await writer.drain.aio()
396
+ ```
390
397
  """
391
398
  data = bytes(self._buffer)
392
399
  self._buffer.clear()
393
- index = self.get_next_index()
400
+ index = self._get_next_index()
394
401
 
395
402
  try:
396
403
  if self._object_type == "sandbox":
modal/io_streams.pyi CHANGED
@@ -26,7 +26,7 @@ class _StreamReader(typing.Generic[T]):
26
26
  by_line: bool = False,
27
27
  ) -> None: ...
28
28
  @property
29
- def file_descriptor(self): ...
29
+ def file_descriptor(self) -> int: ...
30
30
  async def read(self) -> T: ...
31
31
  async def _consume_container_process_stream(self): ...
32
32
  def _stream_container_process(self) -> typing.AsyncGenerator[typing.Tuple[typing.Optional[bytes], str], None]: ...
@@ -38,11 +38,11 @@ class _StreamReader(typing.Generic[T]):
38
38
  class _StreamWriter:
39
39
  def __init__(
40
40
  self, object_id: str, object_type: typing.Literal["sandbox", "container_process"], client: modal.client._Client
41
- ): ...
42
- def get_next_index(self): ...
43
- def write(self, data: typing.Union[bytes, bytearray, memoryview, str]): ...
44
- def write_eof(self): ...
45
- async def drain(self): ...
41
+ ) -> None: ...
42
+ def _get_next_index(self) -> int: ...
43
+ def write(self, data: typing.Union[bytes, bytearray, memoryview, str]) -> None: ...
44
+ def write_eof(self) -> None: ...
45
+ async def drain(self) -> None: ...
46
46
 
47
47
  T_INNER = typing.TypeVar("T_INNER", covariant=True)
48
48
 
@@ -60,7 +60,7 @@ class StreamReader(typing.Generic[T]):
60
60
  by_line: bool = False,
61
61
  ) -> None: ...
62
62
  @property
63
- def file_descriptor(self): ...
63
+ def file_descriptor(self) -> int: ...
64
64
 
65
65
  class __read_spec(typing_extensions.Protocol[T_INNER]):
66
66
  def __call__(self) -> T_INNER: ...
@@ -100,13 +100,13 @@ class StreamReader(typing.Generic[T]):
100
100
  class StreamWriter:
101
101
  def __init__(
102
102
  self, object_id: str, object_type: typing.Literal["sandbox", "container_process"], client: modal.client.Client
103
- ): ...
104
- def get_next_index(self): ...
105
- def write(self, data: typing.Union[bytes, bytearray, memoryview, str]): ...
106
- def write_eof(self): ...
103
+ ) -> None: ...
104
+ def _get_next_index(self) -> int: ...
105
+ def write(self, data: typing.Union[bytes, bytearray, memoryview, str]) -> None: ...
106
+ def write_eof(self) -> None: ...
107
107
 
108
108
  class __drain_spec(typing_extensions.Protocol):
109
- def __call__(self): ...
110
- async def aio(self): ...
109
+ def __call__(self) -> None: ...
110
+ async def aio(self) -> None: ...
111
111
 
112
112
  drain: __drain_spec
modal/mount.py CHANGED
@@ -377,7 +377,9 @@ class _Mount(_Object, type_prefix="mo"):
377
377
  )
378
378
 
379
379
  def add_local_file(
380
- self, local_path: Union[str, Path], remote_path: Union[str, PurePosixPath, None] = None
380
+ self,
381
+ local_path: Union[str, Path],
382
+ remote_path: Union[str, PurePosixPath, None] = None,
381
383
  ) -> "_Mount":
382
384
  """
383
385
  Add a local file to the `Mount` object.
@@ -622,12 +624,13 @@ class _Mount(_Object, type_prefix="mo"):
622
624
  client: Optional[_Client] = None,
623
625
  ) -> None:
624
626
  check_object_name(deployment_name, "Mount")
627
+ environment_name = _get_environment_name(environment_name, resolver=None)
625
628
  self._deployment_name = deployment_name
626
629
  self._namespace = namespace
627
630
  self._environment_name = environment_name
628
631
  if client is None:
629
632
  client = await _Client.from_env()
630
- resolver = Resolver(client=client)
633
+ resolver = Resolver(client=client, environment_name=environment_name)
631
634
  await resolver.load(self)
632
635
 
633
636
  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,
@@ -26,13 +26,6 @@ class _NetworkFileSystem(modal.object._Object):
26
26
  _heartbeat_sleep: float = 300,
27
27
  ) -> typing.AsyncContextManager[_NetworkFileSystem]: ...
28
28
  @staticmethod
29
- def persisted(
30
- label: str, namespace=1, environment_name: typing.Optional[str] = None, cloud: typing.Optional[str] = None
31
- ): ...
32
- def persist(
33
- self, label: str, namespace=1, environment_name: typing.Optional[str] = None, cloud: typing.Optional[str] = None
34
- ): ...
35
- @staticmethod
36
29
  async def lookup(
37
30
  label: str,
38
31
  namespace=1,
@@ -85,13 +78,6 @@ class NetworkFileSystem(modal.object.Object):
85
78
  environment_name: typing.Optional[str] = None,
86
79
  _heartbeat_sleep: float = 300,
87
80
  ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[NetworkFileSystem]: ...
88
- @staticmethod
89
- def persisted(
90
- label: str, namespace=1, environment_name: typing.Optional[str] = None, cloud: typing.Optional[str] = None
91
- ): ...
92
- def persist(
93
- self, label: str, namespace=1, environment_name: typing.Optional[str] = None, cloud: typing.Optional[str] = None
94
- ): ...
95
81
 
96
82
  class __lookup_spec(typing_extensions.Protocol):
97
83
  def __call__(