modal 0.71.9__py3-none-any.whl → 0.72.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
modal/image.py CHANGED
@@ -33,6 +33,7 @@ from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
33
33
  from ._utils.deprecation import deprecation_error, deprecation_warning
34
34
  from ._utils.docker_utils import (
35
35
  extract_copy_command_patterns,
36
+ find_dockerignore_file,
36
37
  )
37
38
  from ._utils.function_utils import FunctionInfo
38
39
  from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES, retry_transient_errors
@@ -71,6 +72,17 @@ LOCAL_REQUIREMENTS_DIR = Path(__file__).parent / "requirements"
71
72
  CONTAINER_REQUIREMENTS_PATH = "/modal_requirements.txt"
72
73
 
73
74
 
75
+ class _AutoDockerIgnoreSentinel:
76
+ def __repr__(self) -> str:
77
+ return f"{__name__}.AUTO_DOCKERIGNORE"
78
+
79
+ def __call__(self, _: Path) -> bool:
80
+ raise NotImplementedError("This is only a placeholder. Do not call")
81
+
82
+
83
+ AUTO_DOCKERIGNORE = _AutoDockerIgnoreSentinel()
84
+
85
+
74
86
  def _validate_python_version(
75
87
  python_version: Optional[str], builder_version: ImageBuilderVersion, allow_micro_granularity: bool = True
76
88
  ) -> str:
