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/async_utils_test.py
DELETED
@@ -1,262 +0,0 @@
|
|
1
|
-
# Copyright Modal Labs 2022
|
2
|
-
import asyncio
|
3
|
-
import logging
|
4
|
-
import os
|
5
|
-
import platform
|
6
|
-
import pytest
|
7
|
-
|
8
|
-
from synchronicity import Synchronizer
|
9
|
-
|
10
|
-
from modal._utils import async_utils
|
11
|
-
from modal._utils.async_utils import (
|
12
|
-
ConcurrencyPool,
|
13
|
-
TaskContext,
|
14
|
-
queue_batch_iterator,
|
15
|
-
retry,
|
16
|
-
warn_if_generator_is_not_consumed,
|
17
|
-
)
|
18
|
-
|
19
|
-
skip_github_non_linux = pytest.mark.skipif(
|
20
|
-
(os.environ.get("GITHUB_ACTIONS") == "true" and platform.system() != "Linux"),
|
21
|
-
reason="sleep is inaccurate on GitHub Actions runners.",
|
22
|
-
)
|
23
|
-
|
24
|
-
|
25
|
-
class SampleException(Exception):
|
26
|
-
pass
|
27
|
-
|
28
|
-
|
29
|
-
class FailNTimes:
|
30
|
-
def __init__(self, n_failures, exc=SampleException("Something bad happened")):
|
31
|
-
self.n_failures = n_failures
|
32
|
-
self.n_calls = 0
|
33
|
-
self.exc = exc
|
34
|
-
|
35
|
-
async def __call__(self, x):
|
36
|
-
self.n_calls += 1
|
37
|
-
if self.n_calls <= self.n_failures:
|
38
|
-
raise self.exc
|
39
|
-
else:
|
40
|
-
return x + 1
|
41
|
-
|
42
|
-
|
43
|
-
@pytest.mark.asyncio
|
44
|
-
async def test_retry():
|
45
|
-
f_retry = retry(FailNTimes(2))
|
46
|
-
assert await f_retry(42) == 43
|
47
|
-
|
48
|
-
with pytest.raises(SampleException):
|
49
|
-
f_retry = retry(FailNTimes(3))
|
50
|
-
assert await f_retry(42) == 43
|
51
|
-
|
52
|
-
f_retry = retry(n_attempts=5)(FailNTimes(4))
|
53
|
-
assert await f_retry(42) == 43
|
54
|
-
|
55
|
-
with pytest.raises(SampleException):
|
56
|
-
f_retry = retry(n_attempts=5)(FailNTimes(5))
|
57
|
-
assert await f_retry(42) == 43
|
58
|
-
|
59
|
-
|
60
|
-
@pytest.mark.asyncio
|
61
|
-
async def test_task_context():
|
62
|
-
async with TaskContext() as task_context:
|
63
|
-
t = task_context.create_task(asyncio.sleep(0.1))
|
64
|
-
assert not t.done()
|
65
|
-
# await asyncio.sleep(0.0)
|
66
|
-
await asyncio.sleep(0.0) # just waste a loop step for the cancellation to go through
|
67
|
-
assert t.cancelled()
|
68
|
-
|
69
|
-
|
70
|
-
@pytest.mark.asyncio
|
71
|
-
async def test_task_context_grace():
|
72
|
-
async with TaskContext(grace=0.2) as task_context:
|
73
|
-
u = task_context.create_task(asyncio.sleep(0.1))
|
74
|
-
v = task_context.create_task(asyncio.sleep(0.3))
|
75
|
-
assert not u.done()
|
76
|
-
assert not v.done()
|
77
|
-
await asyncio.sleep(0.0)
|
78
|
-
assert u.done()
|
79
|
-
assert v.cancelled()
|
80
|
-
|
81
|
-
|
82
|
-
async def raise_exception():
|
83
|
-
raise SampleException("foo")
|
84
|
-
|
85
|
-
|
86
|
-
@skip_github_non_linux
|
87
|
-
@pytest.mark.asyncio
|
88
|
-
async def test_task_context_wait():
|
89
|
-
async with TaskContext(grace=0.1) as task_context:
|
90
|
-
u = task_context.create_task(asyncio.sleep(1.1))
|
91
|
-
v = task_context.create_task(asyncio.sleep(1.3))
|
92
|
-
await task_context.wait(u)
|
93
|
-
|
94
|
-
assert u.done()
|
95
|
-
assert v.cancelled()
|
96
|
-
|
97
|
-
with pytest.raises(SampleException):
|
98
|
-
async with TaskContext(grace=0.2) as task_context:
|
99
|
-
u = task_context.create_task(asyncio.sleep(1.1))
|
100
|
-
v = task_context.create_task(raise_exception())
|
101
|
-
await task_context.wait(u)
|
102
|
-
|
103
|
-
assert u.cancelled()
|
104
|
-
assert v.done()
|
105
|
-
|
106
|
-
|
107
|
-
@skip_github_non_linux
|
108
|
-
@pytest.mark.asyncio
|
109
|
-
async def test_task_context_infinite_loop():
|
110
|
-
async with TaskContext(grace=0.01) as task_context:
|
111
|
-
counter = 0
|
112
|
-
|
113
|
-
async def f():
|
114
|
-
nonlocal counter
|
115
|
-
counter += 1
|
116
|
-
|
117
|
-
t = task_context.infinite_loop(f, sleep=0.1)
|
118
|
-
assert not t.done()
|
119
|
-
await asyncio.sleep(0.35)
|
120
|
-
assert counter == 4 # at 0.00, 0.10, 0.20, 0.30
|
121
|
-
await asyncio.sleep(0.0) # just waste a loop step for the cancellation to go through
|
122
|
-
assert not t.cancelled()
|
123
|
-
assert t.done()
|
124
|
-
assert counter == 4 # should be exited immediately
|
125
|
-
|
126
|
-
|
127
|
-
DEBOUNCE_TIME = 0.1
|
128
|
-
|
129
|
-
|
130
|
-
@pytest.mark.asyncio
|
131
|
-
async def test_queue_batch_iterator():
|
132
|
-
queue: asyncio.Queue = asyncio.Queue()
|
133
|
-
await queue.put(1)
|
134
|
-
drained_items = []
|
135
|
-
|
136
|
-
async def drain_queue(logs_queue):
|
137
|
-
async for batch in queue_batch_iterator(logs_queue, debounce_time=DEBOUNCE_TIME):
|
138
|
-
drained_items.extend(batch)
|
139
|
-
|
140
|
-
async with TaskContext(grace=0.0) as tc:
|
141
|
-
tc.create_task(drain_queue(queue))
|
142
|
-
|
143
|
-
# Make sure the queue gets drained.
|
144
|
-
await asyncio.sleep(0.001)
|
145
|
-
|
146
|
-
assert len(drained_items) == 1
|
147
|
-
|
148
|
-
# Add items to the queue and a sentinel while it's still waiting for DEBOUNCE_TIME.
|
149
|
-
await queue.put(2)
|
150
|
-
await queue.put(3)
|
151
|
-
await queue.put(None)
|
152
|
-
|
153
|
-
await asyncio.sleep(DEBOUNCE_TIME + 0.001)
|
154
|
-
|
155
|
-
assert len(drained_items) == 3
|
156
|
-
|
157
|
-
|
158
|
-
@pytest.mark.asyncio
|
159
|
-
async def test_warn_if_generator_is_not_consumed(caplog):
|
160
|
-
@warn_if_generator_is_not_consumed
|
161
|
-
async def my_generator():
|
162
|
-
yield 42
|
163
|
-
|
164
|
-
with caplog.at_level(logging.WARNING):
|
165
|
-
g = my_generator()
|
166
|
-
assert "my_generator" in repr(g)
|
167
|
-
del g # Force destructor
|
168
|
-
|
169
|
-
assert len(caplog.records) == 1
|
170
|
-
assert "my_generator" in caplog.text
|
171
|
-
assert "for" in caplog.text
|
172
|
-
assert "list" in caplog.text
|
173
|
-
|
174
|
-
|
175
|
-
@pytest.mark.asyncio
|
176
|
-
async def test_no_warn_if_generator_is_consumed(caplog):
|
177
|
-
@warn_if_generator_is_not_consumed
|
178
|
-
async def my_generator():
|
179
|
-
yield 42
|
180
|
-
|
181
|
-
with caplog.at_level(logging.WARNING):
|
182
|
-
g = my_generator()
|
183
|
-
async for _ in g:
|
184
|
-
pass
|
185
|
-
del g # Force destructor
|
186
|
-
|
187
|
-
assert len(caplog.records) == 0
|
188
|
-
|
189
|
-
|
190
|
-
def test_exit_handler():
|
191
|
-
result = None
|
192
|
-
sync = Synchronizer()
|
193
|
-
|
194
|
-
async def cleanup():
|
195
|
-
nonlocal result
|
196
|
-
result = "bye"
|
197
|
-
|
198
|
-
async def _setup_code():
|
199
|
-
async_utils.on_shutdown(cleanup())
|
200
|
-
|
201
|
-
setup_code = sync.create_blocking(_setup_code)
|
202
|
-
setup_code()
|
203
|
-
|
204
|
-
sync._close_loop() # this is called on exit by synchronicity, which shuts down the event loop
|
205
|
-
assert result == "bye"
|
206
|
-
|
207
|
-
|
208
|
-
@pytest.mark.asyncio
|
209
|
-
async def test_concurrency_pool():
|
210
|
-
max_running = 0
|
211
|
-
running = 0
|
212
|
-
|
213
|
-
async def f():
|
214
|
-
nonlocal running, max_running
|
215
|
-
running += 1
|
216
|
-
max_running = max(max_running, running)
|
217
|
-
await asyncio.sleep(0.1)
|
218
|
-
running -= 1
|
219
|
-
|
220
|
-
def gen():
|
221
|
-
for i in range(100):
|
222
|
-
yield f()
|
223
|
-
|
224
|
-
await asyncio.wait_for(ConcurrencyPool(50).run_coros(gen()), 0.3)
|
225
|
-
assert max_running == 50
|
226
|
-
|
227
|
-
|
228
|
-
@pytest.mark.asyncio
|
229
|
-
async def test_concurrency_pool_cancels_non_started():
|
230
|
-
counter = 0
|
231
|
-
|
232
|
-
async def f():
|
233
|
-
nonlocal counter
|
234
|
-
counter += 1
|
235
|
-
raise RuntimeError("some error")
|
236
|
-
|
237
|
-
def gen():
|
238
|
-
for i in range(100):
|
239
|
-
yield f()
|
240
|
-
|
241
|
-
with pytest.raises(RuntimeError):
|
242
|
-
await ConcurrencyPool(2).run_coros(gen(), return_exceptions=False)
|
243
|
-
await asyncio.sleep(0.1)
|
244
|
-
assert counter == 2
|
245
|
-
|
246
|
-
|
247
|
-
@pytest.mark.asyncio
|
248
|
-
async def test_concurrency_pool_return_exceptions():
|
249
|
-
async def f(x):
|
250
|
-
if x % 2:
|
251
|
-
raise RuntimeError("some error")
|
252
|
-
else:
|
253
|
-
return 42
|
254
|
-
|
255
|
-
def gen():
|
256
|
-
for x in range(4):
|
257
|
-
yield f(x)
|
258
|
-
|
259
|
-
res = await asyncio.wait_for(ConcurrencyPool(2).run_coros(gen(), return_exceptions=True), 0.1)
|
260
|
-
assert res[0] == res[2] == 42
|
261
|
-
assert isinstance(res[1], RuntimeError)
|
262
|
-
assert isinstance(res[3], RuntimeError)
|
test/blob_test.py
DELETED
@@ -1,67 +0,0 @@
|
|
1
|
-
# Copyright Modal Labs 2022
|
2
|
-
import pytest
|
3
|
-
import random
|
4
|
-
|
5
|
-
from modal._utils.async_utils import synchronize_api
|
6
|
-
from modal._utils.blob_utils import (
|
7
|
-
blob_download as _blob_download,
|
8
|
-
blob_upload as _blob_upload,
|
9
|
-
blob_upload_file as _blob_upload_file,
|
10
|
-
)
|
11
|
-
from modal.exception import ExecutionError
|
12
|
-
|
13
|
-
from .supports.skip import skip_old_py
|
14
|
-
|
15
|
-
blob_upload = synchronize_api(_blob_upload)
|
16
|
-
blob_download = synchronize_api(_blob_download)
|
17
|
-
blob_upload_file = synchronize_api(_blob_upload_file)
|
18
|
-
|
19
|
-
|
20
|
-
@pytest.mark.asyncio
|
21
|
-
async def test_blob_put_get(servicer, blob_server, client):
|
22
|
-
# Upload
|
23
|
-
blob_id = await blob_upload.aio(b"Hello, world", client.stub)
|
24
|
-
|
25
|
-
# Download
|
26
|
-
data = await blob_download.aio(blob_id, client.stub)
|
27
|
-
assert data == b"Hello, world"
|
28
|
-
|
29
|
-
|
30
|
-
@pytest.mark.asyncio
|
31
|
-
async def test_blob_put_failure(servicer, blob_server, client):
|
32
|
-
with pytest.raises(ExecutionError):
|
33
|
-
await blob_upload.aio(b"FAILURE", client.stub)
|
34
|
-
|
35
|
-
|
36
|
-
@pytest.mark.asyncio
|
37
|
-
async def test_blob_get_failure(servicer, blob_server, client):
|
38
|
-
with pytest.raises(ExecutionError):
|
39
|
-
await blob_download.aio("bl-failure", client.stub)
|
40
|
-
|
41
|
-
|
42
|
-
@pytest.mark.asyncio
|
43
|
-
async def test_blob_large(servicer, blob_server, client):
|
44
|
-
data = b"*" * 10_000_000
|
45
|
-
blob_id = await blob_upload.aio(data, client.stub)
|
46
|
-
assert await blob_download.aio(blob_id, client.stub) == data
|
47
|
-
|
48
|
-
|
49
|
-
@skip_old_py("random.randbytes() was introduced in python 3.9", (3, 9))
|
50
|
-
@pytest.mark.asyncio
|
51
|
-
async def test_blob_multipart(servicer, blob_server, client, monkeypatch, tmp_path):
|
52
|
-
monkeypatch.setattr("modal._utils.blob_utils.DEFAULT_SEGMENT_CHUNK_SIZE", 128)
|
53
|
-
multipart_threshold = 1024
|
54
|
-
servicer.blob_multipart_threshold = multipart_threshold
|
55
|
-
# - set high # of parts, to test concurrency correctness
|
56
|
-
# - make last part significantly shorter than rest, creating uneven upload time.
|
57
|
-
data_len = (256 * multipart_threshold) + (multipart_threshold // 2)
|
58
|
-
data = random.randbytes(data_len) # random data will not hide byte re-ordering corruption
|
59
|
-
blob_id = await blob_upload.aio(data, client.stub)
|
60
|
-
assert await blob_download.aio(blob_id, client.stub) == data
|
61
|
-
|
62
|
-
data_len = (256 * multipart_threshold) + (multipart_threshold // 2)
|
63
|
-
data = random.randbytes(data_len) # random data will not hide byte re-ordering corruption
|
64
|
-
data_filepath = tmp_path / "temp.bin"
|
65
|
-
data_filepath.write_bytes(data)
|
66
|
-
blob_id = await blob_upload_file.aio(data_filepath.open("rb"), client.stub)
|
67
|
-
assert await blob_download.aio(blob_id, client.stub) == data
|
test/cli_imports_test.py
DELETED
@@ -1,149 +0,0 @@
|
|
1
|
-
# Copyright Modal Labs 2023
|
2
|
-
import pytest
|
3
|
-
|
4
|
-
from modal._utils.async_utils import synchronizer
|
5
|
-
from modal.cli.import_refs import (
|
6
|
-
DEFAULT_STUB_NAME,
|
7
|
-
get_by_object_path,
|
8
|
-
import_file_or_module,
|
9
|
-
parse_import_ref,
|
10
|
-
)
|
11
|
-
from modal.stub import _LocalEntrypoint, _Stub
|
12
|
-
|
13
|
-
# Some helper vars for import_stub tests:
|
14
|
-
local_entrypoint_src = """
|
15
|
-
import modal
|
16
|
-
|
17
|
-
stub = modal.Stub()
|
18
|
-
@stub.local_entrypoint()
|
19
|
-
def main():
|
20
|
-
pass
|
21
|
-
"""
|
22
|
-
python_module_src = """
|
23
|
-
import modal
|
24
|
-
stub = modal.Stub("FOO")
|
25
|
-
other_stub = modal.Stub("BAR")
|
26
|
-
@other_stub.function()
|
27
|
-
def func():
|
28
|
-
pass
|
29
|
-
@stub.cls()
|
30
|
-
class Parent:
|
31
|
-
@modal.method()
|
32
|
-
def meth(self):
|
33
|
-
pass
|
34
|
-
|
35
|
-
assert not __package__
|
36
|
-
"""
|
37
|
-
|
38
|
-
python_package_src = """
|
39
|
-
import modal
|
40
|
-
stub = modal.Stub("FOO")
|
41
|
-
other_stub = modal.Stub("BAR")
|
42
|
-
@other_stub.function()
|
43
|
-
def func():
|
44
|
-
pass
|
45
|
-
assert __package__ == "pack"
|
46
|
-
"""
|
47
|
-
|
48
|
-
python_subpackage_src = """
|
49
|
-
import modal
|
50
|
-
stub = modal.Stub("FOO")
|
51
|
-
other_stub = modal.Stub("BAR")
|
52
|
-
@other_stub.function()
|
53
|
-
def func():
|
54
|
-
pass
|
55
|
-
assert __package__ == "pack.sub"
|
56
|
-
"""
|
57
|
-
|
58
|
-
python_file_src = """
|
59
|
-
import modal
|
60
|
-
stub = modal.Stub("FOO")
|
61
|
-
other_stub = modal.Stub("BAR")
|
62
|
-
@other_stub.function()
|
63
|
-
def func():
|
64
|
-
pass
|
65
|
-
|
66
|
-
assert __package__ == ""
|
67
|
-
"""
|
68
|
-
|
69
|
-
empty_dir_with_python_file = {"mod.py": python_module_src}
|
70
|
-
|
71
|
-
|
72
|
-
dir_containing_python_package = {
|
73
|
-
"dir": {"sub": {"mod.py": python_module_src, "subfile.py": python_file_src}},
|
74
|
-
"pack": {
|
75
|
-
"file.py": python_file_src,
|
76
|
-
"mod.py": python_package_src,
|
77
|
-
"local.py": local_entrypoint_src,
|
78
|
-
"__init__.py": "",
|
79
|
-
"sub": {"mod.py": python_subpackage_src, "__init__.py": "", "subfile.py": python_file_src},
|
80
|
-
},
|
81
|
-
}
|
82
|
-
|
83
|
-
|
84
|
-
@pytest.mark.parametrize(
|
85
|
-
["dir_structure", "ref", "expected_object_type"],
|
86
|
-
[
|
87
|
-
# # file syntax
|
88
|
-
(empty_dir_with_python_file, "mod.py", _Stub),
|
89
|
-
(empty_dir_with_python_file, "mod.py::stub", _Stub),
|
90
|
-
(empty_dir_with_python_file, "mod.py::other_stub", _Stub),
|
91
|
-
(dir_containing_python_package, "pack/file.py", _Stub),
|
92
|
-
(dir_containing_python_package, "pack/sub/subfile.py", _Stub),
|
93
|
-
(dir_containing_python_package, "dir/sub/subfile.py", _Stub),
|
94
|
-
# # python module syntax
|
95
|
-
(empty_dir_with_python_file, "mod", _Stub),
|
96
|
-
(empty_dir_with_python_file, "mod::stub", _Stub),
|
97
|
-
(empty_dir_with_python_file, "mod::other_stub", _Stub),
|
98
|
-
(dir_containing_python_package, "pack.mod", _Stub),
|
99
|
-
(dir_containing_python_package, "pack.mod::other_stub", _Stub),
|
100
|
-
(dir_containing_python_package, "pack/local.py::stub.main", _LocalEntrypoint),
|
101
|
-
],
|
102
|
-
)
|
103
|
-
def test_import_object(dir_structure, ref, expected_object_type, mock_dir):
|
104
|
-
with mock_dir(dir_structure):
|
105
|
-
import_ref = parse_import_ref(ref)
|
106
|
-
module = import_file_or_module(import_ref.file_or_module)
|
107
|
-
imported_object = get_by_object_path(module, import_ref.object_path or DEFAULT_STUB_NAME)
|
108
|
-
_translated_obj = synchronizer._translate_in(imported_object)
|
109
|
-
assert isinstance(_translated_obj, expected_object_type)
|
110
|
-
|
111
|
-
|
112
|
-
def test_import_package_and_module_names(monkeypatch, supports_dir):
|
113
|
-
# We try to reproduce the package/module naming standard that the `python` command line tool uses,
|
114
|
-
# i.e. when loading using a module path (-m flag w/ python) you get a fully qualified package/module name
|
115
|
-
# but when loading using a filename, some/mod.py it will not have a __package__
|
116
|
-
|
117
|
-
# The biggest difference is that __name__ of the imported "entrypoint" script
|
118
|
-
# is __main__ when using `python` but in the Modal runtime it's the name of the
|
119
|
-
# file minus the ".py", since Modal has its own __main__
|
120
|
-
monkeypatch.chdir(supports_dir)
|
121
|
-
mod1 = import_file_or_module("assert_package")
|
122
|
-
assert mod1.__package__ == ""
|
123
|
-
assert mod1.__name__ == "assert_package"
|
124
|
-
|
125
|
-
monkeypatch.chdir(supports_dir.parent)
|
126
|
-
mod2 = import_file_or_module("test.supports.assert_package")
|
127
|
-
assert mod2.__package__ == "test.supports"
|
128
|
-
assert mod2.__name__ == "test.supports.assert_package"
|
129
|
-
|
130
|
-
mod3 = import_file_or_module("supports/assert_package.py")
|
131
|
-
assert mod3.__package__ == ""
|
132
|
-
assert mod3.__name__ == "assert_package"
|
133
|
-
|
134
|
-
|
135
|
-
def test_get_by_object_path():
|
136
|
-
class NS(dict):
|
137
|
-
def __getattr__(self, n):
|
138
|
-
return dict.__getitem__(self, n)
|
139
|
-
|
140
|
-
# simple
|
141
|
-
assert get_by_object_path(NS(foo="bar"), "foo") == "bar"
|
142
|
-
assert get_by_object_path(NS(foo="bar"), "bar") is None
|
143
|
-
|
144
|
-
# nested simple
|
145
|
-
assert get_by_object_path(NS(foo=NS(bar="baz")), "foo.bar") == "baz"
|
146
|
-
|
147
|
-
# try to find item keys with periods in them (ugh).
|
148
|
-
# this helps resolving lifecycled functions
|
149
|
-
assert get_by_object_path(NS({"foo.bar": "baz"}), "foo.bar") == "baz"
|