modal 0.62.115__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 +13 -9
  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 +407 -398
  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 -60
  11. modal/_resources.py +26 -7
  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/{execution_context.py → _runtime/execution_context.py} +11 -2
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +123 -6
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +50 -14
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +386 -104
  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 +299 -98
  29. modal/_utils/grpc_testing.py +47 -34
  30. modal/_utils/grpc_utils.py +54 -21
  31. modal/_utils/hash_utils.py +51 -10
  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 +3 -3
  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 +12 -10
  43. modal/app.py +561 -323
  44. modal/app.pyi +474 -262
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +22 -6
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +203 -42
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +61 -13
  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 +21 -48
  55. modal/cli/launch.py +28 -14
  56. modal/cli/network_file_system.py +57 -21
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +34 -9
  59. modal/cli/programs/vscode.py +58 -8
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +199 -96
  62. modal/cli/secret.py +5 -4
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +74 -8
  65. modal/cli/volume.py +97 -56
  66. modal/client.py +248 -144
  67. modal/client.pyi +156 -124
  68. modal/cloud_bucket_mount.py +43 -30
  69. modal/cloud_bucket_mount.pyi +32 -25
  70. modal/cls.py +528 -141
  71. modal/cls.pyi +189 -145
  72. modal/config.py +32 -15
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +50 -54
  76. modal/dict.pyi +120 -164
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +30 -43
  80. modal/experimental.py +62 -2
  81. modal/file_io.py +537 -0
  82. modal/file_io.pyi +235 -0
  83. modal/file_pattern_matcher.py +197 -0
  84. modal/functions.py +846 -428
  85. modal/functions.pyi +446 -387
  86. modal/gpu.py +57 -44
  87. modal/image.py +946 -417
  88. modal/image.pyi +584 -245
  89. modal/io_streams.py +434 -0
  90. modal/io_streams.pyi +122 -0
  91. modal/mount.py +223 -90
  92. modal/mount.pyi +241 -243
  93. modal/network_file_system.py +85 -86
  94. modal/network_file_system.pyi +151 -110
  95. modal/object.py +66 -36
  96. modal/object.pyi +166 -143
  97. modal/output.py +63 -0
  98. modal/parallel_map.py +73 -47
  99. modal/parallel_map.pyi +51 -63
  100. modal/partial_function.py +272 -107
  101. modal/partial_function.pyi +219 -120
  102. modal/proxy.py +15 -12
  103. modal/proxy.pyi +3 -8
  104. modal/queue.py +96 -72
  105. modal/queue.pyi +210 -135
  106. modal/requirements/2024.04.txt +2 -1
  107. modal/requirements/2024.10.txt +16 -0
  108. modal/requirements/README.md +21 -0
  109. modal/requirements/base-images.json +22 -0
  110. modal/retries.py +45 -4
  111. modal/runner.py +325 -203
  112. modal/runner.pyi +124 -110
  113. modal/running_app.py +27 -4
  114. modal/sandbox.py +509 -231
  115. modal/sandbox.pyi +396 -169
  116. modal/schedule.py +2 -2
  117. modal/scheduler_placement.py +20 -3
  118. modal/secret.py +41 -25
  119. modal/secret.pyi +62 -42
  120. modal/serving.py +39 -49
  121. modal/serving.pyi +37 -43
  122. modal/stream_type.py +15 -0
  123. modal/token_flow.py +5 -3
  124. modal/token_flow.pyi +37 -32
  125. modal/volume.py +123 -137
  126. modal/volume.pyi +228 -221
  127. {modal-0.62.115.dist-info → modal-0.72.11.dist-info}/METADATA +5 -5
  128. modal-0.72.11.dist-info/RECORD +174 -0
  129. {modal-0.62.115.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
  130. modal_docs/gen_reference_docs.py +3 -1
  131. modal_docs/mdmd/mdmd.py +0 -1
  132. modal_docs/mdmd/signatures.py +1 -2
  133. modal_global_objects/images/base_images.py +28 -0
  134. modal_global_objects/mounts/python_standalone.py +2 -2
  135. modal_proto/__init__.py +1 -1
  136. modal_proto/api.proto +1231 -531
  137. modal_proto/api_grpc.py +750 -430
  138. modal_proto/api_pb2.py +2102 -1176
  139. modal_proto/api_pb2.pyi +8859 -0
  140. modal_proto/api_pb2_grpc.py +1329 -675
  141. modal_proto/api_pb2_grpc.pyi +1416 -0
  142. modal_proto/modal_api_grpc.py +149 -0
  143. modal_proto/modal_options_grpc.py +3 -0
  144. modal_proto/options_pb2.pyi +20 -0
  145. modal_proto/options_pb2_grpc.pyi +7 -0
  146. modal_proto/py.typed +0 -0
  147. modal_version/__init__.py +1 -1
  148. modal_version/_version_generated.py +2 -2
  149. modal/_asgi.py +0 -370
  150. modal/_container_exec.py +0 -128
  151. modal/_container_io_manager.py +0 -646
  152. modal/_container_io_manager.pyi +0 -412
  153. modal/_sandbox_shell.py +0 -49
  154. modal/app_utils.py +0 -20
  155. modal/app_utils.pyi +0 -17
  156. modal/execution_context.pyi +0 -37
  157. modal/shared_volume.py +0 -23
  158. modal/shared_volume.pyi +0 -24
  159. modal-0.62.115.dist-info/RECORD +0 -207
  160. modal_global_objects/images/conda.py +0 -15
  161. modal_global_objects/images/debian_slim.py +0 -15
  162. modal_global_objects/images/micromamba.py +0 -15
  163. test/__init__.py +0 -1
  164. test/aio_test.py +0 -12
  165. test/async_utils_test.py +0 -279
  166. test/blob_test.py +0 -67
  167. test/cli_imports_test.py +0 -149
  168. test/cli_test.py +0 -674
  169. test/client_test.py +0 -203
  170. test/cloud_bucket_mount_test.py +0 -22
  171. test/cls_test.py +0 -636
  172. test/config_test.py +0 -149
  173. test/conftest.py +0 -1485
  174. test/container_app_test.py +0 -50
  175. test/container_test.py +0 -1405
  176. test/cpu_test.py +0 -23
  177. test/decorator_test.py +0 -85
  178. test/deprecation_test.py +0 -34
  179. test/dict_test.py +0 -51
  180. test/e2e_test.py +0 -68
  181. test/error_test.py +0 -7
  182. test/function_serialization_test.py +0 -32
  183. test/function_test.py +0 -791
  184. test/function_utils_test.py +0 -101
  185. test/gpu_test.py +0 -159
  186. test/grpc_utils_test.py +0 -82
  187. test/helpers.py +0 -47
  188. test/image_test.py +0 -814
  189. test/live_reload_test.py +0 -80
  190. test/lookup_test.py +0 -70
  191. test/mdmd_test.py +0 -329
  192. test/mount_test.py +0 -162
  193. test/mounted_files_test.py +0 -327
  194. test/network_file_system_test.py +0 -188
  195. test/notebook_test.py +0 -66
  196. test/object_test.py +0 -41
  197. test/package_utils_test.py +0 -25
  198. test/queue_test.py +0 -115
  199. test/resolver_test.py +0 -59
  200. test/retries_test.py +0 -67
  201. test/runner_test.py +0 -85
  202. test/sandbox_test.py +0 -191
  203. test/schedule_test.py +0 -15
  204. test/scheduler_placement_test.py +0 -57
  205. test/secret_test.py +0 -89
  206. test/serialization_test.py +0 -50
  207. test/stub_composition_test.py +0 -10
  208. test/stub_test.py +0 -361
  209. test/test_asgi_wrapper.py +0 -234
  210. test/token_flow_test.py +0 -18
  211. test/traceback_test.py +0 -135
  212. test/tunnel_test.py +0 -29
  213. test/utils_test.py +0 -88
  214. test/version_test.py +0 -14
  215. test/volume_test.py +0 -397
  216. test/watcher_test.py +0 -58
  217. test/webhook_test.py +0 -145
  218. {modal-0.62.115.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
  219. {modal-0.62.115.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
  220. {modal-0.62.115.dist-info → modal-0.72.11.dist-info}/entry_points.txt +0 -0
test/cli_test.py DELETED
@@ -1,674 +0,0 @@
1
- # Copyright Modal Labs 2022-2023
2
- import asyncio
3
- import contextlib
4
- import json
5
- import os
6
- import pytest
7
- import re
8
- import subprocess
9
- import sys
10
- import tempfile
11
- import traceback
12
- from typing import List, Optional
13
- from unittest import mock
14
-
15
- import click
16
- import click.testing
17
- import toml
18
-
19
- from modal.cli.entry_point import entrypoint_cli
20
- from modal_proto import api_pb2
21
-
22
- from .supports.skip import skip_windows
23
-
24
- dummy_app_file = """
25
- import modal
26
-
27
- import other_module
28
-
29
- app = modal.App("my_app")
30
-
31
- # Sanity check that the module is imported properly
32
- import sys
33
- mod = sys.modules[__name__]
34
- assert mod.app == app
35
- """
36
-
37
- dummy_other_module_file = "x = 42"
38
-
39
-
40
- def _run(args: List[str], expected_exit_code: int = 0, expected_stderr: Optional[str] = ""):
41
- runner = click.testing.CliRunner(mix_stderr=False)
42
- with mock.patch.object(sys, "argv", args):
43
- res = runner.invoke(entrypoint_cli, args)
44
- if res.exit_code != expected_exit_code:
45
- print("stdout:", repr(res.stdout))
46
- print("stderr:", repr(res.stderr))
47
- traceback.print_tb(res.exc_info[2])
48
- print(res.exception, file=sys.stderr)
49
- assert res.exit_code == expected_exit_code
50
- if expected_stderr is not None:
51
- assert res.stderr == expected_stderr
52
- return res
53
-
54
-
55
- def test_app_deploy_success(servicer, mock_dir, set_env_client):
56
- with mock_dir({"myapp.py": dummy_app_file, "other_module.py": dummy_other_module_file}):
57
- # Deploy as a script in cwd
58
- _run(["deploy", "myapp.py"])
59
-
60
- # Deploy as a module
61
- _run(["deploy", "myapp"])
62
-
63
- # Deploy as a script with an absolute path
64
- _run(["deploy", os.path.abspath("myapp.py")])
65
-
66
- assert "my_app" in servicer.deployed_apps
67
-
68
-
69
- def test_app_deploy_with_name(servicer, mock_dir, set_env_client):
70
- with mock_dir({"myapp.py": dummy_app_file, "other_module.py": dummy_other_module_file}):
71
- _run(["deploy", "myapp.py", "--name", "my_app_foo"])
72
-
73
- assert "my_app_foo" in servicer.deployed_apps
74
-
75
-
76
- def test_secret_create(servicer, set_env_client):
77
- # fail without any keys
78
- _run(["secret", "create", "foo"], 2, None)
79
-
80
- _run(["secret", "create", "foo", "bar=baz"])
81
- assert len(servicer.secrets) == 1
82
-
83
- # Creating the same one again should fail
84
- _run(["secret", "create", "foo", "bar=baz"], expected_exit_code=1)
85
-
86
- # But it should succeed with --force
87
- _run(["secret", "create", "foo", "bar=baz", "--force"])
88
-
89
-
90
- def test_secret_list(servicer, set_env_client):
91
- res = _run(["secret", "list"])
92
- assert "dummy-secret-0" not in res.stdout
93
-
94
- _run(["secret", "create", "foo", "bar=baz"])
95
- _run(["secret", "create", "bar", "baz=buz"])
96
- _run(["secret", "create", "eric", "baz=bu 123z=b\n\t\r #(Q)JO5️⃣5️⃣😤WMLE🔧:GWam "])
97
-
98
- res = _run(["secret", "list"])
99
- assert "dummy-secret-0" in res.stdout
100
- assert "dummy-secret-1" in res.stdout
101
- assert "dummy-secret-2" in res.stdout
102
- assert "dummy-secret-3" not in res.stdout
103
-
104
-
105
- def test_app_token_new(servicer, set_env_client, server_url_env, modal_config):
106
- with modal_config() as config_file_path:
107
- _run(["token", "new", "--profile", "_test"])
108
- assert "_test" in toml.load(config_file_path)
109
-
110
-
111
- def test_app_setup(servicer, set_env_client, server_url_env, modal_config):
112
- with modal_config() as config_file_path:
113
- _run(["setup", "--profile", "_test"])
114
- assert "_test" in toml.load(config_file_path)
115
-
116
-
117
- def test_run(servicer, set_env_client, test_dir):
118
- app_file = test_dir / "supports" / "app_run_tests" / "default_app.py"
119
- _run(["run", app_file.as_posix()])
120
- _run(["run", app_file.as_posix() + "::app"])
121
- _run(["run", app_file.as_posix() + "::foo"])
122
- _run(["run", app_file.as_posix() + "::bar"], expected_exit_code=1, expected_stderr=None)
123
- file_with_entrypoint = test_dir / "supports" / "app_run_tests" / "local_entrypoint.py"
124
- _run(["run", file_with_entrypoint.as_posix()])
125
- _run(["run", file_with_entrypoint.as_posix() + "::main"])
126
- _run(["run", file_with_entrypoint.as_posix() + "::app.main"])
127
-
128
-
129
- def test_run_stub(servicer, set_env_client, test_dir):
130
- app_file = test_dir / "supports" / "app_run_tests" / "app_was_once_stub.py"
131
- _run(["run", app_file.as_posix()])
132
- _run(["run", app_file.as_posix() + "::stub"])
133
- _run(["run", app_file.as_posix() + "::foo"])
134
-
135
-
136
- def test_run_stub_with_app(servicer, set_env_client, test_dir):
137
- app_file = test_dir / "supports" / "app_run_tests" / "app_and_stub.py"
138
- with pytest.warns(match="`app`"):
139
- _run(["run", app_file.as_posix()])
140
- _run(["run", app_file.as_posix() + "::stub"])
141
- _run(["run", app_file.as_posix() + "::foo"])
142
-
143
-
144
- def test_run_async(servicer, set_env_client, test_dir):
145
- sync_fn = test_dir / "supports" / "app_run_tests" / "local_entrypoint.py"
146
- res = _run(["run", sync_fn.as_posix()])
147
- assert "called locally" in res.stdout
148
-
149
- async_fn = test_dir / "supports" / "app_run_tests" / "local_entrypoint_async.py"
150
- res = _run(["run", async_fn.as_posix()])
151
- assert "called locally (async)" in res.stdout
152
-
153
-
154
- def test_run_generator(servicer, set_env_client, test_dir):
155
- app_file = test_dir / "supports" / "app_run_tests" / "generator.py"
156
- result = _run(["run", app_file.as_posix()], expected_exit_code=1)
157
- assert "generator functions" in str(result.exception)
158
-
159
-
160
- def test_help_message_unspecified_function(servicer, set_env_client, test_dir):
161
- app_file = test_dir / "supports" / "app_run_tests" / "app_with_multiple_functions.py"
162
- result = _run(["run", app_file.as_posix()], expected_exit_code=2, expected_stderr=None)
163
-
164
- # should suggest available functions on the app:
165
- assert "foo" in result.stderr
166
- assert "bar" in result.stderr
167
-
168
- result = _run(
169
- ["run", app_file.as_posix(), "--help"], expected_exit_code=2, expected_stderr=None
170
- ) # TODO: help should not return non-zero
171
- # help should also available functions on the app:
172
- assert "foo" in result.stderr
173
- assert "bar" in result.stderr
174
-
175
-
176
- def test_run_states(servicer, set_env_client, test_dir):
177
- app_file = test_dir / "supports" / "app_run_tests" / "default_app.py"
178
- _run(["run", app_file.as_posix()])
179
- assert servicer.app_state_history["ap-1"] == [
180
- api_pb2.APP_STATE_INITIALIZING,
181
- api_pb2.APP_STATE_EPHEMERAL,
182
- api_pb2.APP_STATE_STOPPED,
183
- ]
184
-
185
-
186
- def test_run_detach(servicer, set_env_client, test_dir):
187
- app_file = test_dir / "supports" / "app_run_tests" / "default_app.py"
188
- _run(["run", "--detach", app_file.as_posix()])
189
- assert servicer.app_state_history["ap-1"] == [api_pb2.APP_STATE_INITIALIZING, api_pb2.APP_STATE_DETACHED]
190
-
191
-
192
- def test_run_quiet(servicer, set_env_client, test_dir):
193
- app_file = test_dir / "supports" / "app_run_tests" / "default_app.py"
194
- # Just tests that the command runs without error for now (tests end up defaulting to `show_progress=False` anyway,
195
- # without a TTY).
196
- _run(["run", "--quiet", app_file.as_posix()])
197
-
198
-
199
- def test_deploy(servicer, set_env_client, test_dir):
200
- app_file = test_dir / "supports" / "app_run_tests" / "default_app.py"
201
- _run(["deploy", "--name=deployment_name", app_file.as_posix()])
202
- assert servicer.app_state_history["ap-1"] == [api_pb2.APP_STATE_INITIALIZING, api_pb2.APP_STATE_DEPLOYED]
203
-
204
-
205
- def test_run_custom_app(servicer, set_env_client, test_dir):
206
- app_file = test_dir / "supports" / "app_run_tests" / "custom_app.py"
207
- res = _run(["run", app_file.as_posix() + "::app"], expected_exit_code=1, expected_stderr=None)
208
- assert "Could not find" in res.stderr
209
- res = _run(["run", app_file.as_posix() + "::app.foo"], expected_exit_code=1, expected_stderr=None)
210
- assert "Could not find" in res.stderr
211
-
212
- _run(["run", app_file.as_posix() + "::foo"])
213
-
214
-
215
- def test_run_aiofunc(servicer, set_env_client, test_dir):
216
- app_file = test_dir / "supports" / "app_run_tests" / "async_app.py"
217
- _run(["run", app_file.as_posix()])
218
- assert len(servicer.client_calls) == 1
219
-
220
-
221
- def test_run_local_entrypoint(servicer, set_env_client, test_dir):
222
- app_file = test_dir / "supports" / "app_run_tests" / "local_entrypoint.py"
223
-
224
- res = _run(["run", app_file.as_posix() + "::app.main"]) # explicit name
225
- assert "called locally" in res.stdout
226
- assert len(servicer.client_calls) == 2
227
-
228
- res = _run(["run", app_file.as_posix()]) # only one entry-point, no name needed
229
- assert "called locally" in res.stdout
230
- assert len(servicer.client_calls) == 4
231
-
232
-
233
- def test_run_local_entrypoint_invalid_with_app_run(servicer, set_env_client, test_dir):
234
- app_file = test_dir / "supports" / "app_run_tests" / "local_entrypoint_invalid.py"
235
-
236
- res = _run(["run", app_file.as_posix()], expected_exit_code=1)
237
- assert "app is already running" in str(res.exception.__cause__).lower()
238
- assert "unreachable" not in res.stdout
239
- assert len(servicer.client_calls) == 0
240
-
241
-
242
- def test_run_parse_args_entrypoint(servicer, set_env_client, test_dir):
243
- app_file = test_dir / "supports" / "app_run_tests" / "cli_args.py"
244
- res = _run(["run", app_file.as_posix()], expected_exit_code=2, expected_stderr=None)
245
- assert "You need to specify a Modal function or local entrypoint to run" in res.stderr
246
-
247
- valid_call_args = [
248
- (
249
- [
250
- "run",
251
- f"{app_file.as_posix()}::app.dt_arg",
252
- "--dt",
253
- "2022-10-31",
254
- ],
255
- "the day is 31",
256
- ),
257
- (["run", f"{app_file.as_posix()}::dt_arg", "--dt=2022-10-31"], "the day is 31"),
258
- (["run", f"{app_file.as_posix()}::int_arg", "--i=200"], "200 <class 'int'>"),
259
- (["run", f"{app_file.as_posix()}::default_arg"], "10 <class 'int'>"),
260
- (["run", f"{app_file.as_posix()}::unannotated_arg", "--i=2022-10-31"], "'2022-10-31' <class 'str'>"),
261
- (["run", f"{app_file.as_posix()}::unannotated_default_arg"], "10 <class 'int'>"),
262
- (["run", f"{app_file.as_posix()}::optional_arg", "--i=20"], "20 <class 'int'>"),
263
- (["run", f"{app_file.as_posix()}::optional_arg"], "None <class 'NoneType'>"),
264
- (["run", f"{app_file.as_posix()}::optional_arg_postponed"], "None <class 'NoneType'>"),
265
- ]
266
- if sys.version_info >= (3, 10):
267
- valid_call_args.extend(
268
- [
269
- (["run", f"{app_file.as_posix()}::optional_arg_pep604", "--i=20"], "20 <class 'int'>"),
270
- (["run", f"{app_file.as_posix()}::optional_arg_pep604"], "None <class 'NoneType'>"),
271
- ]
272
- )
273
- for args, expected in valid_call_args:
274
- res = _run(args)
275
- assert expected in res.stdout
276
- assert len(servicer.client_calls) == 0
277
-
278
- if sys.version_info >= (3, 10):
279
- res = _run(["run", f"{app_file.as_posix()}::unparseable_annot", "--i=20"], expected_exit_code=1)
280
- assert "Parameter `i` has unparseable annotation: typing.Union[int, str]" in str(res.exception)
281
-
282
- if sys.version_info <= (3, 10):
283
- res = _run(["run", f"{app_file.as_posix()}::optional_arg_pep604"], expected_exit_code=1)
284
- assert "Unable to generate command line interface for app entrypoint." in str(res.exception)
285
-
286
-
287
- def test_run_parse_args_function(servicer, set_env_client, test_dir):
288
- app_file = test_dir / "supports" / "app_run_tests" / "cli_args.py"
289
- res = _run(["run", app_file.as_posix()], expected_exit_code=2, expected_stderr=None)
290
- assert "You need to specify a Modal function or local entrypoint to run" in res.stderr
291
-
292
- # HACK: all the tests use the same arg, i.
293
- @servicer.function_body
294
- def print_type(i):
295
- print(repr(i), type(i))
296
-
297
- valid_call_args = [
298
- (["run", f"{app_file.as_posix()}::int_arg_fn", "--i=200"], "200 <class 'int'>"),
299
- (["run", f"{app_file.as_posix()}::ALifecycle.some_method", "--i=hello"], "'hello' <class 'str'>"),
300
- (["run", f"{app_file.as_posix()}::ALifecycle.some_method_int", "--i=42"], "42 <class 'int'>"),
301
- (["run", f"{app_file.as_posix()}::optional_arg_fn"], "None <class 'NoneType'>"),
302
- ]
303
- for args, expected in valid_call_args:
304
- res = _run(args)
305
- assert expected in res.stdout
306
-
307
-
308
- def test_run_user_script_exception(servicer, set_env_client, test_dir):
309
- app_file = test_dir / "supports" / "app_run_tests" / "raises_error.py"
310
- res = _run(["run", app_file.as_posix()], expected_exit_code=1)
311
- assert res.exc_info[1].user_source == str(app_file.resolve())
312
-
313
-
314
- @pytest.fixture
315
- def fresh_main_thread_assertion_module(test_dir):
316
- modules_to_unload = [n for n in sys.modules.keys() if "main_thread_assertion" in n]
317
- assert len(modules_to_unload) <= 1
318
- for mod in modules_to_unload:
319
- sys.modules.pop(mod)
320
- yield test_dir / "supports" / "app_run_tests" / "main_thread_assertion.py"
321
-
322
-
323
- def test_no_user_code_in_synchronicity_run(servicer, set_env_client, test_dir, fresh_main_thread_assertion_module):
324
- pytest._did_load_main_thread_assertion = False # type: ignore
325
- _run(["run", fresh_main_thread_assertion_module.as_posix()])
326
- assert pytest._did_load_main_thread_assertion # type: ignore
327
- print()
328
-
329
-
330
- def test_no_user_code_in_synchronicity_deploy(servicer, set_env_client, test_dir, fresh_main_thread_assertion_module):
331
- pytest._did_load_main_thread_assertion = False # type: ignore
332
- _run(["deploy", "--name", "foo", fresh_main_thread_assertion_module.as_posix()])
333
- assert pytest._did_load_main_thread_assertion # type: ignore
334
-
335
-
336
- def test_serve(servicer, set_env_client, server_url_env, test_dir):
337
- app_file = test_dir / "supports" / "app_run_tests" / "webhook.py"
338
- _run(["serve", app_file.as_posix(), "--timeout", "3"], expected_exit_code=0)
339
-
340
-
341
- @pytest.fixture
342
- def mock_shell_pty():
343
- def mock_get_pty_info(shell: bool) -> api_pb2.PTYInfo:
344
- rows, cols = (64, 128)
345
- return api_pb2.PTYInfo(
346
- enabled=True,
347
- winsz_rows=rows,
348
- winsz_cols=cols,
349
- env_term=os.environ.get("TERM"),
350
- env_colorterm=os.environ.get("COLORTERM"),
351
- env_term_program=os.environ.get("TERM_PROGRAM"),
352
- pty_type=api_pb2.PTYInfo.PTY_TYPE_SHELL,
353
- )
354
-
355
- captured_out = []
356
- fake_stdin = [b"echo foo\n", b"exit\n"]
357
-
358
- async def write_to_fd(fd: int, data: bytes):
359
- nonlocal captured_out
360
- captured_out.append((fd, data))
361
-
362
- @contextlib.asynccontextmanager
363
- async def fake_stream_from_stdin(handle_input, use_raw_terminal=False):
364
- async def _write():
365
- message_index = 0
366
- while True:
367
- if message_index == len(fake_stdin):
368
- break
369
- data = fake_stdin[message_index]
370
- await handle_input(data, message_index)
371
- message_index += 1
372
-
373
- write_task = asyncio.create_task(_write())
374
- yield
375
- write_task.cancel()
376
-
377
- with mock.patch("rich.console.Console.is_terminal", True), mock.patch(
378
- "modal._pty.get_pty_info", mock_get_pty_info
379
- ), mock.patch("modal.runner.get_pty_info", mock_get_pty_info), mock.patch(
380
- "modal._utils.shell_utils.stream_from_stdin", fake_stream_from_stdin
381
- ), mock.patch("modal._sandbox_shell.write_to_fd", write_to_fd):
382
- yield fake_stdin, captured_out
383
-
384
-
385
- @skip_windows("modal shell is not supported on Windows.")
386
- def test_shell(servicer, set_env_client, test_dir, mock_shell_pty):
387
- app_file = test_dir / "supports" / "app_run_tests" / "default_app.py"
388
- webhook_app_file = test_dir / "supports" / "app_run_tests" / "webhook.py"
389
- fake_stdin, captured_out = mock_shell_pty
390
-
391
- fake_stdin.clear()
392
- fake_stdin.extend([b'echo "Hello World"\n', b"exit\n"])
393
-
394
- # Function is explicitly specified
395
- _run(["shell", app_file.as_posix() + "::foo"])
396
-
397
- shell_prompt = servicer.sandbox_shell_prompt.encode("utf-8")
398
-
399
- # first captured message is the empty message the mock server sends
400
- assert captured_out == [(1, shell_prompt), (1, b"Hello World\n")]
401
- captured_out.clear()
402
-
403
- # Function is explicitly specified
404
- _run(["shell", webhook_app_file.as_posix() + "::foo"])
405
- assert captured_out == [(1, shell_prompt), (1, b"Hello World\n")]
406
- captured_out.clear()
407
-
408
- # Function must be inferred
409
- _run(["shell", webhook_app_file.as_posix()])
410
- assert captured_out == [(1, shell_prompt), (1, b"Hello World\n")]
411
- captured_out.clear()
412
-
413
-
414
- @skip_windows("modal shell is not supported on Windows.")
415
- def test_shell_cmd(servicer, set_env_client, test_dir, mock_shell_pty):
416
- app_file = test_dir / "supports" / "app_run_tests" / "default_app.py"
417
- _, captured_out = mock_shell_pty
418
- _run(["shell", "--cmd", "pwd", app_file.as_posix() + "::foo"])
419
- expected_output = subprocess.run(["pwd"], capture_output=True, check=True).stdout
420
- shell_prompt = servicer.sandbox_shell_prompt.encode("utf-8")
421
- assert captured_out == [(1, shell_prompt), (1, expected_output)]
422
-
423
-
424
- def test_app_descriptions(servicer, server_url_env, test_dir):
425
- app_file = test_dir / "supports" / "app_run_tests" / "prints_desc_app.py"
426
- _run(["run", "--detach", app_file.as_posix() + "::foo"])
427
-
428
- create_reqs = [s for s in servicer.requests if isinstance(s, api_pb2.AppCreateRequest)]
429
- assert len(create_reqs) == 1
430
- assert create_reqs[0].app_state == api_pb2.APP_STATE_DETACHED
431
- description = create_reqs[0].description
432
- assert "prints_desc_app.py::foo" in description
433
- assert "run --detach " not in description
434
-
435
- _run(["serve", "--timeout", "0.0", app_file.as_posix()])
436
- create_reqs = [s for s in servicer.requests if isinstance(s, api_pb2.AppCreateRequest)]
437
- assert len(create_reqs) == 2
438
- description = create_reqs[1].description
439
- assert "prints_desc_app.py" in description
440
- assert "serve" not in description
441
- assert "--timeout 0.0" not in description
442
-
443
-
444
- def test_logs(servicer, server_url_env):
445
- async def app_done(self, stream):
446
- await stream.recv_message()
447
- log = api_pb2.TaskLogs(data="hello\n", file_descriptor=api_pb2.FILE_DESCRIPTOR_STDOUT)
448
- await stream.send_message(api_pb2.TaskLogsBatch(entry_id="1", items=[log]))
449
- await stream.send_message(api_pb2.TaskLogsBatch(app_done=True))
450
-
451
- with servicer.intercept() as ctx:
452
- ctx.set_responder("AppGetLogs", app_done)
453
- res = _run(["app", "logs", "ap-123"], expected_exit_code=0)
454
- assert res.stdout == "hello\n"
455
-
456
-
457
- def test_nfs_get(set_env_client, servicer):
458
- nfs_name = "my-shared-nfs"
459
- _run(["nfs", "create", nfs_name])
460
- with tempfile.TemporaryDirectory() as tmpdir:
461
- upload_path = os.path.join(tmpdir, "upload.txt")
462
- with open(upload_path, "w") as f:
463
- f.write("foo bar baz")
464
- f.flush()
465
- _run(["nfs", "put", nfs_name, upload_path, "test.txt"])
466
-
467
- _run(["nfs", "get", nfs_name, "test.txt", tmpdir])
468
- with open(os.path.join(tmpdir, "test.txt"), "r") as f:
469
- assert f.read() == "foo bar baz"
470
-
471
-
472
- def test_volume_cli(set_env_client):
473
- _run(["volume", "--help"])
474
-
475
-
476
- def test_volume_get(servicer, set_env_client):
477
- vol_name = "my-test-vol"
478
- _run(["volume", "create", vol_name])
479
- file_path = b"test.txt"
480
- file_contents = b"foo bar baz"
481
- with tempfile.TemporaryDirectory() as tmpdir:
482
- upload_path = os.path.join(tmpdir, "upload.txt")
483
- with open(upload_path, "wb") as f:
484
- f.write(file_contents)
485
- f.flush()
486
- _run(["volume", "put", vol_name, upload_path, file_path.decode()])
487
-
488
- _run(["volume", "get", vol_name, file_path.decode(), tmpdir])
489
- with open(os.path.join(tmpdir, file_path.decode()), "rb") as f:
490
- assert f.read() == file_contents
491
-
492
- with tempfile.TemporaryDirectory() as tmpdir2:
493
- _run(["volume", "get", vol_name, "/", tmpdir2])
494
- with open(os.path.join(tmpdir2, file_path.decode()), "rb") as f:
495
- assert f.read() == file_contents
496
-
497
-
498
- def test_volume_put_force(servicer, set_env_client):
499
- vol_name = "my-test-vol"
500
- _run(["volume", "create", vol_name])
501
- file_path = "test.txt"
502
- file_contents = b"foo bar baz"
503
- with tempfile.TemporaryDirectory() as tmpdir:
504
- upload_path = os.path.join(tmpdir, "upload.txt")
505
- with open(upload_path, "wb") as f:
506
- f.write(file_contents)
507
- f.flush()
508
-
509
- # File upload
510
- _run(["volume", "put", vol_name, upload_path, file_path]) # Seed the volume
511
- with servicer.intercept() as ctx:
512
- _run(["volume", "put", vol_name, upload_path, file_path], expected_exit_code=2, expected_stderr=None)
513
- assert ctx.pop_request("VolumePutFiles").disallow_overwrite_existing_files
514
-
515
- _run(["volume", "put", vol_name, upload_path, file_path, "--force"])
516
- assert not ctx.pop_request("VolumePutFiles").disallow_overwrite_existing_files
517
-
518
- # Dir upload
519
- _run(["volume", "put", vol_name, tmpdir]) # Seed the volume
520
- with servicer.intercept() as ctx:
521
- _run(["volume", "put", vol_name, tmpdir], expected_exit_code=2, expected_stderr=None)
522
- assert ctx.pop_request("VolumePutFiles").disallow_overwrite_existing_files
523
-
524
- _run(["volume", "put", vol_name, tmpdir, "--force"])
525
- assert not ctx.pop_request("VolumePutFiles").disallow_overwrite_existing_files
526
-
527
-
528
- def test_volume_rm(servicer, set_env_client):
529
- vol_name = "my-test-vol"
530
- _run(["volume", "create", vol_name])
531
- file_path = b"test.txt"
532
- file_contents = b"foo bar baz"
533
- with tempfile.TemporaryDirectory() as tmpdir:
534
- upload_path = os.path.join(tmpdir, "upload.txt")
535
- with open(upload_path, "wb") as f:
536
- f.write(file_contents)
537
- f.flush()
538
- _run(["volume", "put", vol_name, upload_path, file_path.decode()])
539
-
540
- _run(["volume", "get", vol_name, file_path.decode(), tmpdir])
541
- with open(os.path.join(tmpdir, file_path.decode()), "rb") as f:
542
- assert f.read() == file_contents
543
-
544
- _run(["volume", "rm", vol_name, file_path.decode()])
545
- _run(["volume", "get", vol_name, file_path.decode()], expected_exit_code=1, expected_stderr=None)
546
-
547
-
548
- @pytest.mark.parametrize("command", [["run"], ["deploy"], ["serve", "--timeout=1"], ["shell"]])
549
- @pytest.mark.usefixtures("set_env_client", "mock_shell_pty")
550
- @skip_windows("modal shell is not supported on Windows.")
551
- def test_environment_flag(test_dir, servicer, command):
552
- @servicer.function_body
553
- def nothing(
554
- arg=None,
555
- ): # hacky - compatible with both argless modal run and interactive mode which always sends an arg...
556
- pass
557
-
558
- app_file = test_dir / "supports" / "app_run_tests" / "app_with_lookups.py"
559
- with servicer.intercept() as ctx:
560
- ctx.add_response(
561
- "MountGetOrCreate",
562
- api_pb2.MountGetOrCreateResponse(
563
- mount_id="mo-123",
564
- handle_metadata=api_pb2.MountHandleMetadata(content_checksum_sha256_hex="abc123"),
565
- ),
566
- request_filter=lambda req: req.deployment_name.startswith("modal-client-mount")
567
- and req.namespace == api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
568
- ) # built-in client lookup
569
- ctx.add_response(
570
- "SharedVolumeGetOrCreate",
571
- api_pb2.SharedVolumeGetOrCreateResponse(shared_volume_id="sv-123"),
572
- request_filter=lambda req: req.deployment_name == "volume_app" and req.environment_name == "staging",
573
- )
574
- _run(command + ["--env=staging", str(app_file)])
575
-
576
- app_create: api_pb2.AppCreateRequest = ctx.pop_request("AppCreate")
577
- assert app_create.environment_name == "staging"
578
-
579
-
580
- @pytest.mark.parametrize("command", [["run"], ["deploy"], ["serve", "--timeout=1"], ["shell"]])
581
- @pytest.mark.usefixtures("set_env_client", "mock_shell_pty")
582
- @skip_windows("modal shell is not supported on Windows.")
583
- def test_environment_noflag(test_dir, servicer, command, monkeypatch):
584
- monkeypatch.setenv("MODAL_ENVIRONMENT", "some_weird_default_env")
585
-
586
- @servicer.function_body
587
- def nothing(
588
- arg=None,
589
- ): # hacky - compatible with both argless modal run and interactive mode which always sends an arg...
590
- pass
591
-
592
- app_file = test_dir / "supports" / "app_run_tests" / "app_with_lookups.py"
593
- with servicer.intercept() as ctx:
594
- ctx.add_response(
595
- "MountGetOrCreate",
596
- api_pb2.MountGetOrCreateResponse(
597
- mount_id="mo-123",
598
- handle_metadata=api_pb2.MountHandleMetadata(content_checksum_sha256_hex="abc123"),
599
- ),
600
- request_filter=lambda req: req.deployment_name.startswith("modal-client-mount")
601
- and req.namespace == api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
602
- ) # built-in client lookup
603
- ctx.add_response(
604
- "SharedVolumeGetOrCreate",
605
- api_pb2.SharedVolumeGetOrCreateResponse(shared_volume_id="sv-123"),
606
- request_filter=lambda req: req.deployment_name == "volume_app"
607
- and req.environment_name == "some_weird_default_env",
608
- )
609
- _run(command + [str(app_file)])
610
-
611
- app_create: api_pb2.AppCreateRequest = ctx.pop_request("AppCreate")
612
- assert app_create.environment_name == "some_weird_default_env"
613
-
614
-
615
- def test_cls(servicer, set_env_client, test_dir):
616
- app_file = test_dir / "supports" / "app_run_tests" / "cls.py"
617
-
618
- _run(["run", app_file.as_posix(), "--x", "42", "--y", "1000"])
619
- _run(["run", f"{app_file.as_posix()}::AParametrized.some_method", "--x", "42", "--y", "1000"])
620
-
621
-
622
- def test_profile_list(servicer, server_url_env, modal_config):
623
- config = """
624
- [test-profile]
625
- token_id = "ak-abc"
626
- token_secret = "as-xyz"
627
-
628
- [other-profile]
629
- token_id = "ak-123"
630
- token_secret = "as-789"
631
- active = true
632
- """
633
-
634
- with modal_config(config):
635
- res = _run(["profile", "list"])
636
- table_rows = res.stdout.split("\n")
637
- assert re.search("Profile .+ Workspace", table_rows[1])
638
- assert re.search("test-profile .+ test-username", table_rows[3])
639
- assert re.search("other-profile .+ test-username", table_rows[4])
640
-
641
- res = _run(["profile", "list", "--json"])
642
- json_data = json.loads(res.stdout)
643
- assert json_data[0]["name"] == "test-profile"
644
- assert json_data[0]["workspace"] == "test-username"
645
- assert json_data[1]["name"] == "other-profile"
646
- assert json_data[1]["workspace"] == "test-username"
647
-
648
- orig_env_token_id = os.environ.get("MODAL_TOKEN_ID")
649
- orig_env_token_secret = os.environ.get("MODAL_TOKEN_SECRET")
650
- os.environ["MODAL_TOKEN_ID"] = "ak-abc"
651
- os.environ["MODAL_TOKEN_SECRET"] = "as-xyz"
652
- try:
653
- res = _run(["profile", "list"])
654
- assert "Using test-username workspace based on environment variables" in res.stdout
655
- finally:
656
- if orig_env_token_id:
657
- os.environ["MODAL_TOKEN_ID"] = orig_env_token_id
658
- else:
659
- del os.environ["MODAL_TOKEN_ID"]
660
- if orig_env_token_secret:
661
- os.environ["MODAL_TOKEN_SECRET"] = orig_env_token_secret
662
- else:
663
- del os.environ["MODAL_TOKEN_SECRET"]
664
-
665
-
666
- def test_list_apps(servicer, mock_dir, set_env_client):
667
- res = _run(["app", "list"])
668
- assert "my_app_foo" not in res.stdout
669
-
670
- with mock_dir({"myapp.py": dummy_app_file, "other_module.py": dummy_other_module_file}):
671
- _run(["deploy", "myapp.py", "--name", "my_app_foo"])
672
-
673
- res = _run(["app", "list"])
674
- assert "my_app_foo" in res.stdout