@@ -266,6 +278,47 @@ def _create_context_mount(
266
278
  return _Mount._add_local_dir(Path("./"), PurePosixPath("/"), ignore=ignore_with_include)
267
279
 
268
280
 
281
+ def _create_context_mount_function(
282
+ ignore: Union[Sequence[str], Callable[[Path], bool]],
283
+ dockerfile_cmds: list[str] = [],
284
+ dockerfile_path: Optional[Path] = None,
285
+ context_mount: Optional[_Mount] = None,
286
+ ):
287
+ if dockerfile_path and dockerfile_cmds:
288
+ raise InvalidError("Cannot provide both dockerfile and docker commands")
289
+
290
+ if context_mount:
291
+ if ignore is not AUTO_DOCKERIGNORE:
292
+ raise InvalidError("Cannot set both `context_mount` and `ignore`")
293
+
294
+ def identity_context_mount_fn() -> Optional[_Mount]:
295
+ return context_mount
296
+
297
+ return identity_context_mount_fn
298
+ elif ignore is AUTO_DOCKERIGNORE:
299
+
300
+ def auto_created_context_mount_fn() -> Optional[_Mount]:
301
+ context_dir = Path.cwd()
302
+ dockerignore_file = find_dockerignore_file(context_dir, dockerfile_path)
303
+ ignore_fn = (
304
+ FilePatternMatcher(*dockerignore_file.read_text("utf8").splitlines())
305
+ if dockerignore_file
306
+ else _ignore_fn(())
307
+ )
308
+
309
+ cmds = dockerfile_path.read_text("utf8").splitlines() if dockerfile_path else dockerfile_cmds
310
+ return _create_context_mount(cmds, ignore_fn=ignore_fn, context_dir=context_dir)
311
+
312
+ return auto_created_context_mount_fn
313
+
314
+ def auto_created_context_mount_fn() -> Optional[_Mount]:
315
+ # use COPY commands and ignore patterns to construct implicit context mount
316
+ cmds = dockerfile_path.read_text("utf8").splitlines() if dockerfile_path else dockerfile_cmds
317
+ return _create_context_mount(cmds, ignore_fn=_ignore_fn(ignore), context_dir=Path.cwd())
318
+
319
+ return auto_created_context_mount_fn
320
+
321
+
269
322
  class _ImageRegistryConfig:
270
323
  """mdmd:hidden"""
271
324
 
@@ -654,7 +707,7 @@ class _Image(_Object, type_prefix="im"):
654
707
  if remote_path.endswith("/"):
655
708
  remote_path = remote_path + Path(local_path).name
656
709
 
657
- mount = _Mount.from_local_file(local_path, remote_path)
710
+ mount = _Mount._from_local_file(local_path, remote_path)
658
711
  return self._add_mount_layer_or_copy(mount, copy=copy)
659
712
 
660
713
  def add_local_dir(
@@ -683,6 +736,7 @@ class _Image(_Object, type_prefix="im"):
683
736
  **Usage:**
684
737
 
685
738
  ```python
739
+ from pathlib import Path
686
740
  from modal import FilePatternMatcher
687
741
 
688
742
  image = modal.Image.debian_slim().add_local_dir(
@@ -697,18 +751,25 @@ class _Image(_Object, type_prefix="im"):
697
751
  ignore=lambda p: p.is_relative_to(".venv"),
698
752
  )
699
753
 
700
- image = modal.Image.debian_slim().copy_local_dir(
754
+ image = modal.Image.debian_slim().add_local_dir(
701
755
  "~/assets",
702
756
  remote_path="/assets",
703
757
  ignore=FilePatternMatcher("**/*.txt"),
704
758
  )
705
759
 
706
760
  # When including files is simpler than excluding them, you can use the `~` operator to invert the matcher.
707
- image = modal.Image.debian_slim().copy_local_dir(
761
+ image = modal.Image.debian_slim().add_local_dir(
708
762
  "~/assets",
709
763
  remote_path="/assets",
710
764
  ignore=~FilePatternMatcher("**/*.py"),
711
765
  )
766
+
767
+ # You can also read ignore patterns from a file.
768
+ image = modal.Image.debian_slim().add_local_dir(
769
+ "~/assets",
770
+ remote_path="/assets",
771
+ ignore=FilePatternMatcher.from_file(Path("/path/to/ignorefile")),
772
+ )
712
773
  ```
713
774
  """
714
775
  if not PurePosixPath(remote_path).is_absolute():
@@ -734,7 +795,7 @@ class _Image(_Object, type_prefix="im"):
734
795
  return _Image._from_args(
735
796
  base_images={"base": self},
736
797
  dockerfile_function=build_dockerfile,
737
- context_mount_function=lambda: _Mount.from_local_file(local_path, remote_path=f"/{basename}"),
798
+ context_mount_function=lambda: _Mount._from_local_file(local_path, remote_path=f"/{basename}"),
738
799
  )
739
800
 
740
801
  def add_local_python_source(
@@ -772,7 +833,7 @@ class _Image(_Object, type_prefix="im"):
772
833
  )
773
834
  ```
774
835
  """
775
- mount = _Mount.from_local_python_packages(*modules, ignore=ignore)
836
+ mount = _Mount._from_local_python_packages(*modules, ignore=ignore)
776
837
  return self._add_mount_layer_or_copy(mount, copy=copy)
777
838
 
778
839
  def copy_local_dir(
@@ -792,6 +853,7 @@ class _Image(_Object, type_prefix="im"):
792
853
  **Usage:**
793
854
 
794
855
  ```python
856
+ from pathlib import Path
795
857
  from modal import FilePatternMatcher
796
858
 
797
859
  image = modal.Image.debian_slim().copy_local_dir(
@@ -818,6 +880,13 @@ class _Image(_Object, type_prefix="im"):
818
880
  remote_path="/assets",
819
881
  ignore=~FilePatternMatcher("**/*.py"),
820
882
  )
883
+
884
+ # You can also read ignore patterns from a file.
885
+ image = modal.Image.debian_slim().copy_local_dir(
886
+ "~/assets",
887
+ remote_path="/assets",
888
+ ignore=FilePatternMatcher.from_file(Path("/path/to/ignorefile")),
889
+ )
821
890
  ```
822
891
  """
823
892
 
@@ -1205,28 +1274,53 @@ class _Image(_Object, type_prefix="im"):
1205
1274
  # modal.Mount with local files to supply as build context for COPY commands
1206
1275
  context_mount: Optional[_Mount] = None,
1207
1276
  force_build: bool = False, # Ignore cached builds, similar to 'docker build --no-cache'
1208
- ignore: Union[Sequence[str], Callable[[Path], bool]] = (),
1277
+ ignore: Union[Sequence[str], Callable[[Path], bool]] = AUTO_DOCKERIGNORE,
1209
1278
  ) -> "_Image":
1210
- """Extend an image with arbitrary Dockerfile-like commands."""
1211
- cmds = _flatten_str_args("dockerfile_commands", "dockerfile_commands", dockerfile_commands)
1212
- if not cmds:
1213
- return self
1279
+ """
1280
+ Extend an image with arbitrary Dockerfile-like commands.
1214
1281
 
1215
- if context_mount:
1216
- if ignore:
1217
- raise InvalidError("Cannot set both `context_mount` and `ignore`")
1282
+ **Usage:**
1218
1283
 
1219
- def identity_context_mount_fn() -> Optional[_Mount]:
1220
- return context_mount
1284
+ ```python
1285
+ from pathlib import Path
1286
+ from modal import FilePatternMatcher
1221
1287
 
1222
- context_mount_function = identity_context_mount_fn
1223
- else:
1288
+ # By default a .dockerignore file is used if present in the current working directory
1289
+ image = modal.Image.debian_slim().dockerfile_commands(
1290
+ ["COPY data /data"],
1291
+ )
1224
1292
 
1225
- def auto_created_context_mount_fn() -> Optional[_Mount]:
1226
- # use COPY commands and ignore patterns to construct implicit context mount
1227
- return _create_context_mount(cmds, ignore_fn=_ignore_fn(ignore), context_dir=Path.cwd())
1293
+ image = modal.Image.debian_slim().dockerfile_commands(
1294
+ ["COPY data /data"],
1295
+ ignore=["*.venv"],
1296
+ )
1228
1297
 
1229
- context_mount_function = auto_created_context_mount_fn
1298
+ image = modal.Image.debian_slim().dockerfile_commands(
1299
+ ["COPY data /data"],
1300
+ ignore=lambda p: p.is_relative_to(".venv"),
1301
+ )
1302
+
1303
+ image = modal.Image.debian_slim().dockerfile_commands(
1304
+ ["COPY data /data"],
1305
+ ignore=FilePatternMatcher("**/*.txt"),
1306
+ )
1307
+
1308
+ # When including files is simpler than excluding them, you can use the `~` operator to invert the matcher.
1309
+ image = modal.Image.debian_slim().dockerfile_commands(
1310
+ ["COPY data /data"],
1311
+ ignore=~FilePatternMatcher("**/*.py"),
1312
+ )
1313
+
1314
+ # You can also read ignore patterns from a file.
1315
+ image = modal.Image.debian_slim().dockerfile_commands(
1316
+ ["COPY data /data"],
1317
+ ignore=FilePatternMatcher.from_file(Path("/path/to/dockerignore")),
1318
+ )
1319
+ ```
1320
+ """
1321
+ cmds = _flatten_str_args("dockerfile_commands", "dockerfile_commands", dockerfile_commands)
1322
+ if not cmds:
1323
+ return self
1230
1324
 
1231
1325
  def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
1232
1326
  return DockerfileSpec(commands=["FROM base", *cmds], context_files=context_files)
@@ -1236,7 +1330,9 @@ class _Image(_Object, type_prefix="im"):
1236
1330
  dockerfile_function=build_dockerfile,
1237
1331
  secrets=secrets,
1238
1332
  gpu_config=parse_gpu_config(gpu),
1239
- context_mount_function=context_mount_function,
1333
+ context_mount_function=_create_context_mount_function(
1334
+ ignore=ignore, dockerfile_cmds=cmds, context_mount=context_mount
1335
+ ),
1240
1336
  force_build=self.force_build or force_build,
1241
1337
  )
1242
1338
 
@@ -1408,9 +1504,14 @@ class _Image(_Object, type_prefix="im"):
1408
1504
  _validate_python_version(add_python, builder_version, allow_micro_granularity=False)
1409
1505
  add_python_commands = [
1410
1506
  "COPY /python/. /usr/local",
1411
- "RUN ln -s /usr/local/bin/python3 /usr/local/bin/python",
1412
1507
  "ENV TERMINFO_DIRS=/etc/terminfo:/lib/terminfo:/usr/share/terminfo:/usr/lib/terminfo",
1413
1508
  ]
1509
+ if add_python < "3.13":
1510
+ # Previous versions did not include the `python` binary, but later ones do.
1511
+ # (The important factor is not the Python version itself, but the standalone dist version.)
1512
+ # We insert the command in the list at the position it was previously always added
1513
+ # for backwards compatibility with existing images.
1514
+ add_python_commands.insert(1, "RUN ln -s /usr/local/bin/python3 /usr/local/bin/python")
1414
1515
 
1415
1516
  # Note: this change is because we install dependencies with uv in 2024.10+
1416
1517
  requirements_prefix = "python -m " if builder_version < "2024.10" else ""
@@ -1601,35 +1702,58 @@ class _Image(_Object, type_prefix="im"):
1601
1702
  secrets: Sequence[_Secret] = [],
1602
1703
  gpu: GPU_T = None,
1603
1704
  add_python: Optional[str] = None,
1604
- ignore: Union[Sequence[str], Callable[[Path], bool]] = (),
1705
+ ignore: Union[Sequence[str], Callable[[Path], bool]] = AUTO_DOCKERIGNORE,
1605
1706
  ) -> "_Image":
1606
1707
  """Build a Modal image from a local Dockerfile.
1607
1708
 
1608
1709
  If your Dockerfile does not have Python installed, you can use the `add_python` parameter
1609
1710
  to specify a version of Python to add to the image.
1610
1711
 
1611
- **Example**
1712
+ **Usage:**
1612
1713
 
1613
1714
  ```python
1614
- image = modal.Image.from_dockerfile("./Dockerfile", add_python="3.12")
1615
- ```
1616
- """
1715
+ from pathlib import Path
1716
+ from modal import FilePatternMatcher
1617
1717
 
1618
- if context_mount:
1619
- if ignore:
1620
- raise InvalidError("Cannot set both `context_mount` and `ignore`")
1718
+ # By default a .dockerignore file is used if present in the current working directory
1719
+ image = modal.Image.from_dockerfile(
1720
+ "./Dockerfile",
1721
+ add_python="3.12",
1722
+ )
1621
1723
 
1622
- def identity_context_mount_fn() -> Optional[_Mount]:
1623
- return context_mount
1724
+ image = modal.Image.from_dockerfile(
1725
+ "./Dockerfile",
1726
+ add_python="3.12",
1727
+ ignore=["*.venv"],
1728
+ )
1624
1729
 
1625
- context_mount_function = identity_context_mount_fn
1626
- else:
1730
+ image = modal.Image.from_dockerfile(
1731
+ "./Dockerfile",
1732
+ add_python="3.12",
1733
+ ignore=lambda p: p.is_relative_to(".venv"),
1734
+ )
1627
1735
 
1628
- def auto_created_context_mount_fn() -> Optional[_Mount]:
1629
- lines = Path(path).read_text("utf8").splitlines()
1630
- return _create_context_mount(lines, ignore_fn=_ignore_fn(ignore), context_dir=Path.cwd())
1736
+ image = modal.Image.from_dockerfile(
1737
+ "./Dockerfile",
1738
+ add_python="3.12",
1739
+ ignore=FilePatternMatcher("**/*.txt"),
1740
+ )
1631
1741
 
1632
- context_mount_function = auto_created_context_mount_fn
1742
+ # When including files is simpler than excluding them, you can use the `~` operator to invert the matcher.
1743
+ image = modal.Image.from_dockerfile(
1744
+ "./Dockerfile",
1745
+ add_python="3.12",
1746
+ ignore=~FilePatternMatcher("**/*.py"),
1747
+ )
1748
+
1749
+ # You can also read ignore patterns from a file.
1750
+ image = modal.Image.from_dockerfile(
1751
+ "./Dockerfile",
1752
+ add_python="3.12",
1753
+ ignore=FilePatternMatcher.from_file(Path("/path/to/dockerignore")),
1754
+ )
1755
+ ```
1756
+ """
1633
1757
 
1634
1758
  # --- Build the base dockerfile
1635
1759
 
@@ -1641,7 +1765,9 @@ class _Image(_Object, type_prefix="im"):
1641
1765
  gpu_config = parse_gpu_config(gpu)
1642
1766
  base_image = _Image._from_args(
1643
1767
  dockerfile_function=build_dockerfile_base,
1644
- context_mount_function=context_mount_function,
1768
+ context_mount_function=_create_context_mount_function(
1769
+ ignore=ignore, dockerfile_path=Path(path), context_mount=context_mount
1770
+ ),
1645
1771
  gpu_config=gpu_config,
1646
1772
  secrets=secrets,
1647
1773
  )
modal/image.pyi CHANGED
@@ -16,6 +16,12 @@ import typing_extensions
16
16
 
17
17
  ImageBuilderVersion = typing.Literal["2023.12", "2024.04", "2024.10"]
18
18
 
19
+ class _AutoDockerIgnoreSentinel:
20
+ def __repr__(self) -> str: ...
21
+ def __call__(self, _: pathlib.Path) -> bool: ...
22
+
23
+ AUTO_DOCKERIGNORE: _AutoDockerIgnoreSentinel
24
+
19
25
  def _validate_python_version(
20
26
  python_version: typing.Optional[str],
21
27
  builder_version: typing.Literal["2023.12", "2024.04", "2024.10"],
@@ -49,6 +55,12 @@ def _create_context_mount(
49
55
  ignore_fn: typing.Callable[[pathlib.Path], bool],
50
56
  context_dir: pathlib.Path,
51
57
  ) -> typing.Optional[modal.mount._Mount]: ...
58
+ def _create_context_mount_function(
59
+ ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]],
60
+ dockerfile_cmds: list[str] = [],
61
+ dockerfile_path: typing.Optional[pathlib.Path] = None,
62
+ context_mount: typing.Optional[modal.mount._Mount] = None,
63
+ ): ...
52
64
 
53
65
  class _ImageRegistryConfig:
54
66
  def __init__(self, registry_auth_type: int = 0, secret: typing.Optional[modal.secret._Secret] = None): ...
@@ -202,7 +214,9 @@ class _Image(modal.object._Object):
202
214
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
203
215
  context_mount: typing.Optional[modal.mount._Mount] = None,
204
216
  force_build: bool = False,
205
- ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = (),
217
+ ignore: typing.Union[
218
+ collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]
219
+ ] = modal.image.AUTO_DOCKERIGNORE,
206
220
  ) -> _Image: ...
