modal 0.62.115__py3-none-any.whl → 0.72.13__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 (220) hide show
  1. modal/__init__.py +13 -9
  2. modal/__main__.py +41 -3
  3. modal/_clustered_functions.py +80 -0
  4. modal/_clustered_functions.pyi +22 -0
  5. modal/_container_entrypoint.py +402 -398
  6. modal/_ipython.py +3 -13
  7. modal/_location.py +17 -10
  8. modal/_output.py +243 -99
  9. modal/_pty.py +2 -2
  10. modal/_resolver.py +55 -60
  11. modal/_resources.py +26 -7
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1025 -0
  15. modal/{execution_context.py → _runtime/execution_context.py} +11 -2
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +123 -6
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +50 -14
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +386 -104
  24. modal/_utils/blob_utils.py +157 -186
  25. modal/_utils/bytes_io_segment_payload.py +97 -0
  26. modal/_utils/deprecation.py +89 -0
  27. modal/_utils/docker_utils.py +98 -0
  28. modal/_utils/function_utils.py +299 -98
  29. modal/_utils/grpc_testing.py +47 -34
  30. modal/_utils/grpc_utils.py +54 -21
  31. modal/_utils/hash_utils.py +51 -10
  32. modal/_utils/http_utils.py +39 -9
  33. modal/_utils/logger.py +2 -1
  34. modal/_utils/mount_utils.py +34 -16
  35. modal/_utils/name_utils.py +58 -0
  36. modal/_utils/package_utils.py +14 -1
  37. modal/_utils/pattern_utils.py +205 -0
  38. modal/_utils/rand_pb_testing.py +3 -3
  39. modal/_utils/shell_utils.py +15 -49
  40. modal/_vendor/a2wsgi_wsgi.py +62 -72
  41. modal/_vendor/cloudpickle.py +1 -1
  42. modal/_watcher.py +12 -10
  43. modal/app.py +561 -323
  44. modal/app.pyi +474 -262
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +22 -6
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +203 -42
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +61 -13
  51. modal/cli/dict.py +128 -0
  52. modal/cli/entry_point.py +26 -13
  53. modal/cli/environment.py +40 -9
  54. modal/cli/import_refs.py +21 -48
  55. modal/cli/launch.py +28 -14
  56. modal/cli/network_file_system.py +57 -21
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +34 -9
  59. modal/cli/programs/vscode.py +58 -8
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +199 -96
  62. modal/cli/secret.py +5 -4
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +74 -8
  65. modal/cli/volume.py +97 -56
  66. modal/client.py +248 -144
  67. modal/client.pyi +156 -124
  68. modal/cloud_bucket_mount.py +43 -30
  69. modal/cloud_bucket_mount.pyi +32 -25
  70. modal/cls.py +528 -141
  71. modal/cls.pyi +189 -145
  72. modal/config.py +32 -15
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +50 -54
  76. modal/dict.pyi +120 -164
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +30 -43
  80. modal/experimental.py +62 -2
  81. modal/file_io.py +537 -0
  82. modal/file_io.pyi +235 -0
  83. modal/file_pattern_matcher.py +196 -0
  84. modal/functions.py +846 -428
  85. modal/functions.pyi +446 -387
  86. modal/gpu.py +57 -44
  87. modal/image.py +943 -417
  88. modal/image.pyi +584 -245
  89. modal/io_streams.py +434 -0
  90. modal/io_streams.pyi +122 -0
  91. modal/mount.py +223 -90
  92. modal/mount.pyi +241 -243
  93. modal/network_file_system.py +85 -86
  94. modal/network_file_system.pyi +151 -110
  95. modal/object.py +66 -36
  96. modal/object.pyi +166 -143
  97. modal/output.py +63 -0
  98. modal/parallel_map.py +73 -47
  99. modal/parallel_map.pyi +51 -63
  100. modal/partial_function.py +272 -107
  101. modal/partial_function.pyi +219 -120
  102. modal/proxy.py +15 -12
  103. modal/proxy.pyi +3 -8
  104. modal/queue.py +96 -72
  105. modal/queue.pyi +210 -135
  106. modal/requirements/2024.04.txt +2 -1
  107. modal/requirements/2024.10.txt +16 -0
  108. modal/requirements/README.md +21 -0
  109. modal/requirements/base-images.json +22 -0
  110. modal/retries.py +45 -4
  111. modal/runner.py +325 -203
  112. modal/runner.pyi +124 -110
  113. modal/running_app.py +27 -4
  114. modal/sandbox.py +509 -231
  115. modal/sandbox.pyi +396 -169
  116. modal/schedule.py +2 -2
  117. modal/scheduler_placement.py +20 -3
  118. modal/secret.py +41 -25
  119. modal/secret.pyi +62 -42
  120. modal/serving.py +39 -49
  121. modal/serving.pyi +37 -43
  122. modal/stream_type.py +15 -0
  123. modal/token_flow.py +5 -3
  124. modal/token_flow.pyi +37 -32
  125. modal/volume.py +123 -137
  126. modal/volume.pyi +228 -221
  127. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
  128. modal-0.72.13.dist-info/RECORD +174 -0
  129. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
  130. modal_docs/gen_reference_docs.py +3 -1
  131. modal_docs/mdmd/mdmd.py +0 -1
  132. modal_docs/mdmd/signatures.py +1 -2
  133. modal_global_objects/images/base_images.py +28 -0
  134. modal_global_objects/mounts/python_standalone.py +2 -2
  135. modal_proto/__init__.py +1 -1
  136. modal_proto/api.proto +1231 -531
  137. modal_proto/api_grpc.py +750 -430
  138. modal_proto/api_pb2.py +2102 -1176
  139. modal_proto/api_pb2.pyi +8859 -0
  140. modal_proto/api_pb2_grpc.py +1329 -675
  141. modal_proto/api_pb2_grpc.pyi +1416 -0
  142. modal_proto/modal_api_grpc.py +149 -0
  143. modal_proto/modal_options_grpc.py +3 -0
  144. modal_proto/options_pb2.pyi +20 -0
  145. modal_proto/options_pb2_grpc.pyi +7 -0
  146. modal_proto/py.typed +0 -0
  147. modal_version/__init__.py +1 -1
  148. modal_version/_version_generated.py +2 -2
  149. modal/_asgi.py +0 -370
  150. modal/_container_exec.py +0 -128
  151. modal/_container_io_manager.py +0 -646
  152. modal/_container_io_manager.pyi +0 -412
  153. modal/_sandbox_shell.py +0 -49
  154. modal/app_utils.py +0 -20
  155. modal/app_utils.pyi +0 -17
  156. modal/execution_context.pyi +0 -37
  157. modal/shared_volume.py +0 -23
  158. modal/shared_volume.pyi +0 -24
  159. modal-0.62.115.dist-info/RECORD +0 -207
  160. modal_global_objects/images/conda.py +0 -15
  161. modal_global_objects/images/debian_slim.py +0 -15
  162. modal_global_objects/images/micromamba.py +0 -15
  163. test/__init__.py +0 -1
  164. test/aio_test.py +0 -12
  165. test/async_utils_test.py +0 -279
  166. test/blob_test.py +0 -67
  167. test/cli_imports_test.py +0 -149
  168. test/cli_test.py +0 -674
  169. test/client_test.py +0 -203
  170. test/cloud_bucket_mount_test.py +0 -22
  171. test/cls_test.py +0 -636
  172. test/config_test.py +0 -149
  173. test/conftest.py +0 -1485
  174. test/container_app_test.py +0 -50
  175. test/container_test.py +0 -1405
  176. test/cpu_test.py +0 -23
  177. test/decorator_test.py +0 -85
  178. test/deprecation_test.py +0 -34
  179. test/dict_test.py +0 -51
  180. test/e2e_test.py +0 -68
  181. test/error_test.py +0 -7
  182. test/function_serialization_test.py +0 -32
  183. test/function_test.py +0 -791
  184. test/function_utils_test.py +0 -101
  185. test/gpu_test.py +0 -159
  186. test/grpc_utils_test.py +0 -82
  187. test/helpers.py +0 -47
  188. test/image_test.py +0 -814
  189. test/live_reload_test.py +0 -80
  190. test/lookup_test.py +0 -70
  191. test/mdmd_test.py +0 -329
  192. test/mount_test.py +0 -162
  193. test/mounted_files_test.py +0 -327
  194. test/network_file_system_test.py +0 -188
  195. test/notebook_test.py +0 -66
  196. test/object_test.py +0 -41
  197. test/package_utils_test.py +0 -25
  198. test/queue_test.py +0 -115
  199. test/resolver_test.py +0 -59
  200. test/retries_test.py +0 -67
  201. test/runner_test.py +0 -85
  202. test/sandbox_test.py +0 -191
  203. test/schedule_test.py +0 -15
  204. test/scheduler_placement_test.py +0 -57
  205. test/secret_test.py +0 -89
  206. test/serialization_test.py +0 -50
  207. test/stub_composition_test.py +0 -10
  208. test/stub_test.py +0 -361
  209. test/test_asgi_wrapper.py +0 -234
  210. test/token_flow_test.py +0 -18
  211. test/traceback_test.py +0 -135
  212. test/tunnel_test.py +0 -29
  213. test/utils_test.py +0 -88
  214. test/version_test.py +0 -14
  215. test/volume_test.py +0 -397
  216. test/watcher_test.py +0 -58
  217. test/webhook_test.py +0 -145
  218. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
  219. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
  220. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/entry_points.txt +0 -0
