modal 0.62.115__py3-none-any.whl → 0.72.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +402 -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 +1025 -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 +196 -0
  84. modal/functions.py +846 -428
  85. modal/functions.pyi +446 -387
  86. modal/gpu.py +57 -44
  87. modal/image.py +943 -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.13.dist-info}/METADATA +5 -5
  128. modal-0.72.13.dist-info/RECORD +174 -0
  129. {modal-0.62.115.dist-info → modal-0.72.13.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.13.dist-info}/LICENSE +0 -0
  219. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
  220. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/entry_points.txt +0 -0
test/cls_test.py DELETED
@@ -1,636 +0,0 @@
1
- # Copyright Modal Labs 2022
2
- import pytest
3
- import threading
4
- from typing import TYPE_CHECKING, Callable, Dict
5
-
6
- from typing_extensions import assert_type
7
-
8
- from modal import App, Cls, Function, Image, Queue, build, enter, exit, method
9
- from modal._serialization import deserialize
10
- from modal.exception import DeprecationError, ExecutionError, InvalidError
11
- from modal.partial_function import (
12
- _find_callables_for_obj,
13
- _find_partial_methods_for_cls,
14
- _PartialFunction,
15
- _PartialFunctionFlags,
16
- )
17
- from modal.runner import deploy_app
18
- from modal.running_app import RunningApp
19
- from modal_proto import api_pb2
20
-
21
- from .supports.base_class import BaseCls2
22
-
23
- app = App("app")
24
-
25
-
26
- @pytest.fixture(autouse=True)
27
- def auto_use_set_env_client(set_env_client):
28
- # TODO(elias): remove set_env_client fixture here if/when possible - this is required only since
29
- # Client.from_env happens to inject an unused client when loading the
30
- # parameterized function
31
- return
32
-
33
-
34
- @app.cls(cpu=42)
35
- class Foo:
36
- @method()
37
- def bar(self, x: int) -> float:
38
- return x**3
39
-
40
-
41
- def test_run_class(client, servicer):
42
- assert servicer.n_functions == 0
43
- with app.run(client=client):
44
- function_id = Foo.bar.object_id
45
- assert isinstance(Foo, Cls)
46
- class_id = Foo.object_id
47
- app_id = app.app_id
48
-
49
- objects = servicer.app_objects[app_id]
50
- assert len(objects) == 2 # classes and functions
51
- assert objects["Foo.bar"] == function_id
52
- assert objects["Foo"] == class_id
53
-
54
-
55
- def test_call_class_sync(client, servicer):
56
- with app.run(client=client):
57
- foo: Foo = Foo()
58
- ret: float = foo.bar.remote(42)
59
- assert ret == 1764
60
-
61
-
62
- # Reusing the app runs into an issue with stale function handles.
63
- # TODO (akshat): have all the client tests use separate apps, and throw
64
- # an exception if the user tries to reuse an app.
65
- app_remote = App()
66
-
67
-
68
- @app_remote.cls(cpu=42)
69
- class FooRemote:
70
- def __init__(self, x: int, y: str) -> None:
71
- self.x = x
72
- self.y = y
73
-
74
- @method()
75
- def bar(self, z: int):
76
- return z**3
77
-
78
-
79
- def test_call_cls_remote_sync(client):
80
- with app_remote.run(client=client):
81
- foo_remote: FooRemote = FooRemote(3, "hello")
82
- ret: float = foo_remote.bar.remote(8)
83
- assert ret == 64 # Mock servicer just squares the argument
84
-
85
-
86
- def test_call_cls_remote_invalid_type(client):
87
- with app_remote.run(client=client):
88
-
89
- def my_function():
90
- print("Hello, world!")
91
-
92
- with pytest.raises(ValueError) as excinfo:
93
- FooRemote(42, my_function) # type: ignore
94
-
95
- exc = excinfo.value
96
- assert "function" in str(exc)
97
-
98
-
99
- def test_call_cls_remote_modal_type(client):
100
- with app_remote.run(client=client):
101
- with Queue.ephemeral(client) as q:
102
- FooRemote(42, q) # type: ignore
103
-
104
-
105
- app_2 = App()
106
-
107
-
108
- @app_2.cls(cpu=42)
109
- class Bar:
110
- @method()
111
- def baz(self, x):
112
- return x**3
113
-
114
-
115
- @pytest.mark.asyncio
116
- async def test_call_class_async(client, servicer):
117
- async with app_2.run(client=client):
118
- bar = Bar()
119
- assert await bar.baz.remote.aio(42) == 1764
120
-
121
-
122
- def test_run_class_serialized(client, servicer):
123
- app_ser = App()
124
-
125
- @app_ser.cls(cpu=42, serialized=True)
126
- class FooSer:
127
- @method()
128
- def bar(self, x):
129
- return x**3
130
-
131
- assert servicer.n_functions == 0
132
- with app_ser.run(client=client):
133
- pass
134
-
135
- assert servicer.n_functions == 1
136
- (function_id,) = servicer.app_functions.keys()
137
- function = servicer.app_functions[function_id]
138
- assert function.function_name.endswith("FooSer.bar") # because it's defined in a local scope
139
- assert function.definition_type == api_pb2.Function.DEFINITION_TYPE_SERIALIZED
140
- cls = deserialize(function.class_serialized, client)
141
- fun = deserialize(function.function_serialized, client)
142
-
143
- # Create bound method
144
- obj = cls()
145
- meth = fun.__get__(obj, cls)
146
-
147
- # Make sure it's callable
148
- assert meth(100) == 1000000
149
-
150
-
151
- app_remote_2 = App()
152
-
153
-
154
- @app_remote_2.cls(cpu=42)
155
- class BarRemote:
156
- def __init__(self, x: int, y: str) -> None:
157
- self.x = x
158
- self.y = y
159
-
160
- @method()
161
- def baz(self, z: int):
162
- return z**3
163
-
164
-
165
- @pytest.mark.asyncio
166
- async def test_call_cls_remote_async(client):
167
- async with app_remote_2.run(client=client):
168
- bar_remote = BarRemote(3, "hello")
169
- assert await bar_remote.baz.remote.aio(8) == 64 # Mock servicer just squares the argument
170
-
171
-
172
- app_local = App()
173
-
174
-
175
- @app_local.cls(cpu=42)
176
- class FooLocal:
177
- @method()
178
- def bar(self, x):
179
- return x**3
180
-
181
- @method()
182
- def baz(self, y):
183
- return self.bar.local(y + 1)
184
-
185
-
186
- def test_can_call_locally(client):
187
- foo = FooLocal()
188
- assert foo.bar.local(4) == 64
189
- assert foo.baz.local(4) == 125
190
- with app_local.run(client=client):
191
- assert foo.baz.local(2) == 27
192
-
193
-
194
- def test_can_call_remotely_from_local(client):
195
- with app_local.run(client=client):
196
- foo = FooLocal()
197
- # remote calls use the mockservicer func impl
198
- # which just squares the arguments
199
- assert foo.bar.remote(8) == 64
200
- assert foo.baz.remote(9) == 81
201
-
202
-
203
- app_remote_3 = App()
204
-
205
-
206
- @app_remote_3.cls(cpu=42)
207
- class NoArgRemote:
208
- def __init__(self) -> None:
209
- pass
210
-
211
- @method()
212
- def baz(self, z: int):
213
- return z**3
214
-
215
-
216
- def test_call_cls_remote_no_args(client):
217
- with app_remote_3.run(client=client):
218
- foo_remote = NoArgRemote()
219
- assert foo_remote.baz.remote(8) == 64 # Mock servicer just squares the argument
220
-
221
-
222
- if TYPE_CHECKING:
223
- # Check that type annotations carry through to the decorated classes
224
- assert_type(Foo(), Foo)
225
- assert_type(Foo().bar, Function)
226
-
227
-
228
- def test_lookup(client, servicer):
229
- deploy_app(app, "my-cls-app", client=client)
230
-
231
- cls: Cls = Cls.lookup("my-cls-app", "Foo", client=client)
232
-
233
- assert cls.object_id.startswith("cs-")
234
- assert cls.bar.object_id.startswith("fu-")
235
-
236
- # Check that function properties are preserved
237
- assert cls.bar.is_generator is False
238
-
239
- # Make sure we can instantiate the class
240
- obj = cls("foo", 234)
241
-
242
- # Make sure we can methods
243
- # (mock servicer just returns the sum of the squares of the args)
244
- assert obj.bar.remote(42, 77) == 7693
245
-
246
- # Make sure local calls fail
247
- with pytest.raises(ExecutionError):
248
- assert obj.bar.local(1, 2)
249
-
250
-
251
- def test_lookup_lazy_remote(client, servicer):
252
- # See #972 (PR) and #985 (revert PR): adding unit test to catch regression
253
- deploy_app(app, "my-cls-app", client=client)
254
- cls: Cls = Cls.lookup("my-cls-app", "Foo", client=client)
255
- obj = cls("foo", 234)
256
- assert obj.bar.remote(42, 77) == 7693
257
-
258
-
259
- def test_lookup_lazy_spawn(client, servicer):
260
- # See #1071
261
- deploy_app(app, "my-cls-app", client=client)
262
- cls: Cls = Cls.lookup("my-cls-app", "Foo", client=client)
263
- obj = cls("foo", 234)
264
- function_call = obj.bar.spawn(42, 77)
265
- assert function_call.get() == 7693
266
-
267
-
268
- baz_app = App()
269
-
270
-
271
- @baz_app.cls()
272
- class Baz:
273
- def __init__(self, x):
274
- self.x = x
275
-
276
- def not_modal_method(self, y: int) -> int:
277
- return self.x * y
278
-
279
-
280
- def test_call_not_modal_method():
281
- baz: Baz = Baz(5)
282
- assert baz.x == 5
283
- assert baz.not_modal_method(7) == 35
284
-
285
-
286
- cls_with_enter_app = App()
287
-
288
-
289
- def get_thread_id():
290
- return threading.current_thread().name
291
-
292
-
293
- @cls_with_enter_app.cls()
294
- class ClsWithEnter:
295
- def __init__(self, thread_id):
296
- self.inited = True
297
- self.entered = False
298
- self.thread_id = thread_id
299
- assert get_thread_id() == self.thread_id
300
-
301
- @enter()
302
- def enter(self):
303
- self.entered = True
304
- assert get_thread_id() == self.thread_id
305
-
306
- def not_modal_method(self, y: int) -> int:
307
- return y**2
308
-
309
- @method()
310
- def modal_method(self, y: int) -> int:
311
- return y**2
312
-
313
-
314
- def test_dont_enter_on_local_access():
315
- obj = ClsWithEnter(get_thread_id())
316
- with pytest.raises(AttributeError):
317
- obj.doesnt_exist # type: ignore
318
- assert obj.inited
319
- assert not obj.entered
320
-
321
-
322
- def test_dont_enter_on_local_non_modal_call():
323
- obj = ClsWithEnter(get_thread_id())
324
- assert obj.not_modal_method(7) == 49
325
- assert obj.inited
326
- assert not obj.entered
327
-
328
-
329
- def test_enter_on_local_modal_call():
330
- obj = ClsWithEnter(get_thread_id())
331
- assert obj.modal_method.local(7) == 49
332
- assert obj.inited
333
- assert obj.entered
334
-
335
-
336
- @cls_with_enter_app.cls()
337
- class ClsWithAsyncEnter:
338
- def __init__(self):
339
- self.inited = True
340
- self.entered = False
341
-
342
- @enter()
343
- async def enter(self):
344
- self.entered = True
345
-
346
- @method()
347
- async def modal_method(self, y: int) -> int:
348
- return y**2
349
-
350
-
351
- @pytest.mark.asyncio
352
- async def test_async_enter_on_local_modal_call():
353
- obj = ClsWithAsyncEnter()
354
- assert await obj.modal_method.local(7) == 49
355
- assert obj.inited
356
- assert obj.entered
357
-
358
-
359
- inheritance_app = App()
360
-
361
-
362
- class BaseCls:
363
- @enter()
364
- def enter(self):
365
- self.x = 2
366
-
367
- @method()
368
- def run(self, y):
369
- return self.x * y
370
-
371
-
372
- @inheritance_app.cls()
373
- class DerivedCls(BaseCls):
374
- pass
375
-
376
-
377
- def test_derived_cls(client, servicer):
378
- with inheritance_app.run(client=client):
379
- # default servicer fn just squares the number
380
- assert DerivedCls().run.remote(3) == 9
381
-
382
-
383
- inheritance_app_2 = App()
384
-
385
-
386
- @inheritance_app_2.cls()
387
- class DerivedCls2(BaseCls2):
388
- pass
389
-
390
-
391
- def test_derived_cls_external_file(client, servicer):
392
- with inheritance_app_2.run(client=client):
393
- # default servicer fn just squares the number
394
- assert DerivedCls2().run.remote(3) == 9
395
-
396
-
397
- def test_rehydrate(client, servicer, reset_container_app):
398
- # Issue introduced in #922 - brief description in #931
399
-
400
- # Sanity check that local calls work
401
- obj = Foo()
402
- assert obj.bar.local(7) == 343
403
-
404
- # Deploy app to get an app id
405
- app_id = deploy_app(app, "my-cls-app", client=client).app_id
406
-
407
- # Initialize a container
408
- container_app = RunningApp(app_id=app_id)
409
-
410
- # Associate app with app
411
- app._init_container(client, container_app)
412
-
413
- # Hydration shouldn't overwrite local function definition
414
- obj = Foo()
415
- assert obj.bar.local(7) == 343
416
-
417
-
418
- app_unhydrated = App()
419
-
420
-
421
- @app_unhydrated.cls()
422
- class FooUnhydrated:
423
- @method()
424
- def bar(self):
425
- ...
426
-
427
-
428
- def test_unhydrated():
429
- foo = FooUnhydrated()
430
- with pytest.raises(ExecutionError, match="hydrated"):
431
- foo.bar.remote(42)
432
-
433
-
434
- app_method_args = App()
435
-
436
-
437
- @app_method_args.cls()
438
- class XYZ:
439
- @method(keep_warm=3)
440
- def foo(self):
441
- ...
442
-
443
- @method(keep_warm=7)
444
- def bar(self):
445
- ...
446
-
447
-
448
- def test_method_args(servicer, client):
449
- with app_method_args.run(client=client):
450
- funcs = servicer.app_functions.values()
451
- assert [f.function_name for f in funcs] == ["XYZ.foo", "XYZ.bar"]
452
- assert [f.warm_pool_size for f in funcs] == [3, 7]
453
-
454
-
455
- class ClsWith1Method:
456
- @method()
457
- def foo(self):
458
- ...
459
-
460
-
461
- class ClsWith2Methods:
462
- @method()
463
- def foo(self):
464
- ...
465
-
466
- @method()
467
- def bar(self):
468
- ...
469
-
470
-
471
- def test_keep_warm_depr():
472
- app = App()
473
-
474
- # This should be fine
475
- app.cls(keep_warm=2)(ClsWith1Method)
476
-
477
- with pytest.warns(DeprecationError, match="@method"):
478
- app.cls(keep_warm=2)(ClsWith2Methods)
479
-
480
-
481
- class ClsWithHandlers:
482
- @build()
483
- def my_build(self):
484
- pass
485
-
486
- @enter(snap=True)
487
- def my_memory_snapshot(self):
488
- pass
489
-
490
- @enter()
491
- def my_enter(self):
492
- pass
493
-
494
- @build()
495
- @enter()
496
- def my_build_and_enter(self):
497
- pass
498
-
499
- @exit()
500
- def my_exit(self):
501
- pass
502
-
503
-
504
- def test_handlers():
505
- pfs: Dict[str, _PartialFunction]
506
-
507
- pfs = _find_partial_methods_for_cls(ClsWithHandlers, _PartialFunctionFlags.BUILD)
508
- assert list(pfs.keys()) == ["my_build", "my_build_and_enter"]
509
-
510
- pfs = _find_partial_methods_for_cls(ClsWithHandlers, _PartialFunctionFlags.ENTER_PRE_CHECKPOINT)
511
- assert list(pfs.keys()) == ["my_memory_snapshot"]
512
-
513
- pfs = _find_partial_methods_for_cls(ClsWithHandlers, _PartialFunctionFlags.ENTER_POST_CHECKPOINT)
514
- assert list(pfs.keys()) == ["my_enter", "my_build_and_enter"]
515
-
516
- pfs = _find_partial_methods_for_cls(ClsWithHandlers, _PartialFunctionFlags.EXIT)
517
- assert list(pfs.keys()) == ["my_exit"]
518
-
519
-
520
- handler_app = App("handler-app")
521
-
522
-
523
- image = Image.debian_slim().pip_install("xyz")
524
-
525
-
526
- @handler_app.cls(image=image)
527
- class ClsWithBuild:
528
- @build()
529
- def build(self):
530
- pass
531
-
532
- @method()
533
- def method(self):
534
- pass
535
-
536
-
537
- def test_build_image(client, servicer):
538
- with handler_app.run(client=client):
539
- f_def = servicer.app_functions[ClsWithBuild.method.object_id]
540
- # The function image should have added a new layer with original image as the parent
541
- f_image = servicer.images[f_def.image_id]
542
- assert f_image.base_images[0].image_id == image.object_id
543
-
544
-
545
- @pytest.mark.parametrize("decorator", [build, enter, exit])
546
- def test_disallow_lifecycle_decorators_with_method(decorator):
547
- name = decorator.__name__.split("_")[-1] # remove synchronicity prefix
548
- with pytest.raises(InvalidError, match=f"Cannot use `@{name}` decorator with `@method`."):
549
-
550
- class ClsDecoratorMethodStack:
551
- @decorator()
552
- @method()
553
- def f(self):
554
- pass
555
-
556
-
557
- def test_deprecated_sync_methods():
558
- with pytest.warns(DeprecationError, match="Support for decorating parameterized methods with `@exit`"):
559
-
560
- class ClsWithDeprecatedSyncMethods:
561
- def __enter__(self):
562
- return 42
563
-
564
- @enter()
565
- def my_enter(self):
566
- return 43
567
-
568
- def __exit__(self, exc_type, exc, tb):
569
- return 44
570
-
571
- @exit()
572
- def my_exit(self, exc_type, exc, tb):
573
- return 45
574
-
575
- obj = ClsWithDeprecatedSyncMethods()
576
-
577
- with pytest.warns(DeprecationError, match="Using `__enter__`.+`modal.enter` decorator"):
578
- enter_methods: Dict[str, Callable] = _find_callables_for_obj(obj, _PartialFunctionFlags.ENTER_POST_CHECKPOINT)
579
- assert [meth() for meth in enter_methods.values()] == [42, 43]
580
-
581
- with pytest.warns(DeprecationError, match="Using `__exit__`.+`modal.exit` decorator"):
582
- exit_methods: Dict[str, Callable] = _find_callables_for_obj(obj, _PartialFunctionFlags.EXIT)
583
- assert [meth(None, None, None) for meth in exit_methods.values()] == [44, 45]
584
-
585
- app = App("deprecated-sync-cls")
586
- with pytest.warns(DeprecationError):
587
- app.cls()(ClsWithDeprecatedSyncMethods)()
588
-
589
-
590
- @pytest.mark.asyncio
591
- async def test_deprecated_async_methods():
592
- with pytest.warns(DeprecationError, match="Support for decorating parameterized methods with `@exit`"):
593
-
594
- class ClsWithDeprecatedAsyncMethods:
595
- async def __aenter__(self):
596
- return 42
597
-
598
- @enter()
599
- async def my_enter(self):
600
- return 43
601
-
602
- async def __aexit__(self, exc_type, exc, tb):
603
- return 44
604
-
605
- @exit()
606
- async def my_exit(self, exc_type, exc, tb):
607
- return 45
608
-
609
- obj = ClsWithDeprecatedAsyncMethods()
610
-
611
- with pytest.warns(DeprecationError, match=r"Using `__aenter__`.+`modal.enter` decorator \(on an async method\)"):
612
- enter_methods: Dict[str, Callable] = _find_callables_for_obj(obj, _PartialFunctionFlags.ENTER_POST_CHECKPOINT)
613
- assert [await meth() for meth in enter_methods.values()] == [42, 43]
614
-
615
- with pytest.warns(DeprecationError, match=r"Using `__aexit__`.+`modal.exit` decorator \(on an async method\)"):
616
- exit_methods: Dict[str, Callable] = _find_callables_for_obj(obj, _PartialFunctionFlags.EXIT)
617
- assert [await meth(None, None, None) for meth in exit_methods.values()] == [44, 45]
618
-
619
- app = App("deprecated-async-cls")
620
- with pytest.warns(DeprecationError):
621
- app.cls()(ClsWithDeprecatedAsyncMethods)()
622
-
623
-
624
- class HasSnapMethod:
625
- @enter(snap=True)
626
- def enter(self):
627
- pass
628
-
629
- @method()
630
- def f(self):
631
- pass
632
-
633
-
634
- def test_snap_method_without_snapshot_enabled():
635
- with pytest.raises(InvalidError, match="A class must have `enable_memory_snapshot=True`"):
636
- app.cls(enable_memory_snapshot=False)(HasSnapMethod)