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.
- modal/__init__.py +17 -13
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +420 -937
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -59
- modal/_resources.py +51 -0
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1036 -0
- modal/_runtime/execution_context.py +89 -0
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +134 -9
- modal/_traceback.py +47 -187
- modal/_tunnel.py +52 -16
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +479 -100
- 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 +460 -171
- modal/_utils/grpc_testing.py +47 -31
- modal/_utils/grpc_utils.py +62 -109
- modal/_utils/hash_utils.py +61 -19
- 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 +5 -7
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +14 -12
- modal/app.py +1003 -314
- modal/app.pyi +540 -264
- modal/call_graph.py +7 -6
- modal/cli/_download.py +63 -53
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +205 -45
- modal/cli/config.py +12 -5
- modal/cli/container.py +62 -14
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +64 -58
- modal/cli/launch.py +32 -18
- modal/cli/network_file_system.py +64 -83
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +35 -10
- modal/cli/programs/vscode.py +60 -10
- modal/cli/queues.py +131 -0
- modal/cli/run.py +234 -131
- modal/cli/secret.py +8 -7
- modal/cli/token.py +7 -2
- modal/cli/utils.py +79 -10
- modal/cli/volume.py +110 -109
- modal/client.py +250 -144
- modal/client.pyi +157 -118
- modal/cloud_bucket_mount.py +108 -34
- modal/cloud_bucket_mount.pyi +32 -38
- modal/cls.py +535 -148
- modal/cls.pyi +190 -146
- modal/config.py +41 -19
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +111 -65
- modal/dict.pyi +136 -131
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +34 -43
- modal/experimental.py +61 -2
- modal/extensions/ipython.py +5 -5
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +197 -0
- modal/functions.py +906 -911
- modal/functions.pyi +466 -430
- modal/gpu.py +57 -44
- modal/image.py +1089 -479
- modal/image.pyi +584 -228
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +314 -101
- modal/mount.pyi +241 -235
- modal/network_file_system.py +92 -92
- modal/network_file_system.pyi +152 -110
- modal/object.py +67 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +434 -0
- modal/parallel_map.pyi +75 -0
- modal/partial_function.py +282 -117
- modal/partial_function.pyi +222 -129
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +182 -65
- modal/queue.pyi +218 -118
- modal/requirements/2024.04.txt +29 -0
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +48 -7
- modal/runner.py +459 -156
- modal/runner.pyi +135 -71
- modal/running_app.py +38 -0
- modal/sandbox.py +514 -236
- modal/sandbox.pyi +397 -169
- modal/schedule.py +4 -4
- modal/scheduler_placement.py +20 -3
- modal/secret.py +56 -31
- modal/secret.pyi +62 -42
- modal/serving.py +51 -56
- modal/serving.pyi +44 -36
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +285 -157
- modal/volume.pyi +249 -184
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
- modal-0.72.11.dist-info/RECORD +174 -0
- {modal-0.62.16.dist-info → modal-0.72.11.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 +5 -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 +1288 -533
- modal_proto/api_grpc.py +856 -456
- modal_proto/api_pb2.py +2165 -1157
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1674 -855
- 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_entrypoint.pyi +0 -378
- modal/_container_exec.py +0 -128
- modal/_sandbox_shell.py +0 -49
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal/stub.py +0 -783
- modal/stub.pyi +0 -332
- modal-0.62.16.dist-info/RECORD +0 -198
- 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 -262
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -659
- test/client_test.py +0 -194
- test/cls_test.py +0 -630
- test/config_test.py +0 -137
- test/conftest.py +0 -1420
- test/container_app_test.py +0 -32
- test/container_test.py +0 -1389
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -33
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -653
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -141
- test/helpers.py +0 -42
- test/image_test.py +0 -669
- 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 -329
- test/network_file_system_test.py +0 -181
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -97
- test/resolver_test.py +0 -58
- 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 -29
- test/secret_test.py +0 -78
- test/serialization_test.py +0 -42
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -360
- 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 -341
- test/watcher_test.py +0 -30
- test/webhook_test.py +0 -146
- /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
- /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
- {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)
|