modal 0.71.12__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.
@@ -1,7 +1,8 @@
1
1
  # Copyright Modal Labs 2024
2
2
  import re
3
3
  import shlex
4
- from typing import Sequence
4
+ from pathlib import Path
5
+ from typing import Optional, Sequence
5
6
 
6
7
  from ..exception import InvalidError
7
8
 
@@ -62,3 +63,36 @@ def extract_copy_command_patterns(dockerfile_lines: Sequence[str]) -> list[str]:
62
63
  current_command = ""
63
64
 
64
65
  return list(copy_source_patterns)
66
+
67
+
68
+ def find_dockerignore_file(context_directory: Path, dockerfile_path: Optional[Path] = None) -> Optional[Path]:
69
+ """
70
+ Find dockerignore file relative to current context directory
71
+ and if dockerfile path is provided, check if specific <dockerfile_name>.dockerignore
72
+ file exists in the same directory as <dockerfile_name>
73
+ Finds the most specific dockerignore file that exists.
74
+ """
75
+
76
+ def valid_dockerignore_file(fp):
77
+ # fp has to exist
78
+ if not fp.exists():
79
+ return False
80
+ # fp has to be subpath to current working directory
81
+ if not fp.is_relative_to(context_directory):
82
+ return False
83
+
84
+ return True
85
+
86
+ generic_name = ".dockerignore"
87
+ possible_locations = []
88
+ if dockerfile_path:
89
+ specific_name = f"{dockerfile_path.name}.dockerignore"
90
+ # 1. check if specific <dockerfile_name>.dockerignore file exists in the same directory as <dockerfile_name>
91
+ possible_locations.append(dockerfile_path.parent / specific_name)
92
+ # 2. check if generic .dockerignore file exists in the same directory as <dockerfile_name>
93
+ possible_locations.append(dockerfile_path.parent / generic_name)
94
+
95
+ # 3. check if generic .dockerignore file exists in current working directory
96
+ possible_locations.append(context_directory / generic_name)
97
+
98
+ return next((e for e in possible_locations if valid_dockerignore_file(e)), None)
@@ -326,11 +326,11 @@ class FunctionInfo:
326
326
  # make sure the function's own entrypoint is included:
327
327
  if self._type == FunctionInfoType.PACKAGE:
328
328
  if config.get("automount"):
329
- return [_Mount.from_local_python_packages(self.module_name)]
329
+ return [_Mount._from_local_python_packages(self.module_name)]
330
330
  elif not self.is_serialized():
331
331
  # mount only relevant file and __init__.py:s