207
221
  def entrypoint(self, entrypoint_commands: list[str]) -> _Image: ...
208
222
  def shell(self, shell_commands: list[str]) -> _Image: ...
@@ -288,7 +302,9 @@ class _Image(modal.object._Object):
288
302
  secrets: collections.abc.Sequence[modal.secret._Secret] = [],
289
303
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
290
304
  add_python: typing.Optional[str] = None,
291
- ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = (),
305
+ ignore: typing.Union[
306
+ collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]
307
+ ] = modal.image.AUTO_DOCKERIGNORE,
292
308
  ) -> _Image: ...
293
309
  @staticmethod
294
310
  def debian_slim(python_version: typing.Optional[str] = None, force_build: bool = False) -> _Image: ...
@@ -470,7 +486,9 @@ class Image(modal.object.Object):
470
486
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
471
487
  context_mount: typing.Optional[modal.mount.Mount] = None,
472
488
  force_build: bool = False,
473
- ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = (),
489
+ ignore: typing.Union[
490
+ collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]
491
+ ] = modal.image.AUTO_DOCKERIGNORE,
474
492
  ) -> Image: ...
475
493
  def entrypoint(self, entrypoint_commands: list[str]) -> Image: ...
476
494
  def shell(self, shell_commands: list[str]) -> Image: ...
