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.
Files changed (220) hide show
  1. modal/__init__.py +13 -9
  2. modal/__main__.py +41 -3
  3. modal/_clustered_functions.py +80 -0
  4. modal/_clustered_functions.pyi +22 -0
  5. modal/_container_entrypoint.py +407 -398
  6. modal/_ipython.py +3 -13
  7. modal/_location.py +17 -10
  8. modal/_output.py +243 -99
  9. modal/_pty.py +2 -2
  10. modal/_resolver.py +55 -60
  11. modal/_resources.py +26 -7
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1036 -0
  15. modal/{execution_context.py → _runtime/execution_context.py} +11 -2
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +123 -6
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +50 -14
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +386 -104
  24. modal/_utils/blob_utils.py +157 -186
  25. modal/_utils/bytes_io_segment_payload.py +97 -0
  26. modal/_utils/deprecation.py +89 -0
  27. modal/_utils/docker_utils.py +98 -0
  28. modal/_utils/function_utils.py +299 -98
  29. modal/_utils/grpc_testing.py +47 -34
  30. modal/_utils/grpc_utils.py +54 -21
  31. modal/_utils/hash_utils.py +51 -10
  32. modal/_utils/http_utils.py +39 -9
  33. modal/_utils/logger.py +2 -1
  34. modal/_utils/mount_utils.py +34 -16
  35. modal/_utils/name_utils.py +58 -0
  36. modal/_utils/package_utils.py +14 -1
  37. modal/_utils/pattern_utils.py +205 -0
  38. modal/_utils/rand_pb_testing.py +3 -3
  39. modal/_utils/shell_utils.py +15 -49
  40. modal/_vendor/a2wsgi_wsgi.py +62 -72
  41. modal/_vendor/cloudpickle.py +1 -1
  42. modal/_watcher.py +12 -10
  43. modal/app.py +561 -323
  44. modal/app.pyi +474 -262
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +22 -6
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +203 -42
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +61 -13
  51. modal/cli/dict.py +128 -0
  52. modal/cli/entry_point.py +26 -13
  53. modal/cli/environment.py +40 -9
  54. modal/cli/import_refs.py +21 -48
  55. modal/cli/launch.py +28 -14
  56. modal/cli/network_file_system.py +57 -21
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +34 -9
  59. modal/cli/programs/vscode.py +58 -8
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +199 -96
  62. modal/cli/secret.py +5 -4
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +74 -8
  65. modal/cli/volume.py +97 -56
  66. modal/client.py +248 -144
  67. modal/client.pyi +156 -124
  68. modal/cloud_bucket_mount.py +43 -30
  69. modal/cloud_bucket_mount.pyi +32 -25
  70. modal/cls.py +528 -141
  71. modal/cls.pyi +189 -145
  72. modal/config.py +32 -15
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +50 -54
  76. modal/dict.pyi +120 -164
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +30 -43
  80. modal/experimental.py +62 -2
  81. modal/file_io.py +537 -0
  82. modal/file_io.pyi +235 -0
  83. modal/file_pattern_matcher.py +197 -0
  84. modal/functions.py +846 -428
  85. modal/functions.pyi +446 -387
  86. modal/gpu.py +57 -44
  87. modal/image.py +946 -417
  88. modal/image.pyi +584 -245
  89. modal/io_streams.py +434 -0
  90. modal/io_streams.pyi +122 -0
  91. modal/mount.py +223 -90
  92. modal/mount.pyi +241 -243
  93. modal/network_file_system.py +85 -86
  94. modal/network_file_system.pyi +151 -110
  95. modal/object.py +66 -36
  96. modal/object.pyi +166 -143
  97. modal/output.py +63 -0
  98. modal/parallel_map.py +73 -47
  99. modal/parallel_map.pyi +51 -63
  100. modal/partial_function.py +272 -107
  101. modal/partial_function.pyi +219 -120
  102. modal/proxy.py +15 -12
  103. modal/proxy.pyi +3 -8
  104. modal/queue.py +96 -72
  105. modal/queue.pyi +210 -135
  106. modal/requirements/2024.04.txt +2 -1
  107. modal/requirements/2024.10.txt +16 -0
  108. modal/requirements/README.md +21 -0
  109. modal/requirements/base-images.json +22 -0
  110. modal/retries.py +45 -4
  111. modal/runner.py +325 -203
  112. modal/runner.pyi +124 -110
  113. modal/running_app.py +27 -4
  114. modal/sandbox.py +509 -231
  115. modal/sandbox.pyi +396 -169
  116. modal/schedule.py +2 -2
  117. modal/scheduler_placement.py +20 -3
  118. modal/secret.py +41 -25
  119. modal/secret.pyi +62 -42
  120. modal/serving.py +39 -49
  121. modal/serving.pyi +37 -43
  122. modal/stream_type.py +15 -0
  123. modal/token_flow.py +5 -3
  124. modal/token_flow.pyi +37 -32
  125. modal/volume.py +123 -137
  126. modal/volume.pyi +228 -221
  127. {modal-0.62.115.dist-info → modal-0.72.11.dist-info}/METADATA +5 -5
  128. modal-0.72.11.dist-info/RECORD +174 -0
  129. {modal-0.62.115.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
  130. modal_docs/gen_reference_docs.py +3 -1
  131. modal_docs/mdmd/mdmd.py +0 -1
  132. modal_docs/mdmd/signatures.py +1 -2
  133. modal_global_objects/images/base_images.py +28 -0
  134. modal_global_objects/mounts/python_standalone.py +2 -2
  135. modal_proto/__init__.py +1 -1
  136. modal_proto/api.proto +1231 -531
  137. modal_proto/api_grpc.py +750 -430
  138. modal_proto/api_pb2.py +2102 -1176
  139. modal_proto/api_pb2.pyi +8859 -0
  140. modal_proto/api_pb2_grpc.py +1329 -675
  141. modal_proto/api_pb2_grpc.pyi +1416 -0
  142. modal_proto/modal_api_grpc.py +149 -0
  143. modal_proto/modal_options_grpc.py +3 -0
  144. modal_proto/options_pb2.pyi +20 -0
  145. modal_proto/options_pb2_grpc.pyi +7 -0
  146. modal_proto/py.typed +0 -0
  147. modal_version/__init__.py +1 -1
  148. modal_version/_version_generated.py +2 -2
  149. modal/_asgi.py +0 -370
  150. modal/_container_exec.py +0 -128
  151. modal/_container_io_manager.py +0 -646
  152. modal/_container_io_manager.pyi +0 -412
  153. modal/_sandbox_shell.py +0 -49
  154. modal/app_utils.py +0 -20
  155. modal/app_utils.pyi +0 -17
  156. modal/execution_context.pyi +0 -37
  157. modal/shared_volume.py +0 -23
  158. modal/shared_volume.pyi +0 -24
  159. modal-0.62.115.dist-info/RECORD +0 -207
  160. modal_global_objects/images/conda.py +0 -15
  161. modal_global_objects/images/debian_slim.py +0 -15
  162. modal_global_objects/images/micromamba.py +0 -15
  163. test/__init__.py +0 -1
  164. test/aio_test.py +0 -12
  165. test/async_utils_test.py +0 -279
  166. test/blob_test.py +0 -67
  167. test/cli_imports_test.py +0 -149
  168. test/cli_test.py +0 -674
  169. test/client_test.py +0 -203
  170. test/cloud_bucket_mount_test.py +0 -22
  171. test/cls_test.py +0 -636
  172. test/config_test.py +0 -149
  173. test/conftest.py +0 -1485
  174. test/container_app_test.py +0 -50
  175. test/container_test.py +0 -1405
  176. test/cpu_test.py +0 -23
  177. test/decorator_test.py +0 -85
  178. test/deprecation_test.py +0 -34
  179. test/dict_test.py +0 -51
  180. test/e2e_test.py +0 -68
  181. test/error_test.py +0 -7
  182. test/function_serialization_test.py +0 -32
  183. test/function_test.py +0 -791
  184. test/function_utils_test.py +0 -101
  185. test/gpu_test.py +0 -159
  186. test/grpc_utils_test.py +0 -82
  187. test/helpers.py +0 -47
  188. test/image_test.py +0 -814
  189. test/live_reload_test.py +0 -80
  190. test/lookup_test.py +0 -70
  191. test/mdmd_test.py +0 -329
  192. test/mount_test.py +0 -162
  193. test/mounted_files_test.py +0 -327
  194. test/network_file_system_test.py +0 -188
  195. test/notebook_test.py +0 -66
  196. test/object_test.py +0 -41
  197. test/package_utils_test.py +0 -25
  198. test/queue_test.py +0 -115
  199. test/resolver_test.py +0 -59
  200. test/retries_test.py +0 -67
  201. test/runner_test.py +0 -85
  202. test/sandbox_test.py +0 -191
  203. test/schedule_test.py +0 -15
  204. test/scheduler_placement_test.py +0 -57
  205. test/secret_test.py +0 -89
  206. test/serialization_test.py +0 -50
  207. test/stub_composition_test.py +0 -10
  208. test/stub_test.py +0 -361
  209. test/test_asgi_wrapper.py +0 -234
  210. test/token_flow_test.py +0 -18
  211. test/traceback_test.py +0 -135
  212. test/tunnel_test.py +0 -29
  213. test/utils_test.py +0 -88
  214. test/version_test.py +0 -14
  215. test/volume_test.py +0 -397
  216. test/watcher_test.py +0 -58
  217. test/webhook_test.py +0 -145
  218. {modal-0.62.115.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
  219. {modal-0.62.115.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
  220. {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]