modal 0.66.44__py3-none-any.whl → 0.67.0__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/_resolver.py +7 -2
- modal/_runtime/container_io_manager.py +1 -1
- modal/app.py +39 -24
- modal/app.pyi +4 -2
- modal/cli/config.py +3 -0
- modal/cli/import_refs.py +2 -2
- modal/cli/launch.py +6 -4
- modal/cli/run.py +18 -5
- modal/client.pyi +2 -2
- modal/cls.py +49 -44
- modal/cls.pyi +2 -4
- modal/config.py +1 -1
- modal/functions.py +4 -98
- modal/functions.pyi +0 -10
- modal/image.py +81 -67
- modal/image.pyi +12 -7
- modal/object.py +2 -2
- modal/object.pyi +4 -2
- modal/parallel_map.py +2 -2
- modal/runner.py +13 -7
- modal/volume.py +13 -6
- modal/volume.pyi +10 -4
- {modal-0.66.44.dist-info → modal-0.67.0.dist-info}/METADATA +1 -1
- {modal-0.66.44.dist-info → modal-0.67.0.dist-info}/RECORD +32 -32
- modal_proto/api.proto +3 -3
- modal_proto/api_pb2.pyi +3 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +1 -1
- {modal-0.66.44.dist-info → modal-0.67.0.dist-info}/LICENSE +0 -0
- {modal-0.66.44.dist-info → modal-0.67.0.dist-info}/WHEEL +0 -0
- {modal-0.66.44.dist-info → modal-0.67.0.dist-info}/entry_points.txt +0 -0
- {modal-0.66.44.dist-info → modal-0.67.0.dist-info}/top_level.txt +0 -0
modal/_resolver.py
CHANGED
@@ -130,9 +130,14 @@ class Resolver:
|
|
130
130
|
raise NotFoundError(exc.message)
|
131
131
|
raise
|
132
132
|
|
133
|
-
# Check that the id of functions
|
133
|
+
# Check that the id of functions didn't change
|
134
134
|
# Persisted refs are ignored because their life cycle is managed independently.
|
135
|
-
if
|
135
|
+
if (
|
136
|
+
not obj._is_another_app
|
137
|
+
and existing_object_id is not None
|
138
|
+
and existing_object_id.startswith("fu-")
|
139
|
+
and obj.object_id != existing_object_id
|
140
|
+
):
|
136
141
|
raise Exception(
|
137
142
|
f"Tried creating an object using existing id {existing_object_id}"
|
138
143
|
f" but it has id {obj.object_id}"
|
@@ -452,7 +452,7 @@ class _ContainerIOManager:
|
|
452
452
|
await asyncio.sleep(DYNAMIC_CONCURRENCY_INTERVAL_SECS)
|
453
453
|
|
454
454
|
async def get_app_objects(self) -> RunningApp:
|
455
|
-
req = api_pb2.AppGetObjectsRequest(app_id=self.app_id, include_unindexed=True)
|
455
|
+
req = api_pb2.AppGetObjectsRequest(app_id=self.app_id, include_unindexed=True, only_class_function=True)
|
456
456
|
resp = await retry_transient_errors(self._client.stub.AppGetObjects, req)
|
457
457
|
logger.debug(f"AppGetObjects received {len(resp.items)} objects for app {self.app_id}")
|
458
458
|
|
modal/app.py
CHANGED
@@ -177,7 +177,8 @@ class _App:
|
|
177
177
|
|
178
178
|
_name: Optional[str]
|
179
179
|
_description: Optional[str]
|
180
|
-
|
180
|
+
_functions: Dict[str, _Function]
|
181
|
+
_classes: Dict[str, _Cls]
|
181
182
|
|
182
183
|
_image: Optional[_Image]
|
183
184
|
_mounts: Sequence[_Mount]
|
@@ -223,7 +224,8 @@ class _App:
|
|
223
224
|
if image is not None and not isinstance(image, _Image):
|
224
225
|
raise InvalidError("image has to be a modal Image or AioImage object")
|
225
226
|
|
226
|
-
self.
|
227
|
+
self._functions = {}
|
228
|
+
self._classes = {}
|
227
229
|
self._image = image
|
228
230
|
self._mounts = mounts
|
229
231
|
self._secrets = secrets
|
@@ -312,6 +314,7 @@ class _App:
|
|
312
314
|
raise InvalidError(f"App attribute `{key}` with value {value!r} is not a valid Modal object")
|
313
315
|
|
314
316
|
def _add_object(self, tag, obj):
|
317
|
+
# TODO(erikbern): replace this with _add_function and _add_class
|
315
318
|
if self._running_app:
|
316
319
|
# If this is inside a container, then objects can be defined after app initialization.
|
317
320
|
# So we may have to initialize objects once they get bound to the app.
|
@@ -320,7 +323,12 @@ class _App:
|
|
320
323
|
metadata: Message = self._running_app.object_handle_metadata[object_id]
|
321
324
|
obj._hydrate(object_id, self._client, metadata)
|
322
325
|
|
323
|
-
|
326
|
+
if isinstance(obj, _Function):
|
327
|
+
self._functions[tag] = obj
|
328
|
+
elif isinstance(obj, _Cls):
|
329
|
+
self._classes[tag] = obj
|
330
|
+
else:
|
331
|
+
raise RuntimeError(f"Expected `obj` to be a _Function or _Cls (got {type(obj)}")
|
324
332
|
|
325
333
|
def __getitem__(self, tag: str):
|
326
334
|
deprecation_error((2024, 3, 25), _app_attr_error)
|
@@ -334,7 +342,7 @@ class _App:
|
|
334
342
|
if tag.startswith("__"):
|
335
343
|
# Hacky way to avoid certain issues, e.g. pickle will try to look this up
|
336
344
|
raise AttributeError(f"App has no member {tag}")
|
337
|
-
if tag not in self.
|
345
|
+
if tag not in self._functions or tag not in self._classes:
|
338
346
|
# Primarily to make hasattr work
|
339
347
|
raise AttributeError(f"App has no member {tag}")
|
340
348
|
deprecation_error((2024, 3, 25), _app_attr_error)
|
@@ -360,7 +368,9 @@ class _App:
|
|
360
368
|
|
361
369
|
def _uncreate_all_objects(self):
|
362
370
|
# TODO(erikbern): this doesn't unhydrate objects that aren't tagged
|
363
|
-
for obj in self.
|
371
|
+
for obj in self._functions.values():
|
372
|
+
obj._unhydrate()
|
373
|
+
for obj in self._classes.values():
|
364
374
|
obj._unhydrate()
|
365
375
|
|
366
376
|
@asynccontextmanager
|
@@ -459,18 +469,17 @@ class _App:
|
|
459
469
|
return [m for m in all_mounts if m.is_local()]
|
460
470
|
|
461
471
|
def _add_function(self, function: _Function, is_web_endpoint: bool):
|
462
|
-
if function.tag in self.
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
logger.warning(f"Warning: tag {function.tag} exists but is overridden by function")
|
472
|
+
if function.tag in self._functions:
|
473
|
+
if not is_notebook():
|
474
|
+
old_function: _Function = self._functions[function.tag]
|
475
|
+
logger.warning(
|
476
|
+
f"Warning: Tag '{function.tag}' collision!"
|
477
|
+
" Overriding existing function "
|
478
|
+
f"[{old_function._info.module_name}].{old_function._info.function_name}"
|
479
|
+
f" with new function [{function._info.module_name}].{function._info.function_name}"
|
480
|
+
)
|
481
|
+
if function.tag in self._classes:
|
482
|
+
logger.warning(f"Warning: tag {function.tag} exists but is overridden by function")
|
474
483
|
|
475
484
|
self._add_object(function.tag, function)
|
476
485
|
if is_web_endpoint:
|
@@ -484,21 +493,22 @@ class _App:
|
|
484
493
|
_App._container_app = running_app
|
485
494
|
|
486
495
|
# Hydrate objects on app
|
496
|
+
indexed_objects = dict(**self._functions, **self._classes)
|
487
497
|
for tag, object_id in running_app.tag_to_object_id.items():
|
488
|
-
if tag in
|
489
|
-
obj =
|
498
|
+
if tag in indexed_objects:
|
499
|
+
obj = indexed_objects[tag]
|
490
500
|
handle_metadata = running_app.object_handle_metadata[object_id]
|
491
501
|
obj._hydrate(object_id, client, handle_metadata)
|
492
502
|
|
493
503
|
@property
|
494
504
|
def registered_functions(self) -> Dict[str, _Function]:
|
495
505
|
"""All modal.Function objects registered on the app."""
|
496
|
-
return
|
506
|
+
return self._functions
|
497
507
|
|
498
508
|
@property
|
499
509
|
def registered_classes(self) -> Dict[str, _Function]:
|
500
510
|
"""All modal.Cls objects registered on the app."""
|
501
|
-
return
|
511
|
+
return self._classes
|
502
512
|
|
503
513
|
@property
|
504
514
|
def registered_entrypoints(self) -> Dict[str, _LocalEntrypoint]:
|
@@ -507,7 +517,11 @@ class _App:
|
|
507
517
|
|
508
518
|
@property
|
509
519
|
def indexed_objects(self) -> Dict[str, _Object]:
|
510
|
-
|
520
|
+
deprecation_warning(
|
521
|
+
(2024, 11, 25),
|
522
|
+
"`app.indexed_objects` is deprecated! Use `app.registered_functions` or `app.registered_classes` instead.",
|
523
|
+
)
|
524
|
+
return dict(**self._functions, **self._classes)
|
511
525
|
|
512
526
|
@property
|
513
527
|
def registered_web_endpoints(self) -> List[str]:
|
@@ -1002,8 +1016,9 @@ class _App:
|
|
1002
1016
|
bar.remote()
|
1003
1017
|
```
|
1004
1018
|
"""
|
1005
|
-
|
1006
|
-
|
1019
|
+
indexed_objects = dict(**other_app._functions, **other_app._classes)
|
1020
|
+
for tag, object in indexed_objects.items():
|
1021
|
+
existing_object = indexed_objects.get(tag)
|
1007
1022
|
if existing_object and existing_object != object:
|
1008
1023
|
logger.warning(
|
1009
1024
|
f"Named app object {tag} with existing value {existing_object} is being "
|
modal/app.pyi
CHANGED
@@ -76,7 +76,8 @@ class _App:
|
|
76
76
|
_container_app: typing.ClassVar[typing.Optional[modal.running_app.RunningApp]]
|
77
77
|
_name: typing.Optional[str]
|
78
78
|
_description: typing.Optional[str]
|
79
|
-
|
79
|
+
_functions: typing.Dict[str, modal.functions._Function]
|
80
|
+
_classes: typing.Dict[str, modal.cls._Cls]
|
80
81
|
_image: typing.Optional[modal.image._Image]
|
81
82
|
_mounts: typing.Sequence[modal.mount._Mount]
|
82
83
|
_secrets: typing.Sequence[modal.secret._Secret]
|
@@ -270,7 +271,8 @@ class App:
|
|
270
271
|
_container_app: typing.ClassVar[typing.Optional[modal.running_app.RunningApp]]
|
271
272
|
_name: typing.Optional[str]
|
272
273
|
_description: typing.Optional[str]
|
273
|
-
|
274
|
+
_functions: typing.Dict[str, modal.functions.Function]
|
275
|
+
_classes: typing.Dict[str, modal.cls.Cls]
|
274
276
|
_image: typing.Optional[modal.image.Image]
|
275
277
|
_mounts: typing.Sequence[modal.mount.Mount]
|
276
278
|
_secrets: typing.Sequence[modal.secret.Secret]
|
modal/cli/config.py
CHANGED
@@ -3,6 +3,7 @@ import typer
|
|
3
3
|
from rich.console import Console
|
4
4
|
|
5
5
|
from modal.config import _profile, _store_user_config, config
|
6
|
+
from modal.environments import Environment
|
6
7
|
|
7
8
|
config_cli = typer.Typer(
|
8
9
|
name="config",
|
@@ -38,6 +39,8 @@ when running a command that requires an environment.
|
|
38
39
|
|
39
40
|
@config_cli.command(help=SET_DEFAULT_ENV_HELP)
|
40
41
|
def set_environment(environment_name: str):
|
42
|
+
# Confirm that the environment exists by looking it up
|
43
|
+
Environment.lookup(environment_name)
|
41
44
|
_store_user_config({"environment": environment_name})
|
42
45
|
typer.echo(f"New default environment for profile {_profile}: {environment_name}")
|
43
46
|
|
modal/cli/import_refs.py
CHANGED
@@ -110,7 +110,7 @@ def get_by_object_path(obj: Any, obj_path: str) -> Optional[Any]:
|
|
110
110
|
def _infer_function_or_help(
|
111
111
|
app: App, module, accept_local_entrypoint: bool, accept_webhook: bool
|
112
112
|
) -> Union[Function, LocalEntrypoint]:
|
113
|
-
function_choices = set(
|
113
|
+
function_choices = set(app.registered_functions)
|
114
114
|
if not accept_webhook:
|
115
115
|
function_choices -= set(app.registered_web_endpoints)
|
116
116
|
if accept_local_entrypoint:
|
@@ -154,7 +154,7 @@ Registered functions and local entrypoints on the selected app are:
|
|
154
154
|
# entrypoint is in entrypoint registry, for now
|
155
155
|
return app.registered_entrypoints[function_name]
|
156
156
|
|
157
|
-
function = app.
|
157
|
+
function = app.registered_functions[function_name]
|
158
158
|
assert isinstance(function, Function)
|
159
159
|
return function
|
160
160
|
|
modal/cli/launch.py
CHANGED
@@ -25,7 +25,7 @@ launch_cli = Typer(
|
|
25
25
|
)
|
26
26
|
|
27
27
|
|
28
|
-
def _launch_program(name: str, filename: str, args: Dict[str, Any]) -> None:
|
28
|
+
def _launch_program(name: str, filename: str, detach: bool, args: Dict[str, Any]) -> None:
|
29
29
|
os.environ["MODAL_LAUNCH_ARGS"] = json.dumps(args)
|
30
30
|
|
31
31
|
program_path = str(Path(__file__).parent / "programs" / filename)
|
@@ -37,7 +37,7 @@ def _launch_program(name: str, filename: str, args: Dict[str, Any]) -> None:
|
|
37
37
|
func = entrypoint.info.raw_f
|
38
38
|
isasync = inspect.iscoroutinefunction(func)
|
39
39
|
with enable_output():
|
40
|
-
with run_app(app):
|
40
|
+
with run_app(app, detach=detach):
|
41
41
|
try:
|
42
42
|
if isasync:
|
43
43
|
asyncio.run(func())
|
@@ -57,6 +57,7 @@ def jupyter(
|
|
57
57
|
add_python: Optional[str] = "3.11",
|
58
58
|
mount: Optional[str] = None, # Create a `modal.Mount` from a local directory.
|
59
59
|
volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
|
60
|
+
detach: bool = False, # Run the app in "detached" mode to persist after local client disconnects
|
60
61
|
):
|
61
62
|
args = {
|
62
63
|
"cpu": cpu,
|
@@ -68,7 +69,7 @@ def jupyter(
|
|
68
69
|
"mount": mount,
|
69
70
|
"volume": volume,
|
70
71
|
}
|
71
|
-
_launch_program("jupyter", "run_jupyter.py", args)
|
72
|
+
_launch_program("jupyter", "run_jupyter.py", detach, args)
|
72
73
|
|
73
74
|
|
74
75
|
@launch_cli.command(name="vscode", help="Start Visual Studio Code on Modal.")
|
@@ -79,6 +80,7 @@ def vscode(
|
|
79
80
|
timeout: int = 3600,
|
80
81
|
mount: Optional[str] = None, # Create a `modal.Mount` from a local directory.
|
81
82
|
volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
|
83
|
+
detach: bool = False, # Run the app in "detached" mode to persist after local client disconnects
|
82
84
|
):
|
83
85
|
args = {
|
84
86
|
"cpu": cpu,
|
@@ -88,4 +90,4 @@ def vscode(
|
|
88
90
|
"mount": mount,
|
89
91
|
"volume": volume,
|
90
92
|
}
|
91
|
-
_launch_program("vscode", "vscode.py", args)
|
93
|
+
_launch_program("vscode", "vscode.py", detach, args)
|
modal/cli/run.py
CHANGED
@@ -136,7 +136,13 @@ def _get_clean_app_description(func_ref: str) -> str:
|
|
136
136
|
|
137
137
|
|
138
138
|
def _get_click_command_for_function(app: App, function_tag):
|
139
|
-
function = app.
|
139
|
+
function = app.registered_functions.get(function_tag)
|
140
|
+
if not function or (isinstance(function, Function) and function.info.user_cls is not None):
|
141
|
+
# This is either a function_tag for a class method function (e.g MyClass.foo) or a function tag for a
|
142
|
+
# class service function (MyClass.*)
|
143
|
+
class_name, method_name = function_tag.rsplit(".", 1)
|
144
|
+
if not function:
|
145
|
+
function = app.registered_functions.get(f"{class_name}.*")
|
140
146
|
assert isinstance(function, Function)
|
141
147
|
function = typing.cast(Function, function)
|
142
148
|
if function.is_generator:
|
@@ -144,12 +150,19 @@ def _get_click_command_for_function(app: App, function_tag):
|
|
144
150
|
|
145
151
|
signature: Dict[str, ParameterMetadata]
|
146
152
|
cls: Optional[Cls] = None
|
147
|
-
method_name: Optional[str] = None
|
148
153
|
if function.info.user_cls is not None:
|
149
|
-
|
150
|
-
cls = typing.cast(Cls, app.indexed_objects[class_name])
|
154
|
+
cls = typing.cast(Cls, app.registered_classes[class_name])
|
151
155
|
cls_signature = _get_signature(function.info.user_cls)
|
152
|
-
|
156
|
+
if method_name == "*":
|
157
|
+
method_names = list(cls._get_partial_functions().keys())
|
158
|
+
if len(method_names) == 1:
|
159
|
+
method_name = method_names[0]
|
160
|
+
else:
|
161
|
+
class_name = function.info.user_cls.__name__
|
162
|
+
raise click.UsageError(
|
163
|
+
f"Please specify a specific method of {class_name} to run, e.g. `modal run foo.py::MyClass.bar`" # noqa: E501
|
164
|
+
)
|
165
|
+
fun_signature = _get_signature(getattr(cls, method_name).info.raw_f, is_method=True)
|
153
166
|
signature = dict(**cls_signature, **fun_signature) # Pool all arguments
|
154
167
|
# TODO(erikbern): assert there's no overlap?
|
155
168
|
else:
|
modal/client.pyi
CHANGED
@@ -31,7 +31,7 @@ class _Client:
|
|
31
31
|
server_url: str,
|
32
32
|
client_type: int,
|
33
33
|
credentials: typing.Optional[typing.Tuple[str, str]],
|
34
|
-
version: str = "0.
|
34
|
+
version: str = "0.67.0",
|
35
35
|
): ...
|
36
36
|
def is_closed(self) -> bool: ...
|
37
37
|
@property
|
@@ -90,7 +90,7 @@ class Client:
|
|
90
90
|
server_url: str,
|
91
91
|
client_type: int,
|
92
92
|
credentials: typing.Optional[typing.Tuple[str, str]],
|
93
|
-
version: str = "0.
|
93
|
+
version: str = "0.67.0",
|
94
94
|
): ...
|
95
95
|
def is_closed(self) -> bool: ...
|
96
96
|
@property
|
modal/cls.py
CHANGED
@@ -244,7 +244,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
244
244
|
_class_service_function: Optional[
|
245
245
|
_Function
|
246
246
|
] # The _Function serving *all* methods of the class, used for version >=v0.63
|
247
|
-
_method_functions: Dict[str, _Function] # Placeholder _Functions for each method
|
247
|
+
_method_functions: Optional[Dict[str, _Function]] = None # Placeholder _Functions for each method
|
248
248
|
_options: Optional[api_pb2.FunctionOptions]
|
249
249
|
_callables: Dict[str, Callable[..., Any]]
|
250
250
|
_from_other_workspace: Optional[bool] # Functions require FunctionBindParams before invocation.
|
@@ -253,7 +253,6 @@ class _Cls(_Object, type_prefix="cs"):
|
|
253
253
|
def _initialize_from_empty(self):
|
254
254
|
self._user_cls = None
|
255
255
|
self._class_service_function = None
|
256
|
-
self._method_functions = {}
|
257
256
|
self._options = None
|
258
257
|
self._callables = {}
|
259
258
|
self._from_other_workspace = None
|
@@ -273,28 +272,46 @@ class _Cls(_Object, type_prefix="cs"):
|
|
273
272
|
|
274
273
|
def _hydrate_metadata(self, metadata: Message):
|
275
274
|
assert isinstance(metadata, api_pb2.ClassHandleMetadata)
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
)
|
275
|
+
if (
|
276
|
+
self._class_service_function
|
277
|
+
and self._class_service_function._method_handle_metadata
|
278
|
+
and len(self._class_service_function._method_handle_metadata)
|
279
|
+
):
|
280
|
+
# The class only has a class service service function and no method placeholders (v0.67+)
|
281
|
+
if self._method_functions:
|
282
|
+
# We're here when the Cls is loaded locally (e.g. _Cls.from_local) so the _method_functions mapping is
|
283
|
+
# populated with (un-hydrated) _Function objects
|
284
|
+
for (
|
285
|
+
method_name,
|
286
|
+
method_handle_metadata,
|
287
|
+
) in self._class_service_function._method_handle_metadata.items():
|
288
|
+
self._method_functions[method_name]._hydrate(
|
289
|
+
self._class_service_function.object_id, self._client, method_handle_metadata
|
290
|
+
)
|
284
291
|
else:
|
292
|
+
# We're here when the function is loaded remotely (e.g. _Cls.from_name)
|
293
|
+
self._method_functions = {}
|
294
|
+
for (
|
295
|
+
method_name,
|
296
|
+
method_handle_metadata,
|
297
|
+
) in self._class_service_function._method_handle_metadata.items():
|
298
|
+
self._method_functions[method_name] = _Function._new_hydrated(
|
299
|
+
self._class_service_function.object_id, self._client, method_handle_metadata
|
300
|
+
)
|
301
|
+
elif self._class_service_function:
|
302
|
+
# A class with a class service function and method placeholder functions
|
303
|
+
self._method_functions = {}
|
304
|
+
for method in metadata.methods:
|
285
305
|
self._method_functions[method.function_name] = _Function._new_hydrated(
|
286
|
-
|
306
|
+
self._class_service_function.object_id, self._client, method.function_handle_metadata
|
287
307
|
)
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
function_name=f_name, function_id=f.object_id, function_handle_metadata=f._get_metadata()
|
308
|
+
else:
|
309
|
+
# pre 0.63 class that does not have a class service function and only method functions
|
310
|
+
self._method_functions = {}
|
311
|
+
for method in metadata.methods:
|
312
|
+
self._method_functions[method.function_name] = _Function._new_hydrated(
|
313
|
+
method.function_id, self._client, method.function_handle_metadata
|
295
314
|
)
|
296
|
-
)
|
297
|
-
return class_handle_metadata
|
298
315
|
|
299
316
|
@staticmethod
|
300
317
|
def validate_construction_mechanism(user_cls):
|
@@ -327,16 +344,17 @@ class _Cls(_Object, type_prefix="cs"):
|
|
327
344
|
# validate signature
|
328
345
|
_Cls.validate_construction_mechanism(user_cls)
|
329
346
|
|
330
|
-
|
347
|
+
method_functions: Dict[str, _Function] = {}
|
331
348
|
partial_functions: Dict[str, _PartialFunction] = _find_partial_methods_for_user_cls(
|
332
349
|
user_cls, _PartialFunctionFlags.FUNCTION
|
333
350
|
)
|
334
351
|
|
335
352
|
for method_name, partial_function in partial_functions.items():
|
336
|
-
method_function = class_service_function.
|
337
|
-
|
353
|
+
method_function = class_service_function._bind_method(user_cls, method_name, partial_function)
|
354
|
+
if partial_function.webhook_config is not None:
|
355
|
+
app._web_endpoints.append(method_function.tag)
|
338
356
|
partial_function.wrapped = True
|
339
|
-
|
357
|
+
method_functions[method_name] = method_function
|
340
358
|
|
341
359
|
# Disable the warning that these are not wrapped
|
342
360
|
for partial_function in _find_partial_methods_for_user_cls(user_cls, ~_PartialFunctionFlags.FUNCTION).values():
|
@@ -344,31 +362,17 @@ class _Cls(_Object, type_prefix="cs"):
|
|
344
362
|
|
345
363
|
# Get all callables
|
346
364
|
callables: Dict[str, Callable] = {
|
347
|
-
k: pf.raw_f for k, pf in _find_partial_methods_for_user_cls(user_cls,
|
365
|
+
k: pf.raw_f for k, pf in _find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.all()).items()
|
348
366
|
}
|
349
367
|
|
350
368
|
def _deps() -> List[_Function]:
|
351
|
-
return [class_service_function]
|
369
|
+
return [class_service_function]
|
352
370
|
|
353
371
|
async def _load(self: "_Cls", resolver: Resolver, existing_object_id: Optional[str]):
|
354
|
-
req = api_pb2.ClassCreateRequest(
|
355
|
-
|
356
|
-
|
357
|
-
api_pb2.ClassMethod(
|
358
|
-
function_name=f_name, function_id=f.object_id, function_handle_metadata=f._get_metadata()
|
359
|
-
)
|
360
|
-
)
|
372
|
+
req = api_pb2.ClassCreateRequest(
|
373
|
+
app_id=resolver.app_id, existing_class_id=existing_object_id, only_class_function=True
|
374
|
+
)
|
361
375
|
resp = await resolver.client.stub.ClassCreate(req)
|
362
|
-
# Even though we already have the function_handle_metadata for this method locally,
|
363
|
-
# The RPC is going to replace it with function_handle_metadata derived from the server.
|
364
|
-
# We need to overwrite the definition_id sent back from the server here with the definition_id
|
365
|
-
# previously stored in function metadata, which may have been sent back from FunctionCreate.
|
366
|
-
# The problem is that this metadata propagates back and overwrites the metadata on the Function
|
367
|
-
# object itself. This is really messy. Maybe better to exclusively populate the method metadata
|
368
|
-
# from the function metadata we already have locally? Really a lot to clean up here...
|
369
|
-
for method in resp.handle_metadata.methods:
|
370
|
-
f_metadata = self._method_functions[method.function_name]._get_metadata()
|
371
|
-
method.function_handle_metadata.definition_id = f_metadata.definition_id
|
372
376
|
self._hydrate(resp.class_id, resolver.client, resp.handle_metadata)
|
373
377
|
|
374
378
|
rep = f"Cls({user_cls.__name__})"
|
@@ -376,7 +380,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
376
380
|
cls._app = app
|
377
381
|
cls._user_cls = user_cls
|
378
382
|
cls._class_service_function = class_service_function
|
379
|
-
cls._method_functions =
|
383
|
+
cls._method_functions = method_functions
|
380
384
|
cls._callables = callables
|
381
385
|
cls._from_other_workspace = False
|
382
386
|
return cls
|
@@ -415,6 +419,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
415
419
|
environment_name=_environment_name,
|
416
420
|
lookup_published=workspace is not None,
|
417
421
|
workspace_name=workspace,
|
422
|
+
only_class_function=True,
|
418
423
|
)
|
419
424
|
try:
|
420
425
|
response = await retry_transient_errors(resolver.client.stub.ClassGet, request)
|
modal/cls.pyi
CHANGED
@@ -86,7 +86,7 @@ class Obj:
|
|
86
86
|
class _Cls(modal.object._Object):
|
87
87
|
_user_cls: typing.Optional[type]
|
88
88
|
_class_service_function: typing.Optional[modal.functions._Function]
|
89
|
-
_method_functions: typing.Dict[str, modal.functions._Function]
|
89
|
+
_method_functions: typing.Optional[typing.Dict[str, modal.functions._Function]]
|
90
90
|
_options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
|
91
91
|
_callables: typing.Dict[str, typing.Callable[..., typing.Any]]
|
92
92
|
_from_other_workspace: typing.Optional[bool]
|
@@ -96,7 +96,6 @@ class _Cls(modal.object._Object):
|
|
96
96
|
def _initialize_from_other(self, other: _Cls): ...
|
97
97
|
def _get_partial_functions(self) -> typing.Dict[str, modal.partial_function._PartialFunction]: ...
|
98
98
|
def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
|
99
|
-
def _get_metadata(self) -> modal_proto.api_pb2.ClassHandleMetadata: ...
|
100
99
|
@staticmethod
|
101
100
|
def validate_construction_mechanism(user_cls): ...
|
102
101
|
@staticmethod
|
@@ -139,7 +138,7 @@ class _Cls(modal.object._Object):
|
|
139
138
|
class Cls(modal.object.Object):
|
140
139
|
_user_cls: typing.Optional[type]
|
141
140
|
_class_service_function: typing.Optional[modal.functions.Function]
|
142
|
-
_method_functions: typing.Dict[str, modal.functions.Function]
|
141
|
+
_method_functions: typing.Optional[typing.Dict[str, modal.functions.Function]]
|
143
142
|
_options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
|
144
143
|
_callables: typing.Dict[str, typing.Callable[..., typing.Any]]
|
145
144
|
_from_other_workspace: typing.Optional[bool]
|
@@ -150,7 +149,6 @@ class Cls(modal.object.Object):
|
|
150
149
|
def _initialize_from_other(self, other: Cls): ...
|
151
150
|
def _get_partial_functions(self) -> typing.Dict[str, modal.partial_function.PartialFunction]: ...
|
152
151
|
def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
|
153
|
-
def _get_metadata(self) -> modal_proto.api_pb2.ClassHandleMetadata: ...
|
154
152
|
@staticmethod
|
155
153
|
def validate_construction_mechanism(user_cls): ...
|
156
154
|
@staticmethod
|