@@ -556,7 +574,9 @@ class Image(modal.object.Object):
556
574
  secrets: collections.abc.Sequence[modal.secret.Secret] = [],
557
575
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
558
576
  add_python: typing.Optional[str] = None,
559
- ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = (),
577
+ ignore: typing.Union[
578
+ collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]
579
+ ] = modal.image.AUTO_DOCKERIGNORE,
560
580
  ) -> Image: ...
561
581
  @staticmethod
562
582
  def debian_slim(python_version: typing.Optional[str] = None, force_build: bool = False) -> Image: ...
modal/mount.py CHANGED
@@ -23,7 +23,7 @@ from modal_version import __version__
23
23
  from ._resolver import Resolver
24
24
  from ._utils.async_utils import aclosing, async_map, synchronize_api
25
25
  from ._utils.blob_utils import FileUploadSpec, blob_upload_file, get_file_upload_spec_from_path
26
- from ._utils.deprecation import renamed_parameter
26
+ from ._utils.deprecation import deprecation_warning, renamed_parameter
27
27
  from ._utils.grpc_utils import retry_transient_errors
28
28
  from ._utils.name_utils import check_object_name
29
29
  from ._utils.package_utils import get_module_mount_info
@@ -48,6 +48,11 @@ PYTHON_STANDALONE_VERSIONS: dict[str, tuple[str, str]] = {
48
48
  "3.13": ("20241008", "3.13.0"),
49
49
  }
