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.
- modal/__init__.py +13 -9
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +402 -398
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -60
- modal/_resources.py +26 -7
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1025 -0
- modal/{execution_context.py → _runtime/execution_context.py} +11 -2
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +123 -6
- modal/_traceback.py +47 -187
- modal/_tunnel.py +50 -14
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +386 -104
- 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 +299 -98
- modal/_utils/grpc_testing.py +47 -34
- modal/_utils/grpc_utils.py +54 -21
- modal/_utils/hash_utils.py +51 -10
- 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 +3 -3
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +12 -10
- modal/app.py +561 -323
- modal/app.pyi +474 -262
- modal/call_graph.py +7 -6
- modal/cli/_download.py +22 -6
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +203 -42
- modal/cli/config.py +12 -5
- modal/cli/container.py +61 -13
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +21 -48
- modal/cli/launch.py +28 -14
- modal/cli/network_file_system.py +57 -21
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +34 -9
- modal/cli/programs/vscode.py +58 -8
- modal/cli/queues.py +131 -0
- modal/cli/run.py +199 -96
- modal/cli/secret.py +5 -4
- modal/cli/token.py +7 -2
- modal/cli/utils.py +74 -8
- modal/cli/volume.py +97 -56
- modal/client.py +248 -144
- modal/client.pyi +156 -124
- modal/cloud_bucket_mount.py +43 -30
- modal/cloud_bucket_mount.pyi +32 -25
- modal/cls.py +528 -141
- modal/cls.pyi +189 -145
- modal/config.py +32 -15
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +50 -54
- modal/dict.pyi +120 -164
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +30 -43
- modal/experimental.py +62 -2
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +196 -0
- modal/functions.py +846 -428
- modal/functions.pyi +446 -387
- modal/gpu.py +57 -44
- modal/image.py +943 -417
- modal/image.pyi +584 -245
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +223 -90
- modal/mount.pyi +241 -243
- modal/network_file_system.py +85 -86
- modal/network_file_system.pyi +151 -110
- modal/object.py +66 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +73 -47
- modal/parallel_map.pyi +51 -63
- modal/partial_function.py +272 -107
- modal/partial_function.pyi +219 -120
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +96 -72
- modal/queue.pyi +210 -135
- modal/requirements/2024.04.txt +2 -1
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +45 -4
- modal/runner.py +325 -203
- modal/runner.pyi +124 -110
- modal/running_app.py +27 -4
- modal/sandbox.py +509 -231
- modal/sandbox.pyi +396 -169
- modal/schedule.py +2 -2
- modal/scheduler_placement.py +20 -3
- modal/secret.py +41 -25
- modal/secret.pyi +62 -42
- modal/serving.py +39 -49
- modal/serving.pyi +37 -43
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +123 -137
- modal/volume.pyi +228 -221
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
- modal-0.72.13.dist-info/RECORD +174 -0
- {modal-0.62.115.dist-info → modal-0.72.13.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 +1 -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 +1231 -531
- modal_proto/api_grpc.py +750 -430
- modal_proto/api_pb2.py +2102 -1176
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1329 -675
- 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_exec.py +0 -128
- modal/_container_io_manager.py +0 -646
- modal/_container_io_manager.pyi +0 -412
- modal/_sandbox_shell.py +0 -49
- modal/app_utils.py +0 -20
- modal/app_utils.pyi +0 -17
- modal/execution_context.pyi +0 -37
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal-0.62.115.dist-info/RECORD +0 -207
- 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 -279
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -674
- test/client_test.py +0 -203
- test/cloud_bucket_mount_test.py +0 -22
- test/cls_test.py +0 -636
- test/config_test.py +0 -149
- test/conftest.py +0 -1485
- test/container_app_test.py +0 -50
- test/container_test.py +0 -1405
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -51
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -791
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -82
- test/helpers.py +0 -47
- test/image_test.py +0 -814
- 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 -327
- test/network_file_system_test.py +0 -188
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -115
- test/resolver_test.py +0 -59
- 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 -57
- test/secret_test.py +0 -89
- test/serialization_test.py +0 -50
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -361
- 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 -397
- test/watcher_test.py +0 -58
- test/webhook_test.py +0 -145
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/entry_points.txt +0 -0
test/image_test.py
DELETED
@@ -1,814 +0,0 @@
|
|
1
|
-
# Copyright Modal Labs 2022
|
2
|
-
import os
|
3
|
-
import pytest
|
4
|
-
import re
|
5
|
-
import sys
|
6
|
-
import threading
|
7
|
-
from hashlib import sha256
|
8
|
-
from tempfile import NamedTemporaryFile
|
9
|
-
from typing import List, Literal, get_args
|
10
|
-
from unittest import mock
|
11
|
-
|
12
|
-
from modal import App, Image, Mount, Secret, build, gpu, method
|
13
|
-
from modal._serialization import serialize
|
14
|
-
from modal.client import Client
|
15
|
-
from modal.exception import DeprecationError, InvalidError, VersionError
|
16
|
-
from modal.image import (
|
17
|
-
SUPPORTED_PYTHON_SERIES,
|
18
|
-
ImageBuilderVersion,
|
19
|
-
_dockerhub_debian_codename,
|
20
|
-
_dockerhub_python_version,
|
21
|
-
_get_modal_requirements_path,
|
22
|
-
_validate_python_version,
|
23
|
-
)
|
24
|
-
from modal.mount import PYTHON_STANDALONE_VERSIONS
|
25
|
-
from modal_proto import api_pb2
|
26
|
-
|
27
|
-
from .supports.skip import skip_windows
|
28
|
-
|
29
|
-
|
30
|
-
def test_supported_python_series():
|
31
|
-
assert SUPPORTED_PYTHON_SERIES == PYTHON_STANDALONE_VERSIONS.keys()
|
32
|
-
|
33
|
-
|
34
|
-
def get_image_layers(image_id: str, servicer) -> List[api_pb2.Image]:
|
35
|
-
"""Follow pointers to the previous image recursively in the servicer's list of images,
|
36
|
-
and return a list of image layers from top to bottom."""
|
37
|
-
|
38
|
-
result = []
|
39
|
-
|
40
|
-
while True:
|
41
|
-
if image_id not in servicer.images:
|
42
|
-
break
|
43
|
-
|
44
|
-
image = servicer.images[image_id]
|
45
|
-
result.append(servicer.images[image_id])
|
46
|
-
|
47
|
-
if not image.base_images:
|
48
|
-
break
|
49
|
-
|
50
|
-
image_id = image.base_images[0].image_id
|
51
|
-
|
52
|
-
return result
|
53
|
-
|
54
|
-
|
55
|
-
def get_all_dockerfile_commands(image_id: str, servicer) -> str:
|
56
|
-
layers = get_image_layers(image_id, servicer)
|
57
|
-
return "\n".join([cmd for layer in layers for cmd in layer.dockerfile_commands])
|
58
|
-
|
59
|
-
|
60
|
-
@pytest.fixture(params=get_args(ImageBuilderVersion))
|
61
|
-
def builder_version(request, server_url_env, modal_config):
|
62
|
-
version = request.param
|
63
|
-
with modal_config():
|
64
|
-
with mock.patch("test.conftest.ImageBuilderVersion", Literal[version]): # type: ignore
|
65
|
-
yield version
|
66
|
-
|
67
|
-
|
68
|
-
def test_python_version_validation():
|
69
|
-
assert _validate_python_version(None) == "{0}.{1}".format(*sys.version_info)
|
70
|
-
assert _validate_python_version("3.12") == "3.12"
|
71
|
-
assert _validate_python_version("3.12.0") == "3.12.0"
|
72
|
-
|
73
|
-
with pytest.raises(InvalidError, match="Unsupported Python version"):
|
74
|
-
_validate_python_version("3.7")
|
75
|
-
|
76
|
-
with pytest.raises(InvalidError, match="Python version must be specified as a string"):
|
77
|
-
_validate_python_version(3.10) # type: ignore
|
78
|
-
|
79
|
-
with pytest.raises(InvalidError, match="Invalid Python version"):
|
80
|
-
_validate_python_version("3.10.2.9")
|
81
|
-
|
82
|
-
with pytest.raises(InvalidError, match="Invalid Python version"):
|
83
|
-
_validate_python_version("3.10.x")
|
84
|
-
|
85
|
-
with pytest.raises(InvalidError, match="Python version must be specified as 'major.minor'"):
|
86
|
-
_validate_python_version("3.10.5", allow_micro_granularity=False)
|
87
|
-
|
88
|
-
|
89
|
-
def test_dockerhub_python_version(builder_version):
|
90
|
-
assert _dockerhub_python_version(builder_version, "3.9.1") == "3.9.1"
|
91
|
-
|
92
|
-
expected_39_full = {"2023.12": "3.9.15", "2024.04": "3.9.19"}[builder_version]
|
93
|
-
assert _dockerhub_python_version(builder_version, "3.9") == expected_39_full
|
94
|
-
|
95
|
-
v = _dockerhub_python_version(builder_version, None).split(".")
|
96
|
-
assert len(v) == 3
|
97
|
-
assert (int(v[0]), int(v[1])) == sys.version_info[:2]
|
98
|
-
|
99
|
-
|
100
|
-
def test_image_base(builder_version, servicer, client, test_dir):
|
101
|
-
app = App()
|
102
|
-
constructors = [
|
103
|
-
(Image.debian_slim, ()),
|
104
|
-
(Image.from_registry, ("ubuntu",)),
|
105
|
-
(Image.from_dockerfile, (test_dir / "supports" / "test-dockerfile",)),
|
106
|
-
(Image.conda, ()),
|
107
|
-
(Image.micromamba, ()),
|
108
|
-
]
|
109
|
-
for meth, args in constructors:
|
110
|
-
app.image = meth(*args) # type: ignore
|
111
|
-
with app.run(client=client):
|
112
|
-
commands = get_all_dockerfile_commands(app.image.object_id, servicer)
|
113
|
-
assert "COPY /modal_requirements.txt /modal_requirements.txt" in commands
|
114
|
-
if builder_version == "2023.12":
|
115
|
-
assert "pip install -r /modal_requirements.txt" in commands
|
116
|
-
else:
|
117
|
-
assert "pip install --no-cache --no-deps -r /modal_requirements.txt" in commands
|
118
|
-
assert "rm /modal_requirements.txt" in commands
|
119
|
-
|
120
|
-
|
121
|
-
@pytest.mark.parametrize("python_version", [None, "3.10", "3.11.4"])
|
122
|
-
def test_python_version(builder_version, servicer, client, python_version):
|
123
|
-
local_python = "{0}.{1}".format(*sys.version_info)
|
124
|
-
expected_python = local_python if python_version is None else python_version
|
125
|
-
|
126
|
-
app = App()
|
127
|
-
app.image = Image.debian_slim() if python_version is None else Image.debian_slim(python_version)
|
128
|
-
expected_dockerhub_python = _dockerhub_python_version(builder_version, expected_python)
|
129
|
-
expected_dockerhub_debian = _dockerhub_debian_codename(builder_version)
|
130
|
-
assert expected_dockerhub_python.startswith(expected_python)
|
131
|
-
with app.run(client):
|
132
|
-
commands = get_all_dockerfile_commands(app.image.object_id, servicer)
|
133
|
-
assert re.match(rf"FROM python:{expected_dockerhub_python}-slim-{expected_dockerhub_debian}", commands)
|
134
|
-
|
135
|
-
for constructor in [Image.conda, Image.micromamba]:
|
136
|
-
app.image = constructor() if python_version is None else constructor(python_version)
|
137
|
-
if python_version is None and builder_version == "2023.12":
|
138
|
-
expected_python = "3.9"
|
139
|
-
with app.run(client):
|
140
|
-
commands = get_all_dockerfile_commands(app.image.object_id, servicer)
|
141
|
-
assert re.search(rf"install.* python={expected_python}", commands)
|
142
|
-
|
143
|
-
|
144
|
-
def test_image_python_packages(builder_version, servicer, client):
|
145
|
-
app = App()
|
146
|
-
app.image = (
|
147
|
-
Image.debian_slim()
|
148
|
-
.pip_install("sklearn[xyz]")
|
149
|
-
.pip_install("numpy", "scipy", extra_index_url="https://xyz", find_links="https://abc?q=123", pre=True)
|
150
|
-
)
|
151
|
-
with app.run(client=client):
|
152
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
153
|
-
assert any("pip install 'sklearn[xyz]'" in cmd for cmd in layers[1].dockerfile_commands)
|
154
|
-
assert any(
|
155
|
-
"pip install numpy scipy --find-links 'https://abc?q=123' --extra-index-url https://xyz --pre" in cmd
|
156
|
-
for cmd in layers[0].dockerfile_commands
|
157
|
-
)
|
158
|
-
|
159
|
-
|
160
|
-
def test_image_kwargs_validation(builder_version, servicer, client):
|
161
|
-
app = App()
|
162
|
-
app.image = Image.debian_slim().run_commands(
|
163
|
-
"echo hi", secrets=[Secret.from_dict({"xyz": "123"}), Secret.from_name("foo")]
|
164
|
-
)
|
165
|
-
with pytest.raises(InvalidError):
|
166
|
-
app.image = Image.debian_slim().run_commands(
|
167
|
-
"echo hi",
|
168
|
-
secrets=[
|
169
|
-
Secret.from_dict({"xyz": "123"}),
|
170
|
-
Secret.from_name("foo"),
|
171
|
-
Mount.from_local_dir("/", remote_path="/"), # type: ignore
|
172
|
-
], # Mount is not a valid Secret
|
173
|
-
)
|
174
|
-
|
175
|
-
app = App()
|
176
|
-
app.image = Image.debian_slim().copy_local_dir("/", remote_path="/dummy")
|
177
|
-
app.image = Image.debian_slim().copy_mount(Mount.from_name("foo"), remote_path="/dummy")
|
178
|
-
with pytest.raises(InvalidError):
|
179
|
-
# Secret is not a valid Mount
|
180
|
-
app.image = Image.debian_slim().copy_mount(Secret.from_dict({"xyz": "123"}), remote_path="/dummy") # type: ignore
|
181
|
-
|
182
|
-
|
183
|
-
def test_wrong_type(builder_version, servicer, client):
|
184
|
-
image = Image.debian_slim()
|
185
|
-
for m in [image.pip_install, image.apt_install, image.run_commands]:
|
186
|
-
m(["xyz"]) # type: ignore
|
187
|
-
m("xyz") # type: ignore
|
188
|
-
m("xyz", ["def", "foo"], "ghi") # type: ignore
|
189
|
-
with pytest.raises(InvalidError):
|
190
|
-
m(3) # type: ignore
|
191
|
-
with pytest.raises(InvalidError):
|
192
|
-
m([3]) # type: ignore
|
193
|
-
with pytest.raises(InvalidError):
|
194
|
-
m([["double-nested-package"]]) # type: ignore
|
195
|
-
|
196
|
-
|
197
|
-
def test_image_requirements_txt(builder_version, servicer, client):
|
198
|
-
requirements_txt = os.path.join(os.path.dirname(__file__), "supports/test-requirements.txt")
|
199
|
-
|
200
|
-
app = App()
|
201
|
-
app.image = Image.debian_slim().pip_install_from_requirements(requirements_txt)
|
202
|
-
with app.run(client=client):
|
203
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
204
|
-
|
205
|
-
assert any("COPY /.requirements.txt /.requirements.txt" in cmd for cmd in layers[0].dockerfile_commands)
|
206
|
-
assert any("pip install -r /.requirements.txt" in cmd for cmd in layers[0].dockerfile_commands)
|
207
|
-
assert any(b"banana" in f.data for f in layers[0].context_files)
|
208
|
-
|
209
|
-
|
210
|
-
def test_empty_install(builder_version, servicer, client):
|
211
|
-
# Install functions with no packages should be ignored.
|
212
|
-
app = App(
|
213
|
-
image=Image.debian_slim()
|
214
|
-
.pip_install()
|
215
|
-
.pip_install([], [], [], [])
|
216
|
-
.apt_install([])
|
217
|
-
.run_commands()
|
218
|
-
.conda_install()
|
219
|
-
)
|
220
|
-
|
221
|
-
with app.run(client=client):
|
222
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
223
|
-
assert len(layers) == 1
|
224
|
-
|
225
|
-
|
226
|
-
def test_debian_slim_apt_install(builder_version, servicer, client):
|
227
|
-
app = App(image=Image.debian_slim().pip_install("numpy").apt_install("git", "ssh").pip_install("scikit-learn"))
|
228
|
-
|
229
|
-
with app.run(client=client):
|
230
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
231
|
-
|
232
|
-
assert any("pip install scikit-learn" in cmd for cmd in layers[0].dockerfile_commands)
|
233
|
-
assert any("apt-get install -y git ssh" in cmd for cmd in layers[1].dockerfile_commands)
|
234
|
-
assert any("pip install numpy" in cmd for cmd in layers[2].dockerfile_commands)
|
235
|
-
|
236
|
-
|
237
|
-
def test_image_pip_install_pyproject(builder_version, servicer, client):
|
238
|
-
pyproject_toml = os.path.join(os.path.dirname(__file__), "supports/test-pyproject.toml")
|
239
|
-
|
240
|
-
app = App()
|
241
|
-
app.image = Image.debian_slim().pip_install_from_pyproject(pyproject_toml)
|
242
|
-
with app.run(client=client):
|
243
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
244
|
-
|
245
|
-
print(layers[0].dockerfile_commands)
|
246
|
-
assert any("pip install 'banana >=1.2.0' 'potato >=0.1.0'" in cmd for cmd in layers[0].dockerfile_commands)
|
247
|
-
|
248
|
-
|
249
|
-
def test_image_pip_install_pyproject_with_optionals(builder_version, servicer, client):
|
250
|
-
pyproject_toml = os.path.join(os.path.dirname(__file__), "supports/test-pyproject.toml")
|
251
|
-
|
252
|
-
app = App()
|
253
|
-
app.image = Image.debian_slim().pip_install_from_pyproject(pyproject_toml, optional_dependencies=["dev", "test"])
|
254
|
-
with app.run(client=client):
|
255
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
256
|
-
|
257
|
-
print(layers[0].dockerfile_commands)
|
258
|
-
assert any(
|
259
|
-
"pip install 'banana >=1.2.0' 'linting-tool >=0.0.0' 'potato >=0.1.0' 'pytest >=1.2.0'" in cmd
|
260
|
-
for cmd in layers[0].dockerfile_commands
|
261
|
-
)
|
262
|
-
assert not (any("'mkdocs >=1.4.2'" in cmd for cmd in layers[0].dockerfile_commands))
|
263
|
-
|
264
|
-
|
265
|
-
def test_image_pip_install_private_repos(builder_version, servicer, client):
|
266
|
-
app = App()
|
267
|
-
with pytest.raises(InvalidError):
|
268
|
-
app.image = Image.debian_slim().pip_install_private_repos(
|
269
|
-
"github.com/ecorp/private-one@1.0.0",
|
270
|
-
git_user="erikbern",
|
271
|
-
secrets=[], # Invalid: missing secret
|
272
|
-
)
|
273
|
-
|
274
|
-
bad_repo_refs = [
|
275
|
-
"ecorp/private-one@1.0.0",
|
276
|
-
"gitspace.com/corp/private-one@1.0.0",
|
277
|
-
]
|
278
|
-
for invalid_ref in bad_repo_refs:
|
279
|
-
with pytest.raises(InvalidError):
|
280
|
-
app.image = Image.debian_slim().pip_install_private_repos(
|
281
|
-
invalid_ref,
|
282
|
-
git_user="erikbern",
|
283
|
-
secrets=[Secret.from_name("test-gh-read")],
|
284
|
-
)
|
285
|
-
|
286
|
-
app.image = Image.debian_slim().pip_install_private_repos(
|
287
|
-
"github.com/corp/private-one@1.0.0",
|
288
|
-
"gitlab.com/corp2/private-two@0.0.2",
|
289
|
-
git_user="erikbern",
|
290
|
-
secrets=[
|
291
|
-
Secret.from_dict({"GITHUB_TOKEN": "not-a-secret"}),
|
292
|
-
Secret.from_dict({"GITLAB_TOKEN": "not-a-secret"}),
|
293
|
-
],
|
294
|
-
)
|
295
|
-
|
296
|
-
with app.run(client=client):
|
297
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
298
|
-
assert len(layers[0].secret_ids) == 2
|
299
|
-
assert any(
|
300
|
-
'pip install "git+https://erikbern:$GITHUB_TOKEN@github.com/corp/private-one@1.0.0"' in cmd
|
301
|
-
for cmd in layers[0].dockerfile_commands
|
302
|
-
)
|
303
|
-
assert any(
|
304
|
-
'pip install "git+https://erikbern:$GITLAB_TOKEN@gitlab.com/corp2/private-two@0.0.2"' in cmd
|
305
|
-
for cmd in layers[0].dockerfile_commands
|
306
|
-
)
|
307
|
-
|
308
|
-
|
309
|
-
def test_conda_install(builder_version, servicer, client):
|
310
|
-
app = App(image=Image.conda().pip_install("numpy").conda_install("pymc3", "theano").pip_install("scikit-learn"))
|
311
|
-
|
312
|
-
with app.run(client=client):
|
313
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
314
|
-
|
315
|
-
assert any("pip install scikit-learn" in cmd for cmd in layers[0].dockerfile_commands)
|
316
|
-
assert any("conda install pymc3 theano --yes" in cmd for cmd in layers[1].dockerfile_commands)
|
317
|
-
assert any("pip install numpy" in cmd for cmd in layers[2].dockerfile_commands)
|
318
|
-
|
319
|
-
|
320
|
-
def test_dockerfile_image(builder_version, servicer, client):
|
321
|
-
path = os.path.join(os.path.dirname(__file__), "supports/test-dockerfile")
|
322
|
-
|
323
|
-
app = App(image=Image.from_dockerfile(path))
|
324
|
-
|
325
|
-
with app.run(client=client):
|
326
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
327
|
-
|
328
|
-
assert any("RUN pip install numpy" in cmd for cmd in layers[1].dockerfile_commands)
|
329
|
-
|
330
|
-
|
331
|
-
def test_conda_update_from_environment(builder_version, servicer, client):
|
332
|
-
path = os.path.join(os.path.dirname(__file__), "supports/test-conda-environment.yml")
|
333
|
-
|
334
|
-
app = App(image=Image.conda().conda_update_from_environment(path))
|
335
|
-
|
336
|
-
with app.run(client=client):
|
337
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
338
|
-
|
339
|
-
assert any("RUN conda env update" in cmd for cmd in layers[0].dockerfile_commands)
|
340
|
-
assert any(b"foo=1.0" in f.data for f in layers[0].context_files)
|
341
|
-
assert any(b"bar=2.1" in f.data for f in layers[0].context_files)
|
342
|
-
|
343
|
-
|
344
|
-
def test_run_commands(builder_version, servicer, client):
|
345
|
-
base = Image.debian_slim()
|
346
|
-
|
347
|
-
command = "echo 'Hello Modal'"
|
348
|
-
app = App(image=base.run_commands(command))
|
349
|
-
with app.run(client=client):
|
350
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
351
|
-
assert layers[0].dockerfile_commands[1] == f"RUN {command}"
|
352
|
-
|
353
|
-
commands = ["echo 'Hello world'", "touch agi.yaml"]
|
354
|
-
for image in [base.run_commands(commands), base.run_commands(*commands)]:
|
355
|
-
app = App(image=image)
|
356
|
-
with app.run(client=client):
|
357
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
358
|
-
for i, cmd in enumerate(commands, 1):
|
359
|
-
assert layers[0].dockerfile_commands[i] == f"RUN {cmd}"
|
360
|
-
|
361
|
-
|
362
|
-
def test_dockerhub_install(builder_version, servicer, client):
|
363
|
-
app = App(image=Image.from_registry("gisops/valhalla:latest", setup_dockerfile_commands=["RUN apt-get update"]))
|
364
|
-
|
365
|
-
with app.run(client=client):
|
366
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
367
|
-
|
368
|
-
assert any("FROM gisops/valhalla:latest" in cmd for cmd in layers[0].dockerfile_commands)
|
369
|
-
assert any("RUN apt-get update" in cmd for cmd in layers[0].dockerfile_commands)
|
370
|
-
|
371
|
-
|
372
|
-
def test_ecr_install(builder_version, servicer, client):
|
373
|
-
image_tag = "000000000000.dkr.ecr.us-east-1.amazonaws.com/my-private-registry:latest"
|
374
|
-
app = App(
|
375
|
-
image=Image.from_aws_ecr(
|
376
|
-
image_tag,
|
377
|
-
setup_dockerfile_commands=["RUN apt-get update"],
|
378
|
-
secret=Secret.from_dict({"AWS_ACCESS_KEY_ID": "", "AWS_SECRET_ACCESS_KEY": ""}),
|
379
|
-
)
|
380
|
-
)
|
381
|
-
|
382
|
-
with app.run(client=client):
|
383
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
384
|
-
|
385
|
-
assert any(f"FROM {image_tag}" in cmd for cmd in layers[0].dockerfile_commands)
|
386
|
-
assert any("RUN apt-get update" in cmd for cmd in layers[0].dockerfile_commands)
|
387
|
-
|
388
|
-
|
389
|
-
def run_f():
|
390
|
-
print("foo!")
|
391
|
-
|
392
|
-
|
393
|
-
def test_image_run_function(builder_version, servicer, client):
|
394
|
-
app = App()
|
395
|
-
app.image = (
|
396
|
-
Image.debian_slim().pip_install("pandas").run_function(run_f, secrets=[Secret.from_dict({"xyz": "123"})])
|
397
|
-
)
|
398
|
-
|
399
|
-
with app.run(client=client):
|
400
|
-
image_id = app.image.object_id
|
401
|
-
layers = get_image_layers(image_id, servicer)
|
402
|
-
assert "foo!" in layers[0].build_function.definition
|
403
|
-
assert "Secret.from_dict([xyz])" in layers[0].build_function.definition
|
404
|
-
# globals is none when no globals are referenced
|
405
|
-
assert layers[0].build_function.globals == b""
|
406
|
-
|
407
|
-
function_id = servicer.image_build_function_ids[image_id]
|
408
|
-
assert function_id
|
409
|
-
assert servicer.app_functions[function_id].function_name == "run_f"
|
410
|
-
assert len(servicer.app_functions[function_id].secret_ids) == 1
|
411
|
-
|
412
|
-
|
413
|
-
def test_image_run_function_interactivity(builder_version, servicer, client):
|
414
|
-
app = App()
|
415
|
-
app.image = Image.debian_slim().pip_install("pandas").run_function(run_f)
|
416
|
-
|
417
|
-
from modal.runner import run_app
|
418
|
-
|
419
|
-
with run_app(app, client=client, shell=True):
|
420
|
-
image_id = app.image.object_id
|
421
|
-
layers = get_image_layers(image_id, servicer)
|
422
|
-
assert "foo!" in layers[0].build_function.definition
|
423
|
-
|
424
|
-
function_id = servicer.image_build_function_ids[image_id]
|
425
|
-
assert function_id
|
426
|
-
assert servicer.app_functions[function_id].function_name == "run_f"
|
427
|
-
assert not servicer.app_functions[function_id].pty_info.enabled
|
428
|
-
|
429
|
-
|
430
|
-
VARIABLE_1 = 1
|
431
|
-
VARIABLE_2 = 3
|
432
|
-
|
433
|
-
|
434
|
-
def run_f_globals():
|
435
|
-
print("foo!", VARIABLE_1)
|
436
|
-
|
437
|
-
|
438
|
-
def test_image_run_function_globals(builder_version, servicer, client):
|
439
|
-
global VARIABLE_1, VARIABLE_2
|
440
|
-
|
441
|
-
app = App()
|
442
|
-
app.image = Image.debian_slim().run_function(run_f_globals)
|
443
|
-
|
444
|
-
with app.run(client=client):
|
445
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
446
|
-
old_globals = layers[0].build_function.globals
|
447
|
-
assert b"VARIABLE_1" in old_globals
|
448
|
-
assert b"VARIABLE_2" not in old_globals
|
449
|
-
|
450
|
-
VARIABLE_1 = 3
|
451
|
-
with app.run(client=client):
|
452
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
453
|
-
assert layers[0].build_function.globals != old_globals
|
454
|
-
|
455
|
-
VARIABLE_1 = 1
|
456
|
-
with app.run(client=client):
|
457
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
458
|
-
assert layers[0].build_function.globals == old_globals
|
459
|
-
|
460
|
-
|
461
|
-
VARIABLE_3 = threading.Lock()
|
462
|
-
VARIABLE_4 = "bar"
|
463
|
-
|
464
|
-
|
465
|
-
def run_f_unserializable_globals():
|
466
|
-
print("foo!", VARIABLE_3, VARIABLE_4)
|
467
|
-
|
468
|
-
|
469
|
-
def test_image_run_unserializable_function(builder_version, servicer, client):
|
470
|
-
app = App()
|
471
|
-
app.image = Image.debian_slim().run_function(run_f_unserializable_globals)
|
472
|
-
|
473
|
-
with app.run(client=client):
|
474
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
475
|
-
old_globals = layers[0].build_function.globals
|
476
|
-
assert b"VARIABLE_4" in old_globals
|
477
|
-
|
478
|
-
|
479
|
-
def run_f_with_args(arg, *, kwarg):
|
480
|
-
print("building!", arg, kwarg)
|
481
|
-
|
482
|
-
|
483
|
-
def test_image_run_function_with_args(builder_version, servicer, client):
|
484
|
-
app = App()
|
485
|
-
app.image = Image.debian_slim().run_function(run_f_with_args, args=("foo",), kwargs={"kwarg": "bar"})
|
486
|
-
|
487
|
-
with app.run(client=client):
|
488
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
489
|
-
input = layers[0].build_function.input
|
490
|
-
assert input.args == serialize((("foo",), {"kwarg": "bar"}))
|
491
|
-
|
492
|
-
|
493
|
-
def test_poetry(builder_version, servicer, client):
|
494
|
-
path = os.path.join(os.path.dirname(__file__), "supports/pyproject.toml")
|
495
|
-
|
496
|
-
# No lockfile provided and there's no lockfile found
|
497
|
-
# TODO we deferred the exception until _load runs, not sure how to test that here
|
498
|
-
# with pytest.raises(NotFoundError):
|
499
|
-
# Image.debian_slim().poetry_install_from_file(path)
|
500
|
-
|
501
|
-
# Explicitly ignore lockfile - this should work
|
502
|
-
Image.debian_slim().poetry_install_from_file(path, ignore_lockfile=True)
|
503
|
-
|
504
|
-
# Provide lockfile explicitly - this should also work
|
505
|
-
lockfile_path = os.path.join(os.path.dirname(__file__), "supports/special_poetry.lock")
|
506
|
-
image = Image.debian_slim().poetry_install_from_file(path, lockfile_path)
|
507
|
-
|
508
|
-
# Build iamge
|
509
|
-
app = App()
|
510
|
-
app.image = image
|
511
|
-
with app.run(client=client):
|
512
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
513
|
-
context_files = {f.filename for layer in layers for f in layer.context_files}
|
514
|
-
assert context_files == {"/.poetry.lock", "/.pyproject.toml", "/modal_requirements.txt"}
|
515
|
-
|
516
|
-
|
517
|
-
@pytest.fixture
|
518
|
-
def tmp_path_with_content(tmp_path):
|
519
|
-
(tmp_path / "data.txt").write_text("hello")
|
520
|
-
(tmp_path / "data").mkdir()
|
521
|
-
(tmp_path / "data" / "sub").write_text("world")
|
522
|
-
return tmp_path
|
523
|
-
|
524
|
-
|
525
|
-
def test_image_copy_local_dir(builder_version, servicer, client, tmp_path_with_content):
|
526
|
-
app = App()
|
527
|
-
app.image = Image.debian_slim().copy_local_dir(tmp_path_with_content, remote_path="/dummy")
|
528
|
-
|
529
|
-
with app.run(client=client):
|
530
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
531
|
-
assert "COPY . /dummy" in layers[0].dockerfile_commands
|
532
|
-
assert set(servicer.mount_contents["mo-1"].keys()) == {"/data.txt", "/data/sub"}
|
533
|
-
|
534
|
-
|
535
|
-
def test_image_docker_command_copy(builder_version, servicer, client, tmp_path_with_content):
|
536
|
-
app = App()
|
537
|
-
data_mount = Mount.from_local_dir(tmp_path_with_content, remote_path="/")
|
538
|
-
app.image = Image.debian_slim().dockerfile_commands(["COPY . /dummy"], context_mount=data_mount)
|
539
|
-
|
540
|
-
with app.run(client=client):
|
541
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
542
|
-
assert "COPY . /dummy" in layers[0].dockerfile_commands
|
543
|
-
files = {f.mount_filename: f.content for f in Mount._get_files(data_mount.entries)}
|
544
|
-
assert files == {"/data.txt": b"hello", "/data/sub": b"world"}
|
545
|
-
|
546
|
-
|
547
|
-
def test_image_dockerfile_copy(builder_version, servicer, client, tmp_path_with_content):
|
548
|
-
dockerfile = NamedTemporaryFile("w", delete=False)
|
549
|
-
dockerfile.write("COPY . /dummy\n")
|
550
|
-
dockerfile.close()
|
551
|
-
|
552
|
-
app = App()
|
553
|
-
data_mount = Mount.from_local_dir(tmp_path_with_content, remote_path="/")
|
554
|
-
app.image = Image.debian_slim().from_dockerfile(dockerfile.name, context_mount=data_mount)
|
555
|
-
|
556
|
-
with app.run(client=client):
|
557
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
558
|
-
assert "COPY . /dummy" in layers[1].dockerfile_commands
|
559
|
-
files = {f.mount_filename: f.content for f in Mount._get_files(data_mount.entries)}
|
560
|
-
assert files == {"/data.txt": b"hello", "/data/sub": b"world"}
|
561
|
-
|
562
|
-
|
563
|
-
def test_image_env(builder_version, servicer, client):
|
564
|
-
app = App(image=Image.debian_slim().env({"HELLO": "world!"}))
|
565
|
-
|
566
|
-
with app.run(client=client):
|
567
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
568
|
-
assert any("ENV HELLO=" in cmd and "world!" in cmd for cmd in layers[0].dockerfile_commands)
|
569
|
-
|
570
|
-
|
571
|
-
def test_image_gpu(builder_version, servicer, client):
|
572
|
-
app = App(image=Image.debian_slim().run_commands("echo 0"))
|
573
|
-
with app.run(client=client):
|
574
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
575
|
-
assert layers[0].gpu_config.type == api_pb2.GPU_TYPE_UNSPECIFIED
|
576
|
-
|
577
|
-
with pytest.warns(DeprecationError):
|
578
|
-
app = App(image=Image.debian_slim().run_commands("echo 1", gpu=True))
|
579
|
-
with app.run(client=client):
|
580
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
581
|
-
assert layers[0].gpu_config.type == api_pb2.GPU_TYPE_ANY
|
582
|
-
|
583
|
-
app = App(image=Image.debian_slim().run_commands("echo 2", gpu=gpu.A10G()))
|
584
|
-
with app.run(client=client):
|
585
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
586
|
-
assert layers[0].gpu_config.type == api_pb2.GPU_TYPE_A10G
|
587
|
-
|
588
|
-
|
589
|
-
def test_image_force_build(builder_version, servicer, client):
|
590
|
-
app = App()
|
591
|
-
app.image = Image.debian_slim().run_commands("echo 1").pip_install("foo", force_build=True).run_commands("echo 2")
|
592
|
-
with app.run(client=client):
|
593
|
-
assert servicer.force_built_images == ["im-3", "im-4"]
|
594
|
-
|
595
|
-
app.image = (
|
596
|
-
Image.from_gcp_artifact_registry("foo", force_build=True)
|
597
|
-
.run_commands("python_packagesecho 1")
|
598
|
-
.pip_install("foo", force_build=True)
|
599
|
-
.run_commands("echo 2")
|
600
|
-
)
|
601
|
-
with app.run(client=client):
|
602
|
-
assert servicer.force_built_images == ["im-3", "im-4", "im-5", "im-6", "im-7", "im-8"]
|
603
|
-
|
604
|
-
|
605
|
-
def test_workdir(builder_version, servicer, client):
|
606
|
-
app = App(image=Image.debian_slim().workdir("/foo/bar"))
|
607
|
-
|
608
|
-
with app.run(client=client):
|
609
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
610
|
-
|
611
|
-
assert any("WORKDIR /foo/bar" in cmd for cmd in layers[0].dockerfile_commands)
|
612
|
-
|
613
|
-
|
614
|
-
cls_app = App()
|
615
|
-
|
616
|
-
VARIABLE_5 = 1
|
617
|
-
VARIABLE_6 = 1
|
618
|
-
|
619
|
-
|
620
|
-
@cls_app.cls(
|
621
|
-
image=Image.debian_slim().pip_install("pandas"),
|
622
|
-
secrets=[Secret.from_dict({"xyz": "123"})],
|
623
|
-
)
|
624
|
-
class Foo:
|
625
|
-
@build()
|
626
|
-
def build_func(self):
|
627
|
-
global VARIABLE_5
|
628
|
-
|
629
|
-
print("foo!", VARIABLE_5)
|
630
|
-
|
631
|
-
@method()
|
632
|
-
def f(self):
|
633
|
-
global VARIABLE_6
|
634
|
-
|
635
|
-
print("bar!", VARIABLE_6)
|
636
|
-
|
637
|
-
|
638
|
-
def test_image_build_snapshot(client, servicer):
|
639
|
-
with cls_app.run(client=client):
|
640
|
-
image_id = list(servicer.images.keys())[-1]
|
641
|
-
layers = get_image_layers(image_id, servicer)
|
642
|
-
|
643
|
-
assert "foo!" in layers[0].build_function.definition
|
644
|
-
assert "Secret.from_dict([xyz])" in layers[0].build_function.definition
|
645
|
-
assert any("pip install pandas" in cmd for cmd in layers[1].dockerfile_commands)
|
646
|
-
|
647
|
-
globals = layers[0].build_function.globals
|
648
|
-
assert b"VARIABLE_5" in globals
|
649
|
-
|
650
|
-
# Globals and def for the main function should not affect build step.
|
651
|
-
assert "bar!" not in layers[0].build_function.definition
|
652
|
-
assert b"VARIABLE_6" not in globals
|
653
|
-
|
654
|
-
function_id = servicer.image_build_function_ids[image_id]
|
655
|
-
assert function_id
|
656
|
-
assert servicer.app_functions[function_id].function_name == "Foo.build_func"
|
657
|
-
assert len(servicer.app_functions[function_id].secret_ids) == 1
|
658
|
-
|
659
|
-
|
660
|
-
def test_inside_ctx_unhydrated(client):
|
661
|
-
image_1 = Image.debian_slim()
|
662
|
-
image_2 = Image.debian_slim()
|
663
|
-
|
664
|
-
with mock.patch.dict(os.environ, {"MODAL_IMAGE_ID": "im-123"}):
|
665
|
-
# This should initially swallow the exception
|
666
|
-
with image_1.imports():
|
667
|
-
raise ImportError("foo")
|
668
|
-
|
669
|
-
# This one too
|
670
|
-
with image_2.imports():
|
671
|
-
raise ImportError("bar")
|
672
|
-
|
673
|
-
# non-ImportErrors should trigger a warning
|
674
|
-
with pytest.warns(match="ImportError"):
|
675
|
-
with image_2.imports():
|
676
|
-
raise Exception("foo")
|
677
|
-
|
678
|
-
# Old one raises
|
679
|
-
with pytest.raises(DeprecationError, match="imports()"):
|
680
|
-
image_1.run_inside()
|
681
|
-
|
682
|
-
# Hydration of the image should raise the exception
|
683
|
-
with pytest.raises(ImportError, match="foo"):
|
684
|
-
image_1._hydrate("im-123", client, None)
|
685
|
-
|
686
|
-
# Should not raise since it's a different image
|
687
|
-
image_2._hydrate("im-456", client, None)
|
688
|
-
|
689
|
-
|
690
|
-
def test_inside_ctx_hydrated(client):
|
691
|
-
image_1 = Image.debian_slim()
|
692
|
-
image_2 = Image.debian_slim()
|
693
|
-
|
694
|
-
with mock.patch.dict(os.environ, {"MODAL_IMAGE_ID": "im-123"}):
|
695
|
-
# Assign ids before the ctx mgr runs
|
696
|
-
image_1._hydrate("im-123", client, None)
|
697
|
-
image_2._hydrate("im-456", client, None)
|
698
|
-
|
699
|
-
# Ctx manager should now raise right away
|
700
|
-
with pytest.raises(ImportError, match="baz"):
|
701
|
-
with image_1.imports():
|
702
|
-
raise ImportError("baz")
|
703
|
-
|
704
|
-
# We're not inside this image so this should be swallowed
|
705
|
-
with image_2.imports():
|
706
|
-
raise ImportError("bar")
|
707
|
-
|
708
|
-
|
709
|
-
@pytest.mark.parametrize("python_version", ["3.11", "3.12", "3.12.1", "3.12.1-gnu"])
|
710
|
-
def test_get_modal_requirements_path(builder_version, python_version):
|
711
|
-
path = _get_modal_requirements_path(builder_version, python_version)
|
712
|
-
if builder_version == "2023.12" and python_version.startswith("3.12"):
|
713
|
-
assert path.endswith("2023.12.312.txt")
|
714
|
-
else:
|
715
|
-
assert path.endswith(f"{builder_version}.txt")
|
716
|
-
|
717
|
-
|
718
|
-
def test_image_builder_version(servicer, test_dir, modal_config):
|
719
|
-
app = App(image=Image.debian_slim())
|
720
|
-
# TODO use a single with statement and tuple of managers when we drop Py3.8
|
721
|
-
test_requirements = str(test_dir / "supports" / "test-requirements.txt")
|
722
|
-
with mock.patch("modal.image._get_modal_requirements_path", lambda *_, **__: test_requirements):
|
723
|
-
with mock.patch("modal.image._dockerhub_python_version", lambda *_, **__: "3.11.0"):
|
724
|
-
with mock.patch("modal.image._dockerhub_debian_codename", lambda *_, **__: "bullseye"):
|
725
|
-
with mock.patch("test.conftest.ImageBuilderVersion", Literal["2000.01"]):
|
726
|
-
with mock.patch("modal.image.ImageBuilderVersion", Literal["2000.01"]):
|
727
|
-
with Client(
|
728
|
-
servicer.remote_addr, api_pb2.CLIENT_TYPE_CONTAINER, ("ak-123", "as-xyz")
|
729
|
-
) as client:
|
730
|
-
with modal_config():
|
731
|
-
with app.run(client=client):
|
732
|
-
assert servicer.image_builder_versions
|
733
|
-
for version in servicer.image_builder_versions.values():
|
734
|
-
assert version == "2000.01"
|
735
|
-
|
736
|
-
|
737
|
-
def test_image_builder_supported_versions(servicer):
|
738
|
-
app = App(image=Image.debian_slim())
|
739
|
-
# TODO use a single with statement and tuple of managers when we drop Py3.8
|
740
|
-
with pytest.raises(VersionError, match=r"This version of the modal client supports.+{'2000.01'}"):
|
741
|
-
with mock.patch("modal.image.ImageBuilderVersion", Literal["2000.01"]):
|
742
|
-
with mock.patch("test.conftest.ImageBuilderVersion", Literal["2023.11"]):
|
743
|
-
with Client(servicer.remote_addr, api_pb2.CLIENT_TYPE_CONTAINER, ("ak-123", "as-xyz")) as client:
|
744
|
-
with app.run(client=client):
|
745
|
-
pass
|
746
|
-
|
747
|
-
|
748
|
-
@pytest.fixture
|
749
|
-
def force_2023_12(modal_config):
|
750
|
-
with mock.patch("test.conftest.ImageBuilderVersion", Literal["2023.12"]):
|
751
|
-
with modal_config():
|
752
|
-
yield
|
753
|
-
|
754
|
-
|
755
|
-
@skip_windows("Different hash values for context file paths")
|
756
|
-
def test_image_stability_on_2023_12(force_2023_12, servicer, client, test_dir):
|
757
|
-
def get_hash(img: Image) -> str:
|
758
|
-
app = App(image=img)
|
759
|
-
with app.run(client=client):
|
760
|
-
layers = get_image_layers(app.image.object_id, servicer)
|
761
|
-
commands = [layer.dockerfile_commands for layer in layers]
|
762
|
-
context_files = [[(f.filename, f.data) for f in layer.context_files] for layer in layers]
|
763
|
-
return sha256(repr(list(zip(commands, context_files))).encode()).hexdigest()
|
764
|
-
|
765
|
-
if sys.version_info[:2] == (3, 11):
|
766
|
-
# Matches my development environment — default is to match Python version from local system
|
767
|
-
img = Image.debian_slim()
|
768
|
-
assert get_hash(img) == "183b86356d9eb3bd3d78adf70f16b35b63ba9bf4e1816b0cacc549541718e555"
|
769
|
-
|
770
|
-
img = Image.debian_slim(python_version="3.12")
|
771
|
-
assert get_hash(img) == "53b6205e1dc2a0ca7ebed862e4f3a5887367587be13e81f65a4ac8f8a1e9be91"
|
772
|
-
|
773
|
-
if sys.version_info[:2] < (3, 12):
|
774
|
-
# Client dependencies on 3.12 are different
|
775
|
-
img = Image.from_registry("ubuntu:22.04")
|
776
|
-
assert get_hash(img) == "b5f1cc544a412d1b23a5ebf9a8859ea9a86975ecbc7325b83defc0ce3fe956d3"
|
777
|
-
|
778
|
-
img = Image.conda()
|
779
|
-
assert get_hash(img) == "f69d6af66fb5f1a2372a61836e6166ce79ebe2cd628d12addea8e8e80cc98dc1"
|
780
|
-
|
781
|
-
img = Image.micromamba()
|
782
|
-
assert get_hash(img) == "fa883741544ea191ecd197c8f83a1ffe9912575faa8c107c66b3dda761b2e401"
|
783
|
-
|
784
|
-
img = Image.from_dockerfile(test_dir / "supports" / "test-dockerfile")
|
785
|
-
assert get_hash(img) == "0aec2f66f28ee7511c1b36604214ae7b40d9bc1fa3e6b8883001e933a966ff78"
|
786
|
-
|
787
|
-
img = Image.conda(python_version="3.12")
|
788
|
-
assert get_hash(img) == "c4b3f7350116d323dded29c9c9b78b62593f0fc943ccf83a09b27185bfdc2a07"
|
789
|
-
|
790
|
-
img = Image.micromamba(python_version="3.12")
|
791
|
-
assert get_hash(img) == "468befe16f703a3ae1a794dfe54c1a3445ca0ffda233f55f1d66c45ad608e8aa"
|
792
|
-
|
793
|
-
base = Image.debian_slim(python_version="3.12")
|
794
|
-
|
795
|
-
img = base.run_commands("echo 'Hello Modal'", "rm /usr/local/bin/kubectl")
|
796
|
-
assert get_hash(img) == "4e1ac62eb33b44dd16940c9d2719eb79f945cee61cbf4641ca99b19cd9e0976d"
|
797
|
-
|
798
|
-
img = base.pip_install("torch~=2.2", "transformers==4.23.0", pre=True, index_url="agi.se")
|
799
|
-
assert get_hash(img) == "2a4fa8e3b32c70a41b3a3efd5416540b1953430543f6c27c984e7f969c2ca874"
|
800
|
-
|
801
|
-
img = base.conda_install("torch=2.2", "transformers<4.23.0", channels=["conda-forge", "my-channel"])
|
802
|
-
assert get_hash(img) == "dd6f27f636293996a64a98c250161d8092cb23d02629d9070493f00aad8d7266"
|
803
|
-
|
804
|
-
img = base.pip_install_from_requirements(test_dir / "supports" / "test-requirements.txt")
|
805
|
-
assert get_hash(img) == "69d41e699d4ecef399e51e8460f8857aa0ec57f71f00eca81c8886ec062e5c2b"
|
806
|
-
|
807
|
-
img = base.conda_update_from_environment(test_dir / "supports" / "test-conda-environment.yml")
|
808
|
-
assert get_hash(img) == "00940e0ee2998bfe0a337f51a5fdf5f4b29bf9d42dda3635641d44bfeb42537e"
|
809
|
-
|
810
|
-
img = base.poetry_install_from_file(
|
811
|
-
test_dir / "supports" / "test-pyproject.toml",
|
812
|
-
poetry_lockfile=test_dir / "supports" / "special_poetry.lock",
|
813
|
-
)
|
814
|
-
assert get_hash(img) == "a25dd4cc2e8d88f92bfdaf2e82b9d74144d1928926bf6be2ca1cdfbbf562189e"
|