modal 0.72.14__py3-none-any.whl → 0.72.16__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/app.py +5 -2
- modal/app.pyi +5 -2
- modal/cli/import_refs.py +123 -56
- modal/cli/launch.py +6 -4
- modal/cli/run.py +78 -57
- modal/client.pyi +2 -2
- modal/cls.py +16 -0
- modal/cls.pyi +10 -0
- modal/partial_function.py +3 -0
- modal/partial_function.pyi +2 -0
- {modal-0.72.14.dist-info → modal-0.72.16.dist-info}/METADATA +1 -1
- {modal-0.72.14.dist-info → modal-0.72.16.dist-info}/RECORD +17 -17
- modal_version/_version_generated.py +1 -1
- {modal-0.72.14.dist-info → modal-0.72.16.dist-info}/LICENSE +0 -0
- {modal-0.72.14.dist-info → modal-0.72.16.dist-info}/WHEEL +0 -0
- {modal-0.72.14.dist-info → modal-0.72.16.dist-info}/entry_points.txt +0 -0
- {modal-0.72.14.dist-info → modal-0.72.16.dist-info}/top_level.txt +0 -0
modal/app.py
CHANGED
@@ -365,6 +365,7 @@ class _App:
|
|
365
365
|
show_progress: Optional[bool] = None,
|
366
366
|
detach: bool = False,
|
367
367
|
interactive: bool = False,
|
368
|
+
environment_name: Optional[str] = None,
|
368
369
|
) -> AsyncGenerator["_App", None]:
|
369
370
|
"""Context manager that runs an app on Modal.
|
370
371
|
|
@@ -420,7 +421,9 @@ class _App:
|
|
420
421
|
elif show_progress is False:
|
421
422
|
deprecation_warning((2024, 11, 20), "`show_progress=False` is deprecated (and has no effect)")
|
422
423
|
|
423
|
-
async with _run_app(
|
424
|
+
async with _run_app(
|
425
|
+
self, client=client, detach=detach, interactive=interactive, environment_name=environment_name
|
426
|
+
):
|
424
427
|
yield self
|
425
428
|
|
426
429
|
def _get_default_image(self):
|
@@ -504,7 +507,7 @@ class _App:
|
|
504
507
|
return self._functions
|
505
508
|
|
506
509
|
@property
|
507
|
-
def registered_classes(self) -> dict[str,
|
510
|
+
def registered_classes(self) -> dict[str, _Cls]:
|
508
511
|
"""All modal.Cls objects registered on the app."""
|
509
512
|
return self._classes
|
510
513
|
|
modal/app.pyi
CHANGED
@@ -132,6 +132,7 @@ class _App:
|
|
132
132
|
show_progress: typing.Optional[bool] = None,
|
133
133
|
detach: bool = False,
|
134
134
|
interactive: bool = False,
|
135
|
+
environment_name: typing.Optional[str] = None,
|
135
136
|
) -> typing.AsyncContextManager[_App]: ...
|
136
137
|
def _get_default_image(self): ...
|
137
138
|
def _get_watch_mounts(self): ...
|
@@ -141,7 +142,7 @@ class _App:
|
|
141
142
|
@property
|
142
143
|
def registered_functions(self) -> dict[str, modal.functions._Function]: ...
|
143
144
|
@property
|
144
|
-
def registered_classes(self) -> dict[str, modal.
|
145
|
+
def registered_classes(self) -> dict[str, modal.cls._Cls]: ...
|
145
146
|
@property
|
146
147
|
def registered_entrypoints(self) -> dict[str, _LocalEntrypoint]: ...
|
147
148
|
@property
|
@@ -352,6 +353,7 @@ class App:
|
|
352
353
|
show_progress: typing.Optional[bool] = None,
|
353
354
|
detach: bool = False,
|
354
355
|
interactive: bool = False,
|
356
|
+
environment_name: typing.Optional[str] = None,
|
355
357
|
) -> synchronicity.combined_types.AsyncAndBlockingContextManager[App]: ...
|
356
358
|
def aio(
|
357
359
|
self,
|
@@ -359,6 +361,7 @@ class App:
|
|
359
361
|
show_progress: typing.Optional[bool] = None,
|
360
362
|
detach: bool = False,
|
361
363
|
interactive: bool = False,
|
364
|
+
environment_name: typing.Optional[str] = None,
|
362
365
|
) -> typing.AsyncContextManager[App]: ...
|
363
366
|
|
364
367
|
run: __run_spec
|
@@ -371,7 +374,7 @@ class App:
|
|
371
374
|
@property
|
372
375
|
def registered_functions(self) -> dict[str, modal.functions.Function]: ...
|
373
376
|
@property
|
374
|
-
def registered_classes(self) -> dict[str, modal.
|
377
|
+
def registered_classes(self) -> dict[str, modal.cls.Cls]: ...
|
375
378
|
@property
|
376
379
|
def registered_entrypoints(self) -> dict[str, LocalEntrypoint]: ...
|
377
380
|
@property
|
modal/cli/import_refs.py
CHANGED
@@ -9,8 +9,11 @@ These functions are only called by the Modal CLI, not in tasks.
|
|
9
9
|
|
10
10
|
import dataclasses
|
11
11
|
import importlib
|
12
|
+
import importlib.util
|
12
13
|
import inspect
|
13
14
|
import sys
|
15
|
+
import types
|
16
|
+
from dataclasses import dataclass
|
14
17
|
from pathlib import Path
|
15
18
|
from typing import Any, Optional, Union
|
16
19
|
|
@@ -19,6 +22,7 @@ from rich.console import Console
|
|
19
22
|
from rich.markdown import Markdown
|
20
23
|
|
21
24
|
from modal.app import App, LocalEntrypoint
|
25
|
+
from modal.cls import Cls
|
22
26
|
from modal.exception import InvalidError, _CliUserExecutionError
|
23
27
|
from modal.functions import Function
|
24
28
|
|
@@ -26,6 +30,12 @@ from modal.functions import Function
|
|
26
30
|
@dataclasses.dataclass
|
27
31
|
class ImportRef:
|
28
32
|
file_or_module: str
|
33
|
+
|
34
|
+
# object_path is a .-delimited path to the object to execute, or a parent from which to infer the object
|
35
|
+
# e.g.
|
36
|
+
# function or local_entrypoint in module scope
|
37
|
+
# app in module scope [+ method name]
|
38
|
+
# app [+ function/entrypoint on that app]
|
29
39
|
object_path: Optional[str]
|
30
40
|
|
31
41
|
|
@@ -62,11 +72,14 @@ def import_file_or_module(file_or_module: str):
|
|
62
72
|
sys.path.insert(0, str(full_path.parent))
|
63
73
|
|
64
74
|
module_name = inspect.getmodulename(file_or_module)
|
75
|
+
assert module_name is not None
|
65
76
|
# Import the module - see https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
|
66
77
|
spec = importlib.util.spec_from_file_location(module_name, file_or_module)
|
78
|
+
assert spec is not None
|
67
79
|
module = importlib.util.module_from_spec(spec)
|
68
80
|
sys.modules[module_name] = module
|
69
81
|
try:
|
82
|
+
assert spec.loader
|
70
83
|
spec.loader.exec_module(module)
|
71
84
|
except Exception as exc:
|
72
85
|
raise _CliUserExecutionError(str(full_path)) from exc
|
@@ -79,23 +92,39 @@ def import_file_or_module(file_or_module: str):
|
|
79
92
|
return module
|
80
93
|
|
81
94
|
|
82
|
-
|
95
|
+
@dataclass
|
96
|
+
class MethodReference:
|
97
|
+
"""This helps with deferring method reference until after the class gets instantiated by the CLI"""
|
98
|
+
|
99
|
+
cls: Cls
|
100
|
+
method_name: str
|
101
|
+
|
102
|
+
|
103
|
+
def get_by_object_path(obj: Any, obj_path: str) -> Union[Function, LocalEntrypoint, MethodReference, App, None]:
|
83
104
|
# Try to evaluate a `.`-delimited object path in a Modal context
|
84
105
|
# With the caveat that some object names can actually have `.` in their name (lifecycled methods' tags)
|
85
106
|
|
86
107
|
# Note: this is eager, so no backtracking is performed in case an
|
87
108
|
# earlier match fails at some later point in the path expansion
|
88
109
|
prefix = ""
|
89
|
-
|
110
|
+
obj_path_segments = obj_path.split(".")
|
111
|
+
for i, segment in enumerate(obj_path_segments):
|
90
112
|
attr = prefix + segment
|
113
|
+
if isinstance(obj, App):
|
114
|
+
if attr in obj.registered_entrypoints:
|
115
|
+
# local entrypoints can't be accessed via getattr
|
116
|
+
obj = obj.registered_entrypoints[attr]
|
117
|
+
continue
|
118
|
+
if isinstance(obj, Cls):
|
119
|
+
remaining_segments = obj_path_segments[i:]
|
120
|
+
remaining_path = ".".join(remaining_segments)
|
121
|
+
if len(remaining_segments) > 1:
|
122
|
+
raise ValueError(f"{obj._get_name()} is a class, but {remaining_path} is not a method reference")
|
123
|
+
# TODO: add method inference here?
|
124
|
+
return MethodReference(obj, remaining_path)
|
125
|
+
|
91
126
|
try:
|
92
|
-
if isinstance(obj, App):
|
93
|
-
if attr in obj.registered_entrypoints:
|
94
|
-
# local entrypoints are not on stub blueprint
|
95
|
-
obj = obj.registered_entrypoints[attr]
|
96
|
-
continue
|
97
127
|
obj = getattr(obj, attr)
|
98
|
-
|
99
128
|
except Exception:
|
100
129
|
prefix = f"{prefix}{segment}."
|
101
130
|
else:
|
@@ -109,54 +138,62 @@ def get_by_object_path(obj: Any, obj_path: str) -> Optional[Any]:
|
|
109
138
|
|
110
139
|
def _infer_function_or_help(
|
111
140
|
app: App, module, accept_local_entrypoint: bool, accept_webhook: bool
|
112
|
-
) -> Union[Function, LocalEntrypoint]:
|
113
|
-
|
114
|
-
if not accept_webhook:
|
115
|
-
function_choices -= set(app.registered_web_endpoints)
|
116
|
-
if accept_local_entrypoint:
|
117
|
-
function_choices |= set(app.registered_entrypoints.keys())
|
141
|
+
) -> Union[Function, LocalEntrypoint, MethodReference]:
|
142
|
+
"""Using only an app - automatically infer a single "runnable" for a `modal run` invocation
|
118
143
|
|
119
|
-
|
120
|
-
|
144
|
+
If a single runnable can't be determined, show CLI help indicating valid choices.
|
145
|
+
"""
|
121
146
|
filtered_local_entrypoints = [
|
122
|
-
|
123
|
-
for
|
147
|
+
entrypoint
|
148
|
+
for entrypoint in app.registered_entrypoints.values()
|
124
149
|
if entrypoint.info.module_name == module.__name__
|
125
150
|
]
|
126
151
|
|
127
|
-
if accept_local_entrypoint
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
152
|
+
if accept_local_entrypoint:
|
153
|
+
if len(filtered_local_entrypoints) == 1:
|
154
|
+
# If there is just a single local entrypoint in the target module, use
|
155
|
+
# that regardless of other functions.
|
156
|
+
return filtered_local_entrypoints[0]
|
157
|
+
elif len(app.registered_entrypoints) == 1:
|
158
|
+
# Otherwise, if there is just a single local entrypoint in the app as a whole,
|
159
|
+
# use that one.
|
160
|
+
return list(app.registered_entrypoints.values())[0]
|
161
|
+
|
162
|
+
# TODO: refactor registered_functions to only contain function services, not class services
|
163
|
+
function_choices: dict[str, Union[Function, LocalEntrypoint, MethodReference]] = {
|
164
|
+
name: f for name, f in app.registered_functions.items() if not name.endswith(".*")
|
165
|
+
}
|
166
|
+
for cls_name, cls in app.registered_classes.items():
|
167
|
+
for method_name in cls._get_method_names():
|
168
|
+
function_choices[f"{cls_name}.{method_name}"] = MethodReference(cls, method_name)
|
169
|
+
|
170
|
+
if not accept_webhook:
|
171
|
+
for web_endpoint_name in app.registered_web_endpoints:
|
172
|
+
function_choices.pop(web_endpoint_name, None)
|
173
|
+
|
174
|
+
if accept_local_entrypoint:
|
175
|
+
function_choices.update(app.registered_entrypoints)
|
176
|
+
|
177
|
+
if len(function_choices) == 1:
|
178
|
+
return list(function_choices.values())[0]
|
179
|
+
|
180
|
+
if len(function_choices) == 0:
|
138
181
|
if app.registered_web_endpoints:
|
139
182
|
err_msg = "Modal app has only web endpoints. Use `modal serve` instead of `modal run`."
|
140
183
|
else:
|
141
184
|
err_msg = "Modal app has no registered functions. Nothing to run."
|
142
185
|
raise click.UsageError(err_msg)
|
143
|
-
|
144
|
-
|
186
|
+
|
187
|
+
# there are multiple choices - we can't determine which one to use:
|
188
|
+
registered_functions_str = "\n".join(sorted(function_choices))
|
189
|
+
help_text = f"""You need to specify a Modal function or local entrypoint to run, e.g.
|
145
190
|
|
146
191
|
modal run app.py::my_function [...args]
|
147
192
|
|
148
193
|
Registered functions and local entrypoints on the selected app are:
|
149
194
|
{registered_functions_str}
|
150
195
|
"""
|
151
|
-
|
152
|
-
|
153
|
-
if function_name in app.registered_entrypoints:
|
154
|
-
# entrypoint is in entrypoint registry, for now
|
155
|
-
return app.registered_entrypoints[function_name]
|
156
|
-
|
157
|
-
function = app.registered_functions[function_name]
|
158
|
-
assert isinstance(function, Function)
|
159
|
-
return function
|
196
|
+
raise click.UsageError(help_text)
|
160
197
|
|
161
198
|
|
162
199
|
def _show_no_auto_detectable_app(app_ref: ImportRef) -> None:
|
@@ -223,30 +260,60 @@ You would run foo as [bold green]{base_cmd} app.py::foo[/bold green]"""
|
|
223
260
|
error_console.print(guidance_msg)
|
224
261
|
|
225
262
|
|
226
|
-
def
|
227
|
-
func_ref: str, base_cmd: str, accept_local_entrypoint=True, accept_webhook=False
|
228
|
-
) -> Union[Function, LocalEntrypoint]:
|
263
|
+
def _import_object(func_ref, base_cmd):
|
229
264
|
import_ref = parse_import_ref(func_ref)
|
230
|
-
|
231
265
|
module = import_file_or_module(import_ref.file_or_module)
|
232
|
-
|
266
|
+
app_function_or_method_ref = get_by_object_path(module, import_ref.object_path or DEFAULT_APP_NAME)
|
233
267
|
|
234
|
-
if
|
268
|
+
if app_function_or_method_ref is None:
|
235
269
|
_show_function_ref_help(import_ref, base_cmd)
|
236
|
-
|
270
|
+
raise SystemExit(1)
|
271
|
+
|
272
|
+
return app_function_or_method_ref, module
|
273
|
+
|
237
274
|
|
238
|
-
|
275
|
+
def _infer_runnable(
|
276
|
+
partial_obj: Union[App, Function, MethodReference, LocalEntrypoint],
|
277
|
+
module: types.ModuleType,
|
278
|
+
accept_local_entrypoint: bool = True,
|
279
|
+
accept_webhook: bool = False,
|
280
|
+
) -> tuple[App, Union[Function, MethodReference, LocalEntrypoint]]:
|
281
|
+
if isinstance(partial_obj, App):
|
239
282
|
# infer function or display help for how to select one
|
240
|
-
app =
|
283
|
+
app = partial_obj
|
241
284
|
function_handle = _infer_function_or_help(app, module, accept_local_entrypoint, accept_webhook)
|
242
|
-
return function_handle
|
243
|
-
elif isinstance(
|
244
|
-
return
|
245
|
-
elif isinstance(
|
285
|
+
return app, function_handle
|
286
|
+
elif isinstance(partial_obj, Function):
|
287
|
+
return partial_obj.app, partial_obj
|
288
|
+
elif isinstance(partial_obj, MethodReference):
|
289
|
+
return partial_obj.cls._get_app(), partial_obj
|
290
|
+
elif isinstance(partial_obj, LocalEntrypoint):
|
246
291
|
if not accept_local_entrypoint:
|
247
292
|
raise click.UsageError(
|
248
|
-
f"{
|
293
|
+
f"{partial_obj.info.function_name} is not a Modal Function "
|
294
|
+
f"(a Modal local_entrypoint can't be used in this context)"
|
249
295
|
)
|
250
|
-
return
|
296
|
+
return partial_obj.app, partial_obj
|
251
297
|
else:
|
252
|
-
raise click.UsageError(
|
298
|
+
raise click.UsageError(
|
299
|
+
f"{partial_obj} is not a Modal entity (should be an App, Local entrypoint, " "Function or Class/Method)"
|
300
|
+
)
|
301
|
+
|
302
|
+
|
303
|
+
def import_and_infer(
|
304
|
+
func_ref: str, base_cmd: str, accept_local_entrypoint=True, accept_webhook=False
|
305
|
+
) -> tuple[App, Union[Function, LocalEntrypoint, MethodReference]]:
|
306
|
+
"""Takes a function ref string and returns something "runnable"
|
307
|
+
|
308
|
+
The function ref can leave out partial information (apart from the file name) as
|
309
|
+
long as the runnable is uniquely identifiable by the provided information.
|
310
|
+
|
311
|
+
When there are multiple runnables within the provided ref, the following rules should
|
312
|
+
be followed:
|
313
|
+
|
314
|
+
1. if there is a single local_entrypoint, that one is used
|
315
|
+
2. if there is a single {function, class} that one is used
|
316
|
+
3. if there is a single method (within a class) that one is used
|
317
|
+
"""
|
318
|
+
app_function_or_method_ref, module = _import_object(func_ref, base_cmd)
|
319
|
+
return _infer_runnable(app_function_or_method_ref, module, accept_local_entrypoint, accept_webhook)
|
modal/cli/launch.py
CHANGED
@@ -8,11 +8,11 @@ from typing import Any, Optional
|
|
8
8
|
|
9
9
|
from typer import Typer
|
10
10
|
|
11
|
-
from ..app import
|
11
|
+
from ..app import LocalEntrypoint
|
12
12
|
from ..exception import _CliUserExecutionError
|
13
13
|
from ..output import enable_output
|
14
14
|
from ..runner import run_app
|
15
|
-
from .import_refs import
|
15
|
+
from .import_refs import import_and_infer
|
16
16
|
|
17
17
|
launch_cli = Typer(
|
18
18
|
name="launch",
|
@@ -29,8 +29,10 @@ def _launch_program(name: str, filename: str, detach: bool, args: dict[str, Any]
|
|
29
29
|
os.environ["MODAL_LAUNCH_ARGS"] = json.dumps(args)
|
30
30
|
|
31
31
|
program_path = str(Path(__file__).parent / "programs" / filename)
|
32
|
-
entrypoint =
|
33
|
-
|
32
|
+
app, entrypoint = import_and_infer(program_path, "modal launch")
|
33
|
+
if not isinstance(entrypoint, LocalEntrypoint):
|
34
|
+
raise ValueError(f"{program_path} has no single local_entrypoint")
|
35
|
+
|
34
36
|
app.set_description(f"modal launch {name}")
|
35
37
|
|
36
38
|
# `launch/` scripts must have a `local_entrypoint()` with no args, for simplicity here.
|
modal/cli/run.py
CHANGED
@@ -7,7 +7,6 @@ import re
|
|
7
7
|
import shlex
|
8
8
|
import sys
|
9
9
|
import time
|
10
|
-
import typing
|
11
10
|
from functools import partial
|
12
11
|
from typing import Any, Callable, Optional, get_type_hints
|
13
12
|
|
@@ -15,7 +14,6 @@ import click
|
|
15
14
|
import typer
|
16
15
|
from typing_extensions import TypedDict
|
17
16
|
|
18
|
-
from .. import Cls
|
19
17
|
from ..app import App, LocalEntrypoint
|
20
18
|
from ..config import config
|
21
19
|
from ..environments import ensure_env
|
@@ -26,7 +24,7 @@ from ..output import enable_output
|
|
26
24
|
from ..runner import deploy_app, interactive_shell, run_app
|
27
25
|
from ..serving import serve_app
|
28
26
|
from ..volume import Volume
|
29
|
-
from .import_refs import
|
27
|
+
from .import_refs import MethodReference, import_and_infer, import_app
|
30
28
|
from .utils import ENV_OPTION, ENV_OPTION_HELP, is_tty, stream_app_logs
|
31
29
|
|
32
30
|
|
@@ -145,39 +143,7 @@ def _write_local_result(result_path: str, res: Any):
|
|
145
143
|
fid.write(res)
|
146
144
|
|
147
145
|
|
148
|
-
def
|
149
|
-
function = app.registered_functions.get(function_tag)
|
150
|
-
if not function or (isinstance(function, Function) and function.info.user_cls is not None):
|
151
|
-
# This is either a function_tag for a class method function (e.g MyClass.foo) or a function tag for a
|
152
|
-
# class service function (MyClass.*)
|
153
|
-
class_name, method_name = function_tag.rsplit(".", 1)
|
154
|
-
if not function:
|
155
|
-
function = app.registered_functions.get(f"{class_name}.*")
|
156
|
-
assert isinstance(function, Function)
|
157
|
-
function = typing.cast(Function, function)
|
158
|
-
if function.is_generator:
|
159
|
-
raise InvalidError("`modal run` is not supported for generator functions")
|
160
|
-
|
161
|
-
signature: dict[str, ParameterMetadata]
|
162
|
-
cls: Optional[Cls] = None
|
163
|
-
if function.info.user_cls is not None:
|
164
|
-
cls = typing.cast(Cls, app.registered_classes[class_name])
|
165
|
-
cls_signature = _get_signature(function.info.user_cls)
|
166
|
-
if method_name == "*":
|
167
|
-
method_names = list(cls._get_partial_functions().keys())
|
168
|
-
if len(method_names) == 1:
|
169
|
-
method_name = method_names[0]
|
170
|
-
else:
|
171
|
-
class_name = function.info.user_cls.__name__
|
172
|
-
raise click.UsageError(
|
173
|
-
f"Please specify a specific method of {class_name} to run, e.g. `modal run foo.py::MyClass.bar`" # noqa: E501
|
174
|
-
)
|
175
|
-
fun_signature = _get_signature(getattr(cls, method_name).info.raw_f, is_method=True)
|
176
|
-
signature = dict(**cls_signature, **fun_signature) # Pool all arguments
|
177
|
-
# TODO(erikbern): assert there's no overlap?
|
178
|
-
else:
|
179
|
-
signature = _get_signature(function.info.raw_f)
|
180
|
-
|
146
|
+
def _make_click_function(app, inner: Callable[[dict[str, Any]], Any]):
|
181
147
|
@click.pass_context
|
182
148
|
def f(ctx, **kwargs):
|
183
149
|
show_progress: bool = ctx.obj["show_progress"]
|
@@ -188,21 +154,65 @@ def _get_click_command_for_function(app: App, function_tag):
|
|
188
154
|
environment_name=ctx.obj["env"],
|
189
155
|
interactive=ctx.obj["interactive"],
|
190
156
|
):
|
191
|
-
|
192
|
-
res = function.remote(**kwargs)
|
193
|
-
else:
|
194
|
-
# unpool class and method arguments
|
195
|
-
# TODO(erikbern): this code is a bit hacky
|
196
|
-
cls_kwargs = {k: kwargs[k] for k in cls_signature}
|
197
|
-
fun_kwargs = {k: kwargs[k] for k in fun_signature}
|
198
|
-
|
199
|
-
instance = cls(**cls_kwargs)
|
200
|
-
method: Function = getattr(instance, method_name)
|
201
|
-
res = method.remote(**fun_kwargs)
|
157
|
+
res = inner(kwargs)
|
202
158
|
|
203
159
|
if result_path := ctx.obj["result_path"]:
|
204
160
|
_write_local_result(result_path, res)
|
205
161
|
|
162
|
+
return f
|
163
|
+
|
164
|
+
|
165
|
+
def _get_click_command_for_function(app: App, function: Function):
|
166
|
+
if function.is_generator:
|
167
|
+
raise InvalidError("`modal run` is not supported for generator functions")
|
168
|
+
|
169
|
+
signature: dict[str, ParameterMetadata] = _get_signature(function.info.raw_f)
|
170
|
+
|
171
|
+
def _inner(click_kwargs):
|
172
|
+
return function.remote(**click_kwargs)
|
173
|
+
|
174
|
+
f = _make_click_function(app, _inner)
|
175
|
+
|
176
|
+
with_click_options = _add_click_options(f, signature)
|
177
|
+
return click.command(with_click_options)
|
178
|
+
|
179
|
+
|
180
|
+
def _get_click_command_for_cls(app: App, method_ref: MethodReference):
|
181
|
+
signature: dict[str, ParameterMetadata]
|
182
|
+
cls = method_ref.cls
|
183
|
+
method_name = method_ref.method_name
|
184
|
+
|
185
|
+
cls_signature = _get_signature(cls._get_user_cls())
|
186
|
+
partial_functions = cls._get_partial_functions()
|
187
|
+
|
188
|
+
if method_name in ("*", ""):
|
189
|
+
# auto infer method name - not sure if we have to support this...
|
190
|
+
method_names = list(partial_functions.keys())
|
191
|
+
if len(method_names) == 1:
|
192
|
+
method_name = method_names[0]
|
193
|
+
else:
|
194
|
+
raise click.UsageError(
|
195
|
+
f"Please specify a specific method of {cls._get_name()} to run, "
|
196
|
+
f"e.g. `modal run foo.py::MyClass.bar`" # noqa: E501
|
197
|
+
)
|
198
|
+
|
199
|
+
partial_function = partial_functions[method_name]
|
200
|
+
fun_signature = _get_signature(partial_function._get_raw_f(), is_method=True)
|
201
|
+
|
202
|
+
# TODO(erikbern): assert there's no overlap?
|
203
|
+
signature = dict(**cls_signature, **fun_signature) # Pool all arguments
|
204
|
+
|
205
|
+
def _inner(click_kwargs):
|
206
|
+
# unpool class and method arguments
|
207
|
+
# TODO(erikbern): this code is a bit hacky
|
208
|
+
cls_kwargs = {k: click_kwargs[k] for k in cls_signature}
|
209
|
+
fun_kwargs = {k: click_kwargs[k] for k in fun_signature}
|
210
|
+
|
211
|
+
instance = cls(**cls_kwargs)
|
212
|
+
method: Function = getattr(instance, method_name)
|
213
|
+
return method.remote(**fun_kwargs)
|
214
|
+
|
215
|
+
f = _make_click_function(app, _inner)
|
206
216
|
with_click_options = _add_click_options(f, signature)
|
207
217
|
return click.command(with_click_options)
|
208
218
|
|
@@ -249,16 +259,20 @@ class RunGroup(click.Group):
|
|
249
259
|
# needs to be handled here, and not in the `run` logic below
|
250
260
|
ctx.ensure_object(dict)
|
251
261
|
ctx.obj["env"] = ensure_env(ctx.params["env"])
|
252
|
-
|
253
|
-
app
|
262
|
+
|
263
|
+
app, imported_object = import_and_infer(func_ref, accept_local_entrypoint=True, base_cmd="modal run")
|
264
|
+
|
254
265
|
if app.description is None:
|
255
266
|
app.set_description(_get_clean_app_description(func_ref))
|
256
|
-
if isinstance(function_or_entrypoint, LocalEntrypoint):
|
257
|
-
click_command = _get_click_command_for_local_entrypoint(app, function_or_entrypoint)
|
258
|
-
else:
|
259
|
-
tag = function_or_entrypoint.info.get_tag()
|
260
|
-
click_command = _get_click_command_for_function(app, tag)
|
261
267
|
|
268
|
+
if isinstance(imported_object, LocalEntrypoint):
|
269
|
+
click_command = _get_click_command_for_local_entrypoint(app, imported_object)
|
270
|
+
elif isinstance(imported_object, Function):
|
271
|
+
click_command = _get_click_command_for_function(app, imported_object)
|
272
|
+
elif isinstance(imported_object, MethodReference):
|
273
|
+
click_command = _get_click_command_for_cls(app, imported_object)
|
274
|
+
else:
|
275
|
+
raise ValueError(f"{imported_object} is neither function, local entrypoint or class/method")
|
262
276
|
return click_command
|
263
277
|
|
264
278
|
|
@@ -464,11 +478,18 @@ def shell(
|
|
464
478
|
exec(container_id=container_or_function, command=shlex.split(cmd), pty=pty)
|
465
479
|
return
|
466
480
|
|
467
|
-
|
481
|
+
original_app, function_or_method_ref = import_and_infer(
|
468
482
|
container_or_function, accept_local_entrypoint=False, accept_webhook=True, base_cmd="modal shell"
|
469
483
|
)
|
470
|
-
|
471
|
-
|
484
|
+
function_spec: _FunctionSpec
|
485
|
+
if isinstance(function_or_method_ref, MethodReference):
|
486
|
+
class_service_function = function_or_method_ref.cls._get_class_service_function()
|
487
|
+
function_spec = class_service_function.spec
|
488
|
+
elif isinstance(function_or_method_ref, Function):
|
489
|
+
function_spec = function_or_method_ref.spec
|
490
|
+
else:
|
491
|
+
raise ValueError("Referenced entity is neither a function nor a class/method.")
|
492
|
+
|
472
493
|
start_shell = partial(
|
473
494
|
interactive_shell,
|
474
495
|
image=function_spec.image,
|
modal/client.pyi
CHANGED
@@ -26,7 +26,7 @@ class _Client:
|
|
26
26
|
_stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
|
27
27
|
|
28
28
|
def __init__(
|
29
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.
|
29
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.16"
|
30
30
|
): ...
|
31
31
|
def is_closed(self) -> bool: ...
|
32
32
|
@property
|
@@ -81,7 +81,7 @@ class Client:
|
|
81
81
|
_stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
|
82
82
|
|
83
83
|
def __init__(
|
84
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.
|
84
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.16"
|
85
85
|
): ...
|
86
86
|
def is_closed(self) -> bool: ...
|
87
87
|
@property
|
modal/cls.py
CHANGED
@@ -397,6 +397,22 @@ class _Cls(_Object, type_prefix="cs"):
|
|
397
397
|
raise AttributeError("You can only get the partial functions of a local Cls instance")
|
398
398
|
return _find_partial_methods_for_user_cls(self._user_cls, _PartialFunctionFlags.all())
|
399
399
|
|
400
|
+
def _get_app(self) -> "modal.app._App":
|
401
|
+
return self._app
|
402
|
+
|
403
|
+
def _get_user_cls(self) -> type:
|
404
|
+
return self._user_cls
|
405
|
+
|
406
|
+
def _get_name(self) -> str:
|
407
|
+
return self._name
|
408
|
+
|
409
|
+
def _get_class_service_function(self) -> "modal.functions._Function":
|
410
|
+
return self._class_service_function
|
411
|
+
|
412
|
+
def _get_method_names(self) -> Collection[str]:
|
413
|
+
# returns method names for a *local* class only for now (used by cli)
|
414
|
+
return self._method_functions.keys()
|
415
|
+
|
400
416
|
def _hydrate_metadata(self, metadata: Message):
|
401
417
|
assert isinstance(metadata, api_pb2.ClassHandleMetadata)
|
402
418
|
if (
|
modal/cls.pyi
CHANGED
@@ -103,6 +103,11 @@ class _Cls(modal.object._Object):
|
|
103
103
|
def _initialize_from_empty(self): ...
|
104
104
|
def _initialize_from_other(self, other: _Cls): ...
|
105
105
|
def _get_partial_functions(self) -> dict[str, modal.partial_function._PartialFunction]: ...
|
106
|
+
def _get_app(self) -> modal.app._App: ...
|
107
|
+
def _get_user_cls(self) -> type: ...
|
108
|
+
def _get_name(self) -> str: ...
|
109
|
+
def _get_class_service_function(self) -> modal.functions._Function: ...
|
110
|
+
def _get_method_names(self) -> collections.abc.Collection[str]: ...
|
106
111
|
def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
|
107
112
|
@staticmethod
|
108
113
|
def validate_construction_mechanism(user_cls): ...
|
@@ -156,6 +161,11 @@ class Cls(modal.object.Object):
|
|
156
161
|
def _initialize_from_empty(self): ...
|
157
162
|
def _initialize_from_other(self, other: Cls): ...
|
158
163
|
def _get_partial_functions(self) -> dict[str, modal.partial_function.PartialFunction]: ...
|
164
|
+
def _get_app(self) -> modal.app.App: ...
|
165
|
+
def _get_user_cls(self) -> type: ...
|
166
|
+
def _get_name(self) -> str: ...
|
167
|
+
def _get_class_service_function(self) -> modal.functions.Function: ...
|
168
|
+
def _get_method_names(self) -> collections.abc.Collection[str]: ...
|
159
169
|
def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
|
160
170
|
@staticmethod
|
161
171
|
def validate_construction_mechanism(user_cls): ...
|
modal/partial_function.py
CHANGED
@@ -89,6 +89,9 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
89
89
|
self.force_build = force_build
|
90
90
|
self.build_timeout = build_timeout
|
91
91
|
|
92
|
+
def _get_raw_f(self) -> Callable[P, ReturnType]:
|
93
|
+
return self.raw_f
|
94
|
+
|
92
95
|
def __get__(self, obj, objtype=None) -> _Function[P, ReturnType, OriginalReturnType]:
|
93
96
|
k = self.raw_f.__name__
|
94
97
|
if obj: # accessing the method on an instance of a class, e.g. `MyClass().fun``
|
modal/partial_function.pyi
CHANGED
@@ -49,6 +49,7 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
49
49
|
force_build: bool = False,
|
50
50
|
build_timeout: typing.Optional[int] = None,
|
51
51
|
): ...
|
52
|
+
def _get_raw_f(self) -> typing.Callable[P, ReturnType]: ...
|
52
53
|
def __get__(self, obj, objtype=None) -> modal.functions._Function[P, ReturnType, OriginalReturnType]: ...
|
53
54
|
def __del__(self): ...
|
54
55
|
def add_flags(self, flags) -> _PartialFunction: ...
|
@@ -78,6 +79,7 @@ class PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
78
79
|
force_build: bool = False,
|
79
80
|
build_timeout: typing.Optional[int] = None,
|
80
81
|
): ...
|
82
|
+
def _get_raw_f(self) -> typing.Callable[P, ReturnType]: ...
|
81
83
|
def __get__(self, obj, objtype=None) -> modal.functions.Function[P, ReturnType, OriginalReturnType]: ...
|
82
84
|
def __del__(self): ...
|
83
85
|
def add_flags(self, flags) -> PartialFunction: ...
|
@@ -15,15 +15,15 @@ modal/_traceback.py,sha256=IZQzB3fVlUfMHOSyKUgw0H6qv4yHnpyq-XVCNZKfUdA,5023
|
|
15
15
|
modal/_tunnel.py,sha256=o-jJhS4vQ6-XswDhHcJWGMZZmD03SC0e9i8fEu1JTjo,6310
|
16
16
|
modal/_tunnel.pyi,sha256=JmmDYAy9F1FpgJ_hWx0xkom2nTOFQjn4mTPYlU3PFo4,1245
|
17
17
|
modal/_watcher.py,sha256=K6LYnlmSGQB4tWWI9JADv-tvSvQ1j522FwT71B51CX8,3584
|
18
|
-
modal/app.py,sha256=
|
19
|
-
modal/app.pyi,sha256=
|
18
|
+
modal/app.py,sha256=t-INVjEAhSXppcu8m_rhaa1r5tzZmEPCS3lhAZg7xkc,45611
|
19
|
+
modal/app.pyi,sha256=279Tf7Jmwq_SvAm_BKRnollAqefYI22g7wQqP94Er1Y,25453
|
20
20
|
modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
|
21
21
|
modal/client.py,sha256=JAnd4-GCN093BwkvOFAK5a6iy5ycxofjpUncMxlrIMw,15253
|
22
|
-
modal/client.pyi,sha256=
|
22
|
+
modal/client.pyi,sha256=dsk-vjYklfH7e4hbBBHShdm5T1HTPusZc6sIHmyRKRs,7280
|
23
23
|
modal/cloud_bucket_mount.py,sha256=G7T7jWLD0QkmrfKR75mSTwdUZ2xNfj7pkVqb4ipmxmI,5735
|
24
24
|
modal/cloud_bucket_mount.pyi,sha256=CEi7vrH3kDUF4LAy4qP6tfImy2UJuFRcRbsgRNM1wo8,1403
|
25
|
-
modal/cls.py,sha256=
|
26
|
-
modal/cls.pyi,sha256=
|
25
|
+
modal/cls.py,sha256=jbGYPMM1AhS3Uj8Wh2lk0YARDCul_imUDiJmbyrfSSc,32158
|
26
|
+
modal/cls.pyi,sha256=P-BAQZsx8fM7Vchd8xy3Mp0RZkwkCONSZzVyn2lNNtc,8834
|
27
27
|
modal/config.py,sha256=BzhZYUUwOmvVwf6x5kf0ywMC257s648dmuhsnB6g3gk,11041
|
28
28
|
modal/container_process.py,sha256=WTqLn01dJPVkPpwR_0w_JH96ceN5mV4TGtiu1ZR2RRA,6108
|
29
29
|
modal/container_process.pyi,sha256=dqtqBmyRpXXpRrDooESL6WBVU_1Rh6OG-66P2Hk9E5U,2666
|
@@ -52,8 +52,8 @@ modal/object.pyi,sha256=MO78H9yFSE5i1gExPEwyyQzLdlshkcGHN1aQ0ylyvq0,8802
|
|
52
52
|
modal/output.py,sha256=N0xf4qeudEaYrslzdAl35VKV8rapstgIM2e9wO8_iy0,1967
|
53
53
|
modal/parallel_map.py,sha256=4aoMXIrlG3wl5Ifk2YDNOQkXsGRsm6Xbfm6WtJ2t3WY,16002
|
54
54
|
modal/parallel_map.pyi,sha256=pOhT0P3DDYlwLx0fR3PTsecA7DI8uOdXC1N8i-ZkyOY,2328
|
55
|
-
modal/partial_function.py,sha256=
|
56
|
-
modal/partial_function.pyi,sha256=
|
55
|
+
modal/partial_function.py,sha256=x7GuUrzwru-2WmLACIMFT6JnCfhyT6HAHiAgKWZMOTE,27878
|
56
|
+
modal/partial_function.pyi,sha256=osYgJWVKmtz_noJ8OzBjcp7oH46PuIpURD_M7B2tXPs,9388
|
57
57
|
modal/proxy.py,sha256=ZrOsuQP7dSZFq1OrIxalNnt0Zvsnp1h86Th679sSL40,1417
|
58
58
|
modal/proxy.pyi,sha256=UvygdOYneLTuoDY6hVaMNCyZ947Tmx93IdLjErUqkvM,368
|
59
59
|
modal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -114,12 +114,12 @@ modal/cli/container.py,sha256=nCySVD10VJPzmX3ghTsGmpxdYeVYYMW6ofjsyt2gQcM,3667
|
|
114
114
|
modal/cli/dict.py,sha256=HaEcjfll7i3Uj3Fg56aj4407if5UljsYfr6fIq-D2W8,4589
|
115
115
|
modal/cli/entry_point.py,sha256=aaNxFAqZcmtSjwzkYIA_Ba9CkL4cL4_i2gy5VjoXxkM,4228
|
116
116
|
modal/cli/environment.py,sha256=Ayddkiq9jdj3XYDJ8ZmUqFpPPH8xajYlbexRkzGtUcg,4334
|
117
|
-
modal/cli/import_refs.py,sha256=
|
118
|
-
modal/cli/launch.py,sha256=
|
117
|
+
modal/cli/import_refs.py,sha256=W-jU9AsUkaseUDduAmENQCkEE5mmVKWlJFAgEvulwuI,12023
|
118
|
+
modal/cli/launch.py,sha256=HgaVXJq0KbVCNUSaU3lq1zP9eCksq6jbSBYBNcMUaoA,3097
|
119
119
|
modal/cli/network_file_system.py,sha256=o6VLTgN4xn5XUiNPBfxYec-5uWCgYrDmfFFLM1ZW_eE,8180
|
120
120
|
modal/cli/profile.py,sha256=rLXfjJObfPNjaZvNfHGIKqs7y9bGYyGe-K7V0w-Ni0M,3110
|
121
121
|
modal/cli/queues.py,sha256=MIh2OsliNE2QeL1erubfsRsNuG4fxqcqWA2vgIfQ4Mg,4494
|
122
|
-
modal/cli/run.py,sha256=
|
122
|
+
modal/cli/run.py,sha256=7WSJ7PSF6IrVWhTBmdC8ywEvdJcx-NS6YXYKTrgMV7w,18231
|
123
123
|
modal/cli/secret.py,sha256=uQpwYrMY98iMCWeZOQTcktOYhPTZ8IHnyealDc2CZqo,4206
|
124
124
|
modal/cli/token.py,sha256=mxSgOWakXG6N71hQb1ko61XAR9ZGkTMZD-Txn7gmTac,1924
|
125
125
|
modal/cli/utils.py,sha256=hZmjyzcPjDnQSkLvycZD2LhGdcsfdZshs_rOU78EpvI,3717
|
@@ -165,10 +165,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
|
|
165
165
|
modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
166
166
|
modal_version/__init__.py,sha256=kGya2ZlItX2zB7oHORs-wvP4PG8lg_mtbi1QIK3G6SQ,470
|
167
167
|
modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
|
168
|
-
modal_version/_version_generated.py,sha256=
|
169
|
-
modal-0.72.
|
170
|
-
modal-0.72.
|
171
|
-
modal-0.72.
|
172
|
-
modal-0.72.
|
173
|
-
modal-0.72.
|
174
|
-
modal-0.72.
|
168
|
+
modal_version/_version_generated.py,sha256=IZpf7XDCb2SQQv7KTBBmxzW6Cn7w1wnGHmKHn-926Nc,149
|
169
|
+
modal-0.72.16.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
|
170
|
+
modal-0.72.16.dist-info/METADATA,sha256=4tN0qlbqVcUP3RldeH6Ix8p_83bdR64v1AKQ6v1MiJs,2329
|
171
|
+
modal-0.72.16.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
172
|
+
modal-0.72.16.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
|
173
|
+
modal-0.72.16.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
|
174
|
+
modal-0.72.16.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|