50
50
 
51
+ MOUNT_DEPRECATION_MESSAGE_PATTERN = """modal.Mount usage will soon be deprecated.
52
+
53
+ Use {replacement} instead, which is functionally and performance-wise equivalent.
54
+ """
55
+
51
56
 
52
57
  def client_mount_name() -> str:
53
58
  """Get the deployed name of the client package mount."""
@@ -401,6 +406,23 @@ class _Mount(_Object, type_prefix="mo"):
401
406
  )
402
407
  ```
403
408
  """
409
+ deprecation_warning(
410
+ (2024, 1, 8), MOUNT_DEPRECATION_MESSAGE_PATTERN.format(replacement="image.add_local_dir"), pending=True
411
+ )
412
+ return _Mount._from_local_dir(local_path, remote_path=remote_path, condition=condition, recursive=recursive)
413
+
414
+ @staticmethod
415
+ def _from_local_dir(
416
+ local_path: Union[str, Path],
417
+ *,
418
+ # Where the directory is placed within in the mount
419
+ remote_path: Union[str, PurePosixPath, None] = None,
420
+ # Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
421
+ # Defaults to including all files.
422
+ condition: Optional[Callable[[str], bool]] = None,
423
+ # add files from subdirectories as well
424
+ recursive: bool = True,
425
+ ) -> "_Mount":
404
426
  return _Mount._new().add_local_dir(
405
427
  local_path, remote_path=remote_path, condition=condition, recursive=recursive
406
428
  )
@@ -439,6 +461,13 @@ class _Mount(_Object, type_prefix="mo"):
439
461
  )
440
462
  ```
