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