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.
- modal/__init__.py +13 -9
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +402 -398
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -60
- modal/_resources.py +26 -7
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1025 -0
- modal/{execution_context.py → _runtime/execution_context.py} +11 -2
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +123 -6
- modal/_traceback.py +47 -187
- modal/_tunnel.py +50 -14
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +386 -104
- modal/_utils/blob_utils.py +157 -186
- modal/_utils/bytes_io_segment_payload.py +97 -0
- modal/_utils/deprecation.py +89 -0
- modal/_utils/docker_utils.py +98 -0
- modal/_utils/function_utils.py +299 -98
- modal/_utils/grpc_testing.py +47 -34
- modal/_utils/grpc_utils.py +54 -21
- modal/_utils/hash_utils.py +51 -10
- modal/_utils/http_utils.py +39 -9
- modal/_utils/logger.py +2 -1
- modal/_utils/mount_utils.py +34 -16
- modal/_utils/name_utils.py +58 -0
- modal/_utils/package_utils.py +14 -1
- modal/_utils/pattern_utils.py +205 -0
- modal/_utils/rand_pb_testing.py +3 -3
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +12 -10
- modal/app.py +561 -323
- modal/app.pyi +474 -262
- modal/call_graph.py +7 -6
- modal/cli/_download.py +22 -6
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +203 -42
- modal/cli/config.py +12 -5
- modal/cli/container.py +61 -13
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +21 -48
- modal/cli/launch.py +28 -14
- modal/cli/network_file_system.py +57 -21
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +34 -9
- modal/cli/programs/vscode.py +58 -8
- modal/cli/queues.py +131 -0
- modal/cli/run.py +199 -96
- modal/cli/secret.py +5 -4
- modal/cli/token.py +7 -2
- modal/cli/utils.py +74 -8
- modal/cli/volume.py +97 -56
- modal/client.py +248 -144
- modal/client.pyi +156 -124
- modal/cloud_bucket_mount.py +43 -30
- modal/cloud_bucket_mount.pyi +32 -25
- modal/cls.py +528 -141
- modal/cls.pyi +189 -145
- modal/config.py +32 -15
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +50 -54
- modal/dict.pyi +120 -164
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +30 -43
- modal/experimental.py +62 -2
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +196 -0
- modal/functions.py +846 -428
- modal/functions.pyi +446 -387
- modal/gpu.py +57 -44
- modal/image.py +943 -417
- modal/image.pyi +584 -245
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +223 -90
- modal/mount.pyi +241 -243
- modal/network_file_system.py +85 -86
- modal/network_file_system.pyi +151 -110
- modal/object.py +66 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +73 -47
- modal/parallel_map.pyi +51 -63
- modal/partial_function.py +272 -107
- modal/partial_function.pyi +219 -120
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +96 -72
- modal/queue.pyi +210 -135
- modal/requirements/2024.04.txt +2 -1
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +45 -4
- modal/runner.py +325 -203
- modal/runner.pyi +124 -110
- modal/running_app.py +27 -4
- modal/sandbox.py +509 -231
- modal/sandbox.pyi +396 -169
- modal/schedule.py +2 -2
- modal/scheduler_placement.py +20 -3
- modal/secret.py +41 -25
- modal/secret.pyi +62 -42
- modal/serving.py +39 -49
- modal/serving.pyi +37 -43
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +123 -137
- modal/volume.pyi +228 -221
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
- modal-0.72.13.dist-info/RECORD +174 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
- modal_docs/gen_reference_docs.py +3 -1
- modal_docs/mdmd/mdmd.py +0 -1
- modal_docs/mdmd/signatures.py +1 -2
- modal_global_objects/images/base_images.py +28 -0
- modal_global_objects/mounts/python_standalone.py +2 -2
- modal_proto/__init__.py +1 -1
- modal_proto/api.proto +1231 -531
- modal_proto/api_grpc.py +750 -430
- modal_proto/api_pb2.py +2102 -1176
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1329 -675
- modal_proto/api_pb2_grpc.pyi +1416 -0
- modal_proto/modal_api_grpc.py +149 -0
- modal_proto/modal_options_grpc.py +3 -0
- modal_proto/options_pb2.pyi +20 -0
- modal_proto/options_pb2_grpc.pyi +7 -0
- modal_proto/py.typed +0 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +2 -2
- modal/_asgi.py +0 -370
- modal/_container_exec.py +0 -128
- modal/_container_io_manager.py +0 -646
- modal/_container_io_manager.pyi +0 -412
- modal/_sandbox_shell.py +0 -49
- modal/app_utils.py +0 -20
- modal/app_utils.pyi +0 -17
- modal/execution_context.pyi +0 -37
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal-0.62.115.dist-info/RECORD +0 -207
- modal_global_objects/images/conda.py +0 -15
- modal_global_objects/images/debian_slim.py +0 -15
- modal_global_objects/images/micromamba.py +0 -15
- test/__init__.py +0 -1
- test/aio_test.py +0 -12
- test/async_utils_test.py +0 -279
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -674
- test/client_test.py +0 -203
- test/cloud_bucket_mount_test.py +0 -22
- test/cls_test.py +0 -636
- test/config_test.py +0 -149
- test/conftest.py +0 -1485
- test/container_app_test.py +0 -50
- test/container_test.py +0 -1405
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -51
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -791
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -82
- test/helpers.py +0 -47
- test/image_test.py +0 -814
- test/live_reload_test.py +0 -80
- test/lookup_test.py +0 -70
- test/mdmd_test.py +0 -329
- test/mount_test.py +0 -162
- test/mounted_files_test.py +0 -327
- test/network_file_system_test.py +0 -188
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -115
- test/resolver_test.py +0 -59
- test/retries_test.py +0 -67
- test/runner_test.py +0 -85
- test/sandbox_test.py +0 -191
- test/schedule_test.py +0 -15
- test/scheduler_placement_test.py +0 -57
- test/secret_test.py +0 -89
- test/serialization_test.py +0 -50
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -361
- test/test_asgi_wrapper.py +0 -234
- test/token_flow_test.py +0 -18
- test/traceback_test.py +0 -135
- test/tunnel_test.py +0 -29
- test/utils_test.py +0 -88
- test/version_test.py +0 -14
- test/volume_test.py +0 -397
- test/watcher_test.py +0 -58
- test/webhook_test.py +0 -145
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
- {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)
|