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.
Files changed (220) hide show
  1. modal/__init__.py +17 -13
  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 +420 -937
  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 -59
  11. modal/_resources.py +51 -0
  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/_runtime/execution_context.py +89 -0
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +134 -9
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +52 -16
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +479 -100
  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 +460 -171
  29. modal/_utils/grpc_testing.py +47 -31
  30. modal/_utils/grpc_utils.py +62 -109
  31. modal/_utils/hash_utils.py +61 -19
  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 +5 -7
  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 +14 -12
  43. modal/app.py +1003 -314
  44. modal/app.pyi +540 -264
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +63 -53
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +205 -45
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +62 -14
  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 +64 -58
  55. modal/cli/launch.py +32 -18
  56. modal/cli/network_file_system.py +64 -83
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +35 -10
  59. modal/cli/programs/vscode.py +60 -10
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +234 -131
  62. modal/cli/secret.py +8 -7
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +79 -10
  65. modal/cli/volume.py +110 -109
  66. modal/client.py +250 -144
  67. modal/client.pyi +157 -118
  68. modal/cloud_bucket_mount.py +108 -34
  69. modal/cloud_bucket_mount.pyi +32 -38
  70. modal/cls.py +535 -148
  71. modal/cls.pyi +190 -146
  72. modal/config.py +41 -19
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +111 -65
  76. modal/dict.pyi +136 -131
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +34 -43
  80. modal/experimental.py +61 -2
  81. modal/extensions/ipython.py +5 -5
  82. modal/file_io.py +537 -0
  83. modal/file_io.pyi +235 -0
  84. modal/file_pattern_matcher.py +197 -0
  85. modal/functions.py +906 -911
  86. modal/functions.pyi +466 -430
  87. modal/gpu.py +57 -44
  88. modal/image.py +1089 -479
  89. modal/image.pyi +584 -228
  90. modal/io_streams.py +434 -0
  91. modal/io_streams.pyi +122 -0
  92. modal/mount.py +314 -101
  93. modal/mount.pyi +241 -235
  94. modal/network_file_system.py +92 -92
  95. modal/network_file_system.pyi +152 -110
  96. modal/object.py +67 -36
  97. modal/object.pyi +166 -143
  98. modal/output.py +63 -0
  99. modal/parallel_map.py +434 -0
  100. modal/parallel_map.pyi +75 -0
  101. modal/partial_function.py +282 -117
  102. modal/partial_function.pyi +222 -129
  103. modal/proxy.py +15 -12
  104. modal/proxy.pyi +3 -8
  105. modal/queue.py +182 -65
  106. modal/queue.pyi +218 -118
  107. modal/requirements/2024.04.txt +29 -0
  108. modal/requirements/2024.10.txt +16 -0
  109. modal/requirements/README.md +21 -0
  110. modal/requirements/base-images.json +22 -0
  111. modal/retries.py +48 -7
  112. modal/runner.py +459 -156
  113. modal/runner.pyi +135 -71
  114. modal/running_app.py +38 -0
  115. modal/sandbox.py +514 -236
  116. modal/sandbox.pyi +397 -169
  117. modal/schedule.py +4 -4
  118. modal/scheduler_placement.py +20 -3
  119. modal/secret.py +56 -31
  120. modal/secret.pyi +62 -42
  121. modal/serving.py +51 -56
  122. modal/serving.pyi +44 -36
  123. modal/stream_type.py +15 -0
  124. modal/token_flow.py +5 -3
  125. modal/token_flow.pyi +37 -32
  126. modal/volume.py +285 -157
  127. modal/volume.pyi +249 -184
  128. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
  129. modal-0.72.11.dist-info/RECORD +174 -0
  130. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
  131. modal_docs/gen_reference_docs.py +3 -1
  132. modal_docs/mdmd/mdmd.py +0 -1
  133. modal_docs/mdmd/signatures.py +5 -2
  134. modal_global_objects/images/base_images.py +28 -0
  135. modal_global_objects/mounts/python_standalone.py +2 -2
  136. modal_proto/__init__.py +1 -1
  137. modal_proto/api.proto +1288 -533
  138. modal_proto/api_grpc.py +856 -456
  139. modal_proto/api_pb2.py +2165 -1157
  140. modal_proto/api_pb2.pyi +8859 -0
  141. modal_proto/api_pb2_grpc.py +1674 -855
  142. modal_proto/api_pb2_grpc.pyi +1416 -0
  143. modal_proto/modal_api_grpc.py +149 -0
  144. modal_proto/modal_options_grpc.py +3 -0
  145. modal_proto/options_pb2.pyi +20 -0
  146. modal_proto/options_pb2_grpc.pyi +7 -0
  147. modal_proto/py.typed +0 -0
  148. modal_version/__init__.py +1 -1
  149. modal_version/_version_generated.py +2 -2
  150. modal/_asgi.py +0 -370
  151. modal/_container_entrypoint.pyi +0 -378
  152. modal/_container_exec.py +0 -128
  153. modal/_sandbox_shell.py +0 -49
  154. modal/shared_volume.py +0 -23
  155. modal/shared_volume.pyi +0 -24
  156. modal/stub.py +0 -783
  157. modal/stub.pyi +0 -332
  158. modal-0.62.16.dist-info/RECORD +0 -198
  159. modal_global_objects/images/conda.py +0 -15
  160. modal_global_objects/images/debian_slim.py +0 -15
  161. modal_global_objects/images/micromamba.py +0 -15
  162. test/__init__.py +0 -1
  163. test/aio_test.py +0 -12
  164. test/async_utils_test.py +0 -262
  165. test/blob_test.py +0 -67
  166. test/cli_imports_test.py +0 -149
  167. test/cli_test.py +0 -659
  168. test/client_test.py +0 -194
  169. test/cls_test.py +0 -630
  170. test/config_test.py +0 -137
  171. test/conftest.py +0 -1420
  172. test/container_app_test.py +0 -32
  173. test/container_test.py +0 -1389
  174. test/cpu_test.py +0 -23
  175. test/decorator_test.py +0 -85
  176. test/deprecation_test.py +0 -34
  177. test/dict_test.py +0 -33
  178. test/e2e_test.py +0 -68
  179. test/error_test.py +0 -7
  180. test/function_serialization_test.py +0 -32
  181. test/function_test.py +0 -653
  182. test/function_utils_test.py +0 -101
  183. test/gpu_test.py +0 -159
  184. test/grpc_utils_test.py +0 -141
  185. test/helpers.py +0 -42
  186. test/image_test.py +0 -669
  187. test/live_reload_test.py +0 -80
  188. test/lookup_test.py +0 -70
  189. test/mdmd_test.py +0 -329
  190. test/mount_test.py +0 -162
  191. test/mounted_files_test.py +0 -329
  192. test/network_file_system_test.py +0 -181
  193. test/notebook_test.py +0 -66
  194. test/object_test.py +0 -41
  195. test/package_utils_test.py +0 -25
  196. test/queue_test.py +0 -97
  197. test/resolver_test.py +0 -58
  198. test/retries_test.py +0 -67
  199. test/runner_test.py +0 -85
  200. test/sandbox_test.py +0 -191
  201. test/schedule_test.py +0 -15
  202. test/scheduler_placement_test.py +0 -29
  203. test/secret_test.py +0 -78
  204. test/serialization_test.py +0 -42
  205. test/stub_composition_test.py +0 -10
  206. test/stub_test.py +0 -360
  207. test/test_asgi_wrapper.py +0 -234
  208. test/token_flow_test.py +0 -18
  209. test/traceback_test.py +0 -135
  210. test/tunnel_test.py +0 -29
  211. test/utils_test.py +0 -88
  212. test/version_test.py +0 -14
  213. test/volume_test.py +0 -341
  214. test/watcher_test.py +0 -30
  215. test/webhook_test.py +0 -146
  216. /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
  217. /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
  218. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
  219. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
  220. {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)