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/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"