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.
@@ -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.code_deps is not None: # this is not set for serialized or non-global scope functions
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.code_deps) != len(dep_object_ids):
476
+ if len(service.service_deps) != len(dep_object_ids):
477
477
  raise ExecutionError(
478
- f"Function has {len(service.code_deps)} dependencies"
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.code_deps}\n"
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.code_deps):
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
- backend_timeout = min(OUTPUTS_TIMEOUT, timeout) # refresh backend call every 55s
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
- items_total: Union[int, None] = None # populated when self.run_function() completes
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
- gpus: Union[GPU_T, list[GPU_T]] # TODO(irfansharif): Somehow assert that it's the first kind, in sandboxes
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
- _obj: Optional["modal.cls._Obj"] = None # only set for InstanceServiceFunctions and bound instance methods
383
+ # only set for InstanceServiceFunctions and bound instance methods
384
+ _obj: Optional["modal.cls._Obj"] = None
381
385
 
382
- _webhook_config: Optional[api_pb2.WebhookConfig] = None # this is set in definition scope, only locally
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
- _raw_f: Optional[Callable[..., Any]] # this is set to None for a "class service [function]"
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
- keep_warm: Optional[int] = None, # keep_warm=True is equivalent to keep_warm=1
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
- cluster_size: Optional[int] = None, # Experimental: Clustered functions
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
- serve_mounts = {m for m in all_mounts if m.is_local()} # needed for modal.serve file watching
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
- terminate_containers: bool = False, # if true, containers running the inputs are forcibly terminated
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
- code_deps: Optional[Sequence["modal._object._Object"]]
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
- code_deps: Optional[Sequence["modal._object._Object"]]
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
- code_deps: Optional[Sequence["modal._object._Object"]]
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
- function: Optional[_Function] = None
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
- function = _cls._method_functions.get(
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
- code_deps,
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
- code_deps: Optional[Sequence["modal._object._Object"]]
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
- code_deps = service_function.deps(only_explicit_mounts=True)
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
- code_deps = None
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
- code_deps,
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.54"
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.54"
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, class_bound_method: _Function):
76
- """Binds an "instance service function" to a specific method name.
77
- This "dummy" _Function gets no unique object_id and isn't backend-backed at the moment, since all
78
- it does it forward invocations to the underlying instance_service_function with the specified method,
79
- and we don't support web_config for parametrized methods at the moment.
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
- method_name = class_bound_method._use_method_name
87
-
88
- def hydrate_from_instance_service_function(method_placeholder_fun):
89
- method_placeholder_fun._hydrate_from_other(service_function)
90
- method_placeholder_fun._obj = service_function._obj
91
- method_placeholder_fun._web_url = (
92
- class_bound_method._web_url
93
- ) # TODO: this shouldn't be set when actual parameters are used
94
- method_placeholder_fun._function_name = f"{class_bound_method._function_name}[parametrized]"
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
- fun._info = class_bound_method._info
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 = class_bound_method._app
130
- fun._spec = class_bound_method._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
- def _get_method_bound_function() -> Optional["_Function"]:
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._method_functions is None:
290
- raise ExecutionError("Method is not local and not hydrated")
291
-
292
- if class_bound_method := self._cls._method_functions.get(k, None):
293
- # If we know the user is accessing a *method* and not another attribute,
294
- # we don't have to create an instance of the user class yet.
295
- # This is because it might just be a call to `.remote()` on it which
296
- # doesn't require a local instance.
297
- # As long as we have the service function or params, we can do remote calls
298
- # without calling the constructor of the class in the calling context.
299
- return _bind_instance_method(self._cached_service_function(), class_bound_method)
300
-
301
- return None # The attribute isn't a method
302
-
303
- if self._cls._method_functions is not None:
304
- # We get here with either a hydrated Cls or an unhydrated one with local definition
305
- if method := _get_method_bound_function():
306
- return method
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 = _get_method_bound_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
- repr,
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
- _callables: dict[str, Callable[..., Any]]
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._method_functions = other._method_functions
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) -> "modal.functions._Function":
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._method_functions.keys()
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
- assert self._class_service_function.is_hydrated
403
-
404
- if self._class_service_function._method_handle_metadata and len(
405
- self._class_service_function._method_handle_metadata
406
- ):
407
- # Method metadata stored on the backend Function object (v0.67+)
408
- if self._method_functions:
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
- self._method_functions = {}
424
+ method_metadata = {}
434
425
  for method in metadata.methods:
435
- self._method_functions[method.function_name] = _Function._new_hydrated(
436
- self._class_service_function.object_id, self._client, method.function_handle_metadata
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
- method_functions: dict[str, _Function] = {}
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 partial_functions.items():
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
- app._web_endpoints.append(method_function.tag)
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 these are not wrapped
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._method_functions = method_functions
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._method_functions:
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._method_functions[k]
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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: modal
3
- Version: 0.73.54
3
+ Version: 0.73.55
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -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=YtfJ852XUDtAWBD-yVs99zy933-VBEKQyIngEj36Qcw,29286
6
- modal/_functions.py,sha256=VY9SfEWbcw-kzkALDlg7yHxGlCVVuEaAahH6jAuaNHk,72450
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=LprjbulAA-06i_GnsqddKKY06yDMjwtusjYQcg28VaA,7593
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=nJhJr-YuttOmdNWxtMHcBV-q6iQmY5qXkEy9yO43clY,31130
29
- modal/cls.pyi,sha256=QqYoRKVCKhP_HkHittKpEZ6sqXkEdtEl4n1wMqeoe3M,8897
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=R_I4Foho4htNCFhYc0YKhWoS_jLqZNOrNfTrAA-Clew,14395
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=qDasIvffN3SJjUeT1DHcpN35lWNepnbomS0Z8yD65mA,14740
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=s2jlryO9xPf8xcGboFikDfwaF7F-1rGoYvZQiLDJfRQ,149
172
- modal-0.73.54.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
173
- modal-0.73.54.dist-info/METADATA,sha256=d8cxP_JRVLAGhVsYMGMB3xcOdFTKEy_-mOFT13MlO0I,2452
174
- modal-0.73.54.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
175
- modal-0.73.54.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
176
- modal-0.73.54.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
177
- modal-0.73.54.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
 
3
3
  # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client.
4
- build_number = 54 # git: 786ca3a
4
+ build_number = 55 # git: 4d645ae