modal/mount.py CHANGED
@@ -9,24 +9,28 @@ import sys
9
9
  import sysconfig
10
10
  import time
11
11
  import typing
12
+ from collections.abc import AsyncGenerator
12
13
  from pathlib import Path, PurePosixPath
13
- from typing import AsyncGenerator, Callable, List, Optional, Tuple, Type, Union
14
+ from typing import Callable, Optional, Sequence, Union
14
15
 
15
- import aiostream
16
16
  from google.protobuf.message import Message
17
17
 
18
18
  import modal.exception
19
+ import modal.file_pattern_matcher
19
20
  from modal_proto import api_pb2
20
21
  from modal_version import __version__
21
22
 
22
23
  from ._resolver import Resolver
23
- from ._utils.async_utils import synchronize_api
24
+ from ._utils.async_utils import aclosing, async_map, synchronize_api
24
25
  from ._utils.blob_utils import FileUploadSpec, blob_upload_file, get_file_upload_spec_from_path
26
+ from ._utils.deprecation import deprecation_warning, renamed_parameter
25
27
  from ._utils.grpc_utils import retry_transient_errors
28
+ from ._utils.name_utils import check_object_name
26
29
  from ._utils.package_utils import get_module_mount_info
27
30
  from .client import _Client
28
31
  from .config import config, logger