441
463
  """
464
+ deprecation_warning(
465
+ (2024, 1, 8), MOUNT_DEPRECATION_MESSAGE_PATTERN.format(replacement="image.add_local_file"), pending=True
466
+ )
467
+ return _Mount._from_local_file(local_path, remote_path)
468
+
469
+ @staticmethod
470
+ def _from_local_file(local_path: Union[str, Path], remote_path: Union[str, PurePosixPath, None] = None) -> "_Mount":
442
471
  return _Mount._new().add_local_file(local_path, remote_path=remote_path)
443
472
 
444
473
  @staticmethod
@@ -601,7 +630,24 @@ class _Mount(_Object, type_prefix="mo"):
601
630
  my_local_module.do_stuff()
602
631
  ```
603
632
  """
633
+ deprecation_warning(
634
+ (2024, 1, 8),
635
+ MOUNT_DEPRECATION_MESSAGE_PATTERN.format(replacement="image.add_local_python_source"),
636
+ pending=True,
637
+ )
638
+ return _Mount._from_local_python_packages(
639
+ *module_names, remote_dir=remote_dir, condition=condition, ignore=ignore
640
+ )
604
641
 
642
+ @staticmethod
643
+ def _from_local_python_packages(
644
+ *module_names: str,
645
+ remote_dir: Union[str, PurePosixPath] = ROOT_DIR.as_posix(),
646
+ # Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
647
+ # Defaults to including all files.
648
+ condition: Optional[Callable[[str], bool]] = None,
649
+ ignore: Optional[Union[Sequence[str], Callable[[Path], bool]]] = None,
650
+ ) -> "_Mount":
605
651
  # Don't re-run inside container.