332
332
  return [
333
- _Mount.from_local_dir(
333
+ _Mount._from_local_dir(
334
334
  self._base_dir,
335
335
  remote_path=self._remote_dir,
336
336
  recursive=True,
@@ -341,7 +341,7 @@ class FunctionInfo:
341
341
  remote_path = ROOT_DIR / Path(self._file).name
342
342
  if not _is_modal_path(remote_path):
343
343
  return [
344
- _Mount.from_local_file(
344
+ _Mount._from_local_file(
345
345
  self._file,
346
346
  remote_path=remote_path,
347
347
  )
modal/app.py CHANGED
@@ -200,10 +200,9 @@ class _App:
200
200
 
201
201
  ```python notest
202
202
  image = modal.Image.debian_slim().pip_install(...)
203
- mount = modal.Mount.from_local_dir("./config")
204
203
  secret = modal.Secret.from_name("my-secret")
205
204
  volume = modal.Volume.from_name("my-data")
206
- app = modal.App(image=image, mounts=[mount], secrets=[secret], volumes={"/mnt/data": volume})
205
+ app = modal.App(image=image, secrets=[secret], volumes={"/mnt/data": volume})
207
206
  ```
208
207
  """
209
208
  if name is not None and not isinstance(name, str):
modal/cli/launch.py CHANGED
@@ -55,7 +55,7 @@ def jupyter(
55
55
  timeout: int = 3600,
56
56
  image: str = "ubuntu:22.04",
57
57
  add_python: Optional[str] = "3.11",
58
- mount: Optional[str] = None, # Create a `modal.Mount` from a local directory.
58
+ mount: Optional[str] = None, # Adds a local directory to the jupyter container
59
59
  volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
60
60
  detach: bool = False, # Run the app in "detached" mode to persist after local client disconnects
61
61
  ):
@@ -10,25 +10,20 @@ import time
10
10
  import webbrowser
11
11
  from typing import Any
12
12
 
13
- from modal import App, Image, Mount, Queue, Secret, Volume, forward
13
+ from modal import App, Image, Queue, Secret, Volume, forward
14
14
 
15
15
  # Passed by `modal launch` locally via CLI, plumbed to remote runner through secrets.
16
16
  args: dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_ARGS", "{}"))
17
17
 
18
-
19
18
  app = App()
20
- app.image = Image.from_registry(args.get("image"), add_python=args.get("add_python")).pip_install("jupyterlab")
21
19
 
20
+ image = Image.from_registry(args.get("image"), add_python=args.get("add_python")).pip_install("jupyterlab")
22
21
 
23
- mount = (
24
- Mount.from_local_dir(
22
+ if args.get("mount"):
23
+ image = image.add_local_dir(
25
24
  args.get("mount"),
26
25
  remote_path="/root/lab/mount",
27
26
  )
28
- if args.get("mount")
29
- else None
30
- )
31
- mounts = [mount] if mount else []
32
27
 
33
28
  volume = (
34
29
  Volume.from_name(
@@ -55,12 +50,12 @@ def wait_for_port(url: str, q: Queue):
55
50
 
56
51
 
57
52
  @app.function(
53
+ image=image,
58
54
  cpu=args.get("cpu"),
59
55
  memory=args.get("memory"),
60
56
  gpu=args.get("gpu"),
61
57
  timeout=args.get("timeout"),
62
58
  secrets=[Secret.from_dict({"MODAL_LAUNCH_ARGS": json.dumps(args)})],
63
- mounts=mounts,
64
59
  volumes=volumes,
65
60
  concurrency_limit=1 if volume else None,
66
61
  )
@@ -10,7 +10,7 @@ import time
10
10
  import webbrowser
11
11
  from typing import Any
12
12
 
13
- from modal import App, Image, Mount, Queue, Secret, Volume, forward
13
+ from modal import App, Image, Queue, Secret, Volume, forward
14
14
 
15
15
  # Passed by `modal launch` locally via CLI, plumbed to remote runner through secrets.
16
16
  args: dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_ARGS", "{}"))
@@ -23,7 +23,7 @@ FIXUD_INSTALLER = "https://github.com/boxboat/fixuid/releases/download/v0.6.0/fi
23
23
 
24
24
 
25
25
  app = App()
26
- app.image = (
26
+ image = (
27
27
  Image.from_registry(args.get("image"), add_python="3.11")
28
28
  .apt_install("curl", "dumb-init", "git", "git-lfs")
29
29
  .run_commands(
@@ -44,16 +44,11 @@ app.image = (
44
44
  .env({"ENTRYPOINTD": ""})
45
45
  )
46
46
 
47
-
48
- mount = (
49
- Mount.from_local_dir(
47
+ if args.get("mount"):
48
+ image = image.add_local_dir(
50
49
  args.get("mount"),
51
50
  remote_path="/home/coder/mount",
52
51
  )
53
- if args.get("mount")
54
- else None
55
- )
56
- mounts = [mount] if mount else []
57
52
 
58
53
  volume = (
59
54
  Volume.from_name(
@@ -80,12 +75,12 @@ def wait_for_port(data: tuple[str, str], q: Queue):
80
75
 
81
76
 
82
77
  @app.function(
78
+ image=image,
83
79
  cpu=args.get("cpu"),
84
80
  memory=args.get("memory"),
85
81
  gpu=args.get("gpu"),
86
82
  timeout=args.get("timeout"),
87
83
  secrets=[Secret.from_dict({"MODAL_LAUNCH_ARGS": json.dumps(args)})],
88
- mounts=mounts,
89
84
  volumes=volumes,
90
85
  concurrency_limit=1 if volume else None,
91
86
  )
modal/client.pyi CHANGED
@@ -26,7 +26,7 @@ class _Client:
26
26
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
27
27
 
28
28
  def __init__(
29
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.71.12"
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.5"
30
30
  ): ...
31
31
  def is_closed(self) -> bool: ...
32
32
  @property
@@ -81,7 +81,7 @@ class Client:
81
81
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
82
82
 
83
83
  def __init__(
84
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.71.12"
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.5"
85
85
  ): ...
86
86
  def is_closed(self) -> bool: ...
87
87
  @property
@@ -35,7 +35,7 @@ class _AbstractPatternMatcher:
35
35
  """
36
36
  return _CustomPatternMatcher(lambda path: not self(path))
37
37
 
38
- def with_repr(self, custom_repr) -> "_AbstractPatternMatcher":
38
+ def _with_repr(self, custom_repr) -> "_AbstractPatternMatcher":
39
39
  # use to give an instance of a matcher a custom name - useful for visualizing default values in signatures
40
40
  self._custom_repr = custom_repr
41
41
  return self
@@ -60,7 +60,45 @@ class _CustomPatternMatcher(_AbstractPatternMatcher):
60
60
 
61
61
 
62
62
  class FilePatternMatcher(_AbstractPatternMatcher):
63
- """Allows matching file paths against a list of patterns."""
63
+ """
64
+ Allows matching file Path objects against a list of patterns.
65
+
66
+ **Usage:**
67
+ ```python
68
+ from pathlib import Path
69
+ from modal import FilePatternMatcher
70
+
71
+ matcher = FilePatternMatcher("*.py")
72
+
73
+ assert matcher(Path("foo.py"))
74
+
75
+ # You can also negate the matcher.
76
+ negated_matcher = ~matcher
77
+
78
+ assert not negated_matcher(Path("foo.py"))
79
+ ```
80
+ """
81
+
82
+ patterns: list[Pattern]
83
+ _delayed_init: Callable[[], None] = None
84
+
85
+ def _set_patterns(self, patterns: Sequence[str]) -> None:
86
+ self.patterns = []
87
+ for pattern in list(patterns):
88
+ pattern = pattern.strip()
89
+ if not pattern:
90
+ continue
91
+ pattern = os.path.normpath(pattern)
92
+ new_pattern = Pattern()
93
+ if pattern[0] == "!":
94
+ if len(pattern) == 1:
95
+ raise ValueError('Illegal exclusion pattern: "!"')
96
+ new_pattern.exclusion = True
97
+ pattern = pattern[1:]
98
+ # In Python, we can proceed without explicit syntax checking
99
+ new_pattern.cleaned_pattern = pattern
100
+ new_pattern.dirs = pattern.split(os.path.sep)
101
+ self.patterns.append(new_pattern)
64
102
 
65
103
  def __init__(self, *pattern: str) -> None:
66
104
  """Initialize a new FilePatternMatcher instance.
@@ -71,24 +109,34 @@ class FilePatternMatcher(_AbstractPatternMatcher):
71
109
  Raises:
72
110
  ValueError: If an illegal exclusion pattern is provided.
73
111
  """
74
- self.patterns: list[Pattern] = []
75
- self.exclusions = False
76
- for p in list(pattern):
77
- p = p.strip()
78
- if not p:
79
- continue
80
- p = os.path.normpath(p)
81
- new_pattern = Pattern()
82
- if p[0] == "!":
83
- if len(p) == 1:
84
- raise ValueError('Illegal exclusion pattern: "!"')
85
- new_pattern.exclusion = True
86
- p = p[1:]
87
- self.exclusions = True
88
- # In Python, we can proceed without explicit syntax checking
89
- new_pattern.cleaned_pattern = p
90
- new_pattern.dirs = p.split(os.path.sep)
91
- self.patterns.append(new_pattern)
112
+ self._set_patterns(pattern)
113
+
114
+ @classmethod
115
+ def from_file(cls, file_path: Path) -> "FilePatternMatcher":
116
+ """Initialize a new FilePatternMatcher instance from a file.
117
+
118
+ The patterns in the file will be read lazily when the matcher is first used.
119
+
120
+ Args:
121
+ file_path (Path): The path to the file containing patterns.
122
+
123
+ **Usage:**
124
+ ```python
125
+ from pathlib import Path
126
+ from modal import FilePatternMatcher
127
+
128
+ matcher = FilePatternMatcher.from_file(Path("/path/to/ignorefile"))
129
+ ```
130
+
131
+ """
132
+ uninitialized = cls.__new__(cls)
133
+
134
+ def _delayed_init():
135
+ uninitialized._set_patterns(file_path.read_text("utf8").splitlines())
136
+ uninitialized._delayed_init = None
137
+
138
+ uninitialized._delayed_init = _delayed_init
139
+ return uninitialized
92
140
 
93
141
  def _matches(self, file_path: str) -> bool:
94
142
  """Check if the file path or any of its parent directories match the patterns.
@@ -97,6 +145,7 @@ class FilePatternMatcher(_AbstractPatternMatcher):
97
145
  library. The reason is that `Matches()` in the original library is
98
146
  deprecated due to buggy behavior.
99
147
  """
148
+
100
149
  matched = False
101
150
  file_path = os.path.normpath(file_path)
102
151
  if file_path == ".":
@@ -128,31 +177,15 @@ class FilePatternMatcher(_AbstractPatternMatcher):
128
177
  return matched
129
178
 
130
179
  def __call__(self, file_path: Path) -> bool:
131
- """Check if the path matches any of the patterns.
132
-
133
- Args:
134
- file_path (Path): The path to check.
135
-
136
- Returns:
137
- True if the path matches any of the patterns.
138
-
139
- Usage:
140
- ```python
141
- from pathlib import Path
142
- from modal import FilePatternMatcher
143
-
144
- matcher = FilePatternMatcher("*.py")
145
-
146
- assert matcher(Path("foo.py"))
147
- ```
148
- """
180
+ if self._delayed_init:
181
+ self._delayed_init()
149
182
  return self._matches(str(file_path))
150
183
 
151
184
 
152
- # with_repr allows us to use this matcher as a default value in a function signature
185
+ # _with_repr allows us to use this matcher as a default value in a function signature
153
186
  # and get a nice repr in the docs and auto-generated type stubs:
154
- NON_PYTHON_FILES = (~FilePatternMatcher("**/*.py")).with_repr(f"{__name__}.NON_PYTHON_FILES")
155
- _NOTHING = (~FilePatternMatcher()).with_repr(f"{__name__}._NOTHING") # match everything = ignore nothing
187
+ NON_PYTHON_FILES = (~FilePatternMatcher("**/*.py"))._with_repr(f"{__name__}.NON_PYTHON_FILES")
188
+ _NOTHING = (~FilePatternMatcher())._with_repr(f"{__name__}._NOTHING") # match everything = ignore nothing
156
189
 
157
190
 
158
191
  def _ignore_fn(ignore: Union[Sequence[str], Callable[[Path], bool]]) -> Callable[[Path], bool]:
modal/functions.pyi CHANGED
@@ -462,11 +462,11 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
462
462
 
463
463
  _call_generator_nowait: ___call_generator_nowait_spec
464
464
 
465
- class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
465
+ class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
466
466
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
467
467
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
468
468
 
469
- remote: __remote_spec[ReturnType, P]
469
+ remote: __remote_spec[P, ReturnType]
470
470
 
471
471
  class __remote_gen_spec(typing_extensions.Protocol):
472
472
  def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
@@ -479,17 +479,17 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
479
479
  def _get_obj(self) -> typing.Optional[modal.cls.Obj]: ...
480
480
  def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
481
481
 
482
- class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
482
+ class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
483
483
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
484
484
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
485
485
 
486
- _experimental_spawn: ___experimental_spawn_spec[ReturnType, P]
486
+ _experimental_spawn: ___experimental_spawn_spec[P, ReturnType]
487
487
 
488
- class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
488
+ class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
489
489
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
490
490
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
491
491
 
492
- spawn: __spawn_spec[ReturnType, P]
492
+ spawn: __spawn_spec[P, ReturnType]
493
493
 
494
494
  def get_raw_f(self) -> typing.Callable[..., typing.Any]: ...
495
495
 
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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.71.12
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
@@ -15,11 +15,11 @@ modal/_traceback.py,sha256=IZQzB3fVlUfMHOSyKUgw0H6qv4yHnpyq-XVCNZKfUdA,5023
15
15
  modal/_tunnel.py,sha256=o-jJhS4vQ6-XswDhHcJWGMZZmD03SC0e9i8fEu1JTjo,6310
16
16
  modal/_tunnel.pyi,sha256=JmmDYAy9F1FpgJ_hWx0xkom2nTOFQjn4mTPYlU3PFo4,1245
17
17
  modal/_watcher.py,sha256=K6LYnlmSGQB4tWWI9JADv-tvSvQ1j522FwT71B51CX8,3584
18
- modal/app.py,sha256=vEE0cK5QPF6_cdW5AJvcuWxz5KmeprHwBEtlDkVRHgE,45582
18
+ modal/app.py,sha256=1h8v96KuIg_SAdWDK9gNWVOYYFciBIerhldRPjvDgxs,45511
19
19
  modal/app.pyi,sha256=Gx7gxjfQ70sxhbwfpx1VjvzEON-ZEMTJ_Vy8qt0oQvo,25302
20
20
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
21
21
  modal/client.py,sha256=JAnd4-GCN093BwkvOFAK5a6iy5ycxofjpUncMxlrIMw,15253
22
- modal/client.pyi,sha256=fidFDEnApJ5cdIG6f0JFX3SIzsgBEH7Ubu26iPbYew4,7280
22
+ modal/client.pyi,sha256=aGMg6G-KZvllpIhVe9lKCjGRODL2eG7ur3StgGlwOGI,7278
23
23
  modal/cloud_bucket_mount.py,sha256=G7T7jWLD0QkmrfKR75mSTwdUZ2xNfj7pkVqb4ipmxmI,5735
24
24
  modal/cloud_bucket_mount.pyi,sha256=CEi7vrH3kDUF4LAy4qP6tfImy2UJuFRcRbsgRNM1wo8,1403
25
25
  modal/cls.py,sha256=3hjb0JcoPjxKZNeK22f5rR43bZRBjoRI7_EMZXY7YrE,31172
@@ -35,16 +35,16 @@ modal/exception.py,sha256=4JyO-SACaLNDe2QC48EjsK8GMkZ8AgEurZ8j1YdRu8E,5263
35
35
  modal/experimental.py,sha256=npfKbyMpI41uZZs9HW_QiB3E4ykWfDXZbACXXbw6qeA,2385
36
36
  modal/file_io.py,sha256=lcMs_E9Xfm0YX1t9U2wNIBPnqHRxmImqjLW1GHqVmyg,20945
37
37
  modal/file_io.pyi,sha256=NrIoB0YjIqZ8MDMe826xAnybT0ww_kxQM3iPLo82REU,8898
38
- modal/file_pattern_matcher.py,sha256=LaI7Paxg0xR9D-D7Tgc60xR0w1KZee22LjGbFie1Vms,5571
38
+ modal/file_pattern_matcher.py,sha256=2ozXoalgCByA-aVk81Jv9W4_DWqLgBZAix_NADwPOrs,6527
39
39
  modal/functions.py,sha256=3uJPbrEAWhpFfLfUnoRjGmvEUC-_wVh-8yNJBx8eVeM,68249
40
- modal/functions.pyi,sha256=3ESJ61f8oEDycDmrpnuNB2vjFKuLBG_aqyliXPTdY7M,25407
40
+ modal/functions.pyi,sha256=LiSDgH-X7jcZ56pAoLMwo3x9Dzdp_3Sd7W5MVAJPoCg,25407
41
41
  modal/gpu.py,sha256=MTxj6ql8EpgfBg8YmZ5a1cLznyuZFssX1qXbEX4LKVM,7503
42
- modal/image.py,sha256=oKqqLhc3Ap2XMG5MKVlERKkMTwJPkNMNcSzxoZh4zuw,85259
43
- modal/image.pyi,sha256=Pa1_LVr3FyNsnu_MhBO08fBgCeLazTEe25phYdu0bzE,25365
42
+ modal/image.py,sha256=_ALAfk8YEiWnv_pHSnixBPoR88RGSDstsAUvlJa1nKc,89802
43
+ modal/image.pyi,sha256=NfZyLkl4rmxpc5fokaO4mmEeGFOwGn0AndV1vKwBdbs,26027
44
44
  modal/io_streams.py,sha256=QkQiizKRzd5bnbKQsap31LJgBYlAnj4-XkV_50xPYX0,15079
45
45
  modal/io_streams.pyi,sha256=bCCVSxkMcosYd8O3PQDDwJw7TQ8JEcnYonLJ5t27TQs,4804
46
- modal/mount.py,sha256=wOr-2vmKImsE3lHBII8hL2gYy5ng46R58QwId4JultQ,29313
47
- modal/mount.pyi,sha256=FiNV1wIKFvd0ZMZ0tm1mz6ZSA5Hjsge-kFSA5tPWfcI,10503
46
+ modal/mount.py,sha256=nGimxizRzCpyZu9cpTdKQRgU9uqMnl3xBrTRR_rD6fE,31480
47
+ modal/mount.pyi,sha256=mcKMYwt8dX3oO8OzzKqCdh7K2EhMhWIHEPoJwzi1rl8,12194
48
48
  modal/network_file_system.py,sha256=INj1TfN_Fsmabmlte7anvey1epodjbMmjBW_TIJSST4,14406
49
49
  modal/network_file_system.pyi,sha256=61M-sdWrtaRjmuNVsvusI6kf1Qw-jUOVXvEAeOkM8Aw,7751
50
50
  modal/object.py,sha256=HZs3N59C6JxlMuPQWJYvrWV1FEEkH9txUovVDorVUbs,9763
@@ -88,8 +88,8 @@ modal/_utils/async_utils.py,sha256=9ubwMkwiDB4gzOYG2jL9j7Fs-5dxHjcifZe3r7JRg-k,2
88
88
  modal/_utils/blob_utils.py,sha256=N66LtZI8PpCkZ7maA7GLW5CAmYUoNJdG-GjaAUR4_NQ,14509
89
89
  modal/_utils/bytes_io_segment_payload.py,sha256=uunxVJS4PE1LojF_UpURMzVK9GuvmYWRqQo_bxEj5TU,3385
90
90
  modal/_utils/deprecation.py,sha256=dycySRBxyZf3ITzEqPNM6MxXTk9-0VVLA8oCPQ5j_Os,3426
91
- modal/_utils/docker_utils.py,sha256=FLz1q0YicL6i_Iq-4inkgDVFfEINVG6YPT2s_P6ly0o,2264
92
- modal/_utils/function_utils.py,sha256=q68HhFH16MwhHRnGD8jvIgqDjduRQVp3a_qMWXPyrgU,25518
91
+ modal/_utils/docker_utils.py,sha256=h1uETghR40mp_y3fSWuZAfbIASH1HMzuphJHghAL6DU,3722
92
+ modal/_utils/function_utils.py,sha256=VFz3RdQc0yOKRmw5u5gkZCRzkJNqm9WoQAKg9CeUnJo,25521
93
93
  modal/_utils/grpc_testing.py,sha256=H1zHqthv19eGPJz2HKXDyWXWGSqO4BRsxah3L5Xaa8A,8619
94
94
  modal/_utils/grpc_utils.py,sha256=PPB5ay-vXencXNIWPVw5modr3EH7gfq2QPcO5YJ1lMU,7737
95
95
  modal/_utils/hash_utils.py,sha256=zg3J6OGxTFGSFri1qQ12giDz90lWk8bzaxCTUCRtiX4,3034
@@ -115,7 +115,7 @@ modal/cli/dict.py,sha256=HaEcjfll7i3Uj3Fg56aj4407if5UljsYfr6fIq-D2W8,4589
115
115
  modal/cli/entry_point.py,sha256=aaNxFAqZcmtSjwzkYIA_Ba9CkL4cL4_i2gy5VjoXxkM,4228
116
116
  modal/cli/environment.py,sha256=Ayddkiq9jdj3XYDJ8ZmUqFpPPH8xajYlbexRkzGtUcg,4334
117
117
  modal/cli/import_refs.py,sha256=wnqE5AMeyAN3IZmQvJCp54KRnJh8Nq_5fMqB6u6GEL8,9147
118
- modal/cli/launch.py,sha256=44oOlGB0KYDBMfuIlkhW2uzjkWbHhDRR64UOEnGwsJ4,2989
118
+ modal/cli/launch.py,sha256=t1XsExzTWip4n9Cqa-Jhzj65P6bVHO0je2d76JQRi_w,2990
119
119
  modal/cli/network_file_system.py,sha256=o6VLTgN4xn5XUiNPBfxYec-5uWCgYrDmfFFLM1ZW_eE,8180
120
120
  modal/cli/profile.py,sha256=rLXfjJObfPNjaZvNfHGIKqs7y9bGYyGe-K7V0w-Ni0M,3110
121
121
  modal/cli/queues.py,sha256=MIh2OsliNE2QeL1erubfsRsNuG4fxqcqWA2vgIfQ4Mg,4494
@@ -125,8 +125,8 @@ modal/cli/token.py,sha256=mxSgOWakXG6N71hQb1ko61XAR9ZGkTMZD-Txn7gmTac,1924
125
125
  modal/cli/utils.py,sha256=hZmjyzcPjDnQSkLvycZD2LhGdcsfdZshs_rOU78EpvI,3717
126
126
  modal/cli/volume.py,sha256=Jrm-1R9u92JbbUM62bkB9RzAM_jO8wi7T2-i6Cb2XG0,10568
127
127
  modal/cli/programs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
128
- modal/cli/programs/run_jupyter.py,sha256=RRr07CqZrStMbLdBM3PpzU6KM8t9FtLbdIPthg2-Mpw,2755
129
- modal/cli/programs/vscode.py,sha256=m80wuQyTALTc7y-kAVqmMjtrcb6muCtpuhxsJm4Va2Y,3453
128
+ modal/cli/programs/run_jupyter.py,sha256=KNaxG4LixbBJdLOzwKZB5IMA6tO1CSjHMKY6SpaIXd0,2685
129
+ modal/cli/programs/vscode.py,sha256=0k65NdAvRL3WGtngEYt-3w_kn9x2y3PqzA-dqdaVHtw,3383
130
130
  modal/extensions/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
131
131
  modal/extensions/ipython.py,sha256=Xvzy-A7cvwMSDa9p4c4CEMLOX2_Xsg9DkM1J9uyu7jc,983
132
132
  modal/requirements/2023.12.312.txt,sha256=zWWUVgVQ92GXBKNYYr2-5vn9rlnXcmkqlwlX5u1eTYw,400
@@ -146,7 +146,7 @@ modal_global_objects/images/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0
146
146
  modal_global_objects/images/base_images.py,sha256=tFc7tzQRJHtq23kURd6DTrnnO4Yp5ujr34WdJOM5ubI,775
147
147
  modal_global_objects/mounts/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
148
148
  modal_global_objects/mounts/modal_client_package.py,sha256=W0E_yShsRojPzWm6LtIQqNVolapdnrZkm2hVEQuZK_4,767
149
- modal_global_objects/mounts/python_standalone.py,sha256=SL_riIxpd8mP4i4CLDCWiFFNj0Ltknm9c_UIGfX5d60,1836
149
+ modal_global_objects/mounts/python_standalone.py,sha256=pEML5GaV2_0ahci_1vpfc_FnySpsfi2fhYmFF5I7IiQ,1837
150
150
  modal_proto/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
151
151
  modal_proto/api.proto,sha256=1hO6_dn7DwgFra9TQSAXBt1NV4ETiiURPHe09bodinc,80368
152
152
  modal_proto/api_grpc.py,sha256=VakjV_Ge3fgZDRJN6EeG2yY_LMkZvn6yVXr5SnFKIDA,103542
@@ -163,12 +163,12 @@ modal_proto/options_pb2.pyi,sha256=l7DBrbLO7q3Ir-XDkWsajm0d0TQqqrfuX54i4BMpdQg,1
163
163
  modal_proto/options_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
164
164
  modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0yJSI,247
165
165
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
166
- modal_version/__init__.py,sha256=BEBWj9tcbFUwzEjUrqly601rauw5cYsHdcmJHs3iu0s,470
166
+ modal_version/__init__.py,sha256=kGya2ZlItX2zB7oHORs-wvP4PG8lg_mtbi1QIK3G6SQ,470
167
167
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
168
- modal_version/_version_generated.py,sha256=tsI6q2yoqEFbt-h7LyODo6hXvuxvd0qyoes8iA52jUA,149
169
- modal-0.71.12.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
170
- modal-0.71.12.dist-info/METADATA,sha256=7OCMKt2uJg0IqSqDJ8V-vcMrWoLr5dWLVp5SQ0NOXvk,2329
171
- modal-0.71.12.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
172
- modal-0.71.12.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
173
- modal-0.71.12.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
174
- modal-0.71.12.dist-info/RECORD,,
168
+ modal_version/_version_generated.py,sha256=YZkNwzq3yMtpJ2OS9vVQTjRRZGXPKR1UDV1uxVuO4WI,148
169
+ modal-0.72.5.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
170
+ modal-0.72.5.dist-info/METADATA,sha256=hYo1a_nRmFW_lrtVpvKcvxu6-OO1mZu1pj5YosbGk1U,2328
171
+ modal-0.72.5.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
172
+ modal-0.72.5.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
173
+ modal-0.72.5.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
174
+ modal-0.72.5.dist-info/RECORD,,
@@ -34,7 +34,7 @@ def publish_python_standalone_mount(client, version: str) -> None:
34
34
  urllib.request.urlretrieve(url, f"{d}/cpython.tar.gz")
35
35
  shutil.unpack_archive(f"{d}/cpython.tar.gz", d)
36
36
  print(f"🌐 Downloaded and unpacked archive to {d}.")
37
- python_mount = Mount.from_local_dir(f"{d}/python")
37
+ python_mount = Mount._from_local_dir(f"{d}/python")
38
38
  python_mount._deploy(
39
39
  mount_name,
40
40
  api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
modal_version/__init__.py CHANGED
@@ -7,7 +7,7 @@ from ._version_generated import build_number
7
7
  major_number = 0
8
8
 
9
9
  # Bump this manually on breaking changes, then reset the number in _version_generated.py
10
- minor_number = 71
10
+ minor_number = 72
11
11
 
12
12
  # Right now, automatically increment the patch number in CI
13
13
  __version__ = f"{major_number}.{minor_number}.{max(build_number, 0)}"
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
 
3
3
  # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client.
4
- build_number = 12 # git: 54a0b70
4
+ build_number = 5 # git: 8504d66