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/function_test.py
DELETED
@@ -1,653 +0,0 @@
|
|
1
|
-
# Copyright Modal Labs 2022
|
2
|
-
import asyncio
|
3
|
-
import inspect
|
4
|
-
import pytest
|
5
|
-
import time
|
6
|
-
import typing
|
7
|
-
|
8
|
-
from synchronicity.exceptions import UserCodeException
|
9
|
-
|
10
|
-
import modal
|
11
|
-
from modal import Image, Mount, NetworkFileSystem, Proxy, Stub, web_endpoint
|
12
|
-
from modal._vendor import cloudpickle
|
13
|
-
from modal.exception import ExecutionError, InvalidError
|
14
|
-
from modal.functions import Function, FunctionCall, gather
|
15
|
-
from modal.runner import deploy_stub
|
16
|
-
from modal_proto import api_pb2
|
17
|
-
|
18
|
-
stub = Stub()
|
19
|
-
|
20
|
-
|
21
|
-
@stub.function()
|
22
|
-
def foo(p, q):
|
23
|
-
return p + q + 11 # not actually used in test (servicer returns sum of square of all args)
|
24
|
-
|
25
|
-
|
26
|
-
@stub.function()
|
27
|
-
async def async_foo(p, q):
|
28
|
-
return p + q + 12
|
29
|
-
|
30
|
-
|
31
|
-
def dummy():
|
32
|
-
pass # not actually used in test (servicer returns sum of square of all args)
|
33
|
-
|
34
|
-
|
35
|
-
def test_run_function(client, servicer):
|
36
|
-
assert len(servicer.cleared_function_calls) == 0
|
37
|
-
with stub.run(client=client):
|
38
|
-
assert foo.remote(2, 4) == 20
|
39
|
-
assert len(servicer.cleared_function_calls) == 1
|
40
|
-
|
41
|
-
|
42
|
-
@pytest.mark.asyncio
|
43
|
-
async def test_call_function_locally(client, servicer):
|
44
|
-
assert foo.local(22, 44) == 77 # call it locally
|
45
|
-
assert await async_foo.local(22, 44) == 78
|
46
|
-
|
47
|
-
with stub.run(client=client):
|
48
|
-
assert foo.remote(2, 4) == 20
|
49
|
-
assert async_foo.remote(2, 4) == 20
|
50
|
-
assert await async_foo.remote.aio(2, 4) == 20
|
51
|
-
|
52
|
-
|
53
|
-
@pytest.mark.parametrize("slow_put_inputs", [False, True])
|
54
|
-
@pytest.mark.timeout(120)
|
55
|
-
def test_map(client, servicer, slow_put_inputs):
|
56
|
-
servicer.slow_put_inputs = slow_put_inputs
|
57
|
-
|
58
|
-
stub = Stub()
|
59
|
-
dummy_modal = stub.function()(dummy)
|
60
|
-
|
61
|
-
assert len(servicer.cleared_function_calls) == 0
|
62
|
-
with stub.run(client=client):
|
63
|
-
assert list(dummy_modal.map([5, 2], [4, 3])) == [41, 13]
|
64
|
-
assert len(servicer.cleared_function_calls) == 1
|
65
|
-
assert set(dummy_modal.map([5, 2], [4, 3], order_outputs=False)) == {13, 41}
|
66
|
-
assert len(servicer.cleared_function_calls) == 2
|
67
|
-
|
68
|
-
|
69
|
-
_side_effect_count = 0
|
70
|
-
|
71
|
-
|
72
|
-
def side_effect(_):
|
73
|
-
global _side_effect_count
|
74
|
-
_side_effect_count += 1
|
75
|
-
|
76
|
-
|
77
|
-
def test_for_each(client, servicer):
|
78
|
-
stub = Stub()
|
79
|
-
side_effect_modal = stub.function()(servicer.function_body(side_effect))
|
80
|
-
assert _side_effect_count == 0
|
81
|
-
with stub.run(client=client):
|
82
|
-
side_effect_modal.for_each(range(10))
|
83
|
-
|
84
|
-
assert _side_effect_count == 10
|
85
|
-
|
86
|
-
|
87
|
-
def custom_function(x):
|
88
|
-
if x % 2 == 0:
|
89
|
-
return x
|
90
|
-
|
91
|
-
|
92
|
-
def test_map_none_values(client, servicer):
|
93
|
-
stub = Stub()
|
94
|
-
|
95
|
-
custom_function_modal = stub.function()(servicer.function_body(custom_function))
|
96
|
-
|
97
|
-
with stub.run(client=client):
|
98
|
-
assert list(custom_function_modal.map(range(4))) == [0, None, 2, None]
|
99
|
-
|
100
|
-
|
101
|
-
def test_starmap(client):
|
102
|
-
stub = Stub()
|
103
|
-
|
104
|
-
dummy_modal = stub.function()(dummy)
|
105
|
-
with stub.run(client=client):
|
106
|
-
assert list(dummy_modal.starmap([[5, 2], [4, 3]])) == [29, 25]
|
107
|
-
|
108
|
-
|
109
|
-
def test_function_memory_request(client):
|
110
|
-
stub = Stub()
|
111
|
-
stub.function(memory=2048)(dummy)
|
112
|
-
|
113
|
-
|
114
|
-
def test_function_cpu_request(client):
|
115
|
-
stub = Stub()
|
116
|
-
stub.function(cpu=2.0)(dummy)
|
117
|
-
|
118
|
-
|
119
|
-
def later():
|
120
|
-
return "hello"
|
121
|
-
|
122
|
-
|
123
|
-
def test_function_future(client, servicer):
|
124
|
-
stub = Stub()
|
125
|
-
|
126
|
-
later_modal = stub.function()(servicer.function_body(later))
|
127
|
-
with stub.run(client=client):
|
128
|
-
future = later_modal.spawn()
|
129
|
-
assert isinstance(future, FunctionCall)
|
130
|
-
|
131
|
-
servicer.function_is_running = True
|
132
|
-
assert future.object_id == "fc-1"
|
133
|
-
|
134
|
-
with pytest.raises(TimeoutError):
|
135
|
-
future.get(0.01)
|
136
|
-
|
137
|
-
servicer.function_is_running = False
|
138
|
-
assert future.get(0.01) == "hello"
|
139
|
-
assert future.object_id not in servicer.cleared_function_calls
|
140
|
-
|
141
|
-
future = later_modal.spawn()
|
142
|
-
|
143
|
-
servicer.function_is_running = True
|
144
|
-
assert future.object_id == "fc-2"
|
145
|
-
|
146
|
-
future.cancel()
|
147
|
-
assert "fc-2" in servicer.cancelled_calls
|
148
|
-
|
149
|
-
assert future.object_id not in servicer.cleared_function_calls
|
150
|
-
|
151
|
-
|
152
|
-
@pytest.mark.asyncio
|
153
|
-
async def test_function_future_async(client, servicer):
|
154
|
-
stub = Stub()
|
155
|
-
|
156
|
-
later_modal = stub.function()(servicer.function_body(later))
|
157
|
-
|
158
|
-
async with stub.run(client=client):
|
159
|
-
future = await later_modal.spawn.aio()
|
160
|
-
servicer.function_is_running = True
|
161
|
-
|
162
|
-
with pytest.raises(TimeoutError):
|
163
|
-
await future.get.aio(0.01)
|
164
|
-
|
165
|
-
servicer.function_is_running = False
|
166
|
-
assert await future.get.aio(0.01) == "hello"
|
167
|
-
assert future.object_id not in servicer.cleared_function_calls # keep results around a bit longer for futures
|
168
|
-
|
169
|
-
|
170
|
-
def later_gen():
|
171
|
-
yield "foo"
|
172
|
-
|
173
|
-
|
174
|
-
async def async_later_gen():
|
175
|
-
yield "foo"
|
176
|
-
|
177
|
-
|
178
|
-
@pytest.mark.asyncio
|
179
|
-
async def test_generator(client, servicer):
|
180
|
-
stub = Stub()
|
181
|
-
|
182
|
-
later_gen_modal = stub.function()(later_gen)
|
183
|
-
|
184
|
-
def dummy():
|
185
|
-
yield "bar"
|
186
|
-
yield "baz"
|
187
|
-
yield "boo"
|
188
|
-
|
189
|
-
servicer.function_body(dummy)
|
190
|
-
|
191
|
-
assert len(servicer.cleared_function_calls) == 0
|
192
|
-
with stub.run(client=client):
|
193
|
-
assert later_gen_modal.is_generator
|
194
|
-
res: typing.Generator = later_gen_modal.remote_gen() # type: ignore
|
195
|
-
# Generators fulfil the *iterator protocol*, which requires both these methods.
|
196
|
-
# https://docs.python.org/3/library/stdtypes.html#typeiter
|
197
|
-
assert hasattr(res, "__iter__") # strangely inspect.isgenerator returns false
|
198
|
-
assert hasattr(res, "__next__")
|
199
|
-
assert next(res) == "bar"
|
200
|
-
assert list(res) == ["baz", "boo"]
|
201
|
-
assert len(servicer.cleared_function_calls) == 1
|
202
|
-
|
203
|
-
|
204
|
-
@pytest.mark.asyncio
|
205
|
-
async def test_generator_map_invalid(client, servicer):
|
206
|
-
stub = Stub()
|
207
|
-
|
208
|
-
later_gen_modal = stub.function()(later_gen)
|
209
|
-
|
210
|
-
def dummy(x):
|
211
|
-
yield x
|
212
|
-
|
213
|
-
servicer.function_body(dummy)
|
214
|
-
|
215
|
-
with stub.run(client=client):
|
216
|
-
with pytest.raises(InvalidError):
|
217
|
-
# Support for .map() on generators was removed in version 0.57
|
218
|
-
for _ in later_gen_modal.map([1, 2, 3]):
|
219
|
-
pass
|
220
|
-
with pytest.raises(InvalidError):
|
221
|
-
later_gen_modal.for_each([1, 2, 3])
|
222
|
-
|
223
|
-
|
224
|
-
@pytest.mark.asyncio
|
225
|
-
async def test_generator_async(client, servicer):
|
226
|
-
stub = Stub()
|
227
|
-
|
228
|
-
later_gen_modal = stub.function()(async_later_gen)
|
229
|
-
|
230
|
-
async def async_dummy():
|
231
|
-
yield "bar"
|
232
|
-
yield "baz"
|
233
|
-
|
234
|
-
servicer.function_body(async_dummy)
|
235
|
-
|
236
|
-
assert len(servicer.cleared_function_calls) == 0
|
237
|
-
async with stub.run(client=client):
|
238
|
-
assert later_gen_modal.is_generator
|
239
|
-
res = later_gen_modal.remote_gen.aio()
|
240
|
-
# Async generators fulfil the *asynchronous iterator protocol*, which requires both these methods.
|
241
|
-
# https://peps.python.org/pep-0525/#support-for-asynchronous-iteration-protocol
|
242
|
-
assert hasattr(res, "__aiter__")
|
243
|
-
assert hasattr(res, "__anext__")
|
244
|
-
# TODO(Jonathon): This works outside of testing, but here gives:
|
245
|
-
# `TypeError: cannot pickle 'async_generator' object`
|
246
|
-
# await res.__anext__() == "bar"
|
247
|
-
# assert len(servicer.cleared_function_calls) == 1
|
248
|
-
|
249
|
-
|
250
|
-
@pytest.mark.asyncio
|
251
|
-
async def test_generator_future(client, servicer):
|
252
|
-
stub = Stub()
|
253
|
-
|
254
|
-
later_gen_modal = stub.function()(later_gen)
|
255
|
-
with stub.run(client=client):
|
256
|
-
assert later_gen_modal.spawn() is None # until we have a nice interface for polling generator futures
|
257
|
-
|
258
|
-
|
259
|
-
def gen_with_arg(i):
|
260
|
-
yield "foo"
|
261
|
-
|
262
|
-
|
263
|
-
async def slo1(sleep_seconds):
|
264
|
-
# need to use async function body in client test to run stuff in parallel
|
265
|
-
# but calling interface is still non-asyncio
|
266
|
-
await asyncio.sleep(sleep_seconds)
|
267
|
-
return sleep_seconds
|
268
|
-
|
269
|
-
|
270
|
-
def test_sync_parallelism(client, servicer):
|
271
|
-
stub = Stub()
|
272
|
-
|
273
|
-
slo1_modal = stub.function()(servicer.function_body(slo1))
|
274
|
-
with stub.run(client=client):
|
275
|
-
t0 = time.time()
|
276
|
-
# NOTE tests breaks in macOS CI if the smaller time is smaller than ~300ms
|
277
|
-
res = gather(slo1_modal.spawn(0.31), slo1_modal.spawn(0.3))
|
278
|
-
t1 = time.time()
|
279
|
-
assert res == [0.31, 0.3] # results should be ordered as inputs, not by completion time
|
280
|
-
assert t1 - t0 < 0.6 # less than the combined runtime, make sure they run in parallel
|
281
|
-
|
282
|
-
|
283
|
-
def test_proxy(client, servicer):
|
284
|
-
stub = Stub()
|
285
|
-
|
286
|
-
stub.function(proxy=Proxy.from_name("my-proxy"))(dummy)
|
287
|
-
with stub.run(client=client):
|
288
|
-
pass
|
289
|
-
|
290
|
-
|
291
|
-
class CustomException(Exception):
|
292
|
-
pass
|
293
|
-
|
294
|
-
|
295
|
-
def failure():
|
296
|
-
raise CustomException("foo!")
|
297
|
-
|
298
|
-
|
299
|
-
def test_function_exception(client, servicer):
|
300
|
-
stub = Stub()
|
301
|
-
|
302
|
-
failure_modal = stub.function()(servicer.function_body(failure))
|
303
|
-
with stub.run(client=client):
|
304
|
-
with pytest.raises(CustomException) as excinfo:
|
305
|
-
failure_modal.remote()
|
306
|
-
assert "foo!" in str(excinfo.value)
|
307
|
-
|
308
|
-
|
309
|
-
@pytest.mark.asyncio
|
310
|
-
async def test_function_exception_async(client, servicer):
|
311
|
-
stub = Stub()
|
312
|
-
|
313
|
-
failure_modal = stub.function()(servicer.function_body(failure))
|
314
|
-
async with stub.run(client=client):
|
315
|
-
with pytest.raises(CustomException) as excinfo:
|
316
|
-
coro = failure_modal.remote.aio()
|
317
|
-
assert inspect.isawaitable(
|
318
|
-
coro
|
319
|
-
) # mostly for mypy, since output could technically be an async generator which isn't awaitable in the same sense
|
320
|
-
await coro
|
321
|
-
assert "foo!" in str(excinfo.value)
|
322
|
-
|
323
|
-
|
324
|
-
def custom_exception_function(x):
|
325
|
-
if x == 4:
|
326
|
-
raise CustomException("bad")
|
327
|
-
return x * x
|
328
|
-
|
329
|
-
|
330
|
-
def test_map_exceptions(client, servicer):
|
331
|
-
stub = Stub()
|
332
|
-
|
333
|
-
custom_function_modal = stub.function()(servicer.function_body(custom_exception_function))
|
334
|
-
|
335
|
-
with stub.run(client=client):
|
336
|
-
assert list(custom_function_modal.map(range(4))) == [0, 1, 4, 9]
|
337
|
-
|
338
|
-
with pytest.raises(CustomException) as excinfo:
|
339
|
-
list(custom_function_modal.map(range(6)))
|
340
|
-
assert "bad" in str(excinfo.value)
|
341
|
-
|
342
|
-
res = list(custom_function_modal.map(range(6), return_exceptions=True))
|
343
|
-
assert res[:4] == [0, 1, 4, 9] and res[5] == 25
|
344
|
-
assert type(res[4]) == UserCodeException and "bad" in str(res[4])
|
345
|
-
|
346
|
-
|
347
|
-
def import_failure():
|
348
|
-
raise ImportError("attempted relative import with no known parent package")
|
349
|
-
|
350
|
-
|
351
|
-
def test_function_relative_import_hint(client, servicer):
|
352
|
-
stub = Stub()
|
353
|
-
|
354
|
-
import_failure_modal = stub.function()(servicer.function_body(import_failure))
|
355
|
-
|
356
|
-
with stub.run(client=client):
|
357
|
-
with pytest.raises(ImportError) as excinfo:
|
358
|
-
import_failure_modal.remote()
|
359
|
-
assert "HINT" in str(excinfo.value)
|
360
|
-
|
361
|
-
|
362
|
-
def test_nonglobal_function():
|
363
|
-
stub = Stub()
|
364
|
-
|
365
|
-
with pytest.raises(InvalidError) as excinfo:
|
366
|
-
|
367
|
-
@stub.function()
|
368
|
-
def f():
|
369
|
-
pass
|
370
|
-
|
371
|
-
assert "global scope" in str(excinfo.value)
|
372
|
-
|
373
|
-
|
374
|
-
def test_non_global_serialized_function():
|
375
|
-
stub = Stub()
|
376
|
-
|
377
|
-
@stub.function(serialized=True)
|
378
|
-
def f():
|
379
|
-
pass
|
380
|
-
|
381
|
-
|
382
|
-
def test_closure_valued_serialized_function(client, servicer):
|
383
|
-
stub = Stub()
|
384
|
-
|
385
|
-
def make_function(s):
|
386
|
-
@stub.function(name=f"ret_{s}", serialized=True)
|
387
|
-
def returner():
|
388
|
-
return s
|
389
|
-
|
390
|
-
for s in ["foo", "bar"]:
|
391
|
-
make_function(s)
|
392
|
-
|
393
|
-
with stub.run(client=client):
|
394
|
-
pass
|
395
|
-
|
396
|
-
functions = {}
|
397
|
-
for func in servicer.app_functions.values():
|
398
|
-
functions[func.function_name] = cloudpickle.loads(func.function_serialized)
|
399
|
-
|
400
|
-
assert len(functions) == 2
|
401
|
-
assert functions["ret_foo"]() == "foo"
|
402
|
-
assert functions["ret_bar"]() == "bar"
|
403
|
-
|
404
|
-
|
405
|
-
def test_new_hydrated_internal(client, servicer):
|
406
|
-
obj = FunctionCall._new_hydrated("fc-123", client, None)
|
407
|
-
assert obj.object_id == "fc-123"
|
408
|
-
|
409
|
-
|
410
|
-
def test_from_id(client, servicer):
|
411
|
-
stub = Stub()
|
412
|
-
|
413
|
-
@stub.function(serialized=True)
|
414
|
-
@web_endpoint()
|
415
|
-
def foo():
|
416
|
-
pass
|
417
|
-
|
418
|
-
deploy_stub(stub, "dummy", client=client)
|
419
|
-
|
420
|
-
function_id = foo.object_id
|
421
|
-
assert function_id
|
422
|
-
assert foo.web_url
|
423
|
-
|
424
|
-
function_call = foo.spawn()
|
425
|
-
assert function_call.object_id
|
426
|
-
# Used in a few examples to construct FunctionCall objects
|
427
|
-
rehydrated_function_call = FunctionCall.from_id(function_call.object_id, client)
|
428
|
-
assert rehydrated_function_call.object_id == function_call.object_id
|
429
|
-
|
430
|
-
|
431
|
-
lc_stub = Stub()
|
432
|
-
|
433
|
-
|
434
|
-
@lc_stub.function()
|
435
|
-
def f(x):
|
436
|
-
return x**2
|
437
|
-
|
438
|
-
|
439
|
-
def test_allow_cross_region_volumes(client, servicer):
|
440
|
-
stub = Stub()
|
441
|
-
vol1 = NetworkFileSystem.from_name("xyz-1", create_if_missing=True)
|
442
|
-
vol2 = NetworkFileSystem.from_name("xyz-2", create_if_missing=True)
|
443
|
-
# Should pass flag for all the function's NetworkFileSystemMounts
|
444
|
-
stub.function(network_file_systems={"/sv-1": vol1, "/sv-2": vol2}, allow_cross_region_volumes=True)(dummy)
|
445
|
-
|
446
|
-
with stub.run(client=client):
|
447
|
-
assert len(servicer.app_functions) == 1
|
448
|
-
for func in servicer.app_functions.values():
|
449
|
-
assert len(func.shared_volume_mounts) == 2
|
450
|
-
for svm in func.shared_volume_mounts:
|
451
|
-
assert svm.allow_cross_region
|
452
|
-
|
453
|
-
|
454
|
-
def test_allow_cross_region_volumes_webhook(client, servicer):
|
455
|
-
# TODO(erikbern): this test seems a bit redundant
|
456
|
-
stub = Stub()
|
457
|
-
vol1 = NetworkFileSystem.from_name("xyz-1", create_if_missing=True)
|
458
|
-
vol2 = NetworkFileSystem.from_name("xyz-2", create_if_missing=True)
|
459
|
-
# Should pass flag for all the function's NetworkFileSystemMounts
|
460
|
-
stub.function(network_file_systems={"/sv-1": vol1, "/sv-2": vol2}, allow_cross_region_volumes=True)(
|
461
|
-
web_endpoint()(dummy)
|
462
|
-
)
|
463
|
-
|
464
|
-
with stub.run(client=client):
|
465
|
-
assert len(servicer.app_functions) == 1
|
466
|
-
for func in servicer.app_functions.values():
|
467
|
-
assert len(func.shared_volume_mounts) == 2
|
468
|
-
for svm in func.shared_volume_mounts:
|
469
|
-
assert svm.allow_cross_region
|
470
|
-
|
471
|
-
|
472
|
-
def test_serialize_deserialize_function_handle(servicer, client):
|
473
|
-
from modal._serialization import deserialize, serialize
|
474
|
-
|
475
|
-
stub = Stub()
|
476
|
-
|
477
|
-
@stub.function(serialized=True)
|
478
|
-
@web_endpoint()
|
479
|
-
def my_handle():
|
480
|
-
pass
|
481
|
-
|
482
|
-
with pytest.raises(InvalidError, match="hasn't been created"):
|
483
|
-
serialize(my_handle) # handle is not "live" yet! should not be serializable yet
|
484
|
-
|
485
|
-
with stub.run(client=client):
|
486
|
-
blob = serialize(my_handle)
|
487
|
-
|
488
|
-
rehydrated_function_handle = deserialize(blob, client)
|
489
|
-
assert rehydrated_function_handle.object_id == my_handle.object_id
|
490
|
-
assert isinstance(rehydrated_function_handle, Function)
|
491
|
-
assert rehydrated_function_handle.web_url == "http://xyz.internal"
|
492
|
-
|
493
|
-
|
494
|
-
def test_default_cloud_provider(client, servicer, monkeypatch):
|
495
|
-
stub = Stub()
|
496
|
-
|
497
|
-
monkeypatch.setenv("MODAL_DEFAULT_CLOUD", "oci")
|
498
|
-
stub.function()(dummy)
|
499
|
-
with stub.run(client=client):
|
500
|
-
object_id: str = stub.indexed_objects["dummy"].object_id
|
501
|
-
f = servicer.app_functions[object_id]
|
502
|
-
|
503
|
-
assert f.cloud_provider == api_pb2.CLOUD_PROVIDER_OCI
|
504
|
-
|
505
|
-
|
506
|
-
def test_not_hydrated():
|
507
|
-
with pytest.raises(ExecutionError):
|
508
|
-
assert foo.remote(2, 4) == 20
|
509
|
-
|
510
|
-
|
511
|
-
def test_invalid_large_serialization(client):
|
512
|
-
big_data = b"1" * 500000
|
513
|
-
|
514
|
-
def f():
|
515
|
-
return big_data
|
516
|
-
|
517
|
-
with pytest.warns(UserWarning, match="larger than the recommended limit"):
|
518
|
-
stub = Stub()
|
519
|
-
stub.function(serialized=True)(f)
|
520
|
-
with stub.run(client=client):
|
521
|
-
pass
|
522
|
-
|
523
|
-
bigger_data = b"1" * 50000000
|
524
|
-
|
525
|
-
def g():
|
526
|
-
return bigger_data
|
527
|
-
|
528
|
-
with pytest.raises(InvalidError):
|
529
|
-
stub = Stub()
|
530
|
-
stub.function(serialized=True)(g)
|
531
|
-
with stub.run(client=client):
|
532
|
-
pass
|
533
|
-
|
534
|
-
|
535
|
-
def test_call_unhydrated_function():
|
536
|
-
with pytest.raises(ExecutionError, match="hydrated"):
|
537
|
-
foo.remote(123)
|
538
|
-
|
539
|
-
|
540
|
-
def test_deps_explicit(client, servicer):
|
541
|
-
stub = Stub()
|
542
|
-
|
543
|
-
image = Image.debian_slim()
|
544
|
-
nfs_1 = NetworkFileSystem.from_name("nfs-1", create_if_missing=True)
|
545
|
-
nfs_2 = NetworkFileSystem.from_name("nfs-2", create_if_missing=True)
|
546
|
-
|
547
|
-
stub.function(image=image, network_file_systems={"/nfs_1": nfs_1, "/nfs_2": nfs_2})(dummy)
|
548
|
-
|
549
|
-
with stub.run(client=client):
|
550
|
-
object_id: str = stub.indexed_objects["dummy"].object_id
|
551
|
-
f = servicer.app_functions[object_id]
|
552
|
-
|
553
|
-
dep_object_ids = set(d.object_id for d in f.object_dependencies)
|
554
|
-
assert dep_object_ids == set([image.object_id, nfs_1.object_id, nfs_2.object_id])
|
555
|
-
|
556
|
-
|
557
|
-
nfs = NetworkFileSystem.from_name("my-persisted-nfs", create_if_missing=True)
|
558
|
-
|
559
|
-
|
560
|
-
def dummy_closurevars():
|
561
|
-
nfs.listdir("/")
|
562
|
-
|
563
|
-
|
564
|
-
def test_deps_closurevars(client, servicer):
|
565
|
-
stub = Stub()
|
566
|
-
|
567
|
-
image = Image.debian_slim()
|
568
|
-
modal_f = stub.function(image=image)(dummy_closurevars)
|
569
|
-
|
570
|
-
with stub.run(client=client):
|
571
|
-
f = servicer.app_functions[modal_f.object_id]
|
572
|
-
|
573
|
-
assert set(d.object_id for d in f.object_dependencies) == set([nfs.object_id, image.object_id])
|
574
|
-
|
575
|
-
|
576
|
-
def assert_is_wrapped_dict(some_arg):
|
577
|
-
assert type(some_arg) == modal.Dict # this should not be a modal._Dict unwrapped instance!
|
578
|
-
return some_arg
|
579
|
-
|
580
|
-
|
581
|
-
def test_calls_should_not_unwrap_modal_objects(servicer, client):
|
582
|
-
some_modal_object = modal.Dict.lookup("blah", create_if_missing=True, client=client)
|
583
|
-
|
584
|
-
stub = Stub()
|
585
|
-
foo = stub.function()(assert_is_wrapped_dict)
|
586
|
-
servicer.function_body(assert_is_wrapped_dict)
|
587
|
-
|
588
|
-
# make sure the serialized object is an actual Dict and not a _Dict in all user code contexts
|
589
|
-
with stub.run(client=client):
|
590
|
-
assert type(foo.remote(some_modal_object)) == modal.Dict
|
591
|
-
fc = foo.spawn(some_modal_object)
|
592
|
-
assert type(fc.get()) == modal.Dict
|
593
|
-
for ret in foo.map([some_modal_object]):
|
594
|
-
assert type(ret) == modal.Dict
|
595
|
-
for ret in foo.starmap([[some_modal_object]]):
|
596
|
-
assert type(ret) == modal.Dict
|
597
|
-
foo.for_each([some_modal_object])
|
598
|
-
|
599
|
-
assert len(servicer.client_calls) == 5
|
600
|
-
|
601
|
-
|
602
|
-
def assert_is_wrapped_dict_gen(some_arg):
|
603
|
-
assert type(some_arg) == modal.Dict # this should not be a modal._Dict unwrapped instance!
|
604
|
-
yield some_arg
|
605
|
-
|
606
|
-
|
607
|
-
def test_calls_should_not_unwrap_modal_objects_gen(servicer, client):
|
608
|
-
some_modal_object = modal.Dict.lookup("blah", create_if_missing=True, client=client)
|
609
|
-
|
610
|
-
stub = Stub()
|
611
|
-
foo = stub.function()(assert_is_wrapped_dict_gen)
|
612
|
-
servicer.function_body(assert_is_wrapped_dict_gen)
|
613
|
-
|
614
|
-
# make sure the serialized object is an actual Dict and not a _Dict in all user code contexts
|
615
|
-
with stub.run(client=client):
|
616
|
-
assert type(next(foo.remote_gen(some_modal_object))) == modal.Dict
|
617
|
-
foo.spawn(some_modal_object) # spawn on generator returns None, but starts the generator
|
618
|
-
|
619
|
-
assert len(servicer.client_calls) == 2
|
620
|
-
|
621
|
-
|
622
|
-
def test_mount_deps_have_ids(client, servicer, monkeypatch, test_dir):
|
623
|
-
# This test can possibly break if a function's deps diverge between
|
624
|
-
# local and remote environments
|
625
|
-
monkeypatch.syspath_prepend(test_dir / "supports")
|
626
|
-
stub = Stub()
|
627
|
-
stub.function(mounts=[Mount.from_local_python_packages("pkg_a")])(dummy)
|
628
|
-
|
629
|
-
with servicer.intercept() as ctx:
|
630
|
-
with stub.run(client=client):
|
631
|
-
pass
|
632
|
-
|
633
|
-
function_create = ctx.pop_request("FunctionCreate")
|
634
|
-
for dep in function_create.function.object_dependencies:
|
635
|
-
assert dep.object_id
|
636
|
-
|
637
|
-
|
638
|
-
def test_no_state_reuse(client, servicer, supports_dir):
|
639
|
-
# two separate instances of the same mount content - triggers deduplication logic
|
640
|
-
mount_instance_1 = Mount.from_local_file(supports_dir / "pyproject.toml")
|
641
|
-
mount_instance_2 = Mount.from_local_file(supports_dir / "pyproject.toml")
|
642
|
-
|
643
|
-
stub = Stub("reuse-mount-stub")
|
644
|
-
stub.function(mounts=[mount_instance_1, mount_instance_2])(dummy)
|
645
|
-
|
646
|
-
deploy_stub(stub, client=client, show_progress=False)
|
647
|
-
first_deploy = {mount_instance_1.object_id, mount_instance_2.object_id}
|
648
|
-
|
649
|
-
deploy_stub(stub, client=client, show_progress=False)
|
650
|
-
second_deploy = {mount_instance_1.object_id, mount_instance_2.object_id}
|
651
|
-
|
652
|
-
# mount ids should not overlap between first and second deploy
|
653
|
-
assert not (first_deploy & second_deploy)
|