606
652
 
607
653
  if condition is not None:
@@ -786,7 +832,7 @@ def get_auto_mounts() -> list[_Mount]:
786
832
 
787
833
  try:
788
834
  # at this point we don't know if the sys.modules module should be mounted or not
789
- potential_mount = _Mount.from_local_python_packages(module_name)
835
+ potential_mount = _Mount._from_local_python_packages(module_name)
790
836
  mount_paths = potential_mount._top_level_paths()
791
837
  except ModuleNotMountable:
792
838
  # this typically happens if the module is a built-in, has binary components or doesn't exist
modal/mount.pyi CHANGED
@@ -113,6 +113,14 @@ class _Mount(modal.object._Object):
113
113
  condition: typing.Optional[typing.Callable[[str], bool]] = None,
114
114
  recursive: bool = True,
115
115
  ) -> _Mount: ...
116
+ @staticmethod
117
+ def _from_local_dir(
118
+ local_path: typing.Union[str, pathlib.Path],
119
+ *,
120
+ remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
121
+ condition: typing.Optional[typing.Callable[[str], bool]] = None,
122
+ recursive: bool = True,
123
+ ) -> _Mount: ...
116
124
  def add_local_file(
117
125
  self,
118
126
  local_path: typing.Union[str, pathlib.Path],
@@ -123,6 +131,10 @@ class _Mount(modal.object._Object):
123
131
  local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None
124
132
  ) -> _Mount: ...
125
133
  @staticmethod
134
+ def _from_local_file(
135
+ local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None
136
+ ) -> _Mount: ...
137
+ @staticmethod
126
138
  def _description(entries: list[_MountEntry]) -> str: ...
127
139
  @staticmethod
128
140
  def _get_files(
@@ -139,6 +151,13 @@ class _Mount(modal.object._Object):
139
151
  ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool], None] = None,
140
152
  ) -> _Mount: ...
141
153
  @staticmethod
154
+ def _from_local_python_packages(
155
+ *module_names: str,
156
+ remote_dir: typing.Union[str, pathlib.PurePosixPath] = "/root",
157
+ condition: typing.Optional[typing.Callable[[str], bool]] = None,
158
+ ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool], None] = None,
159
+ ) -> _Mount: ...
160
+ @staticmethod
142
161
  def from_name(name: str, namespace=1, environment_name: typing.Optional[str] = None) -> _Mount: ...
143
162
  @classmethod
144
163
  async def lookup(
@@ -195,6 +214,14 @@ class Mount(modal.object.Object):
195
214
  condition: typing.Optional[typing.Callable[[str], bool]] = None,
196
215
  recursive: bool = True,
197
216
  ) -> Mount: ...