29
- from .exception import ModuleNotMountable
32
+ from .exception import InvalidError, ModuleNotMountable
33
+ from .file_pattern_matcher import FilePatternMatcher
30
34
  from .object import _get_environment_name, _Object
31
35
 
32
36
  ROOT_DIR: PurePosixPath = PurePosixPath("/root")
@@ -36,14 +40,19 @@ MOUNT_PUT_FILE_CLIENT_TIMEOUT = 10 * 60 # 10 min max for transferring files
36
40
  #
37
41
  # These can be updated safely, but changes will trigger a rebuild for all images
38
42
  # that rely on `add_python()` in their constructor.
39
- PYTHON_STANDALONE_VERSIONS: typing.Dict[str, typing.Tuple[str, str]] = {
40
- "3.8": ("20230826", "3.8.17"),
43
+ PYTHON_STANDALONE_VERSIONS: dict[str, tuple[str, str]] = {
41
44
  "3.9": ("20230826", "3.9.18"),
42
45
  "3.10": ("20230826", "3.10.13"),
43
46
  "3.11": ("20230826", "3.11.5"),
44
47
  "3.12": ("20240107", "3.12.1"),
48
+ "3.13": ("20241008", "3.13.0"),
45
49
  }
46
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
+
47
56
 
48
57
  def client_mount_name() -> str:
49
58
  """Get the deployed name of the client package mount."""
@@ -58,7 +67,8 @@ def python_standalone_mount_name(version: str) -> str:
58
67
  libc = "gnu"
59
68
  if version not in PYTHON_STANDALONE_VERSIONS:
60
69
  raise modal.exception.InvalidError(
61
- f"Unsupported standalone python version: {version!r}, supported values are {list(PYTHON_STANDALONE_VERSIONS)}"
70
+ f"Unsupported standalone python version: {version!r}, supported values are "
71
+ f"{list(PYTHON_STANDALONE_VERSIONS)}"
62
72
  )
63
73
  if libc != "gnu":
64
74
  raise modal.exception.InvalidError(f"Unsupported libc identifier: {libc}")
@@ -72,21 +82,21 @@ class _MountEntry(metaclass=abc.ABCMeta):
72
82
  ...
73
83
 
74
84
  @abc.abstractmethod
75
- def get_files_to_upload(self) -> typing.Iterator[Tuple[Path, str]]:
85
+ def get_files_to_upload(self) -> typing.Iterator[tuple[Path, str]]:
76
86
  ...
77
87
 
78
88
  @abc.abstractmethod
79
- def watch_entry(self) -> Tuple[Path, Path]:
89
+ def watch_entry(self) -> tuple[Path, Path]:
80
90
  ...
81
91
 
82
92
  @abc.abstractmethod
83
- def top_level_paths(self) -> List[Tuple[Path, PurePosixPath]]:
93
+ def top_level_paths(self) -> list[tuple[Path, PurePosixPath]]:
84
94
  ...
85
95
 
86
96
 
87
- def _select_files(entries: List[_MountEntry]) -> List[Tuple[Path, PurePosixPath]]:
97
+ def _select_files(entries: list[_MountEntry]) -> list[tuple[Path, PurePosixPath]]:
88
98
  # TODO: make this async
89
- all_files: typing.Set[Tuple[Path, PurePosixPath]] = set()
99
+ all_files: set[tuple[Path, PurePosixPath]] = set()
90
100
  for entry in entries:
91
101
  all_files |= set(entry.get_files_to_upload())
92
102
  return list(all_files)
@@ -101,7 +111,7 @@ class _MountFile(_MountEntry):
101
111
  return str(self.local_file)
102
112
 
103
113
  def get_files_to_upload(self):
104
- local_file = self.local_file.expanduser().absolute()
114
+ local_file = self.local_file.resolve()
105
115
  if not local_file.exists():
106
116
  raise FileNotFoundError(local_file)
107
117
 
@@ -112,7 +122,7 @@ class _MountFile(_MountEntry):
112
122
  safe_path = self.local_file.expanduser().absolute()
113
123
  return safe_path.parent, safe_path
114
124
 
115
- def top_level_paths(self) -> List[Tuple[Path, PurePosixPath]]:
125
+ def top_level_paths(self) -> list[tuple[Path, PurePosixPath]]:
116
126
  return [(self.local_file, self.remote_path)]
117
127
 
118
128
 
@@ -120,13 +130,15 @@ class _MountFile(_MountEntry):
120
130
  class _MountDir(_MountEntry):
121
131
  local_dir: Path
122
132
  remote_path: PurePosixPath
123
- condition: Callable[[str], bool]
133
+ ignore: Callable[[Path], bool]
124
134
  recursive: bool
125
135
 
126
136
  def description(self):
127
137
  return str(self.local_dir.expanduser().absolute())
128
138
 
129
139
  def get_files_to_upload(self):
140
+ # we can't use .resolve() eagerly here since that could end up "renaming" symlinked files
141
+ # see test_mount_directory_with_symlinked_file
130
142
  local_dir = self.local_dir.expanduser().absolute()
131
143
 
132
144
  if not local_dir.exists():
@@ -141,25 +153,48 @@ class _MountDir(_MountEntry):
141
153
  gen = (dir_entry.path for dir_entry in os.scandir(local_dir) if dir_entry.is_file())
142
154
 
143
155
  for local_filename in gen:
144
- if self.condition(local_filename):
145
- local_relpath = Path(local_filename).expanduser().absolute().relative_to(local_dir)
156
+ local_path = Path(local_filename)
157
+ if not self.ignore(local_path):
158
+ local_relpath = local_path.expanduser().absolute().relative_to(local_dir)
146
159
  mount_path = self.remote_path / local_relpath.as_posix()
147
- yield local_filename, mount_path
160
+ yield local_path.resolve(), mount_path
148
161
 
149
162
  def watch_entry(self):
150
163
  return self.local_dir.resolve().expanduser(), None
151
164
 
152
- def top_level_paths(self) -> List[Tuple[Path, PurePosixPath]]:
165
+ def top_level_paths(self) -> list[tuple[Path, PurePosixPath]]:
153
166
  return [(self.local_dir, self.remote_path)]
154
167
 
155
168
 
156
- def module_mount_condition(f: str):
157
- path = Path(f)
158
- if path.suffix == ".pyc":
159
- return False
160
- if any(p.name.startswith(".") or p.name == "__pycache__" for p in path.parents):
161
- return False
162
- return True
169
+ def module_mount_condition(module_base: Path):
170
+ SKIP_BYTECODE = True # hard coded for now
171
+ SKIP_DOT_PREFIXED = True
172
+
173
+ def condition(f: str):
174
+ path = Path(f)
175
+ if SKIP_BYTECODE and path.suffix == ".pyc":
176
+ return False
177
+
178
+ # Check parent dir names to see if file should be included,
179
+ # but ignore dir names above root of mounted module:
180
+ # /a/.venv/site-packages/mymod/foo.py should be included by default
181
+ # /a/my_mod/.config/foo.py should *not* be included by default
182
+ while path != module_base and path != path.parent:
183
+ if SKIP_BYTECODE and path.name == "__pycache__":
184
+ return False
185
+
186
+ if SKIP_DOT_PREFIXED and path.name.startswith("."):
187
+ return False
188
+
189
+ path = path.parent
190
+
191
+ return True
192
+
193
+ return condition
194
+
195
+
196
+ def module_mount_ignore_condition(module_base: Path):
197
+ return lambda f: not module_mount_condition(module_base)(str(f))
163
198
 
164
199
 
165
200
  @dataclasses.dataclass
@@ -170,12 +205,12 @@ class _MountedPythonModule(_MountEntry):
170
205
 
171
206
  module_name: str
172
207
  remote_dir: Union[PurePosixPath, str] = ROOT_DIR.as_posix() # cast needed here for type stub generation...
173
- condition: typing.Optional[typing.Callable[[str], bool]] = None
208
+ ignore: Optional[Callable[[Path], bool]] = None
174
209
 
175
210
  def description(self) -> str:
176
211
  return f"PythonPackage:{self.module_name}"
177
212
 
178
- def _proxy_entries(self) -> List[_MountEntry]:
213
+ def _proxy_entries(self) -> list[_MountEntry]:
179
214
  mount_infos = get_module_mount_info(self.module_name)
180
215
  entries = []
181
216
  for mount_info in mount_infos:
@@ -184,9 +219,9 @@ class _MountedPythonModule(_MountEntry):
184
219
  remote_dir = PurePosixPath(self.remote_dir, *self.module_name.split("."))
185
220
  entries.append(
186
221
  _MountDir(
187
- Path(base_path),
222
+ base_path,
188
223
  remote_path=remote_dir,
189
- condition=self.condition or module_mount_condition,
224
+ ignore=self.ignore or module_mount_ignore_condition(base_path),
190
225
  recursive=True,
191
226
  )
192
227
  )
@@ -201,16 +236,16 @@ class _MountedPythonModule(_MountEntry):
201
236
  )
