modal 0.73.54__py3-none-any.whl → 0.73.55__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/_container_entrypoint.py +5 -5
- modal/_functions.py +24 -45
- modal/_runtime/user_code_imports.py +11 -16
- modal/client.pyi +2 -2
- modal/cls.py +90 -100
- modal/cls.pyi +9 -9
- modal/functions.pyi +0 -2
- {modal-0.73.54.dist-info → modal-0.73.55.dist-info}/METADATA +1 -1
- {modal-0.73.54.dist-info → modal-0.73.55.dist-info}/RECORD +14 -14
- modal_version/_version_generated.py +1 -1
- {modal-0.73.54.dist-info → modal-0.73.55.dist-info}/LICENSE +0 -0
- {modal-0.73.54.dist-info → modal-0.73.55.dist-info}/WHEEL +0 -0
- {modal-0.73.54.dist-info → modal-0.73.55.dist-info}/entry_points.txt +0 -0
- {modal-0.73.54.dist-info → modal-0.73.55.dist-info}/top_level.txt +0 -0
modal/_container_entrypoint.py
CHANGED
@@ -471,16 +471,16 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
|
|
471
471
|
# TODO(erikbern): we an remove this once we
|
472
472
|
# 1. Enable lazy hydration for all objects
|
473
473
|
# 2. Fully deprecate .new() objects
|
474
|
-
if service.
|
474
|
+
if service.service_deps is not None: # this is not set for serialized or non-global scope functions
|
475
475
|
dep_object_ids: list[str] = [dep.object_id for dep in function_def.object_dependencies]
|
476
|
-
if len(service.
|
476
|
+
if len(service.service_deps) != len(dep_object_ids):
|
477
477
|
raise ExecutionError(
|
478
|
-
f"Function has {len(service.
|
478
|
+
f"Function has {len(service.service_deps)} dependencies"
|
479
479
|
f" but container got {len(dep_object_ids)} object ids.\n"
|
480
|
-
f"Code deps: {service.
|
480
|
+
f"Code deps: {service.service_deps}\n"
|
481
481
|
f"Object ids: {dep_object_ids}"
|
482
482
|
)
|
483
|
-
for object_id, obj in zip(dep_object_ids, service.
|
483
|
+
for object_id, obj in zip(dep_object_ids, service.service_deps):
|
484
484
|
metadata: Message = container_app.object_handle_metadata[object_id]
|
485
485
|
obj._hydrate(object_id, _client, metadata)
|
486
486
|
|
modal/_functions.py
CHANGED
@@ -182,7 +182,8 @@ class _Invocation:
|
|
182
182
|
if timeout is None:
|
183
183
|
backend_timeout = OUTPUTS_TIMEOUT
|
184
184
|
else:
|
185
|
-
|
185
|
+
# refresh backend call every 55s
|
186
|
+
backend_timeout = min(OUTPUTS_TIMEOUT, timeout)
|
186
187
|
|
187
188
|
while True:
|
188
189
|
# always execute at least one poll for results, regardless if timeout is 0
|
@@ -278,7 +279,8 @@ class _Invocation:
|
|
278
279
|
|
279
280
|
async def run_generator(self):
|
280
281
|
items_received = 0
|
281
|
-
|
282
|
+
# populated when self.run_function() completes
|
283
|
+
items_total: Union[int, None] = None
|
282
284
|
async with aclosing(
|
283
285
|
async_merge(
|
284
286
|
_stream_function_call_data(self.client, self.function_call_id, variant="data_out"),
|
@@ -350,7 +352,8 @@ class _FunctionSpec:
|
|
350
352
|
secrets: Sequence[_Secret]
|
351
353
|
network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem]
|
352
354
|
volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]]
|
353
|
-
|
355
|
+
# TODO(irfansharif): Somehow assert that it's the first kind, in sandboxes
|
356
|
+
gpus: Union[GPU_T, list[GPU_T]]
|
354
357
|
cloud: Optional[str]
|
355
358
|
cpu: Optional[Union[float, tuple[float, float]]]
|
356
359
|
memory: Optional[Union[int, tuple[int, int]]]
|
@@ -377,16 +380,19 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
377
380
|
_info: Optional[FunctionInfo]
|
378
381
|
_serve_mounts: frozenset[_Mount] # set at load time, only by loader
|
379
382
|
_app: Optional["modal.app._App"] = None
|
380
|
-
|
383
|
+
# only set for InstanceServiceFunctions and bound instance methods
|
384
|
+
_obj: Optional["modal.cls._Obj"] = None
|
381
385
|
|
382
|
-
|
386
|
+
# this is set in definition scope, only locally
|
387
|
+
_webhook_config: Optional[api_pb2.WebhookConfig] = None
|
383
388
|
_web_url: Optional[str] # this is set on hydration
|
384
389
|
|
385
390
|
_function_name: Optional[str]
|
386
391
|
_is_method: bool
|
387
392
|
_spec: Optional[_FunctionSpec] = None
|
388
393
|
_tag: str
|
389
|
-
|
394
|
+
# this is set to None for a "class service [function]"
|
395
|
+
_raw_f: Optional[Callable[..., Any]]
|
390
396
|
_build_args: dict
|
391
397
|
|
392
398
|
_is_generator: Optional[bool] = None
|
@@ -401,40 +407,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
401
407
|
None # set for 0.67+ class service functions
|
402
408
|
)
|
403
409
|
|
404
|
-
def _bind_method(
|
405
|
-
self,
|
406
|
-
user_cls,
|
407
|
-
method_name: str,
|
408
|
-
partial_function: "modal._partial_function._PartialFunction",
|
409
|
-
):
|
410
|
-
"""mdmd:hidden
|
411
|
-
|
412
|
-
Creates a _Function that is bound to a specific class method name. This _Function is not uniquely tied
|
413
|
-
to any backend function -- its object_id is the function ID of the class service function.
|
414
|
-
|
415
|
-
"""
|
416
|
-
class_service_function = self
|
417
|
-
assert class_service_function._info # has to be a local function to be able to "bind" it
|
418
|
-
assert not class_service_function._is_method # should not be used on an already bound method placeholder
|
419
|
-
assert not class_service_function._obj # should only be used on base function / class service function
|
420
|
-
full_name = f"{user_cls.__name__}.{method_name}"
|
421
|
-
|
422
|
-
rep = f"Method({full_name})"
|
423
|
-
fun = _Object.__new__(_Function)
|
424
|
-
fun._init(rep)
|
425
|
-
fun._tag = full_name
|
426
|
-
fun._raw_f = partial_function.raw_f
|
427
|
-
fun._info = FunctionInfo(
|
428
|
-
partial_function.raw_f, user_cls=user_cls, serialized=class_service_function.info.is_serialized()
|
429
|
-
) # needed for .local()
|
430
|
-
fun._use_method_name = method_name
|
431
|
-
fun._app = class_service_function._app
|
432
|
-
fun._is_generator = partial_function.is_generator
|
433
|
-
fun._cluster_size = partial_function.cluster_size
|
434
|
-
fun._spec = class_service_function._spec
|
435
|
-
fun._is_method = True
|
436
|
-
return fun
|
437
|
-
|
438
410
|
@staticmethod
|
439
411
|
def from_local(
|
440
412
|
info: FunctionInfo,
|
@@ -460,7 +432,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
460
432
|
batch_wait_ms: Optional[int] = None,
|
461
433
|
container_idle_timeout: Optional[int] = None,
|
462
434
|
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
463
|
-
|
435
|
+
# keep_warm=True is equivalent to keep_warm=1
|
436
|
+
keep_warm: Optional[int] = None,
|
464
437
|
cloud: Optional[str] = None,
|
465
438
|
scheduler_placement: Optional[SchedulerPlacement] = None,
|
466
439
|
is_builder_function: bool = False,
|
@@ -468,7 +441,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
468
441
|
enable_memory_snapshot: bool = False,
|
469
442
|
block_network: bool = False,
|
470
443
|
i6pn_enabled: bool = False,
|
471
|
-
|
444
|
+
# Experimental: Clustered functions
|
445
|
+
cluster_size: Optional[int] = None,
|
472
446
|
max_inputs: Optional[int] = None,
|
473
447
|
ephemeral_disk: Optional[int] = None,
|
474
448
|
# current default: first-party, future default: main-package
|
@@ -934,7 +908,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
934
908
|
raise InvalidError(f"Function {info.function_name} is too large to deploy.")
|
935
909
|
raise
|
936
910
|
function_creation_status.set_response(response)
|
937
|
-
|
911
|
+
# needed for modal.serve file watching
|
912
|
+
serve_mounts = {m for m in all_mounts if m.is_local()}
|
938
913
|
serve_mounts |= image._serve_mounts
|
939
914
|
obj._serve_mounts = frozenset(serve_mounts)
|
940
915
|
self._hydrate(response.function_id, resolver.client, response.handle_metadata)
|
@@ -1039,6 +1014,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1039
1014
|
|
1040
1015
|
fun._info = self._info
|
1041
1016
|
fun._obj = obj
|
1017
|
+
fun._spec = self._spec # TODO (elias): fix - this is incorrect when using with_options
|
1042
1018
|
return fun
|
1043
1019
|
|
1044
1020
|
@live_method
|
@@ -1223,7 +1199,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1223
1199
|
|
1224
1200
|
def _hydrate_metadata(self, metadata: Optional[Message]):
|
1225
1201
|
# Overridden concrete implementation of base class method
|
1226
|
-
assert metadata and isinstance(metadata, api_pb2.FunctionHandleMetadata)
|
1202
|
+
assert metadata and isinstance(metadata, api_pb2.FunctionHandleMetadata), (
|
1203
|
+
f"{type(metadata)} is not FunctionHandleMetadata"
|
1204
|
+
)
|
1227
1205
|
self._is_generator = metadata.function_type == api_pb2.Function.FUNCTION_TYPE_GENERATOR
|
1228
1206
|
self._web_url = metadata.web_url
|
1229
1207
|
self._function_name = metadata.function_name
|
@@ -1595,7 +1573,8 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
1595
1573
|
|
1596
1574
|
async def cancel(
|
1597
1575
|
self,
|
1598
|
-
|
1576
|
+
# if true, containers running the inputs are forcibly terminated
|
1577
|
+
terminate_containers: bool = False,
|
1599
1578
|
):
|
1600
1579
|
"""Cancels the function call, which will stop its execution and mark its inputs as
|
1601
1580
|
[`TERMINATED`](/docs/reference/modal.call_graph#modalcall_graphinputstatus).
|
@@ -41,7 +41,7 @@ class Service(metaclass=ABCMeta):
|
|
41
41
|
|
42
42
|
user_cls_instance: Any
|
43
43
|
app: Optional["modal.app._App"]
|
44
|
-
|
44
|
+
service_deps: Optional[Sequence["modal._object._Object"]]
|
45
45
|
|
46
46
|
@abstractmethod
|
47
47
|
def get_finalized_functions(
|
@@ -93,7 +93,7 @@ def construct_webhook_callable(
|
|
93
93
|
class ImportedFunction(Service):
|
94
94
|
user_cls_instance: Any
|
95
95
|
app: Optional["modal.app._App"]
|
96
|
-
|
96
|
+
service_deps: Optional[Sequence["modal._object._Object"]]
|
97
97
|
|
98
98
|
_user_defined_callable: Callable[..., Any]
|
99
99
|
|
@@ -136,7 +136,7 @@ class ImportedFunction(Service):
|
|
136
136
|
class ImportedClass(Service):
|
137
137
|
user_cls_instance: Any
|
138
138
|
app: Optional["modal.app._App"]
|
139
|
-
|
139
|
+
service_deps: Optional[Sequence["modal._object._Object"]]
|
140
140
|
|
141
141
|
_partial_functions: dict[str, "modal._partial_function._PartialFunction"]
|
142
142
|
|
@@ -227,8 +227,7 @@ def import_single_function_service(
|
|
227
227
|
the import) runs on the right thread.
|
228
228
|
"""
|
229
229
|
user_defined_callable: Callable
|
230
|
-
|
231
|
-
code_deps: Optional[Sequence["modal._object._Object"]] = None
|
230
|
+
service_deps: Optional[Sequence["modal._object._Object"]] = None
|
232
231
|
active_app: Optional[modal.app._App] = None
|
233
232
|
|
234
233
|
if ser_fun is not None:
|
@@ -249,6 +248,7 @@ def import_single_function_service(
|
|
249
248
|
f = getattr(module, qual_name)
|
250
249
|
if isinstance(f, Function):
|
251
250
|
function = synchronizer._translate_in(f)
|
251
|
+
service_deps = function.deps(only_explicit_mounts=True)
|
252
252
|
user_defined_callable = function.get_raw_f()
|
253
253
|
active_app = function._app
|
254
254
|
else:
|
@@ -264,9 +264,7 @@ def import_single_function_service(
|
|
264
264
|
# The cls decorator is in global scope
|
265
265
|
_cls = synchronizer._translate_in(cls)
|
266
266
|
user_defined_callable = _cls._callables[fun_name]
|
267
|
-
|
268
|
-
fun_name
|
269
|
-
) # bound to the class service function - there is no instance
|
267
|
+
service_deps = _cls._get_class_service_function().deps(only_explicit_mounts=True)
|
270
268
|
active_app = _cls._app
|
271
269
|
else:
|
272
270
|
# This is non-decorated class
|
@@ -283,13 +281,10 @@ def import_single_function_service(
|
|
283
281
|
else:
|
284
282
|
user_cls_instance = None
|
285
283
|
|
286
|
-
if function:
|
287
|
-
code_deps = function.deps(only_explicit_mounts=True)
|
288
|
-
|
289
284
|
return ImportedFunction(
|
290
285
|
user_cls_instance,
|
291
286
|
active_app,
|
292
|
-
|
287
|
+
service_deps,
|
293
288
|
user_defined_callable,
|
294
289
|
)
|
295
290
|
|
@@ -306,7 +301,7 @@ def import_class_service(
|
|
306
301
|
See import_function.
|
307
302
|
"""
|
308
303
|
active_app: Optional["modal.app._App"]
|
309
|
-
|
304
|
+
service_deps: Optional[Sequence["modal._object._Object"]]
|
310
305
|
cls: typing.Union[type, modal.cls.Cls]
|
311
306
|
|
312
307
|
if function_def.definition_type == api_pb2.Function.DEFINITION_TYPE_SERIALIZED:
|
@@ -337,12 +332,12 @@ def import_class_service(
|
|
337
332
|
_cls = synchronizer._translate_in(cls)
|
338
333
|
method_partials = _cls._get_partial_functions()
|
339
334
|
service_function: _Function = _cls._class_service_function
|
340
|
-
|
335
|
+
service_deps = service_function.deps(only_explicit_mounts=True)
|
341
336
|
active_app = service_function.app
|
342
337
|
else:
|
343
338
|
# Undecorated user class - find all methods
|
344
339
|
method_partials = _find_partial_methods_for_user_cls(cls, _PartialFunctionFlags.all())
|
345
|
-
|
340
|
+
service_deps = None
|
346
341
|
active_app = None
|
347
342
|
|
348
343
|
user_cls_instance = get_user_class_instance(cls, cls_args, cls_kwargs)
|
@@ -350,7 +345,7 @@ def import_class_service(
|
|
350
345
|
return ImportedClass(
|
351
346
|
user_cls_instance,
|
352
347
|
active_app,
|
353
|
-
|
348
|
+
service_deps,
|
354
349
|
# TODO (elias/deven): instead of using method_partials here we should use a set of api_pb2.MethodDefinition
|
355
350
|
method_partials,
|
356
351
|
)
|
modal/client.pyi
CHANGED
@@ -27,7 +27,7 @@ class _Client:
|
|
27
27
|
_snapshotted: bool
|
28
28
|
|
29
29
|
def __init__(
|
30
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.73.
|
30
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.73.55"
|
31
31
|
): ...
|
32
32
|
def is_closed(self) -> bool: ...
|
33
33
|
@property
|
@@ -85,7 +85,7 @@ class Client:
|
|
85
85
|
_snapshotted: bool
|
86
86
|
|
87
87
|
def __init__(
|
88
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.73.
|
88
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.73.55"
|
89
89
|
): ...
|
90
90
|
def is_closed(self) -> bool: ...
|
91
91
|
@property
|
modal/cls.py
CHANGED
@@ -8,7 +8,7 @@ from typing import Any, Callable, Optional, TypeVar, Union
|
|
8
8
|
from google.protobuf.message import Message
|
9
9
|
from grpclib import GRPCError, Status
|
10
10
|
|
11
|
-
from modal._utils.function_utils import CLASS_PARAM_TYPE_MAP
|
11
|
+
from modal._utils.function_utils import CLASS_PARAM_TYPE_MAP, FunctionInfo
|
12
12
|
from modal_proto import api_pb2
|
13
13
|
|
14
14
|
from ._functions import _Function, _parse_retries
|
@@ -72,30 +72,22 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature:
|
|
72
72
|
return inspect.Signature(constructor_parameters)
|
73
73
|
|
74
74
|
|
75
|
-
def _bind_instance_method(service_function: _Function,
|
76
|
-
"""Binds an "instance service function" to a specific method
|
77
|
-
|
78
|
-
|
79
|
-
|
75
|
+
def _bind_instance_method(cls: "_Cls", service_function: _Function, method_name: str):
|
76
|
+
"""Binds an "instance service function" to a specific method using metadata for that method
|
77
|
+
|
78
|
+
This "dummy" _Function gets no unique object_id and isn't backend-backed at all, since all
|
79
|
+
it does it forward invocations to the underlying instance_service_function with the specified method
|
80
80
|
"""
|
81
|
-
# TODO(elias): refactor to not use `_from_loader()` as a crutch for lazy-loading the
|
82
|
-
# underlying instance_service_function. It's currently used in order to take advantage
|
83
|
-
# of resolver logic and get "chained" resolution of lazy loads, even though this thin
|
84
|
-
# object itself doesn't need any "loading"
|
85
81
|
assert service_function._obj
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
method_placeholder_fun._is_generator = class_bound_method._is_generator
|
96
|
-
method_placeholder_fun._cluster_size = class_bound_method._cluster_size
|
97
|
-
method_placeholder_fun._use_method_name = method_name
|
98
|
-
method_placeholder_fun._is_method = True
|
82
|
+
|
83
|
+
def hydrate_from_instance_service_function(new_function: _Function):
|
84
|
+
assert service_function.is_hydrated
|
85
|
+
assert cls.is_hydrated
|
86
|
+
# After 0.67 is minimum required version, we should be able to use method metadata directly
|
87
|
+
# from the service_function instead (see _Cls._hydrate_metadata), but for now we use the Cls
|
88
|
+
# since it can take the data from the cls metadata OR function metadata depending on source
|
89
|
+
method_metadata = cls._method_metadata[method_name]
|
90
|
+
new_function._hydrate(service_function.object_id, service_function.client, method_metadata)
|
99
91
|
|
100
92
|
async def _load(fun: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
|
101
93
|
# there is currently no actual loading logic executed to create each method on
|
@@ -111,7 +103,7 @@ def _bind_instance_method(service_function: _Function, class_bound_method: _Func
|
|
111
103
|
return []
|
112
104
|
return [service_function]
|
113
105
|
|
114
|
-
rep = f"Method({method_name})"
|
106
|
+
rep = f"Method({cls._name}.{method_name})"
|
115
107
|
|
116
108
|
fun = _Function._from_loader(
|
117
109
|
_load,
|
@@ -123,12 +115,19 @@ def _bind_instance_method(service_function: _Function, class_bound_method: _Func
|
|
123
115
|
# Eager hydration (skip load) if the instance service function is already loaded
|
124
116
|
hydrate_from_instance_service_function(fun)
|
125
117
|
|
126
|
-
|
118
|
+
if cls._is_local():
|
119
|
+
partial_function = cls._method_partials[method_name]
|
120
|
+
fun._info = FunctionInfo(
|
121
|
+
# ugly - needed for .local() TODO (elias): Clean up!
|
122
|
+
partial_function.raw_f,
|
123
|
+
user_cls=cls._user_cls,
|
124
|
+
serialized=service_function.info.is_serialized(),
|
125
|
+
)
|
126
|
+
|
127
127
|
fun._obj = service_function._obj
|
128
128
|
fun._is_method = True
|
129
|
-
fun._app =
|
130
|
-
fun._spec =
|
131
|
-
fun._is_web_endpoint = class_bound_method._is_web_endpoint
|
129
|
+
fun._app = service_function._app
|
130
|
+
fun._spec = service_function._spec
|
132
131
|
return fun
|
133
132
|
|
134
133
|
|
@@ -280,31 +279,34 @@ class _Obj:
|
|
280
279
|
# * Support .local() on both hydrated and unhydrated classes (assuming local access to code)
|
281
280
|
# * Support attribute access (when local cls is available)
|
282
281
|
|
283
|
-
|
282
|
+
# The returned _Function objects need to be lazily loaded (including loading the Cls and/or service function)
|
283
|
+
# since we can't assume the class is already loaded when this gets called, e.g.
|
284
|
+
# CLs.from_name(...)().my_func.remote().
|
285
|
+
|
286
|
+
def _get_maybe_method() -> Optional["_Function"]:
|
284
287
|
"""Gets _Function object for method - either for a local or a hydrated remote class
|
285
288
|
|
286
289
|
* If class is neither local or hydrated - raise exception (should never happen)
|
287
290
|
* If attribute isn't a method - return None
|
288
291
|
"""
|
289
|
-
if self._cls.
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
elif self._user_cls:
|
292
|
+
if self._cls._is_local():
|
293
|
+
if k not in self._cls._method_partials:
|
294
|
+
return None
|
295
|
+
elif self._cls.is_hydrated:
|
296
|
+
if k not in self._cls._method_metadata:
|
297
|
+
return None
|
298
|
+
else:
|
299
|
+
raise ExecutionError(
|
300
|
+
"Class is neither hydrated or local - this is probably a bug in the Modal client. Contact support"
|
301
|
+
)
|
302
|
+
|
303
|
+
return _bind_instance_method(self._cls, self._cached_service_function(), k)
|
304
|
+
|
305
|
+
if self._cls.is_hydrated or self._cls._is_local():
|
306
|
+
# Class is hydrated or local so we know which methods exist
|
307
|
+
if maybe_method := _get_maybe_method():
|
308
|
+
return maybe_method
|
309
|
+
elif self._cls._is_local():
|
308
310
|
# We have the local definition, and the attribute isn't a method
|
309
311
|
# so we instantiate if we don't have an instance, and try to get the attribute
|
310
312
|
user_cls_instance = self._cached_user_cls_instance()
|
@@ -319,10 +321,9 @@ class _Obj:
|
|
319
321
|
|
320
322
|
# Not hydrated Cls, and we don't have the class - typically a Cls.from_name that
|
321
323
|
# has not yet been loaded. So use a special loader that loads it lazily:
|
322
|
-
|
323
324
|
async def method_loader(fun, resolver: Resolver, existing_object_id):
|
324
325
|
await resolver.load(self._cls) # load class so we get info about methods
|
325
|
-
method_function =
|
326
|
+
method_function = _get_maybe_method()
|
326
327
|
if method_function is None:
|
327
328
|
raise NotFoundError(
|
328
329
|
f"Class has no method {k}, and attributes can't be accessed for `Cls.from_name` instances"
|
@@ -334,7 +335,7 @@ class _Obj:
|
|
334
335
|
# on local classes.
|
335
336
|
return _Function._from_loader(
|
336
337
|
method_loader,
|
337
|
-
|
338
|
+
rep=f"Method({self._cls._name}.{k})",
|
338
339
|
deps=lambda: [], # TODO: use cls as dep instead of loading inside method_loader?
|
339
340
|
hydrate_lazily=True,
|
340
341
|
)
|
@@ -352,13 +353,19 @@ class _Cls(_Object, type_prefix="cs"):
|
|
352
353
|
Instead, use the [`@app.cls()`](/docs/reference/modal.App#cls) decorator on the App object.
|
353
354
|
"""
|
354
355
|
|
355
|
-
_user_cls: Optional[type]
|
356
356
|
_class_service_function: Optional[_Function] # The _Function (read "service") serving *all* methods of the class
|
357
|
-
_method_functions: Optional[dict[str, _Function]] = None # Placeholder _Functions for each method
|
358
357
|
_options: Optional[api_pb2.FunctionOptions]
|
359
|
-
|
358
|
+
|
360
359
|
_app: Optional["modal.app._App"] = None # not set for lookups
|
361
360
|
_name: Optional[str]
|
361
|
+
# Only set for hydrated classes:
|
362
|
+
_method_metadata: Optional[dict[str, api_pb2.FunctionHandleMetadata]] = None
|
363
|
+
|
364
|
+
# These are only set where source is locally available:
|
365
|
+
# TODO: wrap these in a single optional/property for consistency
|
366
|
+
_user_cls: Optional[type] = None
|
367
|
+
_method_partials: Optional[dict[str, _PartialFunction]] = None
|
368
|
+
_callables: dict[str, Callable[..., Any]]
|
362
369
|
|
363
370
|
def _initialize_from_empty(self):
|
364
371
|
self._user_cls = None
|
@@ -371,10 +378,11 @@ class _Cls(_Object, type_prefix="cs"):
|
|
371
378
|
super()._initialize_from_other(other)
|
372
379
|
self._user_cls = other._user_cls
|
373
380
|
self._class_service_function = other._class_service_function
|
374
|
-
self.
|
381
|
+
self._method_partials = other._method_partials
|
375
382
|
self._options = other._options
|
376
383
|
self._callables = other._callables
|
377
384
|
self._name = other._name
|
385
|
+
self._method_metadata = other._method_metadata
|
378
386
|
|
379
387
|
def _get_partial_functions(self) -> dict[str, _PartialFunction]:
|
380
388
|
if not self._user_cls:
|
@@ -382,59 +390,41 @@ class _Cls(_Object, type_prefix="cs"):
|
|
382
390
|
return _find_partial_methods_for_user_cls(self._user_cls, _PartialFunctionFlags.all())
|
383
391
|
|
384
392
|
def _get_app(self) -> "modal.app._App":
|
393
|
+
assert self._app is not None
|
385
394
|
return self._app
|
386
395
|
|
387
396
|
def _get_user_cls(self) -> type:
|
397
|
+
assert self._user_cls is not None
|
388
398
|
return self._user_cls
|
389
399
|
|
390
400
|
def _get_name(self) -> str:
|
401
|
+
assert self._name is not None
|
391
402
|
return self._name
|
392
403
|
|
393
|
-
def _get_class_service_function(self) ->
|
404
|
+
def _get_class_service_function(self) -> _Function:
|
405
|
+
assert self._class_service_function is not None
|
394
406
|
return self._class_service_function
|
395
407
|
|
396
408
|
def _get_method_names(self) -> Collection[str]:
|
397
409
|
# returns method names for a *local* class only for now (used by cli)
|
398
|
-
return self.
|
410
|
+
return self._method_partials.keys()
|
399
411
|
|
400
412
|
def _hydrate_metadata(self, metadata: Message):
|
401
413
|
assert isinstance(metadata, api_pb2.ClassHandleMetadata)
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
#
|
408
|
-
|
409
|
-
# We're here when the Cls is loaded locally (e.g. _Cls.from_local) so the _method_functions mapping is
|
410
|
-
# populated with (un-hydrated) _Function objects - hydrate using function method metadata
|
411
|
-
for (
|
412
|
-
method_name,
|
413
|
-
method_handle_metadata,
|
414
|
-
) in self._class_service_function._method_handle_metadata.items():
|
415
|
-
self._method_functions[method_name]._hydrate(
|
416
|
-
self._class_service_function.object_id, self._client, method_handle_metadata
|
417
|
-
)
|
418
|
-
else:
|
419
|
-
# We're here when the function is loaded remotely (e.g. _Cls.from_name),
|
420
|
-
# same as above, but we create the method "Functions" from scratch rather
|
421
|
-
# than hydrate existing ones. TODO(elias): feels complicated - refactor?
|
422
|
-
self._method_functions = {}
|
423
|
-
for (
|
424
|
-
method_name,
|
425
|
-
method_handle_metadata,
|
426
|
-
) in self._class_service_function._method_handle_metadata.items():
|
427
|
-
self._method_functions[method_name] = _Function._new_hydrated(
|
428
|
-
self._class_service_function.object_id, self._client, method_handle_metadata
|
429
|
-
)
|
414
|
+
class_service_function = self._get_class_service_function()
|
415
|
+
assert class_service_function.is_hydrated
|
416
|
+
|
417
|
+
if class_service_function._method_handle_metadata and len(class_service_function._method_handle_metadata):
|
418
|
+
# If we have the metadata on the class service function
|
419
|
+
# This should be the case for any loaded class (remote or local) as of v0.67
|
420
|
+
method_metadata = class_service_function._method_handle_metadata
|
430
421
|
else:
|
431
|
-
# Method metadata stored on the backend Cls object - pre 0.67
|
422
|
+
# Method metadata stored on the backend Cls object - pre 0.67 lookups
|
432
423
|
# Can be removed when v0.67 is least supported version (all metadata is on the function)
|
433
|
-
|
424
|
+
method_metadata = {}
|
434
425
|
for method in metadata.methods:
|
435
|
-
|
436
|
-
|
437
|
-
)
|
426
|
+
method_metadata[method.function_name] = method.function_handle_metadata
|
427
|
+
self._method_metadata = method_metadata
|
438
428
|
|
439
429
|
@staticmethod
|
440
430
|
def validate_construction_mechanism(user_cls):
|
@@ -467,19 +457,17 @@ class _Cls(_Object, type_prefix="cs"):
|
|
467
457
|
# validate signature
|
468
458
|
_Cls.validate_construction_mechanism(user_cls)
|
469
459
|
|
470
|
-
|
471
|
-
partial_functions: dict[str, _PartialFunction] = _find_partial_methods_for_user_cls(
|
460
|
+
method_partials: dict[str, _PartialFunction] = _find_partial_methods_for_user_cls(
|
472
461
|
user_cls, _PartialFunctionFlags.FUNCTION
|
473
462
|
)
|
474
463
|
|
475
|
-
for method_name, partial_function in
|
476
|
-
method_function = class_service_function._bind_method(user_cls, method_name, partial_function)
|
464
|
+
for method_name, partial_function in method_partials.items():
|
477
465
|
if partial_function.webhook_config is not None:
|
478
|
-
|
466
|
+
full_name = f"{user_cls.__name__}.{method_name}"
|
467
|
+
app._web_endpoints.append(full_name)
|
479
468
|
partial_function.wrapped = True
|
480
|
-
method_functions[method_name] = method_function
|
481
469
|
|
482
|
-
# Disable the warning that
|
470
|
+
# Disable the warning that lifecycle methods are not wrapped
|
483
471
|
for partial_function in _find_partial_methods_for_user_cls(user_cls, ~_PartialFunctionFlags.FUNCTION).values():
|
484
472
|
partial_function.wrapped = True
|
485
473
|
|
@@ -503,7 +491,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
503
491
|
cls._app = app
|
504
492
|
cls._user_cls = user_cls
|
505
493
|
cls._class_service_function = class_service_function
|
506
|
-
cls.
|
494
|
+
cls._method_partials = method_partials
|
507
495
|
cls._callables = callables
|
508
496
|
cls._name = user_cls.__name__
|
509
497
|
return cls
|
@@ -676,7 +664,8 @@ class _Cls(_Object, type_prefix="cs"):
|
|
676
664
|
|
677
665
|
def __getattr__(self, k):
|
678
666
|
# TODO: remove this method - access to attributes on classes (not instances) should be discouraged
|
679
|
-
if k in self.
|
667
|
+
if not self._is_local() or k in self._method_partials:
|
668
|
+
# if not local (== k *could* be a method) or it is local and we know k is a method
|
680
669
|
deprecation_warning(
|
681
670
|
(2025, 1, 13),
|
682
671
|
"Usage of methods directly on the class will soon be deprecated, "
|
@@ -684,7 +673,8 @@ class _Cls(_Object, type_prefix="cs"):
|
|
684
673
|
f"{self._name}().{k} instead of {self._name}.{k}",
|
685
674
|
pending=True,
|
686
675
|
)
|
687
|
-
return self
|
676
|
+
return getattr(self(), k)
|
677
|
+
# non-method attribute access on local class - arguably shouldn't be used either:
|
688
678
|
return getattr(self._user_cls, k)
|
689
679
|
|
690
680
|
def _is_local(self) -> bool:
|
modal/cls.pyi
CHANGED
@@ -22,9 +22,7 @@ T = typing.TypeVar("T")
|
|
22
22
|
|
23
23
|
def _use_annotation_parameters(user_cls: type) -> bool: ...
|
24
24
|
def _get_class_constructor_signature(user_cls: type) -> inspect.Signature: ...
|
25
|
-
def _bind_instance_method(
|
26
|
-
service_function: modal._functions._Function, class_bound_method: modal._functions._Function
|
27
|
-
): ...
|
25
|
+
def _bind_instance_method(cls: _Cls, service_function: modal._functions._Function, method_name: str): ...
|
28
26
|
|
29
27
|
class _Obj:
|
30
28
|
_cls: _Cls
|
@@ -95,13 +93,14 @@ class Obj:
|
|
95
93
|
def __getattr__(self, k): ...
|
96
94
|
|
97
95
|
class _Cls(modal._object._Object):
|
98
|
-
_user_cls: typing.Optional[type]
|
99
96
|
_class_service_function: typing.Optional[modal._functions._Function]
|
100
|
-
_method_functions: typing.Optional[dict[str, modal._functions._Function]]
|
101
97
|
_options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
|
102
|
-
_callables: dict[str, collections.abc.Callable[..., typing.Any]]
|
103
98
|
_app: typing.Optional[modal.app._App]
|
104
99
|
_name: typing.Optional[str]
|
100
|
+
_method_metadata: typing.Optional[dict[str, modal_proto.api_pb2.FunctionHandleMetadata]]
|
101
|
+
_user_cls: typing.Optional[type]
|
102
|
+
_method_partials: typing.Optional[dict[str, modal._partial_function._PartialFunction]]
|
103
|
+
_callables: dict[str, collections.abc.Callable[..., typing.Any]]
|
105
104
|
|
106
105
|
def _initialize_from_empty(self): ...
|
107
106
|
def _initialize_from_other(self, other: _Cls): ...
|
@@ -152,13 +151,14 @@ class _Cls(modal._object._Object):
|
|
152
151
|
def _is_local(self) -> bool: ...
|
153
152
|
|
154
153
|
class Cls(modal.object.Object):
|
155
|
-
_user_cls: typing.Optional[type]
|
156
154
|
_class_service_function: typing.Optional[modal.functions.Function]
|
157
|
-
_method_functions: typing.Optional[dict[str, modal.functions.Function]]
|
158
155
|
_options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
|
159
|
-
_callables: dict[str, collections.abc.Callable[..., typing.Any]]
|
160
156
|
_app: typing.Optional[modal.app.App]
|
161
157
|
_name: typing.Optional[str]
|
158
|
+
_method_metadata: typing.Optional[dict[str, modal_proto.api_pb2.FunctionHandleMetadata]]
|
159
|
+
_user_cls: typing.Optional[type]
|
160
|
+
_method_partials: typing.Optional[dict[str, modal.partial_function.PartialFunction]]
|
161
|
+
_callables: dict[str, collections.abc.Callable[..., typing.Any]]
|
162
162
|
|
163
163
|
def __init__(self, *args, **kwargs): ...
|
164
164
|
def _initialize_from_empty(self): ...
|
modal/functions.pyi
CHANGED
@@ -14,7 +14,6 @@ import modal.mount
|
|
14
14
|
import modal.network_file_system
|
15
15
|
import modal.object
|
16
16
|
import modal.parallel_map
|
17
|
-
import modal.partial_function
|
18
17
|
import modal.proxy
|
19
18
|
import modal.retries
|
20
19
|
import modal.schedule
|
@@ -55,7 +54,6 @@ class Function(
|
|
55
54
|
_method_handle_metadata: typing.Optional[dict[str, modal_proto.api_pb2.FunctionHandleMetadata]]
|
56
55
|
|
57
56
|
def __init__(self, *args, **kwargs): ...
|
58
|
-
def _bind_method(self, user_cls, method_name: str, partial_function: modal.partial_function.PartialFunction): ...
|
59
57
|
@staticmethod
|
60
58
|
def from_local(
|
61
59
|
info: modal._utils.function_utils.FunctionInfo,
|
@@ -2,8 +2,8 @@ modal/__init__.py,sha256=8tE3_OhuKF5UkP4dcmMe8NAbLNNJyuwJFNgDQRBgCig,2333
|
|
2
2
|
modal/__main__.py,sha256=CgIjP8m1xJjjd4AXc-delmR6LdBCZclw2A_V38CFIio,2870
|
3
3
|
modal/_clustered_functions.py,sha256=kTf-9YBXY88NutC1akI-gCbvf01RhMPCw-zoOI_YIUE,2700
|
4
4
|
modal/_clustered_functions.pyi,sha256=vllkegc99A0jrUOWa8mdlSbdp6uz36TsHhGxysAOpaQ,771
|
5
|
-
modal/_container_entrypoint.py,sha256=
|
6
|
-
modal/_functions.py,sha256=
|
5
|
+
modal/_container_entrypoint.py,sha256=arhkIoF8nQNfa4iwYGSoqN3QMDg5M38QNAODXC8TlKc,29301
|
6
|
+
modal/_functions.py,sha256=EuB4p-9-__OLovkVHOr4qN-RPtUv-ZgwZJ3XRcUnGhg,71203
|
7
7
|
modal/_ipython.py,sha256=TW1fkVOmZL3YYqdS2YlM1hqpf654Yf8ZyybHdBnlhSw,301
|
8
8
|
modal/_location.py,sha256=joiX-0ZeutEUDTrrqLF1GHXCdVLF-rHzstocbMcd_-k,366
|
9
9
|
modal/_object.py,sha256=ItQcsMNkz9Y3kdTsvfNarbW-paJ2qabDyQ7njaqY0XI,11359
|
@@ -22,11 +22,11 @@ modal/app.py,sha256=o5mHoHtn41nkvskX_ekJkyfG6MXwj5rqerRi_nnPd0w,44725
|
|
22
22
|
modal/app.pyi,sha256=0MMCgskIL4r3eq8oBcfm2lLyeao2gXjS3iXaIfmaJ-o,25959
|
23
23
|
modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
|
24
24
|
modal/client.py,sha256=8SQawr7P1PNUCq1UmJMUQXG2jIo4Nmdcs311XqrNLRE,15276
|
25
|
-
modal/client.pyi,sha256=
|
25
|
+
modal/client.pyi,sha256=nZrDHyVUzG8n8lW3ZjoKG0PLL4xEnypRvhECRCu9fFo,7593
|
26
26
|
modal/cloud_bucket_mount.py,sha256=YOe9nnvSr4ZbeCn587d7_VhE9IioZYRvF9VYQTQux08,5914
|
27
27
|
modal/cloud_bucket_mount.pyi,sha256=30T3K1a89l6wzmEJ_J9iWv9SknoGqaZDx59Xs-ZQcmk,1607
|
28
|
-
modal/cls.py,sha256=
|
29
|
-
modal/cls.pyi,sha256=
|
28
|
+
modal/cls.py,sha256=wztMTYkhJyW9iUVqx4_Gga4bJJpUiPgGsS6iacUqy-A,30001
|
29
|
+
modal/cls.pyi,sha256=4Ms1i4Wty1qe49Dh_wsGhJDCiJz7t-XGqXLcpzwhUqs,9084
|
30
30
|
modal/config.py,sha256=XT1W4Y9PVkbYMAXjJRshvQEPDhZmnfW_ZRMwl8XKoqA,11149
|
31
31
|
modal/container_process.py,sha256=WTqLn01dJPVkPpwR_0w_JH96ceN5mV4TGtiu1ZR2RRA,6108
|
32
32
|
modal/container_process.pyi,sha256=Hf0J5JyDdCCXBJSKx6gvkPOo0XrztCm78xzxamtzUjQ,2828
|
@@ -41,7 +41,7 @@ modal/file_io.py,sha256=lcMs_E9Xfm0YX1t9U2wNIBPnqHRxmImqjLW1GHqVmyg,20945
|
|
41
41
|
modal/file_io.pyi,sha256=NTRft1tbPSWf9TlWVeZmTlgB5AZ_Zhu2srWIrWr7brk,9445
|
42
42
|
modal/file_pattern_matcher.py,sha256=trosX-Bp7dOubudN1bLLhRAoidWy1TcoaR4Pv8CedWw,6497
|
43
43
|
modal/functions.py,sha256=kcNHvqeGBxPI7Cgd57NIBBghkfbeFJzXO44WW0jSmao,325
|
44
|
-
modal/functions.pyi,sha256=
|
44
|
+
modal/functions.pyi,sha256=p9H14D_n2lAjeVvymu74wDWsL_BnpBrT0BunbAlwviw,14247
|
45
45
|
modal/gpu.py,sha256=Kbhs_u49FaC2Zi0TjCdrpstpRtT5eZgecynmQi5IZVE,6752
|
46
46
|
modal/image.py,sha256=EtYt7_Rjgi71gt_SutqF8KSjnzWgP5P1Z7XsV-eIoFw,91470
|
47
47
|
modal/image.pyi,sha256=Oc2ndYHSdQTcRpZKHSfTdj-m_2oQAsnc2EWTthbNn-s,26380
|
@@ -90,7 +90,7 @@ modal/_runtime/execution_context.py,sha256=E6ofm6j1POXGPxS841X3V7JU6NheVb8OkQc7J
|
|
90
90
|
modal/_runtime/execution_context.pyi,sha256=wQZwMNADExkeNdB9yKX0PPojovxlFHbap3441wAsiMY,634
|
91
91
|
modal/_runtime/gpu_memory_snapshot.py,sha256=tA3m1d1cwnmHpvpCeN_WijDd6n8byn7LWlpicbIxiOI,3144
|
92
92
|
modal/_runtime/telemetry.py,sha256=T1RoAGyjBDr1swiM6pPsGRSITm7LI5FDK18oNXxY08U,5163
|
93
|
-
modal/_runtime/user_code_imports.py,sha256=
|
93
|
+
modal/_runtime/user_code_imports.py,sha256=2-_cVZqhodtEea9wAii-FAfc3dQmbbXZn5WcEZRAEA8,14653
|
94
94
|
modal/_utils/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
|
95
95
|
modal/_utils/app_utils.py,sha256=88BT4TPLWfYAQwKTHcyzNQRHg8n9B-QE2UyJs96iV-0,108
|
96
96
|
modal/_utils/async_utils.py,sha256=5PdDuI1aSwPOI4a3dIvW0DkPqGw6KZN6RtWE18Dzv1E,25079
|
@@ -168,10 +168,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
|
|
168
168
|
modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
169
169
|
modal_version/__init__.py,sha256=wiJQ53c-OMs0Xf1UeXOxQ7FwlV1VzIjnX6o-pRYZ_Pk,470
|
170
170
|
modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
|
171
|
-
modal_version/_version_generated.py,sha256=
|
172
|
-
modal-0.73.
|
173
|
-
modal-0.73.
|
174
|
-
modal-0.73.
|
175
|
-
modal-0.73.
|
176
|
-
modal-0.73.
|
177
|
-
modal-0.73.
|
171
|
+
modal_version/_version_generated.py,sha256=zVwEkqZxoWoBV_snchjrReQmz4UyRGbwD5TcvDFAEn4,149
|
172
|
+
modal-0.73.55.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
|
173
|
+
modal-0.73.55.dist-info/METADATA,sha256=PIFq_bZuCgqDj8hE86RjViA2cHg33n6D2e5pURrTd7w,2452
|
174
|
+
modal-0.73.55.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
175
|
+
modal-0.73.55.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
|
176
|
+
modal-0.73.55.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
|
177
|
+
modal-0.73.55.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|