217
+ @staticmethod
218
+ def _from_local_dir(
219
+ local_path: typing.Union[str, pathlib.Path],
220
+ *,
221
+ remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
222
+ condition: typing.Optional[typing.Callable[[str], bool]] = None,
223
+ recursive: bool = True,
224
+ ) -> Mount: ...
198
225
  def add_local_file(
199
226
  self,
200
227
  local_path: typing.Union[str, pathlib.Path],
@@ -205,6 +232,10 @@ class Mount(modal.object.Object):
205
232
  local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None
206
233
  ) -> Mount: ...
207
234
  @staticmethod
235
+ def _from_local_file(
236
+ local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None
237
+ ) -> Mount: ...
238
+ @staticmethod
208
239
  def _description(entries: list[_MountEntry]) -> str: ...
209
240
 
210
241
  class ___get_files_spec(typing_extensions.Protocol):
@@ -231,6 +262,13 @@ class Mount(modal.object.Object):
231
262
  ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool], None] = None,
232
263
  ) -> Mount: ...
233
264
  @staticmethod
265
+ def _from_local_python_packages(
266
+ *module_names: str,
267
+ remote_dir: typing.Union[str, pathlib.PurePosixPath] = "/root",
268
+ condition: typing.Optional[typing.Callable[[str], bool]] = None,
269
+ ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool], None] = None,
270
+ ) -> Mount: ...
271
+ @staticmethod
234
272
  def from_name(name: str, namespace=1, environment_name: typing.Optional[str] = None) -> Mount: ...
235
273
  @classmethod
236
274
  def lookup(
modal/volume.py CHANGED
@@ -506,6 +506,18 @@ class _Volume(_Object, type_prefix="vo"):
506
506
  req = api_pb2.VolumeDeleteRequest(volume_id=obj.object_id)
507
507
  await retry_transient_errors(obj._client.stub.VolumeDelete, req)
508
508
 
509
+ @staticmethod
510
+ async def rename(
511
+ old_name: str,
512
+ new_name: str,
513
+ *,
514
+ client: Optional[_Client] = None,
515
+ environment_name: Optional[str] = None,
516
+ ):
517
+ obj = await _Volume.lookup(old_name, client=client, environment_name=environment_name)
518
+ req = api_pb2.VolumeRenameRequest(volume_id=obj.object_id, name=new_name)
519
+ await retry_transient_errors(obj._client.stub.VolumeRename, req)
520
+
509
521
 
510
522
  class _VolumeUploadContextManager:
511
523
  """Context manager for batch-uploading files to a Volume."""
modal/volume.pyi CHANGED
@@ -85,6 +85,14 @@ class _Volume(modal.object._Object):
85
85
  async def delete(
86
86
  name: str, client: typing.Optional[modal.client._Client] = None, environment_name: typing.Optional[str] = None
87
87
  ): ...
88
+ @staticmethod
89
+ async def rename(
90
+ old_name: str,
91
+ new_name: str,
92
+ *,
93
+ client: typing.Optional[modal.client._Client] = None,
94
+ environment_name: typing.Optional[str] = None,
95
+ ): ...
88
96
 
89
97
  class _VolumeUploadContextManager:
90
98
  _volume_id: str
@@ -272,6 +280,26 @@ class Volume(modal.object.Object):
272
280
 
273
281
  delete: __delete_spec
274
282
 
283
+ class __rename_spec(typing_extensions.Protocol):
284
+ def __call__(
285
+ self,
286
+ old_name: str,
287
+ new_name: str,
288
+ *,
289
+ client: typing.Optional[modal.client.Client] = None,
290
+ environment_name: typing.Optional[str] = None,
291
+ ): ...
292
+ async def aio(
293
+ self,
294
+ old_name: str,
295
+ new_name: str,
296
+ *,
297
+ client: typing.Optional[modal.client.Client] = None,
298
+ environment_name: typing.Optional[str] = None,
299
+ ): ...
300
+
301
+ rename: __rename_spec
302
+
275
303
  class VolumeUploadContextManager:
276
304
  _volume_id: str
277
305
  _client: modal.client.Client
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.71.9
3
+ Version: 0.72.5
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com