202
237
  return entries
203
238
 
204
- def get_files_to_upload(self) -> typing.Iterator[Tuple[Path, str]]:
239
+ def get_files_to_upload(self) -> typing.Iterator[tuple[Path, str]]:
205
240
  for entry in self._proxy_entries():
206
241
  yield from entry.get_files_to_upload()
207
242
 
208
- def watch_entry(self) -> Tuple[Path, Path]:
243
+ def watch_entry(self) -> tuple[Path, Path]:
209
244
  for entry in self._proxy_entries():
210
245
  # TODO: fix watch for mounts of multi-path packages
211
246
  return entry.watch_entry()
212
247
 
213
- def top_level_paths(self) -> List[Tuple[Path, PurePosixPath]]:
248
+ def top_level_paths(self) -> list[tuple[Path, PurePosixPath]]:
214
249
  paths = []
215
250
  for sub in self._proxy_entries():
216
251
  paths.extend(sub.top_level_paths())
@@ -231,7 +266,7 @@ class _Mount(_Object, type_prefix="mo"):
231
266
  ```python
232
267
  import modal
233
268
  import os
234
- app = modal.App() # Note: "app" was called "stub" up until April 2024
269
+ app = modal.App()
235
270
 
236
271
  @app.function(mounts=[modal.Mount.from_local_dir("~/foo", remote_path="/root/foo")])
237
272
  def f():
@@ -243,14 +278,14 @@ class _Mount(_Object, type_prefix="mo"):
243
278
  the file's contents to skip uploading files that have been uploaded before.
244
279
  """
245
280
 
246
- _entries: Optional[List[_MountEntry]] = None
281
+ _entries: Optional[list[_MountEntry]] = None
247
282
  _deployment_name: Optional[str] = None
248
283
  _namespace: Optional[int] = None
249
284
  _environment_name: Optional[str] = None
250
285
  _content_checksum_sha256_hex: Optional[str] = None
251
286
 
252
287
  @staticmethod
253
- def _new(entries: List[_MountEntry] = []) -> "_Mount":
288
+ def _new(entries: list[_MountEntry] = []) -> "_Mount":
254
289
  rep = f"Mount({entries})"
255
290
 
256
291
  async def mount_content_deduplication_key():
@@ -279,10 +314,10 @@ class _Mount(_Object, type_prefix="mo"):
279
314
  assert isinstance(handle_metadata, api_pb2.MountHandleMetadata)
280
315
  self._content_checksum_sha256_hex = handle_metadata.content_checksum_sha256_hex
281
316
 
282
- def _top_level_paths(self) -> List[Tuple[Path, PurePosixPath]]:
317
+ def _top_level_paths(self) -> list[tuple[Path, PurePosixPath]]:
283
318
  # Returns [(local_absolute_path, remote_path), ...] for all top level entries in the Mount
284
319
  # Used to determine if a package mount is installed in a sys directory or not
285
- res: List[Tuple[Path, PurePosixPath]] = []
320
+ res: list[tuple[Path, PurePosixPath]] = []
286
321
  for entry in self.entries:
287
322
  res.extend(entry.top_level_paths())
288
323
  return res
@@ -293,13 +328,29 @@ class _Mount(_Object, type_prefix="mo"):
293
328
  # we can't rely on it to be set. Let's clean this up later.
294
329
  return getattr(self, "_is_local", False)
295
330
 
331
+ @staticmethod
332
+ def _add_local_dir(
333
+ local_path: Path,
334
+ remote_path: PurePosixPath,
335
+ ignore: Callable[[Path], bool] = modal.file_pattern_matcher._NOTHING,
336
+ ):
337
+ return _Mount._new()._extend(
338
+ _MountDir(
339
+ local_dir=local_path,
340
+ ignore=ignore,
341
+ remote_path=remote_path,
342
+ recursive=True,
343
+ ),
344
+ )
345
+
296
346
  def add_local_dir(
297
347
  self,
298
348
  local_path: Union[str, Path],
299
349
  *,
300
350
  # Where the directory is placed within in the mount
301
351
  remote_path: Union[str, PurePosixPath, None] = None,
302
- # Filter function for file selection; defaults to including all files
352
+ # Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
353
+ # Defaults to including all files.
303
354
  condition: Optional[Callable[[str], bool]] = None,
304
355
  # add files from subdirectories as well
305
356
  recursive: bool = True,
@@ -318,10 +369,13 @@ class _Mount(_Object, type_prefix="mo"):
318
369
 
319
370
  condition = include_all
320
371
 
372
+ def converted_condition(path: Path) -> bool:
373
+ return not condition(str(path))
374
+
321
375
  return self._extend(
322
376
  _MountDir(
323
377
  local_dir=local_path,
324
- condition=condition,
378
+ ignore=converted_condition,
325
379
  remote_path=remote_path,
326
380
  recursive=recursive,
327
381
  ),
@@ -333,7 +387,8 @@ class _Mount(_Object, type_prefix="mo"):
333
387
  *,
334
388
  # Where the directory is placed within in the mount
335
389
  remote_path: Union[str, PurePosixPath, None] = None,
336
- # Filter function for file selection - default all files
390
+ # Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
391
+ # Defaults to including all files.
337
392
  condition: Optional[Callable[[str], bool]] = None,
338
393
  # add files from subdirectories as well
339
394
  recursive: bool = True,
@@ -351,12 +406,31 @@ class _Mount(_Object, type_prefix="mo"):
351
406
  )
352
407
  ```
