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