modal 0.62.16__py3-none-any.whl → 0.72.11__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/__init__.py +17 -13
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +420 -937
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -59
- modal/_resources.py +51 -0
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1036 -0
- modal/_runtime/execution_context.py +89 -0
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +134 -9
- modal/_traceback.py +47 -187
- modal/_tunnel.py +52 -16
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +479 -100
- modal/_utils/blob_utils.py +157 -186
- modal/_utils/bytes_io_segment_payload.py +97 -0
- modal/_utils/deprecation.py +89 -0
- modal/_utils/docker_utils.py +98 -0
- modal/_utils/function_utils.py +460 -171
- modal/_utils/grpc_testing.py +47 -31
- modal/_utils/grpc_utils.py +62 -109
- modal/_utils/hash_utils.py +61 -19
- modal/_utils/http_utils.py +39 -9
- modal/_utils/logger.py +2 -1
- modal/_utils/mount_utils.py +34 -16
- modal/_utils/name_utils.py +58 -0
- modal/_utils/package_utils.py +14 -1
- modal/_utils/pattern_utils.py +205 -0
- modal/_utils/rand_pb_testing.py +5 -7
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +14 -12
- modal/app.py +1003 -314
- modal/app.pyi +540 -264
- modal/call_graph.py +7 -6
- modal/cli/_download.py +63 -53
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +205 -45
- modal/cli/config.py +12 -5
- modal/cli/container.py +62 -14
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +64 -58
- modal/cli/launch.py +32 -18
- modal/cli/network_file_system.py +64 -83
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +35 -10
- modal/cli/programs/vscode.py +60 -10
- modal/cli/queues.py +131 -0
- modal/cli/run.py +234 -131
- modal/cli/secret.py +8 -7
- modal/cli/token.py +7 -2
- modal/cli/utils.py +79 -10
- modal/cli/volume.py +110 -109
- modal/client.py +250 -144
- modal/client.pyi +157 -118
- modal/cloud_bucket_mount.py +108 -34
- modal/cloud_bucket_mount.pyi +32 -38
- modal/cls.py +535 -148
- modal/cls.pyi +190 -146
- modal/config.py +41 -19
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +111 -65
- modal/dict.pyi +136 -131
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +34 -43
- modal/experimental.py +61 -2
- modal/extensions/ipython.py +5 -5
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +197 -0
- modal/functions.py +906 -911
- modal/functions.pyi +466 -430
- modal/gpu.py +57 -44
- modal/image.py +1089 -479
- modal/image.pyi +584 -228
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +314 -101
- modal/mount.pyi +241 -235
- modal/network_file_system.py +92 -92
- modal/network_file_system.pyi +152 -110
- modal/object.py +67 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +434 -0
- modal/parallel_map.pyi +75 -0
- modal/partial_function.py +282 -117
- modal/partial_function.pyi +222 -129
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +182 -65
- modal/queue.pyi +218 -118
- modal/requirements/2024.04.txt +29 -0
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +48 -7
- modal/runner.py +459 -156
- modal/runner.pyi +135 -71
- modal/running_app.py +38 -0
- modal/sandbox.py +514 -236
- modal/sandbox.pyi +397 -169
- modal/schedule.py +4 -4
- modal/scheduler_placement.py +20 -3
- modal/secret.py +56 -31
- modal/secret.pyi +62 -42
- modal/serving.py +51 -56
- modal/serving.pyi +44 -36
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +285 -157
- modal/volume.pyi +249 -184
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
- modal-0.72.11.dist-info/RECORD +174 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
- modal_docs/gen_reference_docs.py +3 -1
- modal_docs/mdmd/mdmd.py +0 -1
- modal_docs/mdmd/signatures.py +5 -2
- modal_global_objects/images/base_images.py +28 -0
- modal_global_objects/mounts/python_standalone.py +2 -2
- modal_proto/__init__.py +1 -1
- modal_proto/api.proto +1288 -533
- modal_proto/api_grpc.py +856 -456
- modal_proto/api_pb2.py +2165 -1157
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1674 -855
- modal_proto/api_pb2_grpc.pyi +1416 -0
- modal_proto/modal_api_grpc.py +149 -0
- modal_proto/modal_options_grpc.py +3 -0
- modal_proto/options_pb2.pyi +20 -0
- modal_proto/options_pb2_grpc.pyi +7 -0
- modal_proto/py.typed +0 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +2 -2
- modal/_asgi.py +0 -370
- modal/_container_entrypoint.pyi +0 -378
- modal/_container_exec.py +0 -128
- modal/_sandbox_shell.py +0 -49
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal/stub.py +0 -783
- modal/stub.pyi +0 -332
- modal-0.62.16.dist-info/RECORD +0 -198
- modal_global_objects/images/conda.py +0 -15
- modal_global_objects/images/debian_slim.py +0 -15
- modal_global_objects/images/micromamba.py +0 -15
- test/__init__.py +0 -1
- test/aio_test.py +0 -12
- test/async_utils_test.py +0 -262
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -659
- test/client_test.py +0 -194
- test/cls_test.py +0 -630
- test/config_test.py +0 -137
- test/conftest.py +0 -1420
- test/container_app_test.py +0 -32
- test/container_test.py +0 -1389
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -33
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -653
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -141
- test/helpers.py +0 -42
- test/image_test.py +0 -669
- test/live_reload_test.py +0 -80
- test/lookup_test.py +0 -70
- test/mdmd_test.py +0 -329
- test/mount_test.py +0 -162
- test/mounted_files_test.py +0 -329
- test/network_file_system_test.py +0 -181
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -97
- test/resolver_test.py +0 -58
- test/retries_test.py +0 -67
- test/runner_test.py +0 -85
- test/sandbox_test.py +0 -191
- test/schedule_test.py +0 -15
- test/scheduler_placement_test.py +0 -29
- test/secret_test.py +0 -78
- test/serialization_test.py +0 -42
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -360
- test/test_asgi_wrapper.py +0 -234
- test/token_flow_test.py +0 -18
- test/traceback_test.py +0 -135
- test/tunnel_test.py +0 -29
- test/utils_test.py +0 -88
- test/version_test.py +0 -14
- test/volume_test.py +0 -341
- test/watcher_test.py +0 -30
- test/webhook_test.py +0 -146
- /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
- /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/entry_points.txt +0 -0
modal/mount.py
CHANGED
@@ -4,25 +4,33 @@ import asyncio
|
|
4
4
|
import concurrent.futures
|
5
5
|
import dataclasses
|
6
6
|
import os
|
7
|
+
import site
|
8
|
+
import sys
|
9
|
+
import sysconfig
|
7
10
|
import time
|
8
11
|
import typing
|
12
|
+
from collections.abc import AsyncGenerator
|
9
13
|
from pathlib import Path, PurePosixPath
|
10
|
-
from typing import
|
14
|
+
from typing import Callable, Optional, Sequence, Union
|
11
15
|
|
12
|
-
import aiostream
|
13
16
|
from google.protobuf.message import Message
|
14
17
|
|
15
18
|
import modal.exception
|
19
|
+
import modal.file_pattern_matcher
|
16
20
|
from modal_proto import api_pb2
|
17
21
|
from modal_version import __version__
|
18
22
|
|
19
23
|
from ._resolver import Resolver
|
20
|
-
from ._utils.async_utils import synchronize_api
|
24
|
+
from ._utils.async_utils import aclosing, async_map, synchronize_api
|
21
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
|
22
27
|
from ._utils.grpc_utils import retry_transient_errors
|
28
|
+
from ._utils.name_utils import check_object_name
|
23
29
|
from ._utils.package_utils import get_module_mount_info
|
24
30
|
from .client import _Client
|
25
31
|
from .config import config, logger
|
32
|
+
from .exception import InvalidError, ModuleNotMountable
|
33
|
+
from .file_pattern_matcher import FilePatternMatcher
|
26
34
|
from .object import _get_environment_name, _Object
|
27
35
|
|
28
36
|
ROOT_DIR: PurePosixPath = PurePosixPath("/root")
|
@@ -32,14 +40,19 @@ MOUNT_PUT_FILE_CLIENT_TIMEOUT = 10 * 60 # 10 min max for transferring files
|
|
32
40
|
#
|
33
41
|
# These can be updated safely, but changes will trigger a rebuild for all images
|
34
42
|
# that rely on `add_python()` in their constructor.
|
35
|
-
PYTHON_STANDALONE_VERSIONS:
|
36
|
-
"3.8": ("20230826", "3.8.17"),
|
43
|
+
PYTHON_STANDALONE_VERSIONS: dict[str, tuple[str, str]] = {
|
37
44
|
"3.9": ("20230826", "3.9.18"),
|
38
45
|
"3.10": ("20230826", "3.10.13"),
|
39
46
|
"3.11": ("20230826", "3.11.5"),
|
40
47
|
"3.12": ("20240107", "3.12.1"),
|
48
|
+
"3.13": ("20241008", "3.13.0"),
|
41
49
|
}
|
42
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
|
+
|
43
56
|
|
44
57
|
def client_mount_name() -> str:
|
45
58
|
"""Get the deployed name of the client package mount."""
|
@@ -54,7 +67,8 @@ def python_standalone_mount_name(version: str) -> str:
|
|
54
67
|
libc = "gnu"
|
55
68
|
if version not in PYTHON_STANDALONE_VERSIONS:
|
56
69
|
raise modal.exception.InvalidError(
|
57
|
-
f"Unsupported standalone python version: {version}, supported values are
|
70
|
+
f"Unsupported standalone python version: {version!r}, supported values are "
|
71
|
+
f"{list(PYTHON_STANDALONE_VERSIONS)}"
|
58
72
|
)
|
59
73
|
if libc != "gnu":
|
60
74
|
raise modal.exception.InvalidError(f"Unsupported libc identifier: {libc}")
|
@@ -68,21 +82,21 @@ class _MountEntry(metaclass=abc.ABCMeta):
|
|
68
82
|
...
|
69
83
|
|
70
84
|
@abc.abstractmethod
|
71
|
-
def get_files_to_upload(self) -> typing.Iterator[
|
85
|
+
def get_files_to_upload(self) -> typing.Iterator[tuple[Path, str]]:
|
72
86
|
...
|
73
87
|
|
74
88
|
@abc.abstractmethod
|
75
|
-
def watch_entry(self) ->
|
89
|
+
def watch_entry(self) -> tuple[Path, Path]:
|
76
90
|
...
|
77
91
|
|
78
92
|
@abc.abstractmethod
|
79
|
-
def top_level_paths(self) ->
|
93
|
+
def top_level_paths(self) -> list[tuple[Path, PurePosixPath]]:
|
80
94
|
...
|
81
95
|
|
82
96
|
|
83
|
-
def _select_files(entries:
|
97
|
+
def _select_files(entries: list[_MountEntry]) -> list[tuple[Path, PurePosixPath]]:
|
84
98
|
# TODO: make this async
|
85
|
-
all_files:
|
99
|
+
all_files: set[tuple[Path, PurePosixPath]] = set()
|
86
100
|
for entry in entries:
|
87
101
|
all_files |= set(entry.get_files_to_upload())
|
88
102
|
return list(all_files)
|
@@ -97,7 +111,7 @@ class _MountFile(_MountEntry):
|
|
97
111
|
return str(self.local_file)
|
98
112
|
|
99
113
|
def get_files_to_upload(self):
|
100
|
-
local_file = self.local_file.
|
114
|
+
local_file = self.local_file.resolve()
|
101
115
|
if not local_file.exists():
|
102
116
|
raise FileNotFoundError(local_file)
|
103
117
|
|
@@ -108,7 +122,7 @@ class _MountFile(_MountEntry):
|
|
108
122
|
safe_path = self.local_file.expanduser().absolute()
|
109
123
|
return safe_path.parent, safe_path
|
110
124
|
|
111
|
-
def top_level_paths(self) ->
|
125
|
+
def top_level_paths(self) -> list[tuple[Path, PurePosixPath]]:
|
112
126
|
return [(self.local_file, self.remote_path)]
|
113
127
|
|
114
128
|
|
@@ -116,13 +130,15 @@ class _MountFile(_MountEntry):
|
|
116
130
|
class _MountDir(_MountEntry):
|
117
131
|
local_dir: Path
|
118
132
|
remote_path: PurePosixPath
|
119
|
-
|
133
|
+
ignore: Callable[[Path], bool]
|
120
134
|
recursive: bool
|
121
135
|
|
122
136
|
def description(self):
|
123
137
|
return str(self.local_dir.expanduser().absolute())
|
124
138
|
|
125
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
|
126
142
|
local_dir = self.local_dir.expanduser().absolute()
|
127
143
|
|
128
144
|
if not local_dir.exists():
|
@@ -137,25 +153,48 @@ class _MountDir(_MountEntry):
|
|
137
153
|
gen = (dir_entry.path for dir_entry in os.scandir(local_dir) if dir_entry.is_file())
|
138
154
|
|
139
155
|
for local_filename in gen:
|
140
|
-
|
141
|
-
|
156
|
+
local_path = Path(local_filename)
|
157
|
+
if not self.ignore(local_path):
|
158
|
+
local_relpath = local_path.expanduser().absolute().relative_to(local_dir)
|
142
159
|
mount_path = self.remote_path / local_relpath.as_posix()
|
143
|
-
yield
|
160
|
+
yield local_path.resolve(), mount_path
|
144
161
|
|
145
162
|
def watch_entry(self):
|
146
163
|
return self.local_dir.resolve().expanduser(), None
|
147
164
|
|
148
|
-
def top_level_paths(self) ->
|
165
|
+
def top_level_paths(self) -> list[tuple[Path, PurePosixPath]]:
|
149
166
|
return [(self.local_dir, self.remote_path)]
|
150
167
|
|
151
168
|
|
152
|
-
def module_mount_condition(
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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))
|
159
198
|
|
160
199
|
|
161
200
|
@dataclasses.dataclass
|
@@ -166,12 +205,12 @@ class _MountedPythonModule(_MountEntry):
|
|
166
205
|
|
167
206
|
module_name: str
|
168
207
|
remote_dir: Union[PurePosixPath, str] = ROOT_DIR.as_posix() # cast needed here for type stub generation...
|
169
|
-
|
208
|
+
ignore: Optional[Callable[[Path], bool]] = None
|
170
209
|
|
171
210
|
def description(self) -> str:
|
172
211
|
return f"PythonPackage:{self.module_name}"
|
173
212
|
|
174
|
-
def _proxy_entries(self) ->
|
213
|
+
def _proxy_entries(self) -> list[_MountEntry]:
|
175
214
|
mount_infos = get_module_mount_info(self.module_name)
|
176
215
|
entries = []
|
177
216
|
for mount_info in mount_infos:
|
@@ -180,9 +219,9 @@ class _MountedPythonModule(_MountEntry):
|
|
180
219
|
remote_dir = PurePosixPath(self.remote_dir, *self.module_name.split("."))
|
181
220
|
entries.append(
|
182
221
|
_MountDir(
|
183
|
-
|
222
|
+
base_path,
|
184
223
|
remote_path=remote_dir,
|
185
|
-
|
224
|
+
ignore=self.ignore or module_mount_ignore_condition(base_path),
|
186
225
|
recursive=True,
|
187
226
|
)
|
188
227
|
)
|
@@ -197,16 +236,16 @@ class _MountedPythonModule(_MountEntry):
|
|
197
236
|
)
|
198
237
|
return entries
|
199
238
|
|
200
|
-
def get_files_to_upload(self) -> typing.Iterator[
|
239
|
+
def get_files_to_upload(self) -> typing.Iterator[tuple[Path, str]]:
|
201
240
|
for entry in self._proxy_entries():
|
202
241
|
yield from entry.get_files_to_upload()
|
203
242
|
|
204
|
-
def watch_entry(self) ->
|
243
|
+
def watch_entry(self) -> tuple[Path, Path]:
|
205
244
|
for entry in self._proxy_entries():
|
206
245
|
# TODO: fix watch for mounts of multi-path packages
|
207
246
|
return entry.watch_entry()
|
208
247
|
|
209
|
-
def top_level_paths(self) ->
|
248
|
+
def top_level_paths(self) -> list[tuple[Path, PurePosixPath]]:
|
210
249
|
paths = []
|
211
250
|
for sub in self._proxy_entries():
|
212
251
|
paths.extend(sub.top_level_paths())
|
@@ -227,9 +266,9 @@ class _Mount(_Object, type_prefix="mo"):
|
|
227
266
|
```python
|
228
267
|
import modal
|
229
268
|
import os
|
230
|
-
|
269
|
+
app = modal.App()
|
231
270
|
|
232
|
-
@
|
271
|
+
@app.function(mounts=[modal.Mount.from_local_dir("~/foo", remote_path="/root/foo")])
|
233
272
|
def f():
|
234
273
|
# `/root/foo` has the contents of `~/foo`.
|
235
274
|
print(os.listdir("/root/foo/"))
|
@@ -239,14 +278,14 @@ class _Mount(_Object, type_prefix="mo"):
|
|
239
278
|
the file's contents to skip uploading files that have been uploaded before.
|
240
279
|
"""
|
241
280
|
|
242
|
-
_entries: Optional[
|
281
|
+
_entries: Optional[list[_MountEntry]] = None
|
243
282
|
_deployment_name: Optional[str] = None
|
244
283
|
_namespace: Optional[int] = None
|
245
284
|
_environment_name: Optional[str] = None
|
246
285
|
_content_checksum_sha256_hex: Optional[str] = None
|
247
286
|
|
248
287
|
@staticmethod
|
249
|
-
def _new(entries:
|
288
|
+
def _new(entries: list[_MountEntry] = []) -> "_Mount":
|
250
289
|
rep = f"Mount({entries})"
|
251
290
|
|
252
291
|
async def mount_content_deduplication_key():
|
@@ -275,10 +314,10 @@ class _Mount(_Object, type_prefix="mo"):
|
|
275
314
|
assert isinstance(handle_metadata, api_pb2.MountHandleMetadata)
|
276
315
|
self._content_checksum_sha256_hex = handle_metadata.content_checksum_sha256_hex
|
277
316
|
|
278
|
-
def _top_level_paths(self) ->
|
317
|
+
def _top_level_paths(self) -> list[tuple[Path, PurePosixPath]]:
|
279
318
|
# Returns [(local_absolute_path, remote_path), ...] for all top level entries in the Mount
|
280
319
|
# Used to determine if a package mount is installed in a sys directory or not
|
281
|
-
res:
|
320
|
+
res: list[tuple[Path, PurePosixPath]] = []
|
282
321
|
for entry in self.entries:
|
283
322
|
res.extend(entry.top_level_paths())
|
284
323
|
return res
|
@@ -289,13 +328,29 @@ class _Mount(_Object, type_prefix="mo"):
|
|
289
328
|
# we can't rely on it to be set. Let's clean this up later.
|
290
329
|
return getattr(self, "_is_local", False)
|
291
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
|
+
|
292
346
|
def add_local_dir(
|
293
347
|
self,
|
294
348
|
local_path: Union[str, Path],
|
295
349
|
*,
|
296
350
|
# Where the directory is placed within in the mount
|
297
351
|
remote_path: Union[str, PurePosixPath, None] = None,
|
298
|
-
#
|
352
|
+
# Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
|
353
|
+
# Defaults to including all files.
|
299
354
|
condition: Optional[Callable[[str], bool]] = None,
|
300
355
|
# add files from subdirectories as well
|
301
356
|
recursive: bool = True,
|
@@ -314,10 +369,13 @@ class _Mount(_Object, type_prefix="mo"):
|
|
314
369
|
|
315
370
|
condition = include_all
|
316
371
|
|
372
|
+
def converted_condition(path: Path) -> bool:
|
373
|
+
return not condition(str(path))
|
374
|
+
|
317
375
|
return self._extend(
|
318
376
|
_MountDir(
|
319
377
|
local_dir=local_path,
|
320
|
-
|
378
|
+
ignore=converted_condition,
|
321
379
|
remote_path=remote_path,
|
322
380
|
recursive=recursive,
|
323
381
|
),
|
@@ -329,7 +387,8 @@ class _Mount(_Object, type_prefix="mo"):
|
|
329
387
|
*,
|
330
388
|
# Where the directory is placed within in the mount
|
331
389
|
remote_path: Union[str, PurePosixPath, None] = None,
|
332
|
-
#
|
390
|
+
# Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
|
391
|
+
# Defaults to including all files.
|
333
392
|
condition: Optional[Callable[[str], bool]] = None,
|
334
393
|
# add files from subdirectories as well
|
335
394
|
recursive: bool = True,
|
@@ -347,12 +406,31 @@ class _Mount(_Object, type_prefix="mo"):
|
|
347
406
|
)
|
348
407
|
```
|
349
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":
|
350
426
|
return _Mount._new().add_local_dir(
|
351
427
|
local_path, remote_path=remote_path, condition=condition, recursive=recursive
|
352
428
|
)
|
353
429
|
|
354
430
|
def add_local_file(
|
355
|
-
self,
|
431
|
+
self,
|
432
|
+
local_path: Union[str, Path],
|
433
|
+
remote_path: Union[str, PurePosixPath, None] = None,
|
356
434
|
) -> "_Mount":
|
357
435
|
"""
|
358
436
|
Add a local file to the `Mount` object.
|
@@ -383,24 +461,32 @@ class _Mount(_Object, type_prefix="mo"):
|
|
383
461
|
)
|
384
462
|
```
|
385
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":
|
386
471
|
return _Mount._new().add_local_file(local_path, remote_path=remote_path)
|
387
472
|
|
388
473
|
@staticmethod
|
389
|
-
def _description(entries:
|
474
|
+
def _description(entries: list[_MountEntry]) -> str:
|
390
475
|
local_contents = [e.description() for e in entries]
|
391
476
|
return ", ".join(local_contents)
|
392
477
|
|
393
478
|
@staticmethod
|
394
|
-
async def _get_files(entries:
|
479
|
+
async def _get_files(entries: list[_MountEntry]) -> AsyncGenerator[FileUploadSpec, None]:
|
395
480
|
loop = asyncio.get_event_loop()
|
396
481
|
with concurrent.futures.ThreadPoolExecutor() as exe:
|
397
482
|
all_files = await loop.run_in_executor(exe, _select_files, entries)
|
398
483
|
|
399
484
|
futs = []
|
400
485
|
for local_filename, remote_filename in all_files:
|
486
|
+
logger.debug(f"Mounting {local_filename} as {remote_filename}")
|
401
487
|
futs.append(loop.run_in_executor(exe, get_file_upload_spec_from_path, local_filename, remote_filename))
|
402
488
|
|
403
|
-
logger.debug(f"Computing checksums for {len(futs)} files using {exe._max_workers}
|
489
|
+
logger.debug(f"Computing checksums for {len(futs)} files using {exe._max_workers} worker threads")
|
404
490
|
for fut in asyncio.as_completed(futs):
|
405
491
|
try:
|
406
492
|
yield await fut
|
@@ -413,21 +499,20 @@ class _Mount(_Object, type_prefix="mo"):
|
|
413
499
|
resolver: Resolver,
|
414
500
|
existing_object_id: Optional[str],
|
415
501
|
):
|
416
|
-
|
417
|
-
t0 = time.time()
|
418
|
-
n_concurrent_uploads = 16
|
502
|
+
t0 = time.monotonic()
|
419
503
|
|
420
|
-
|
421
|
-
|
422
|
-
total_bytes = 0
|
504
|
+
# Asynchronously list and checksum files with a thread pool, then upload them concurrently.
|
505
|
+
n_seen, n_finished = 0, 0
|
506
|
+
total_uploads, total_bytes = 0, 0
|
507
|
+
accounted_hashes: set[str] = set()
|
423
508
|
message_label = _Mount._description(self._entries)
|
509
|
+
blob_upload_concurrency = asyncio.Semaphore(16) # Limit uploads of large files.
|
424
510
|
status_row = resolver.add_status_row()
|
425
511
|
|
426
512
|
async def _put_file(file_spec: FileUploadSpec) -> api_pb2.MountFile:
|
427
|
-
nonlocal
|
428
|
-
|
429
|
-
|
430
|
-
)
|
513
|
+
nonlocal n_seen, n_finished, total_uploads, total_bytes
|
514
|
+
n_seen += 1
|
515
|
+
status_row.message(f"Creating mount {message_label}: Uploaded {n_finished}/{n_seen} files")
|
431
516
|
|
432
517
|
remote_filename = file_spec.mount_filename
|
433
518
|
mount_file = api_pb2.MountFile(
|
@@ -436,23 +521,28 @@ class _Mount(_Object, type_prefix="mo"):
|
|
436
521
|
mode=file_spec.mode,
|
437
522
|
)
|
438
523
|
|
439
|
-
if file_spec.sha256_hex in
|
524
|
+
if file_spec.sha256_hex in accounted_hashes:
|
525
|
+
n_finished += 1
|
440
526
|
return mount_file
|
441
527
|
|
442
528
|
request = api_pb2.MountPutFileRequest(sha256_hex=file_spec.sha256_hex)
|
529
|
+
accounted_hashes.add(file_spec.sha256_hex)
|
443
530
|
response = await retry_transient_errors(resolver.client.stub.MountPutFile, request, base_delay=1)
|
444
531
|
|
445
|
-
n_files += 1
|
446
532
|
if response.exists:
|
533
|
+
n_finished += 1
|
447
534
|
return mount_file
|
448
535
|
|
449
|
-
|
536
|
+
total_uploads += 1
|
450
537
|
total_bytes += file_spec.size
|
451
538
|
|
452
539
|
if file_spec.use_blob:
|
453
540
|
logger.debug(f"Creating blob file for {file_spec.source_description} ({file_spec.size} bytes)")
|
454
|
-
with
|
455
|
-
|
541
|
+
async with blob_upload_concurrency:
|
542
|
+
with file_spec.source() as fp:
|
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
|
+
)
|
456
546
|
logger.debug(f"Uploading blob file {file_spec.source_description} as {remote_filename}")
|
457
547
|
request2 = api_pb2.MountPutFileRequest(data_blob_id=blob_id, sha256_hex=file_spec.sha256_hex)
|
458
548
|
else:
|
@@ -465,24 +555,25 @@ class _Mount(_Object, type_prefix="mo"):
|
|
465
555
|
while time.monotonic() - start_time < MOUNT_PUT_FILE_CLIENT_TIMEOUT:
|
466
556
|
response = await retry_transient_errors(resolver.client.stub.MountPutFile, request2, base_delay=1)
|
467
557
|
if response.exists:
|
558
|
+
n_finished += 1
|
468
559
|
return mount_file
|
469
560
|
|
470
561
|
raise modal.exception.MountUploadTimeoutError(f"Mounting of {file_spec.source_description} timed out")
|
471
562
|
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
563
|
+
# Upload files, or check if they already exist.
|
564
|
+
n_concurrent_uploads = 512
|
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)
|
480
571
|
|
481
572
|
if not files:
|
482
573
|
logger.warning(f"Mount of '{message_label}' is empty.")
|
483
574
|
|
484
|
-
# Build
|
485
|
-
status_row.message(f"Creating mount {message_label}:
|
575
|
+
# Build the mount.
|
576
|
+
status_row.message(f"Creating mount {message_label}: Finalizing index of {len(files)} files")
|
486
577
|
if self._deployment_name:
|
487
578
|
req = api_pb2.MountGetOrCreateRequest(
|
488
579
|
deployment_name=self._deployment_name,
|
@@ -491,26 +582,38 @@ class _Mount(_Object, type_prefix="mo"):
|
|
491
582
|
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_CREATE_FAIL_IF_EXISTS,
|
492
583
|
files=files,
|
493
584
|
)
|
494
|
-
|
585
|
+
elif resolver.app_id is not None:
|
495
586
|
req = api_pb2.MountGetOrCreateRequest(
|
496
587
|
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_ANONYMOUS_OWNED_BY_APP,
|
497
588
|
files=files,
|
498
589
|
app_id=resolver.app_id,
|
499
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
|
+
|
500
598
|
resp = await retry_transient_errors(resolver.client.stub.MountGetOrCreate, req, base_delay=1)
|
501
599
|
status_row.finish(f"Created mount {message_label}")
|
502
600
|
|
503
|
-
logger.debug(f"Uploaded {
|
601
|
+
logger.debug(f"Uploaded {total_uploads} new files and {total_bytes} bytes in {time.monotonic() - t0}s")
|
504
602
|
self._hydrate(resp.mount_id, resolver.client, resp.handle_metadata)
|
505
603
|
|
506
604
|
@staticmethod
|
507
605
|
def from_local_python_packages(
|
508
606
|
*module_names: str,
|
509
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.
|
510
610
|
condition: Optional[Callable[[str], bool]] = None,
|
611
|
+
ignore: Optional[Union[Sequence[str], Callable[[Path], bool]]] = None,
|
511
612
|
) -> "_Mount":
|
512
|
-
"""
|
513
|
-
|
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`.
|
514
617
|
|
515
618
|
**Usage**
|
516
619
|
|
@@ -518,36 +621,67 @@ class _Mount(_Object, type_prefix="mo"):
|
|
518
621
|
import modal
|
519
622
|
import my_local_module
|
520
623
|
|
521
|
-
|
624
|
+
app = modal.App()
|
522
625
|
|
523
|
-
@
|
626
|
+
@app.function(mounts=[
|
524
627
|
modal.Mount.from_local_python_packages("my_local_module", "my_other_module"),
|
525
628
|
])
|
526
629
|
def f():
|
527
630
|
my_local_module.do_stuff()
|
528
631
|
```
|
529
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
|
+
)
|
530
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":
|
531
651
|
# Don't re-run inside container.
|
532
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
|
+
|
533
664
|
mount = _Mount._new()
|
534
|
-
from
|
665
|
+
from ._runtime.execution_context import is_local
|
535
666
|
|
536
667
|
if not is_local():
|
537
668
|
return mount # empty/non-mountable mount in case it's used from within a container
|
538
669
|
for module_name in module_names:
|
539
|
-
mount = mount._extend(_MountedPythonModule(module_name, remote_dir,
|
670
|
+
mount = mount._extend(_MountedPythonModule(module_name, remote_dir, ignore))
|
540
671
|
return mount
|
541
672
|
|
542
673
|
@staticmethod
|
674
|
+
@renamed_parameter((2024, 12, 18), "label", "name")
|
543
675
|
def from_name(
|
544
|
-
|
676
|
+
name: str,
|
545
677
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
546
678
|
environment_name: Optional[str] = None,
|
547
679
|
) -> "_Mount":
|
680
|
+
"""mdmd:hidden"""
|
681
|
+
|
548
682
|
async def _load(provider: _Mount, resolver: Resolver, existing_object_id: Optional[str]):
|
549
683
|
req = api_pb2.MountGetOrCreateRequest(
|
550
|
-
deployment_name=
|
684
|
+
deployment_name=name,
|
551
685
|
namespace=namespace,
|
552
686
|
environment_name=_get_environment_name(environment_name, resolver),
|
553
687
|
)
|
@@ -557,14 +691,16 @@ class _Mount(_Object, type_prefix="mo"):
|
|
557
691
|
return _Mount._from_loader(_load, "Mount()")
|
558
692
|
|
559
693
|
@classmethod
|
694
|
+
@renamed_parameter((2024, 12, 18), "label", "name")
|
560
695
|
async def lookup(
|
561
|
-
cls:
|
562
|
-
|
696
|
+
cls: type["_Mount"],
|
697
|
+
name: str,
|
563
698
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
564
699
|
client: Optional[_Client] = None,
|
565
700
|
environment_name: Optional[str] = None,
|
566
701
|
) -> "_Mount":
|
567
|
-
|
702
|
+
"""mdmd:hidden"""
|
703
|
+
obj = _Mount.from_name(name, namespace=namespace, environment_name=environment_name)
|
568
704
|
if client is None:
|
569
705
|
client = await _Client.from_env()
|
570
706
|
resolver = Resolver(client=client)
|
@@ -577,13 +713,15 @@ class _Mount(_Object, type_prefix="mo"):
|
|
577
713
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
578
714
|
environment_name: Optional[str] = None,
|
579
715
|
client: Optional[_Client] = None,
|
580
|
-
) ->
|
716
|
+
) -> None:
|
717
|
+
check_object_name(deployment_name, "Mount")
|
718
|
+
environment_name = _get_environment_name(environment_name, resolver=None)
|
581
719
|
self._deployment_name = deployment_name
|
582
720
|
self._namespace = namespace
|
583
721
|
self._environment_name = environment_name
|
584
722
|
if client is None:
|
585
723
|
client = await _Client.from_env()
|
586
|
-
resolver = Resolver(client=client)
|
724
|
+
resolver = Resolver(client=client, environment_name=environment_name)
|
587
725
|
await resolver.load(self)
|
588
726
|
|
589
727
|
def _get_metadata(self) -> api_pb2.MountHandleMetadata:
|
@@ -603,25 +741,27 @@ def _create_client_mount():
|
|
603
741
|
import modal
|
604
742
|
|
605
743
|
# Get the base_path because it also contains `modal_proto`.
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
return (
|
616
|
-
_Mount.from_local_dir(base_path, remote_path="/pkg/", condition=condition, recursive=True)
|
617
|
-
# Mount synchronicity, so version changes don't trigger image rebuilds for users.
|
618
|
-
.add_local_dir(
|
619
|
-
synchronicity.__path__[0],
|
620
|
-
remote_path="/pkg/synchronicity",
|
621
|
-
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),
|
622
753
|
recursive=True,
|
623
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,
|
624
763
|
)
|
764
|
+
return client_mount
|
625
765
|
|
626
766
|
|
627
767
|
create_client_mount = synchronize_api(_create_client_mount)
|
@@ -633,3 +773,76 @@ def _get_client_mount():
|
|
633
773
|
return _create_client_mount()
|
634
774
|
else:
|
635
775
|
return _Mount.from_name(client_mount_name(), namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL)
|
776
|
+
|
777
|
+
|
778
|
+
SYS_PREFIXES = {
|
779
|
+
Path(p)
|
780
|
+
for p in (
|
781
|
+
sys.prefix,
|
782
|
+
sys.base_prefix,
|
783
|
+
sys.exec_prefix,
|
784
|
+
sys.base_exec_prefix,
|
785
|
+
*sysconfig.get_paths().values(),
|
786
|
+
*site.getsitepackages(),
|
787
|
+
site.getusersitepackages(),
|
788
|
+
)
|
789
|
+
}
|
790
|
+
|
791
|
+
|
792
|
+
SYS_PREFIXES |= {p.resolve() for p in SYS_PREFIXES}
|
793
|
+
|
794
|
+
MODAL_PACKAGES = ["modal", "modal_proto", "modal_version"]
|
795
|
+
|
796
|
+
|
797
|
+
def _is_modal_path(remote_path: PurePosixPath):
|
798
|
+
path_prefix = remote_path.parts[:3]
|
799
|
+
remote_python_paths = [("/", "root"), ("/", "pkg")]
|
800
|
+
for base in remote_python_paths:
|
801
|
+
is_modal_path = path_prefix in [base + (mod,) for mod in MODAL_PACKAGES] or path_prefix == base + (
|
802
|
+
"synchronicity",
|
803
|
+
)
|
804
|
+
if is_modal_path:
|
805
|
+
return True
|
806
|
+
return False
|
807
|
+
|
808
|
+
|
809
|
+
def get_auto_mounts() -> list[_Mount]:
|
810
|
+
"""mdmd:hidden
|
811
|
+
|
812
|
+
Auto-mount local modules that have been imported in global scope.
|
813
|
+
This may or may not include the "entrypoint" of the function as well, depending on how modal is invoked
|
814
|
+
Note: sys.modules may change during the iteration
|
815
|
+
"""
|
816
|
+
auto_mounts = []
|
817
|
+
top_level_modules = []
|
818
|
+
skip_prefixes = set()
|
819
|
+
for name, module in sorted(sys.modules.items(), key=lambda kv: len(kv[0])):
|
820
|
+
parent = name.rsplit(".")[0]
|
821
|
+
if parent and parent in skip_prefixes:
|
822
|
+
skip_prefixes.add(name)
|
823
|
+
continue
|
824
|
+
skip_prefixes.add(name)
|
825
|
+
top_level_modules.append((name, module))
|
826
|
+
|
827
|
+
for module_name, module in top_level_modules:
|
828
|
+
if module_name.startswith("__"):
|
829
|
+
# skip "built in" modules like __main__ and __mp_main__
|
830
|
+
# the running function's main file should be included anyway
|
831
|
+
continue
|
832
|
+
|
833
|
+
try:
|
834
|
+
# at this point we don't know if the sys.modules module should be mounted or not
|
835
|
+
potential_mount = _Mount._from_local_python_packages(module_name)
|
836
|
+
mount_paths = potential_mount._top_level_paths()
|
837
|
+
except ModuleNotMountable:
|
838
|
+
# this typically happens if the module is a built-in, has binary components or doesn't exist
|
839
|
+
continue
|
840
|
+
|
841
|
+
for local_path, remote_path in mount_paths:
|
842
|
+
if any(local_path.is_relative_to(p) for p in SYS_PREFIXES) or _is_modal_path(remote_path):
|
843
|
+
# skip any module that has paths in SYS_PREFIXES, or would overwrite the modal Package in the container
|
844
|
+
break
|
845
|
+
else:
|
846
|
+
auto_mounts.append(potential_mount)
|
847
|
+
|
848
|
+
return auto_mounts
|