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