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
test/image_test.py
DELETED
@@ -1,669 +0,0 @@
|
|
1
|
-
# Copyright Modal Labs 2022
|
2
|
-
import os
|
3
|
-
import pytest
|
4
|
-
import sys
|
5
|
-
import threading
|
6
|
-
from hashlib import sha256
|
7
|
-
from tempfile import NamedTemporaryFile
|
8
|
-
from typing import List
|
9
|
-
from unittest import mock
|
10
|
-
|
11
|
-
from modal import Image, Mount, Secret, Stub, build, gpu, method
|
12
|
-
from modal._serialization import serialize
|
13
|
-
from modal.exception import DeprecationError, InvalidError
|
14
|
-
from modal.image import _dockerhub_python_version, _get_client_requirements_path
|
15
|
-
from modal_proto import api_pb2
|
16
|
-
|
17
|
-
from .supports.skip import skip_windows
|
18
|
-
|
19
|
-
|
20
|
-
def test_python_version():
|
21
|
-
assert _dockerhub_python_version("3.9.1") == "3.9.1"
|
22
|
-
assert _dockerhub_python_version("3.9") == "3.9.15"
|
23
|
-
v = _dockerhub_python_version().split(".")
|
24
|
-
assert len(v) == 3
|
25
|
-
assert (int(v[0]), int(v[1])) == sys.version_info[:2]
|
26
|
-
|
27
|
-
|
28
|
-
def get_image_layers(image_id: str, servicer) -> List[api_pb2.Image]:
|
29
|
-
"""Follow pointers to the previous image recursively in the servicer's list of images,
|
30
|
-
and return a list of image layers from top to bottom."""
|
31
|
-
|
32
|
-
result = []
|
33
|
-
|
34
|
-
while True:
|
35
|
-
if image_id not in servicer.images:
|
36
|
-
break
|
37
|
-
|
38
|
-
image = servicer.images[image_id]
|
39
|
-
result.append(servicer.images[image_id])
|
40
|
-
|
41
|
-
if not image.base_images:
|
42
|
-
break
|
43
|
-
|
44
|
-
image_id = image.base_images[0].image_id
|
45
|
-
|
46
|
-
return result
|
47
|
-
|
48
|
-
|
49
|
-
def test_image_python_packages(client, servicer):
|
50
|
-
stub = Stub()
|
51
|
-
stub.image = (
|
52
|
-
Image.debian_slim()
|
53
|
-
.pip_install("sklearn[xyz]")
|
54
|
-
.pip_install("numpy", "scipy", extra_index_url="https://xyz", find_links="https://abc?q=123", pre=True)
|
55
|
-
)
|
56
|
-
with stub.run(client=client):
|
57
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
58
|
-
assert any("pip install 'sklearn[xyz]'" in cmd for cmd in layers[1].dockerfile_commands)
|
59
|
-
assert any(
|
60
|
-
"pip install numpy scipy --find-links 'https://abc?q=123' --extra-index-url https://xyz --pre" in cmd
|
61
|
-
for cmd in layers[0].dockerfile_commands
|
62
|
-
)
|
63
|
-
|
64
|
-
|
65
|
-
def test_image_kwargs_validation(servicer, client):
|
66
|
-
stub = Stub()
|
67
|
-
stub.image = Image.debian_slim().run_commands(
|
68
|
-
"echo hi", secrets=[Secret.from_dict({"xyz": "123"}), Secret.from_name("foo")]
|
69
|
-
)
|
70
|
-
with pytest.raises(InvalidError):
|
71
|
-
stub.image = Image.debian_slim().run_commands(
|
72
|
-
"echo hi",
|
73
|
-
secrets=[
|
74
|
-
Secret.from_dict({"xyz": "123"}),
|
75
|
-
Secret.from_name("foo"),
|
76
|
-
Mount.from_local_dir("/", remote_path="/"), # type: ignore
|
77
|
-
], # Mount is not a valid Secret
|
78
|
-
)
|
79
|
-
|
80
|
-
stub = Stub()
|
81
|
-
stub.image = Image.debian_slim().copy_local_dir("/", remote_path="/dummy")
|
82
|
-
stub.image = Image.debian_slim().copy_mount(Mount.from_name("foo"), remote_path="/dummy")
|
83
|
-
with pytest.raises(InvalidError):
|
84
|
-
# Secret is not a valid Mount
|
85
|
-
stub.image = Image.debian_slim().copy_mount(Secret.from_dict({"xyz": "123"}), remote_path="/dummy") # type: ignore
|
86
|
-
|
87
|
-
|
88
|
-
def test_wrong_type(servicer, client):
|
89
|
-
image = Image.debian_slim()
|
90
|
-
for m in [image.pip_install, image.apt_install, image.run_commands]:
|
91
|
-
m(["xyz"]) # type: ignore
|
92
|
-
m("xyz") # type: ignore
|
93
|
-
m("xyz", ["def", "foo"], "ghi") # type: ignore
|
94
|
-
with pytest.raises(InvalidError):
|
95
|
-
m(3) # type: ignore
|
96
|
-
with pytest.raises(InvalidError):
|
97
|
-
m([3]) # type: ignore
|
98
|
-
with pytest.raises(InvalidError):
|
99
|
-
m([["double-nested-package"]]) # type: ignore
|
100
|
-
|
101
|
-
|
102
|
-
def test_image_requirements_txt(servicer, client):
|
103
|
-
requirements_txt = os.path.join(os.path.dirname(__file__), "supports/test-requirements.txt")
|
104
|
-
|
105
|
-
stub = Stub()
|
106
|
-
stub.image = Image.debian_slim().pip_install_from_requirements(requirements_txt)
|
107
|
-
with stub.run(client=client):
|
108
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
109
|
-
|
110
|
-
assert any("COPY /.requirements.txt /.requirements.txt" in cmd for cmd in layers[0].dockerfile_commands)
|
111
|
-
assert any("pip install -r /.requirements.txt" in cmd for cmd in layers[0].dockerfile_commands)
|
112
|
-
assert any(b"banana" in f.data for f in layers[0].context_files)
|
113
|
-
|
114
|
-
|
115
|
-
def test_empty_install(servicer, client):
|
116
|
-
# Install functions with no packages should be ignored.
|
117
|
-
stub = Stub(
|
118
|
-
image=Image.debian_slim()
|
119
|
-
.pip_install()
|
120
|
-
.pip_install([], [], [], [])
|
121
|
-
.apt_install([])
|
122
|
-
.run_commands()
|
123
|
-
.conda_install()
|
124
|
-
)
|
125
|
-
|
126
|
-
with stub.run(client=client):
|
127
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
128
|
-
assert len(layers) == 1
|
129
|
-
|
130
|
-
|
131
|
-
def test_debian_slim_apt_install(servicer, client):
|
132
|
-
stub = Stub(image=Image.debian_slim().pip_install("numpy").apt_install("git", "ssh").pip_install("scikit-learn"))
|
133
|
-
|
134
|
-
with stub.run(client=client):
|
135
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
136
|
-
|
137
|
-
assert any("pip install scikit-learn" in cmd for cmd in layers[0].dockerfile_commands)
|
138
|
-
assert any("apt-get install -y git ssh" in cmd for cmd in layers[1].dockerfile_commands)
|
139
|
-
assert any("pip install numpy" in cmd for cmd in layers[2].dockerfile_commands)
|
140
|
-
|
141
|
-
|
142
|
-
def test_image_pip_install_pyproject(servicer, client):
|
143
|
-
pyproject_toml = os.path.join(os.path.dirname(__file__), "supports/test-pyproject.toml")
|
144
|
-
|
145
|
-
stub = Stub()
|
146
|
-
stub.image = Image.debian_slim().pip_install_from_pyproject(pyproject_toml)
|
147
|
-
with stub.run(client=client):
|
148
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
149
|
-
|
150
|
-
print(layers[0].dockerfile_commands)
|
151
|
-
assert any("pip install 'banana >=1.2.0' 'potato >=0.1.0'" in cmd for cmd in layers[0].dockerfile_commands)
|
152
|
-
|
153
|
-
|
154
|
-
def test_image_pip_install_pyproject_with_optionals(servicer, client):
|
155
|
-
pyproject_toml = os.path.join(os.path.dirname(__file__), "supports/test-pyproject.toml")
|
156
|
-
|
157
|
-
stub = Stub()
|
158
|
-
stub.image = Image.debian_slim().pip_install_from_pyproject(pyproject_toml, optional_dependencies=["dev", "test"])
|
159
|
-
with stub.run(client=client):
|
160
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
161
|
-
|
162
|
-
print(layers[0].dockerfile_commands)
|
163
|
-
assert any(
|
164
|
-
"pip install 'banana >=1.2.0' 'linting-tool >=0.0.0' 'potato >=0.1.0' 'pytest >=1.2.0'" in cmd
|
165
|
-
for cmd in layers[0].dockerfile_commands
|
166
|
-
)
|
167
|
-
assert not (any("'mkdocs >=1.4.2'" in cmd for cmd in layers[0].dockerfile_commands))
|
168
|
-
|
169
|
-
|
170
|
-
def test_image_pip_install_private_repos(servicer, client):
|
171
|
-
stub = Stub()
|
172
|
-
with pytest.raises(InvalidError):
|
173
|
-
stub.image = Image.debian_slim().pip_install_private_repos(
|
174
|
-
"github.com/ecorp/private-one@1.0.0",
|
175
|
-
git_user="erikbern",
|
176
|
-
secrets=[], # Invalid: missing secret
|
177
|
-
)
|
178
|
-
|
179
|
-
bad_repo_refs = [
|
180
|
-
"ecorp/private-one@1.0.0",
|
181
|
-
"gitspace.com/corp/private-one@1.0.0",
|
182
|
-
]
|
183
|
-
for invalid_ref in bad_repo_refs:
|
184
|
-
with pytest.raises(InvalidError):
|
185
|
-
stub.image = Image.debian_slim().pip_install_private_repos(
|
186
|
-
invalid_ref,
|
187
|
-
git_user="erikbern",
|
188
|
-
secrets=[Secret.from_name("test-gh-read")],
|
189
|
-
)
|
190
|
-
|
191
|
-
stub.image = Image.debian_slim().pip_install_private_repos(
|
192
|
-
"github.com/corp/private-one@1.0.0",
|
193
|
-
"gitlab.com/corp2/private-two@0.0.2",
|
194
|
-
git_user="erikbern",
|
195
|
-
secrets=[
|
196
|
-
Secret.from_dict({"GITHUB_TOKEN": "not-a-secret"}),
|
197
|
-
Secret.from_dict({"GITLAB_TOKEN": "not-a-secret"}),
|
198
|
-
],
|
199
|
-
)
|
200
|
-
|
201
|
-
with stub.run(client=client):
|
202
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
203
|
-
assert len(layers[0].secret_ids) == 2
|
204
|
-
assert any(
|
205
|
-
'pip install "git+https://erikbern:$GITHUB_TOKEN@github.com/corp/private-one@1.0.0"' in cmd
|
206
|
-
for cmd in layers[0].dockerfile_commands
|
207
|
-
)
|
208
|
-
assert any(
|
209
|
-
'pip install "git+https://erikbern:$GITLAB_TOKEN@gitlab.com/corp2/private-two@0.0.2"' in cmd
|
210
|
-
for cmd in layers[0].dockerfile_commands
|
211
|
-
)
|
212
|
-
|
213
|
-
|
214
|
-
def test_conda_install(servicer, client):
|
215
|
-
stub = Stub(image=Image.conda().pip_install("numpy").conda_install("pymc3", "theano").pip_install("scikit-learn"))
|
216
|
-
|
217
|
-
with stub.run(client=client):
|
218
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
219
|
-
|
220
|
-
assert any("pip install scikit-learn" in cmd for cmd in layers[0].dockerfile_commands)
|
221
|
-
assert any("conda install pymc3 theano --yes" in cmd for cmd in layers[1].dockerfile_commands)
|
222
|
-
assert any("pip install numpy" in cmd for cmd in layers[2].dockerfile_commands)
|
223
|
-
|
224
|
-
|
225
|
-
def test_dockerfile_image(servicer, client):
|
226
|
-
path = os.path.join(os.path.dirname(__file__), "supports/test-dockerfile")
|
227
|
-
|
228
|
-
stub = Stub(image=Image.from_dockerfile(path))
|
229
|
-
|
230
|
-
with stub.run(client=client):
|
231
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
232
|
-
|
233
|
-
assert any("RUN pip install numpy" in cmd for cmd in layers[1].dockerfile_commands)
|
234
|
-
|
235
|
-
|
236
|
-
def test_conda_update_from_environment(servicer, client):
|
237
|
-
path = os.path.join(os.path.dirname(__file__), "supports/test-conda-environment.yml")
|
238
|
-
|
239
|
-
stub = Stub(image=Image.conda().conda_update_from_environment(path))
|
240
|
-
|
241
|
-
with stub.run(client=client):
|
242
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
243
|
-
|
244
|
-
assert any("RUN conda env update" in cmd for cmd in layers[0].dockerfile_commands)
|
245
|
-
assert any(b"foo=1.0" in f.data for f in layers[0].context_files)
|
246
|
-
assert any(b"bar=2.1" in f.data for f in layers[0].context_files)
|
247
|
-
|
248
|
-
|
249
|
-
def test_dockerhub_install(servicer, client):
|
250
|
-
stub = Stub(image=Image.from_registry("gisops/valhalla:latest", setup_dockerfile_commands=["RUN apt-get update"]))
|
251
|
-
|
252
|
-
with stub.run(client=client):
|
253
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
254
|
-
|
255
|
-
assert any("FROM gisops/valhalla:latest" in cmd for cmd in layers[0].dockerfile_commands)
|
256
|
-
assert any("RUN apt-get update" in cmd for cmd in layers[0].dockerfile_commands)
|
257
|
-
|
258
|
-
|
259
|
-
def test_ecr_install(servicer, client):
|
260
|
-
image_tag = "000000000000.dkr.ecr.us-east-1.amazonaws.com/my-private-registry:latest"
|
261
|
-
stub = Stub(
|
262
|
-
image=Image.from_aws_ecr(
|
263
|
-
image_tag,
|
264
|
-
setup_dockerfile_commands=["RUN apt-get update"],
|
265
|
-
secret=Secret.from_dict({"AWS_ACCESS_KEY_ID": "", "AWS_SECRET_ACCESS_KEY": ""}),
|
266
|
-
)
|
267
|
-
)
|
268
|
-
|
269
|
-
with stub.run(client=client):
|
270
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
271
|
-
|
272
|
-
assert any(f"FROM {image_tag}" in cmd for cmd in layers[0].dockerfile_commands)
|
273
|
-
assert any("RUN apt-get update" in cmd for cmd in layers[0].dockerfile_commands)
|
274
|
-
|
275
|
-
|
276
|
-
def run_f():
|
277
|
-
print("foo!")
|
278
|
-
|
279
|
-
|
280
|
-
def test_image_run_function(client, servicer):
|
281
|
-
stub = Stub()
|
282
|
-
stub.image = (
|
283
|
-
Image.debian_slim().pip_install("pandas").run_function(run_f, secrets=[Secret.from_dict({"xyz": "123"})])
|
284
|
-
)
|
285
|
-
|
286
|
-
with stub.run(client=client):
|
287
|
-
image_id = stub.image.object_id
|
288
|
-
layers = get_image_layers(image_id, servicer)
|
289
|
-
assert "foo!" in layers[0].build_function.definition
|
290
|
-
assert "Secret.from_dict([xyz])" in layers[0].build_function.definition
|
291
|
-
# globals is none when no globals are referenced
|
292
|
-
assert layers[0].build_function.globals == b""
|
293
|
-
|
294
|
-
function_id = servicer.image_build_function_ids[image_id]
|
295
|
-
assert function_id
|
296
|
-
assert servicer.app_functions[function_id].function_name == "run_f"
|
297
|
-
assert len(servicer.app_functions[function_id].secret_ids) == 1
|
298
|
-
|
299
|
-
|
300
|
-
def test_image_run_function_interactivity(client, servicer):
|
301
|
-
stub = Stub()
|
302
|
-
stub.image = Image.debian_slim().pip_install("pandas").run_function(run_f)
|
303
|
-
|
304
|
-
from modal.runner import run_stub
|
305
|
-
|
306
|
-
with run_stub(stub, client=client, shell=True):
|
307
|
-
image_id = stub.image.object_id
|
308
|
-
layers = get_image_layers(image_id, servicer)
|
309
|
-
assert "foo!" in layers[0].build_function.definition
|
310
|
-
|
311
|
-
function_id = servicer.image_build_function_ids[image_id]
|
312
|
-
assert function_id
|
313
|
-
assert servicer.app_functions[function_id].function_name == "run_f"
|
314
|
-
assert not servicer.app_functions[function_id].pty_info.enabled
|
315
|
-
|
316
|
-
|
317
|
-
VARIABLE_1 = 1
|
318
|
-
VARIABLE_2 = 3
|
319
|
-
|
320
|
-
|
321
|
-
def run_f_globals():
|
322
|
-
print("foo!", VARIABLE_1)
|
323
|
-
|
324
|
-
|
325
|
-
def test_image_run_function_globals(client, servicer):
|
326
|
-
global VARIABLE_1, VARIABLE_2
|
327
|
-
|
328
|
-
stub = Stub()
|
329
|
-
stub.image = Image.debian_slim().run_function(run_f_globals)
|
330
|
-
|
331
|
-
with stub.run(client=client):
|
332
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
333
|
-
old_globals = layers[0].build_function.globals
|
334
|
-
assert b"VARIABLE_1" in old_globals
|
335
|
-
assert b"VARIABLE_2" not in old_globals
|
336
|
-
|
337
|
-
VARIABLE_1 = 3
|
338
|
-
with stub.run(client=client):
|
339
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
340
|
-
assert layers[0].build_function.globals != old_globals
|
341
|
-
|
342
|
-
VARIABLE_1 = 1
|
343
|
-
with stub.run(client=client):
|
344
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
345
|
-
assert layers[0].build_function.globals == old_globals
|
346
|
-
|
347
|
-
|
348
|
-
VARIABLE_3 = threading.Lock()
|
349
|
-
VARIABLE_4 = "bar"
|
350
|
-
|
351
|
-
|
352
|
-
def run_f_unserializable_globals():
|
353
|
-
print("foo!", VARIABLE_3, VARIABLE_4)
|
354
|
-
|
355
|
-
|
356
|
-
def test_image_run_unserializable_function(client, servicer):
|
357
|
-
stub = Stub()
|
358
|
-
stub.image = Image.debian_slim().run_function(run_f_unserializable_globals)
|
359
|
-
|
360
|
-
with stub.run(client=client):
|
361
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
362
|
-
old_globals = layers[0].build_function.globals
|
363
|
-
assert b"VARIABLE_4" in old_globals
|
364
|
-
|
365
|
-
|
366
|
-
def run_f_with_args(arg, *, kwarg):
|
367
|
-
print("building!", arg, kwarg)
|
368
|
-
|
369
|
-
|
370
|
-
def test_image_run_function_with_args(client, servicer):
|
371
|
-
stub = Stub()
|
372
|
-
stub.image = Image.debian_slim().run_function(run_f_with_args, args=("foo",), kwargs={"kwarg": "bar"})
|
373
|
-
|
374
|
-
with stub.run(client=client):
|
375
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
376
|
-
input = layers[0].build_function.input
|
377
|
-
assert input.args == serialize((("foo",), {"kwarg": "bar"}))
|
378
|
-
|
379
|
-
|
380
|
-
def test_poetry(client, servicer):
|
381
|
-
path = os.path.join(os.path.dirname(__file__), "supports/pyproject.toml")
|
382
|
-
|
383
|
-
# No lockfile provided and there's no lockfile found
|
384
|
-
# TODO we deferred the exception until _load runs, not sure how to test that here
|
385
|
-
# with pytest.raises(NotFoundError):
|
386
|
-
# Image.debian_slim().poetry_install_from_file(path)
|
387
|
-
|
388
|
-
# Explicitly ignore lockfile - this should work
|
389
|
-
Image.debian_slim().poetry_install_from_file(path, ignore_lockfile=True)
|
390
|
-
|
391
|
-
# Provide lockfile explicitly - this should also work
|
392
|
-
lockfile_path = os.path.join(os.path.dirname(__file__), "supports/special_poetry.lock")
|
393
|
-
image = Image.debian_slim().poetry_install_from_file(path, lockfile_path)
|
394
|
-
|
395
|
-
# Build iamge
|
396
|
-
stub = Stub()
|
397
|
-
stub.image = image
|
398
|
-
with stub.run(client=client):
|
399
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
400
|
-
context_files = {f.filename for layer in layers for f in layer.context_files}
|
401
|
-
assert context_files == {"/.poetry.lock", "/.pyproject.toml", "/modal_requirements.txt"}
|
402
|
-
|
403
|
-
|
404
|
-
@pytest.fixture
|
405
|
-
def tmp_path_with_content(tmp_path):
|
406
|
-
(tmp_path / "data.txt").write_text("hello")
|
407
|
-
(tmp_path / "data").mkdir()
|
408
|
-
(tmp_path / "data" / "sub").write_text("world")
|
409
|
-
return tmp_path
|
410
|
-
|
411
|
-
|
412
|
-
def test_image_copy_local_dir(client, servicer, tmp_path_with_content):
|
413
|
-
stub = Stub()
|
414
|
-
stub.image = Image.debian_slim().copy_local_dir(tmp_path_with_content, remote_path="/dummy")
|
415
|
-
|
416
|
-
with stub.run(client=client):
|
417
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
418
|
-
assert "COPY . /dummy" in layers[0].dockerfile_commands
|
419
|
-
assert set(servicer.mount_contents["mo-1"].keys()) == {"/data.txt", "/data/sub"}
|
420
|
-
|
421
|
-
|
422
|
-
def test_image_docker_command_copy(client, servicer, tmp_path_with_content):
|
423
|
-
stub = Stub()
|
424
|
-
data_mount = Mount.from_local_dir(tmp_path_with_content, remote_path="/")
|
425
|
-
stub.image = Image.debian_slim().dockerfile_commands(["COPY . /dummy"], context_mount=data_mount)
|
426
|
-
|
427
|
-
with stub.run(client=client):
|
428
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
429
|
-
assert "COPY . /dummy" in layers[0].dockerfile_commands
|
430
|
-
files = {f.mount_filename: f.content for f in Mount._get_files(data_mount.entries)}
|
431
|
-
assert files == {"/data.txt": b"hello", "/data/sub": b"world"}
|
432
|
-
|
433
|
-
|
434
|
-
def test_image_dockerfile_copy(client, servicer, tmp_path_with_content):
|
435
|
-
dockerfile = NamedTemporaryFile("w", delete=False)
|
436
|
-
dockerfile.write("COPY . /dummy\n")
|
437
|
-
dockerfile.close()
|
438
|
-
|
439
|
-
stub = Stub()
|
440
|
-
data_mount = Mount.from_local_dir(tmp_path_with_content, remote_path="/")
|
441
|
-
stub.image = Image.debian_slim().from_dockerfile(dockerfile.name, context_mount=data_mount)
|
442
|
-
|
443
|
-
with stub.run(client=client):
|
444
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
445
|
-
assert "COPY . /dummy" in layers[1].dockerfile_commands
|
446
|
-
files = {f.mount_filename: f.content for f in Mount._get_files(data_mount.entries)}
|
447
|
-
assert files == {"/data.txt": b"hello", "/data/sub": b"world"}
|
448
|
-
|
449
|
-
|
450
|
-
def test_image_env(client, servicer):
|
451
|
-
stub = Stub(image=Image.debian_slim().env({"HELLO": "world!"}))
|
452
|
-
|
453
|
-
with stub.run(client=client):
|
454
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
455
|
-
assert any("ENV HELLO=" in cmd and "world!" in cmd for cmd in layers[0].dockerfile_commands)
|
456
|
-
|
457
|
-
|
458
|
-
def test_image_gpu(client, servicer):
|
459
|
-
stub = Stub(image=Image.debian_slim().run_commands("echo 0"))
|
460
|
-
with stub.run(client=client):
|
461
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
462
|
-
assert layers[0].gpu_config.type == api_pb2.GPU_TYPE_UNSPECIFIED
|
463
|
-
|
464
|
-
with pytest.warns(DeprecationError):
|
465
|
-
stub = Stub(image=Image.debian_slim().run_commands("echo 1", gpu=True))
|
466
|
-
with stub.run(client=client):
|
467
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
468
|
-
assert layers[0].gpu_config.type == api_pb2.GPU_TYPE_ANY
|
469
|
-
|
470
|
-
stub = Stub(image=Image.debian_slim().run_commands("echo 2", gpu=gpu.A10G()))
|
471
|
-
with stub.run(client=client):
|
472
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
473
|
-
assert layers[0].gpu_config.type == api_pb2.GPU_TYPE_A10G
|
474
|
-
|
475
|
-
|
476
|
-
def test_image_force_build(client, servicer):
|
477
|
-
stub = Stub()
|
478
|
-
stub.image = Image.debian_slim().run_commands("echo 1").pip_install("foo", force_build=True).run_commands("echo 2")
|
479
|
-
with stub.run(client=client):
|
480
|
-
assert servicer.force_built_images == ["im-3", "im-4"]
|
481
|
-
|
482
|
-
stub.image = (
|
483
|
-
Image.from_gcp_artifact_registry("foo", force_build=True)
|
484
|
-
.run_commands("python_packagesecho 1")
|
485
|
-
.pip_install("foo", force_build=True)
|
486
|
-
.run_commands("echo 2")
|
487
|
-
)
|
488
|
-
with stub.run(client=client):
|
489
|
-
assert servicer.force_built_images == ["im-3", "im-4", "im-5", "im-6", "im-7", "im-8"]
|
490
|
-
|
491
|
-
|
492
|
-
def test_workdir(servicer, client):
|
493
|
-
stub = Stub(image=Image.debian_slim().workdir("/foo/bar"))
|
494
|
-
|
495
|
-
with stub.run(client=client):
|
496
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
497
|
-
|
498
|
-
assert any("WORKDIR /foo/bar" in cmd for cmd in layers[0].dockerfile_commands)
|
499
|
-
|
500
|
-
|
501
|
-
cls_stub = Stub()
|
502
|
-
|
503
|
-
VARIABLE_5 = 1
|
504
|
-
VARIABLE_6 = 1
|
505
|
-
|
506
|
-
|
507
|
-
@cls_stub.cls(
|
508
|
-
image=Image.debian_slim().pip_install("pandas"),
|
509
|
-
secrets=[Secret.from_dict({"xyz": "123"})],
|
510
|
-
)
|
511
|
-
class Foo:
|
512
|
-
@build()
|
513
|
-
def build_func(self):
|
514
|
-
global VARIABLE_5
|
515
|
-
|
516
|
-
print("foo!", VARIABLE_5)
|
517
|
-
|
518
|
-
@method()
|
519
|
-
def f(self):
|
520
|
-
global VARIABLE_6
|
521
|
-
|
522
|
-
print("bar!", VARIABLE_6)
|
523
|
-
|
524
|
-
|
525
|
-
def test_image_build_snapshot(client, servicer):
|
526
|
-
with cls_stub.run(client=client):
|
527
|
-
image_id = list(servicer.images.keys())[-1]
|
528
|
-
layers = get_image_layers(image_id, servicer)
|
529
|
-
|
530
|
-
assert "foo!" in layers[0].build_function.definition
|
531
|
-
assert "Secret.from_dict([xyz])" in layers[0].build_function.definition
|
532
|
-
assert any("pip install pandas" in cmd for cmd in layers[1].dockerfile_commands)
|
533
|
-
|
534
|
-
globals = layers[0].build_function.globals
|
535
|
-
assert b"VARIABLE_5" in globals
|
536
|
-
|
537
|
-
# Globals and def for the main function should not affect build step.
|
538
|
-
assert "bar!" not in layers[0].build_function.definition
|
539
|
-
assert b"VARIABLE_6" not in globals
|
540
|
-
|
541
|
-
function_id = servicer.image_build_function_ids[image_id]
|
542
|
-
assert function_id
|
543
|
-
assert servicer.app_functions[function_id].function_name == "Foo.build_func"
|
544
|
-
assert len(servicer.app_functions[function_id].secret_ids) == 1
|
545
|
-
|
546
|
-
|
547
|
-
def test_inside_ctx_unhydrated(client):
|
548
|
-
image_1 = Image.debian_slim()
|
549
|
-
image_2 = Image.debian_slim()
|
550
|
-
|
551
|
-
with mock.patch.dict(os.environ, {"MODAL_IMAGE_ID": "im-123"}):
|
552
|
-
# This should initially swallow the exception
|
553
|
-
with image_1.imports():
|
554
|
-
raise ImportError("foo")
|
555
|
-
|
556
|
-
# This one too
|
557
|
-
with image_2.imports():
|
558
|
-
raise ImportError("bar")
|
559
|
-
|
560
|
-
# non-ImportErrors should trigger a warning
|
561
|
-
with pytest.warns(match="ImportError"):
|
562
|
-
with image_2.imports():
|
563
|
-
raise Exception("foo")
|
564
|
-
|
565
|
-
# Make sure run_inside works but is depreated
|
566
|
-
with pytest.warns(DeprecationError, match="imports()"):
|
567
|
-
with image_1.run_inside():
|
568
|
-
pass
|
569
|
-
|
570
|
-
# Hydration of the image should raise the exception
|
571
|
-
with pytest.raises(ImportError, match="foo"):
|
572
|
-
image_1._hydrate("im-123", client, None)
|
573
|
-
|
574
|
-
# Should not raise since it's a different image
|
575
|
-
image_2._hydrate("im-456", client, None)
|
576
|
-
|
577
|
-
|
578
|
-
def test_inside_ctx_hydrated(client):
|
579
|
-
image_1 = Image.debian_slim()
|
580
|
-
image_2 = Image.debian_slim()
|
581
|
-
|
582
|
-
with mock.patch.dict(os.environ, {"MODAL_IMAGE_ID": "im-123"}):
|
583
|
-
# Assign ids before the ctx mgr runs
|
584
|
-
image_1._hydrate("im-123", client, None)
|
585
|
-
image_2._hydrate("im-456", client, None)
|
586
|
-
|
587
|
-
# Ctx manager should now raise right away
|
588
|
-
with pytest.raises(ImportError, match="baz"):
|
589
|
-
with image_1.imports():
|
590
|
-
raise ImportError("baz")
|
591
|
-
|
592
|
-
# We're not inside this image so this should be swallowed
|
593
|
-
with image_2.imports():
|
594
|
-
raise ImportError("bar")
|
595
|
-
|
596
|
-
|
597
|
-
@pytest.mark.parametrize(
|
598
|
-
"version,expected",
|
599
|
-
[
|
600
|
-
("3.12", "requirements.312.txt"),
|
601
|
-
("3.12.1", "requirements.312.txt"),
|
602
|
-
("3.12.1-gnu", "requirements.312.txt"),
|
603
|
-
],
|
604
|
-
)
|
605
|
-
def test_get_client_requirements_path(version, expected):
|
606
|
-
path = _get_client_requirements_path(version)
|
607
|
-
assert os.path.basename(path) == expected
|
608
|
-
|
609
|
-
|
610
|
-
@skip_windows("Different hash values for context file paths")
|
611
|
-
def test_image_stability_on_2023_12(servicer, client, test_dir):
|
612
|
-
def get_hash(img: Image) -> str:
|
613
|
-
stub = Stub(image=img)
|
614
|
-
with stub.run(client=client):
|
615
|
-
layers = get_image_layers(stub.image.object_id, servicer)
|
616
|
-
commands = [layer.dockerfile_commands for layer in layers]
|
617
|
-
context_files = [[(f.filename, f.data) for f in layer.context_files] for layer in layers]
|
618
|
-
return sha256(repr(list(zip(commands, context_files))).encode()).hexdigest()
|
619
|
-
|
620
|
-
if sys.version_info[:2] == (3, 11):
|
621
|
-
# Matches my development environment — default is to match Python version from local system
|
622
|
-
img = Image.debian_slim()
|
623
|
-
assert get_hash(img) == "183b86356d9eb3bd3d78adf70f16b35b63ba9bf4e1816b0cacc549541718e555"
|
624
|
-
|
625
|
-
img = Image.debian_slim(python_version="3.12")
|
626
|
-
assert get_hash(img) == "53b6205e1dc2a0ca7ebed862e4f3a5887367587be13e81f65a4ac8f8a1e9be91"
|
627
|
-
|
628
|
-
if sys.version_info[:2] < (3, 12):
|
629
|
-
# Client dependencies on 3.12 are different
|
630
|
-
img = Image.from_registry("ubuntu:22.04")
|
631
|
-
assert get_hash(img) == "b5f1cc544a412d1b23a5ebf9a8859ea9a86975ecbc7325b83defc0ce3fe956d3"
|
632
|
-
|
633
|
-
img = Image.conda()
|
634
|
-
assert get_hash(img) == "f69d6af66fb5f1a2372a61836e6166ce79ebe2cd628d12addea8e8e80cc98dc1"
|
635
|
-
|
636
|
-
img = Image.micromamba()
|
637
|
-
assert get_hash(img) == "fa883741544ea191ecd197c8f83a1ffe9912575faa8c107c66b3dda761b2e401"
|
638
|
-
|
639
|
-
img = Image.from_dockerfile(test_dir / "supports" / "test-dockerfile")
|
640
|
-
assert get_hash(img) == "0aec2f66f28ee7511c1b36604214ae7b40d9bc1fa3e6b8883001e933a966ff78"
|
641
|
-
|
642
|
-
img = Image.conda(python_version="3.12")
|
643
|
-
assert get_hash(img) == "c4b3f7350116d323dded29c9c9b78b62593f0fc943ccf83a09b27185bfdc2a07"
|
644
|
-
|
645
|
-
img = Image.micromamba(python_version="3.12")
|
646
|
-
assert get_hash(img) == "468befe16f703a3ae1a794dfe54c1a3445ca0ffda233f55f1d66c45ad608e8aa"
|
647
|
-
|
648
|
-
base = Image.debian_slim(python_version="3.12")
|
649
|
-
|
650
|
-
img = base.run_commands("echo 'Hello Modal'", "rm /usr/local/bin/kubectl")
|
651
|
-
assert get_hash(img) == "4e1ac62eb33b44dd16940c9d2719eb79f945cee61cbf4641ca99b19cd9e0976d"
|
652
|
-
|
653
|
-
img = base.pip_install("torch~=2.2", "transformers==4.23.0", pre=True, index_url="agi.se")
|
654
|
-
assert get_hash(img) == "2a4fa8e3b32c70a41b3a3efd5416540b1953430543f6c27c984e7f969c2ca874"
|
655
|
-
|
656
|
-
img = base.conda_install("torch=2.2", "transformers<4.23.0", channels=["conda-forge", "my-channel"])
|
657
|
-
assert get_hash(img) == "dd6f27f636293996a64a98c250161d8092cb23d02629d9070493f00aad8d7266"
|
658
|
-
|
659
|
-
img = base.pip_install_from_requirements(test_dir / "supports" / "test-requirements.txt")
|
660
|
-
assert get_hash(img) == "69d41e699d4ecef399e51e8460f8857aa0ec57f71f00eca81c8886ec062e5c2b"
|
661
|
-
|
662
|
-
img = base.conda_update_from_environment(test_dir / "supports" / "test-conda-environment.yml")
|
663
|
-
assert get_hash(img) == "00940e0ee2998bfe0a337f51a5fdf5f4b29bf9d42dda3635641d44bfeb42537e"
|
664
|
-
|
665
|
-
img = base.poetry_install_from_file(
|
666
|
-
test_dir / "supports" / "test-pyproject.toml",
|
667
|
-
poetry_lockfile=test_dir / "supports" / "special_poetry.lock",
|
668
|
-
)
|
669
|
-
assert get_hash(img) == "a25dd4cc2e8d88f92bfdaf2e82b9d74144d1928926bf6be2ca1cdfbbf562189e"
|