353
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":
354
426
  return _Mount._new().add_local_dir(
355
427
  local_path, remote_path=remote_path, condition=condition, recursive=recursive
356
428
  )
357
429
 
358
430
  def add_local_file(
359
- self, local_path: Union[str, Path], remote_path: Union[str, PurePosixPath, None] = None
431
+ self,
432
+ local_path: Union[str, Path],
433
+ remote_path: Union[str, PurePosixPath, None] = None,
360
434
  ) -> "_Mount":
361
435
  """
362
436
  Add a local file to the `Mount` object.
@@ -387,24 +461,32 @@ class _Mount(_Object, type_prefix="mo"):
387
461
  )
388
462
  ```
389
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":
390
471
  return _Mount._new().add_local_file(local_path, remote_path=remote_path)
391
472
 
392
473
  @staticmethod
393
- def _description(entries: List[_MountEntry]) -> str:
474
+ def _description(entries: list[_MountEntry]) -> str:
394
475
  local_contents = [e.description() for e in entries]
395
476
  return ", ".join(local_contents)
396
477
 
397
478
  @staticmethod
398
- async def _get_files(entries: List[_MountEntry]) -> AsyncGenerator[FileUploadSpec, None]:
479
+ async def _get_files(entries: list[_MountEntry]) -> AsyncGenerator[FileUploadSpec, None]:
399
480
  loop = asyncio.get_event_loop()
400
481
  with concurrent.futures.ThreadPoolExecutor() as exe:
401
482
  all_files = await loop.run_in_executor(exe, _select_files, entries)
402
483
 
403
484
  futs = []
404
485
  for local_filename, remote_filename in all_files:
486
+ logger.debug(f"Mounting {local_filename} as {remote_filename}")
405
487
  futs.append(loop.run_in_executor(exe, get_file_upload_spec_from_path, local_filename, remote_filename))
406
488
 
407
- logger.debug(f"Computing checksums for {len(futs)} files using {exe._max_workers} workers")
489
+ logger.debug(f"Computing checksums for {len(futs)} files using {exe._max_workers} worker threads")
408
490
  for fut in asyncio.as_completed(futs):
409
491
  try:
410
492
  yield await fut
@@ -458,7 +540,9 @@ class _Mount(_Object, type_prefix="mo"):
458
540
  logger.debug(f"Creating blob file for {file_spec.source_description} ({file_spec.size} bytes)")
459
541
  async with blob_upload_concurrency:
460
542
  with file_spec.source() as fp:
461
- blob_id = await blob_upload_file(fp, resolver.client.stub)
543
+ blob_id = await blob_upload_file(
544
+ fp, resolver.client.stub, sha256_hex=file_spec.sha256_hex, md5_hex=file_spec.md5_hex
545
+ )
462
546
  logger.debug(f"Uploading blob file {file_spec.source_description} as {remote_filename}")
463
547
  request2 = api_pb2.MountPutFileRequest(data_blob_id=blob_id, sha256_hex=file_spec.sha256_hex)
464
548
  else:
@@ -476,13 +560,14 @@ class _Mount(_Object, type_prefix="mo"):
476
560
 
477
561
  raise modal.exception.MountUploadTimeoutError(f"Mounting of {file_spec.source_description} timed out")
478
562
 
479
- # Create the asynchronous iterable for file specs.
480
- file_specs = aiostream.stream.iterate(_Mount._get_files(self._entries))
481
-
482
563
  # Upload files, or check if they already exist.
483
564
  n_concurrent_uploads = 512
484
- uploads_stream = aiostream.stream.map(file_specs, _put_file, task_limit=n_concurrent_uploads)
485
- files: List[api_pb2.MountFile] = await aiostream.stream.list(uploads_stream)
565
+ files: list[api_pb2.MountFile] = []
566
+ async with aclosing(
567
+ async_map(_Mount._get_files(self._entries), _put_file, concurrency=n_concurrent_uploads)
568
+ ) as stream:
569
+ async for file in stream:
570
+ files.append(file)
486
571
 
487
572
  if not files:
488
573
  logger.warning(f"Mount of '{message_label}' is empty.")
@@ -497,12 +582,19 @@ class _Mount(_Object, type_prefix="mo"):
497
582
  object_creation_type=api_pb2.OBJECT_CREATION_TYPE_CREATE_FAIL_IF_EXISTS,
498
583
  files=files,
499
584
  )
500
- else:
585
+ elif resolver.app_id is not None:
501
586
  req = api_pb2.MountGetOrCreateRequest(
502
587
  object_creation_type=api_pb2.OBJECT_CREATION_TYPE_ANONYMOUS_OWNED_BY_APP,
503
588
  files=files,
504
589
  app_id=resolver.app_id,
505
590
  )
591
+ else:
592
+ req = api_pb2.MountGetOrCreateRequest(
593
+ object_creation_type=api_pb2.OBJECT_CREATION_TYPE_EPHEMERAL,
594
+ files=files,
595
+ environment_name=resolver.environment_name,
596
+ )
597
+
506
598
  resp = await retry_transient_errors(resolver.client.stub.MountGetOrCreate, req, base_delay=1)
507
599
  status_row.finish(f"Created mount {message_label}")
508
600
 
@@ -513,10 +605,15 @@ class _Mount(_Object, type_prefix="mo"):
513
605
  def from_local_python_packages(
514
606
  *module_names: str,
515
607
  remote_dir: Union[str, PurePosixPath] = ROOT_DIR.as_posix(),
608
+ # Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
609
+ # Defaults to including all files.
516
610
  condition: Optional[Callable[[str], bool]] = None,
611
+ ignore: Optional[Union[Sequence[str], Callable[[Path], bool]]] = None,
517
612
  ) -> "_Mount":
518
- """Returns a `modal.Mount` that makes local modules listed in `module_names` available inside the container.
519
- This works by mounting the local path of each module's package to a directory inside the container that's on `PYTHONPATH`.
613
+ """
614
+ Returns a `modal.Mount` that makes local modules listed in `module_names` available inside the container.
615
+ This works by mounting the local path of each module's package to a directory inside the container
616
+ that's on `PYTHONPATH`.
520
617
 
521
618
  **Usage**
522
619
 
@@ -524,7 +621,7 @@ class _Mount(_Object, type_prefix="mo"):
524
621
  import modal
525
622
  import my_local_module
526
623
 
527
- app = modal.App() # Note: "app" was called "stub" up until April 2024
624
+ app = modal.App()
528
625
 
529
626
  @app.function(mounts=[
530
627
  modal.Mount.from_local_python_packages("my_local_module", "my_other_module"),
@@ -533,27 +630,58 @@ class _Mount(_Object, type_prefix="mo"):
533
630
  my_local_module.do_stuff()
534
631
  ```
535
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
+ )
536
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":
537
651
  # Don't re-run inside container.
538
652
 
653
+ if condition is not None:
654
+ if ignore is not None:
655
+ raise InvalidError("Cannot specify both `ignore` and `condition`")
656
+
657
+ def converted_condition(path: Path) -> bool:
658
+ return not condition(str(path))
659
+
660
+ ignore = converted_condition
661
+ elif isinstance(ignore, list):
662
+ ignore = FilePatternMatcher(*ignore)
663
+
539
664
  mount = _Mount._new()
540
- from .execution_context import is_local
665
+ from ._runtime.execution_context import is_local
541
666
 
542
667
  if not is_local():
543
668
  return mount # empty/non-mountable mount in case it's used from within a container
544
669
  for module_name in module_names:
545
- mount = mount._extend(_MountedPythonModule(module_name, remote_dir, condition))
670
+ mount = mount._extend(_MountedPythonModule(module_name, remote_dir, ignore))
546
671
  return mount
547
672
 
548
673
  @staticmethod
674
+ @renamed_parameter((2024, 12, 18), "label", "name")
549
675
  def from_name(
550
- label: str,
676
+ name: str,
551
677
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
552
678
  environment_name: Optional[str] = None,
553
679
  ) -> "_Mount":
680
+ """mdmd:hidden"""
681
+
554
682
  async def _load(provider: _Mount, resolver: Resolver, existing_object_id: Optional[str]):
555
683
  req = api_pb2.MountGetOrCreateRequest(
556
- deployment_name=label,
684
+ deployment_name=name,
557
685
  namespace=namespace,
558
686
  environment_name=_get_environment_name(environment_name, resolver),
559
687
  )
@@ -563,14 +691,16 @@ class _Mount(_Object, type_prefix="mo"):
563
691
  return _Mount._from_loader(_load, "Mount()")
564
692
 
565
693
  @classmethod
694
+ @renamed_parameter((2024, 12, 18), "label", "name")
566
695
  async def lookup(
567
- cls: Type["_Mount"],
568
- label: str,
696
+ cls: type["_Mount"],
697
+ name: str,
569
698
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
570
699
  client: Optional[_Client] = None,
571
700
  environment_name: Optional[str] = None,
572
701
  ) -> "_Mount":
573
- obj = _Mount.from_name(label, namespace=namespace, environment_name=environment_name)
702
+ """mdmd:hidden"""
703
+ obj = _Mount.from_name(name, namespace=namespace, environment_name=environment_name)
574
704
  if client is None:
575
705
  client = await _Client.from_env()
576
706
  resolver = Resolver(client=client)
@@ -583,13 +713,15 @@ class _Mount(_Object, type_prefix="mo"):
583
713
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
584
714
  environment_name: Optional[str] = None,
585
715
  client: Optional[_Client] = None,
586
- ) -> "_Mount":
716
+ ) -> None:
717
+ check_object_name(deployment_name, "Mount")
718
+ environment_name = _get_environment_name(environment_name, resolver=None)
587
719
  self._deployment_name = deployment_name
588
720
  self._namespace = namespace
589
721
  self._environment_name = environment_name
590
722
  if client is None:
591
723
  client = await _Client.from_env()
592
- resolver = Resolver(client=client)
724
+ resolver = Resolver(client=client, environment_name=environment_name)
593
725
  await resolver.load(self)
594
726
 
595
727
  def _get_metadata(self) -> api_pb2.MountHandleMetadata:
@@ -609,25 +741,27 @@ def _create_client_mount():
609
741
  import modal
610
742
 
611
743
  # Get the base_path because it also contains `modal_proto`.
612
- base_path, _ = os.path.split(modal.__path__[0])
613
-
614
- # TODO(erikbern): this is incredibly dumb, but we only want to include packages that start with "modal"
615
- # TODO(erikbern): merge functionality with function_utils._is_modal_path
616
- prefix = os.path.join(base_path, "modal")
617
-
618
- def condition(arg):
619
- return module_mount_condition(arg) and arg.startswith(prefix)
620
-
621
- return (
622
- _Mount.from_local_dir(base_path, remote_path="/pkg/", condition=condition, recursive=True)
623
- # Mount synchronicity, so version changes don't trigger image rebuilds for users.
624
- .add_local_dir(
625
- synchronicity.__path__[0],
626
- remote_path="/pkg/synchronicity",
627
- condition=module_mount_condition,
744
+ modal_parent_dir, _ = os.path.split(modal.__path__[0])
745
+ client_mount = _Mount._new()
746
+
747
+ for pkg_name in MODAL_PACKAGES:
748
+ package_base_path = Path(modal_parent_dir) / pkg_name
749
+ client_mount = client_mount.add_local_dir(
750
+ package_base_path,
751
+ remote_path=f"/pkg/{pkg_name}",
752
+ condition=module_mount_condition(package_base_path),
628
753
  recursive=True,
629
754
  )
755
+
756
+ # Mount synchronicity, so version changes don't trigger image rebuilds for users.
757
+ synchronicity_base_path = Path(synchronicity.__path__[0])
758
+ client_mount = client_mount.add_local_dir(
759
+ synchronicity_base_path,
760
+ remote_path="/pkg/synchronicity",
761
+ condition=module_mount_condition(synchronicity_base_path),
762
+ recursive=True,
630
763
  )
764
+ return client_mount
631
765
 
632
766
 
633
767
  create_client_mount = synchronize_api(_create_client_mount)
@@ -654,25 +788,25 @@ SYS_PREFIXES = {
654
788
  )
655
789
  }
656
790
 
791
+
657
792
  SYS_PREFIXES |= {p.resolve() for p in SYS_PREFIXES}
658
793
 
794
+ MODAL_PACKAGES = ["modal", "modal_proto", "modal_version"]
795
+
659
796
 
660
797
  def _is_modal_path(remote_path: PurePosixPath):
661
798
  path_prefix = remote_path.parts[:3]
662
799
  remote_python_paths = [("/", "root"), ("/", "pkg")]
663
800
  for base in remote_python_paths:
664
- is_modal_path = path_prefix in [
665
- base + ("modal",),
666
- base + ("modal_proto",),
667
- base + ("modal_version",),
668
- base + ("synchronicity",),
669
- ]
801
+ is_modal_path = path_prefix in [base + (mod,) for mod in MODAL_PACKAGES] or path_prefix == base + (
802
+ "synchronicity",
803
+ )
670
804
  if is_modal_path:
671
805
  return True
672
806
  return False
673
807
 
674
808
 
675
- def get_auto_mounts() -> typing.List[_Mount]:
809
+ def get_auto_mounts() -> list[_Mount]:
676
810
  """mdmd:hidden
677
811
 
678
812
  Auto-mount local modules that have been imported in global scope.
@@ -698,15 +832,14 @@ def get_auto_mounts() -> typing.List[_Mount]:
698
832
 
699
833
  try:
700
834
  # at this point we don't know if the sys.modules module should be mounted or not
701
- potential_mount = _Mount.from_local_python_packages(module_name)
835
+ potential_mount = _Mount._from_local_python_packages(module_name)
702
836
  mount_paths = potential_mount._top_level_paths()
703
837
  except ModuleNotMountable:
704
838
  # this typically happens if the module is a built-in, has binary components or doesn't exist
705
839
  continue
706
840
 
707
841
  for local_path, remote_path in mount_paths:
708
- # TODO: use is_relative_to once we deprecate Python 3.8
709
- if any(str(local_path).startswith(str(p)) for p in SYS_PREFIXES) or _is_modal_path(remote_path):
842
+ if any(local_path.is_relative_to(p) for p in SYS_PREFIXES) or _is_modal_path(remote_path):
710
843
  # skip any module that has paths in SYS_PREFIXES, or would overwrite the modal Package in the container
711
844
  break
712
845
  else: