modal 0.72.5__py3-none-any.whl → 0.72.48__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.
Files changed (73) hide show
  1. modal/_container_entrypoint.py +5 -10
  2. modal/_object.py +297 -0
  3. modal/_resolver.py +7 -5
  4. modal/_runtime/container_io_manager.py +0 -11
  5. modal/_runtime/user_code_imports.py +7 -7
  6. modal/_serialization.py +4 -3
  7. modal/_tunnel.py +1 -1
  8. modal/app.py +14 -61
  9. modal/app.pyi +25 -25
  10. modal/cli/app.py +3 -2
  11. modal/cli/container.py +1 -1
  12. modal/cli/import_refs.py +185 -113
  13. modal/cli/launch.py +10 -5
  14. modal/cli/programs/run_jupyter.py +2 -2
  15. modal/cli/programs/vscode.py +3 -3
  16. modal/cli/run.py +134 -68
  17. modal/client.py +1 -0
  18. modal/client.pyi +18 -14
  19. modal/cloud_bucket_mount.py +4 -0
  20. modal/cloud_bucket_mount.pyi +4 -0
  21. modal/cls.py +33 -5
  22. modal/cls.pyi +20 -5
  23. modal/container_process.pyi +8 -6
  24. modal/dict.py +1 -1
  25. modal/dict.pyi +32 -29
  26. modal/environments.py +1 -1
  27. modal/environments.pyi +2 -1
  28. modal/experimental.py +47 -11
  29. modal/experimental.pyi +29 -0
  30. modal/file_io.pyi +30 -28
  31. modal/file_pattern_matcher.py +3 -4
  32. modal/functions.py +31 -23
  33. modal/functions.pyi +57 -50
  34. modal/gpu.py +19 -26
  35. modal/image.py +47 -19
  36. modal/image.pyi +28 -21
  37. modal/io_streams.pyi +14 -12
  38. modal/mount.py +14 -5
  39. modal/mount.pyi +28 -25
  40. modal/network_file_system.py +7 -7
  41. modal/network_file_system.pyi +27 -24
  42. modal/object.py +2 -265
  43. modal/object.pyi +46 -130
  44. modal/parallel_map.py +2 -2
  45. modal/parallel_map.pyi +10 -7
  46. modal/partial_function.py +22 -3
  47. modal/partial_function.pyi +45 -27
  48. modal/proxy.py +1 -1
  49. modal/proxy.pyi +2 -1
  50. modal/queue.py +1 -1
  51. modal/queue.pyi +26 -23
  52. modal/runner.py +14 -3
  53. modal/sandbox.py +11 -7
  54. modal/sandbox.pyi +30 -27
  55. modal/secret.py +1 -1
  56. modal/secret.pyi +2 -1
  57. modal/token_flow.pyi +6 -4
  58. modal/volume.py +1 -1
  59. modal/volume.pyi +36 -33
  60. {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/METADATA +2 -2
  61. {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/RECORD +73 -71
  62. modal_proto/api.proto +151 -4
  63. modal_proto/api_grpc.py +113 -0
  64. modal_proto/api_pb2.py +998 -795
  65. modal_proto/api_pb2.pyi +430 -11
  66. modal_proto/api_pb2_grpc.py +233 -1
  67. modal_proto/api_pb2_grpc.pyi +75 -3
  68. modal_proto/modal_api_grpc.py +7 -0
  69. modal_version/_version_generated.py +1 -1
  70. {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/LICENSE +0 -0
  71. {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/WHEEL +0 -0
  72. {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/entry_points.txt +0 -0
  73. {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/top_level.txt +0 -0
modal/cli/run.py CHANGED
@@ -7,15 +7,14 @@ import re
7
7
  import shlex
8
8
  import sys
9
9
  import time
10
- import typing
11
10
  from functools import partial
12
11
  from typing import Any, Callable, Optional, get_type_hints
13
12
 
14
13
  import click
15
14
  import typer
15
+ from click import ClickException
16
16
  from typing_extensions import TypedDict
17
17
 
18
- from .. import Cls
19
18
  from ..app import App, LocalEntrypoint
20
19
  from ..config import config
21
20
  from ..environments import ensure_env
@@ -26,7 +25,7 @@ from ..output import enable_output
26
25
  from ..runner import deploy_app, interactive_shell, run_app
27
26
  from ..serving import serve_app
28
27
  from ..volume import Volume
29
- from .import_refs import import_app, import_function
28
+ from .import_refs import CLICommand, MethodReference, _get_runnable_app, import_and_filter, import_app, parse_import_ref
30
29
  from .utils import ENV_OPTION, ENV_OPTION_HELP, is_tty, stream_app_logs
31
30
 
32
31
 
@@ -145,39 +144,7 @@ def _write_local_result(result_path: str, res: Any):
145
144
  fid.write(res)
146
145
 
147
146
 
148
- def _get_click_command_for_function(app: App, function_tag):
149
- function = app.registered_functions.get(function_tag)
150
- if not function or (isinstance(function, Function) and function.info.user_cls is not None):
151
- # This is either a function_tag for a class method function (e.g MyClass.foo) or a function tag for a
152
- # class service function (MyClass.*)
153
- class_name, method_name = function_tag.rsplit(".", 1)
154
- if not function:
155
- function = app.registered_functions.get(f"{class_name}.*")
156
- assert isinstance(function, Function)
157
- function = typing.cast(Function, function)
158
- if function.is_generator:
159
- raise InvalidError("`modal run` is not supported for generator functions")
160
-
161
- signature: dict[str, ParameterMetadata]
162
- cls: Optional[Cls] = None
163
- if function.info.user_cls is not None:
164
- cls = typing.cast(Cls, app.registered_classes[class_name])
165
- cls_signature = _get_signature(function.info.user_cls)
166
- if method_name == "*":
167
- method_names = list(cls._get_partial_functions().keys())
168
- if len(method_names) == 1:
169
- method_name = method_names[0]
170
- else:
171
- class_name = function.info.user_cls.__name__
172
- raise click.UsageError(
173
- f"Please specify a specific method of {class_name} to run, e.g. `modal run foo.py::MyClass.bar`" # noqa: E501
174
- )
175
- fun_signature = _get_signature(getattr(cls, method_name).info.raw_f, is_method=True)
176
- signature = dict(**cls_signature, **fun_signature) # Pool all arguments
177
- # TODO(erikbern): assert there's no overlap?
178
- else:
179
- signature = _get_signature(function.info.raw_f)
180
-
147
+ def _make_click_function(app, inner: Callable[[dict[str, Any]], Any]):
181
148
  @click.pass_context
182
149
  def f(ctx, **kwargs):
183
150
  show_progress: bool = ctx.obj["show_progress"]
@@ -188,21 +155,65 @@ def _get_click_command_for_function(app: App, function_tag):
188
155
  environment_name=ctx.obj["env"],
189
156
  interactive=ctx.obj["interactive"],
190
157
  ):
191
- if cls is None:
192
- res = function.remote(**kwargs)
193
- else:
194
- # unpool class and method arguments
195
- # TODO(erikbern): this code is a bit hacky
196
- cls_kwargs = {k: kwargs[k] for k in cls_signature}
197
- fun_kwargs = {k: kwargs[k] for k in fun_signature}
198
-
199
- instance = cls(**cls_kwargs)
200
- method: Function = getattr(instance, method_name)
201
- res = method.remote(**fun_kwargs)
158
+ res = inner(kwargs)
202
159
 
203
160
  if result_path := ctx.obj["result_path"]:
204
161
  _write_local_result(result_path, res)
205
162
 
163
+ return f
164
+
165
+
166
+ def _get_click_command_for_function(app: App, function: Function):
167
+ if function.is_generator:
168
+ raise InvalidError("`modal run` is not supported for generator functions")
169
+
170
+ signature: dict[str, ParameterMetadata] = _get_signature(function.info.raw_f)
171
+
172
+ def _inner(click_kwargs):
173
+ return function.remote(**click_kwargs)
174
+
175
+ f = _make_click_function(app, _inner)
176
+
177
+ with_click_options = _add_click_options(f, signature)
178
+ return click.command(with_click_options)
179
+
180
+
181
+ def _get_click_command_for_cls(app: App, method_ref: MethodReference):
182
+ signature: dict[str, ParameterMetadata]
183
+ cls = method_ref.cls
184
+ method_name = method_ref.method_name
185
+
186
+ cls_signature = _get_signature(cls._get_user_cls())
187
+ partial_functions = cls._get_partial_functions()
188
+
189
+ if method_name in ("*", ""):
190
+ # auto infer method name - not sure if we have to support this...
191
+ method_names = list(partial_functions.keys())
192
+ if len(method_names) == 1:
193
+ method_name = method_names[0]
194
+ else:
195
+ raise click.UsageError(
196
+ f"Please specify a specific method of {cls._get_name()} to run, "
197
+ f"e.g. `modal run foo.py::MyClass.bar`" # noqa: E501
198
+ )
199
+
200
+ partial_function = partial_functions[method_name]
201
+ fun_signature = _get_signature(partial_function._get_raw_f(), is_method=True)
202
+
203
+ # TODO(erikbern): assert there's no overlap?
204
+ signature = dict(**cls_signature, **fun_signature) # Pool all arguments
205
+
206
+ def _inner(click_kwargs):
207
+ # unpool class and method arguments
208
+ # TODO(erikbern): this code is a bit hacky
209
+ cls_kwargs = {k: click_kwargs[k] for k in cls_signature}
210
+ fun_kwargs = {k: click_kwargs[k] for k in fun_signature}
211
+
212
+ instance = cls(**cls_kwargs)
213
+ method: Function = getattr(instance, method_name)
214
+ return method.remote(**fun_kwargs)
215
+
216
+ f = _make_click_function(app, _inner)
206
217
  with_click_options = _add_click_options(f, signature)
207
218
  return click.command(with_click_options)
208
219
 
@@ -242,6 +253,15 @@ def _get_click_command_for_local_entrypoint(app: App, entrypoint: LocalEntrypoin
242
253
  return click.command(with_click_options)
243
254
 
244
255
 
256
+ def _get_runnable_list(all_usable_commands: list[CLICommand]) -> str:
257
+ usable_command_lines = []
258
+ for cmd in all_usable_commands:
259
+ cmd_names = " / ".join(cmd.names)
260
+ usable_command_lines.append(cmd_names)
261
+
262
+ return "\n".join(usable_command_lines)
263
+
264
+
245
265
  class RunGroup(click.Group):
246
266
  def get_command(self, ctx, func_ref):
247
267
  # note: get_command here is run before the "group logic" in the `run` logic below
@@ -249,16 +269,39 @@ class RunGroup(click.Group):
249
269
  # needs to be handled here, and not in the `run` logic below
250
270
  ctx.ensure_object(dict)
251
271
  ctx.obj["env"] = ensure_env(ctx.params["env"])
252
- function_or_entrypoint = import_function(func_ref, accept_local_entrypoint=True, base_cmd="modal run")
253
- app: App = function_or_entrypoint.app
272
+
273
+ import_ref = parse_import_ref(func_ref)
274
+ runnable, all_usable_commands = import_and_filter(
275
+ import_ref, accept_local_entrypoint=True, accept_webhook=False
276
+ )
277
+ if not runnable:
278
+ help_header = (
279
+ "Specify a Modal Function or local entrypoint to run. E.g.\n"
280
+ f"> modal run {import_ref.file_or_module}::my_function [..args]"
281
+ )
282
+
283
+ if all_usable_commands:
284
+ help_footer = f"'{import_ref.file_or_module}' has the following functions and local entrypoints:\n"
285
+ help_footer += _get_runnable_list(all_usable_commands)
286
+ else:
287
+ help_footer = f"'{import_ref.file_or_module}' has no functions or local entrypoints."
288
+
289
+ raise ClickException(f"{help_header}\n\n{help_footer}")
290
+
291
+ app = _get_runnable_app(runnable)
292
+
254
293
  if app.description is None:
255
294
  app.set_description(_get_clean_app_description(func_ref))
256
- if isinstance(function_or_entrypoint, LocalEntrypoint):
257
- click_command = _get_click_command_for_local_entrypoint(app, function_or_entrypoint)
258
- else:
259
- tag = function_or_entrypoint.info.get_tag()
260
- click_command = _get_click_command_for_function(app, tag)
261
295
 
296
+ if isinstance(runnable, LocalEntrypoint):
297
+ click_command = _get_click_command_for_local_entrypoint(app, runnable)
298
+ elif isinstance(runnable, Function):
299
+ click_command = _get_click_command_for_function(app, runnable)
300
+ elif isinstance(runnable, MethodReference):
301
+ click_command = _get_click_command_for_cls(app, runnable)
302
+ else:
303
+ # This should be unreachable...
304
+ raise ValueError(f"{runnable} is neither function, local entrypoint or class/method")
262
305
  return click_command
263
306
 
264
307
 
@@ -409,36 +452,36 @@ def shell(
409
452
  ):
410
453
  """Run a command or interactive shell inside a Modal container.
411
454
 
412
- \b**Examples:**
455
+ **Examples:**
413
456
 
414
- \bStart an interactive shell inside the default Debian-based image:
457
+ Start an interactive shell inside the default Debian-based image:
415
458
 
416
- \b```
459
+ ```
417
460
  modal shell
418
461
  ```
419
462
 
420
- \bStart an interactive shell with the spec for `my_function` in your App
463
+ Start an interactive shell with the spec for `my_function` in your App
421
464
  (uses the same image, volumes, mounts, etc.):
422
465
 
423
- \b```
466
+ ```
424
467
  modal shell hello_world.py::my_function
425
468
  ```
426
469
 
427
- \bOr, if you're using a [modal.Cls](/docs/reference/modal.Cls), you can refer to a `@modal.method` directly:
470
+ Or, if you're using a [modal.Cls](/docs/reference/modal.Cls), you can refer to a `@modal.method` directly:
428
471
 
429
- \b```
472
+ ```
430
473
  modal shell hello_world.py::MyClass.my_method
431
474
  ```
432
475
 
433
476
  Start a `python` shell:
434
477
 
435
- \b```
478
+ ```
436
479
  modal shell hello_world.py --cmd=python
437
480
  ```
438
481
 
439
- \bRun a command with your function's spec and pipe the output to a file:
482
+ Run a command with your function's spec and pipe the output to a file:
440
483
 
441
- \b```
484
+ ```
442
485
  modal shell hello_world.py -c 'uv pip list' > env.txt
443
486
  ```
444
487
  """
@@ -464,11 +507,34 @@ def shell(
464
507
  exec(container_id=container_or_function, command=shlex.split(cmd), pty=pty)
465
508
  return
466
509
 
467
- function = import_function(
468
- container_or_function, accept_local_entrypoint=False, accept_webhook=True, base_cmd="modal shell"
510
+ import_ref = parse_import_ref(container_or_function)
511
+ runnable, all_usable_commands = import_and_filter(
512
+ import_ref, accept_local_entrypoint=False, accept_webhook=True
469
513
  )
470
- assert isinstance(function, Function)
471
- function_spec: _FunctionSpec = function.spec
514
+ if not runnable:
515
+ help_header = (
516
+ "Specify a Modal function to start a shell session for. E.g.\n"
517
+ f"> modal shell {import_ref.file_or_module}::my_function"
518
+ )
519
+
520
+ if all_usable_commands:
521
+ help_footer = f"The selected module '{import_ref.file_or_module}' has the following choices:\n\n"
522
+ help_footer += _get_runnable_list(all_usable_commands)
523
+ else:
524
+ help_footer = f"The selected module '{import_ref.file_or_module}' has no Modal functions or classes."
525
+
526
+ raise ClickException(f"{help_header}\n\n{help_footer}")
527
+
528
+ function_spec: _FunctionSpec
529
+ if isinstance(runnable, MethodReference):
530
+ # TODO: let users specify a class instead of a method, since they use the same environment
531
+ class_service_function = runnable.cls._get_class_service_function()
532
+ function_spec = class_service_function.spec
533
+ elif isinstance(runnable, Function):
534
+ function_spec = runnable.spec
535
+ else:
536
+ raise ValueError("Referenced entity is not a Modal function or class")
537
+
472
538
  start_shell = partial(
473
539
  interactive_shell,
474
540
  image=function_spec.image,
modal/client.py CHANGED
@@ -73,6 +73,7 @@ class _Client:
73
73
  _cancellation_context: TaskContext
74
74
  _cancellation_context_event_loop: asyncio.AbstractEventLoop = None
75
75
  _stub: Optional[api_grpc.ModalClientStub]
76
+ _snapshotted: bool
76
77
 
77
78
  def __init__(
78
79
  self,
modal/client.pyi CHANGED
@@ -24,9 +24,10 @@ class _Client:
24
24
  _cancellation_context: modal._utils.async_utils.TaskContext
25
25
  _cancellation_context_event_loop: asyncio.events.AbstractEventLoop
26
26
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
27
+ _snapshotted: bool
27
28
 
28
29
  def __init__(
29
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.5"
30
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.48"
30
31
  ): ...
31
32
  def is_closed(self) -> bool: ...
32
33
  @property
@@ -73,37 +74,40 @@ class _Client:
73
74
  ],
74
75
  ) -> collections.abc.AsyncGenerator[typing.Any, None]: ...
75
76
 
77
+ SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
78
+
76
79
  class Client:
77
80
  _client_from_env: typing.ClassVar[typing.Optional[Client]]
78
81
  _client_from_env_lock: typing.ClassVar[typing.Optional[asyncio.locks.Lock]]
79
82
  _cancellation_context: modal._utils.async_utils.TaskContext
80
83
  _cancellation_context_event_loop: asyncio.events.AbstractEventLoop
81
84
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
85
+ _snapshotted: bool
82
86
 
83
87
  def __init__(
84
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.5"
88
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.48"
85
89
  ): ...
86
90
  def is_closed(self) -> bool: ...
87
91
  @property
88
92
  def stub(self) -> modal_proto.modal_api_grpc.ModalClientModal: ...
89
93
 
90
- class ___open_spec(typing_extensions.Protocol):
94
+ class ___open_spec(typing_extensions.Protocol[SUPERSELF]):
91
95
  def __call__(self): ...
92
96
  async def aio(self): ...
93
97
 
94
- _open: ___open_spec
98
+ _open: ___open_spec[typing_extensions.Self]
95
99
 
96
- class ___close_spec(typing_extensions.Protocol):
100
+ class ___close_spec(typing_extensions.Protocol[SUPERSELF]):
97
101
  def __call__(self, prep_for_restore: bool = False): ...
98
102
  async def aio(self, prep_for_restore: bool = False): ...
99
103
 
100
- _close: ___close_spec
104
+ _close: ___close_spec[typing_extensions.Self]
101
105
 
102
- class __hello_spec(typing_extensions.Protocol):
106
+ class __hello_spec(typing_extensions.Protocol[SUPERSELF]):
103
107
  def __call__(self): ...
104
108
  async def aio(self): ...
105
109
 
106
- hello: __hello_spec
110
+ hello: __hello_spec[typing_extensions.Self]
107
111
 
108
112
  def __enter__(self): ...
109
113
  async def __aenter__(self): ...
@@ -120,23 +124,23 @@ class Client:
120
124
  @classmethod
121
125
  def set_env_client(cls, client: typing.Optional[Client]): ...
122
126
 
123
- class ___call_safely_spec(typing_extensions.Protocol):
127
+ class ___call_safely_spec(typing_extensions.Protocol[SUPERSELF]):
124
128
  def __call__(self, coro, readable_method: str): ...
125
129
  async def aio(self, coro, readable_method: str): ...
126
130
 
127
- _call_safely: ___call_safely_spec
131
+ _call_safely: ___call_safely_spec[typing_extensions.Self]
128
132
 
129
- class ___reset_on_pid_change_spec(typing_extensions.Protocol):
133
+ class ___reset_on_pid_change_spec(typing_extensions.Protocol[SUPERSELF]):
130
134
  def __call__(self): ...
131
135
  async def aio(self): ...
132
136
 
133
- _reset_on_pid_change: ___reset_on_pid_change_spec
137
+ _reset_on_pid_change: ___reset_on_pid_change_spec[typing_extensions.Self]
134
138
 
135
- class ___get_grpclib_method_spec(typing_extensions.Protocol):
139
+ class ___get_grpclib_method_spec(typing_extensions.Protocol[SUPERSELF]):
136
140
  def __call__(self, method_name: str) -> typing.Any: ...
137
141
  async def aio(self, method_name: str) -> typing.Any: ...
138
142
 
139
- _get_grpclib_method: ___get_grpclib_method_spec
143
+ _get_grpclib_method: ___get_grpclib_method_spec[typing_extensions.Self]
140
144
 
141
145
  async def _call_unary(
142
146
  self,
@@ -112,6 +112,9 @@ class _CloudBucketMount:
112
112
  # If the bucket is publicly accessible, the secret is unnecessary and can be omitted.
113
113
  secret: Optional[_Secret] = None
114
114
 
115
+ # Role ARN used for using OIDC authentication to access a cloud bucket.
116
+ oidc_auth_role_arn: Optional[str] = None
117
+
115
118
  read_only: bool = False
116
119
  requester_pays: bool = False
117
120
 
@@ -155,6 +158,7 @@ def cloud_bucket_mounts_to_proto(mounts: list[tuple[str, _CloudBucketMount]]) ->
155
158
  bucket_type=bucket_type,
156
159
  requester_pays=mount.requester_pays,
157
160
  key_prefix=key_prefix,
161
+ oidc_auth_role_arn=mount.oidc_auth_role_arn,
158
162
  )
159
163
  cloud_bucket_mounts.append(cloud_bucket_mount)
160
164
 
@@ -7,6 +7,7 @@ class _CloudBucketMount:
7
7
  bucket_endpoint_url: typing.Optional[str]
8
8
  key_prefix: typing.Optional[str]
9
9
  secret: typing.Optional[modal.secret._Secret]
10
+ oidc_auth_role_arn: typing.Optional[str]
10
11
  read_only: bool
11
12
  requester_pays: bool
12
13
 
@@ -16,6 +17,7 @@ class _CloudBucketMount:
16
17
  bucket_endpoint_url: typing.Optional[str] = None,
17
18
  key_prefix: typing.Optional[str] = None,
18
19
  secret: typing.Optional[modal.secret._Secret] = None,
20
+ oidc_auth_role_arn: typing.Optional[str] = None,
19
21
  read_only: bool = False,
20
22
  requester_pays: bool = False,
21
23
  ) -> None: ...
@@ -31,6 +33,7 @@ class CloudBucketMount:
31
33
  bucket_endpoint_url: typing.Optional[str]
32
34
  key_prefix: typing.Optional[str]
33
35
  secret: typing.Optional[modal.secret.Secret]
36
+ oidc_auth_role_arn: typing.Optional[str]
34
37
  read_only: bool
35
38
  requester_pays: bool
36
39
 
@@ -40,6 +43,7 @@ class CloudBucketMount:
40
43
  bucket_endpoint_url: typing.Optional[str] = None,
41
44
  key_prefix: typing.Optional[str] = None,
42
45
  secret: typing.Optional[modal.secret.Secret] = None,
46
+ oidc_auth_role_arn: typing.Optional[str] = None,
43
47
  read_only: bool = False,
44
48
  requester_pays: bool = False,
45
49
  ) -> None: ...
modal/cls.py CHANGED
@@ -11,19 +11,19 @@ from grpclib import GRPCError, Status
11
11
  from modal._utils.function_utils import CLASS_PARAM_TYPE_MAP
12
12
  from modal_proto import api_pb2
13
13
 
14
+ from ._object import _get_environment_name, _Object
14
15
  from ._resolver import Resolver
15
16
  from ._resources import convert_fn_config_to_resources_config
16
17
  from ._serialization import check_valid_cls_constructor_arg
17
18
  from ._traceback import print_server_warnings
18
19
  from ._utils.async_utils import synchronize_api, synchronizer
19
- from ._utils.deprecation import renamed_parameter
20
+ from ._utils.deprecation import deprecation_warning, renamed_parameter
20
21
  from ._utils.grpc_utils import retry_transient_errors
21
22
  from ._utils.mount_utils import validate_volumes
22
23
  from .client import _Client
23
24
  from .exception import ExecutionError, InvalidError, NotFoundError, VersionError
24
25
  from .functions import _Function, _parse_retries
25
26
  from .gpu import GPU_T
26
- from .object import _get_environment_name, _Object
27
27
  from .partial_function import (
28
28
  _find_callables_for_obj,
29
29
  _find_partial_methods_for_user_cls,
@@ -72,9 +72,7 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature:
72
72
 
73
73
 
74
74
  def _bind_instance_method(service_function: _Function, class_bound_method: _Function):
75
- """mdmd:hidden
76
-
77
- Binds an "instance service function" to a specific method name.
75
+ """Binds an "instance service function" to a specific method name.
78
76
  This "dummy" _Function gets no unique object_id and isn't backend-backed at the moment, since all
79
77
  it does it forward invocations to the underlying instance_service_function with the specified method,
80
78
  and we don't support web_config for parameterized methods at the moment.
@@ -129,6 +127,7 @@ def _bind_instance_method(service_function: _Function, class_bound_method: _Func
129
127
  fun._is_method = True
130
128
  fun._app = class_bound_method._app
131
129
  fun._spec = class_bound_method._spec
130
+ fun._is_web_endpoint = class_bound_method._is_web_endpoint
132
131
  return fun
133
132
 
134
133
 
@@ -374,12 +373,14 @@ class _Cls(_Object, type_prefix="cs"):
374
373
  _options: Optional[api_pb2.FunctionOptions]
375
374
  _callables: dict[str, Callable[..., Any]]
376
375
  _app: Optional["modal.app._App"] = None # not set for lookups
376
+ _name: Optional[str]
377
377
 
378
378
  def _initialize_from_empty(self):
379
379
  self._user_cls = None
380
380
  self._class_service_function = None
381
381
  self._options = None
382
382
  self._callables = {}
383
+ self._name = None
383
384
 
384
385
  def _initialize_from_other(self, other: "_Cls"):
385
386
  super()._initialize_from_other(other)
@@ -388,12 +389,29 @@ class _Cls(_Object, type_prefix="cs"):
388
389
  self._method_functions = other._method_functions
389
390
  self._options = other._options
390
391
  self._callables = other._callables
392
+ self._name = other._name
391
393
 
392
394
  def _get_partial_functions(self) -> dict[str, _PartialFunction]:
393
395
  if not self._user_cls:
394
396
  raise AttributeError("You can only get the partial functions of a local Cls instance")
395
397
  return _find_partial_methods_for_user_cls(self._user_cls, _PartialFunctionFlags.all())
396
398
 
399
+ def _get_app(self) -> "modal.app._App":
400
+ return self._app
401
+
402
+ def _get_user_cls(self) -> type:
403
+ return self._user_cls
404
+
405
+ def _get_name(self) -> str:
406
+ return self._name
407
+
408
+ def _get_class_service_function(self) -> "modal.functions._Function":
409
+ return self._class_service_function
410
+
411
+ def _get_method_names(self) -> Collection[str]:
412
+ # returns method names for a *local* class only for now (used by cli)
413
+ return self._method_functions.keys()
414
+
397
415
  def _hydrate_metadata(self, metadata: Message):
398
416
  assert isinstance(metadata, api_pb2.ClassHandleMetadata)
399
417
  if (
@@ -412,6 +430,7 @@ class _Cls(_Object, type_prefix="cs"):
412
430
  self._method_functions[method_name]._hydrate(
413
431
  self._class_service_function.object_id, self._client, method_handle_metadata
414
432
  )
433
+
415
434
  else:
416
435
  # We're here when the function is loaded remotely (e.g. _Cls.from_name)
417
436
  self._method_functions = {}
@@ -506,6 +525,7 @@ class _Cls(_Object, type_prefix="cs"):
506
525
  cls._class_service_function = class_service_function
507
526
  cls._method_functions = method_functions
508
527
  cls._callables = callables
528
+ cls._name = user_cls.__name__
509
529
  return cls
510
530
 
511
531
  def _uses_common_service_function(self):
@@ -576,6 +596,7 @@ class _Cls(_Object, type_prefix="cs"):
576
596
  rep = f"Ref({app_name})"
577
597
  cls = cls._from_loader(_load_remote, rep, is_another_app=True, hydrate_lazily=True)
578
598
  # TODO: when pre 0.63 is phased out, we can set class_service_function here instead
599
+ cls._name = name
579
600
  return cls
580
601
 
581
602
  def with_options(
@@ -681,6 +702,13 @@ class _Cls(_Object, type_prefix="cs"):
681
702
  # Used by CLI and container entrypoint
682
703
  # TODO: remove this method - access to attributes on classes should be discouraged
683
704
  if k in self._method_functions:
705
+ deprecation_warning(
706
+ (2025, 1, 13),
707
+ "Usage of methods directly on the class will soon be deprecated, "
708
+ "instantiate classes before using methods, e.g.:\n"
709
+ f"{self._name}().{k} instead of {self._name}.{k}",
710
+ pending=True,
711
+ )
684
712
  return self._method_functions[k]
685
713
  return getattr(self._user_cls, k)
686
714
 
modal/cls.pyi CHANGED
@@ -1,6 +1,7 @@
1
1
  import collections.abc
2
2
  import google.protobuf.message
3
3
  import inspect
4
+ import modal._object
4
5
  import modal.app
5
6
  import modal.client
6
7
  import modal.functions
@@ -54,6 +55,8 @@ class _Obj:
54
55
  async def _aenter(self): ...
55
56
  def __getattr__(self, k): ...
56
57
 
58
+ SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
59
+
57
60
  class Obj:
58
61
  _cls: Cls
59
62
  _functions: dict[str, modal.functions.Function]
@@ -76,11 +79,11 @@ class Obj:
76
79
  def _get_parameter_values(self) -> dict[str, typing.Any]: ...
77
80
  def _new_user_cls_instance(self): ...
78
81
 
79
- class __keep_warm_spec(typing_extensions.Protocol):
82
+ class __keep_warm_spec(typing_extensions.Protocol[SUPERSELF]):
80
83
  def __call__(self, warm_pool_size: int) -> None: ...
81
84
  async def aio(self, warm_pool_size: int) -> None: ...
82
85
 
83
- keep_warm: __keep_warm_spec
86
+ keep_warm: __keep_warm_spec[typing_extensions.Self]
84
87
 
85
88
  def _cached_user_cls_instance(self): ...
86
89
  def _enter(self): ...
@@ -91,17 +94,23 @@ class Obj:
91
94
  async def _aenter(self): ...
92
95
  def __getattr__(self, k): ...
93
96
 
94
- class _Cls(modal.object._Object):
97
+ class _Cls(modal._object._Object):
95
98
  _user_cls: typing.Optional[type]
96
99
  _class_service_function: typing.Optional[modal.functions._Function]
97
100
  _method_functions: typing.Optional[dict[str, modal.functions._Function]]
98
101
  _options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
99
- _callables: dict[str, typing.Callable[..., typing.Any]]
102
+ _callables: dict[str, collections.abc.Callable[..., typing.Any]]
100
103
  _app: typing.Optional[modal.app._App]
104
+ _name: typing.Optional[str]
101
105
 
102
106
  def _initialize_from_empty(self): ...
103
107
  def _initialize_from_other(self, other: _Cls): ...
104
108
  def _get_partial_functions(self) -> dict[str, modal.partial_function._PartialFunction]: ...
109
+ def _get_app(self) -> modal.app._App: ...
110
+ def _get_user_cls(self) -> type: ...
111
+ def _get_name(self) -> str: ...
112
+ def _get_class_service_function(self) -> modal.functions._Function: ...
113
+ def _get_method_names(self) -> collections.abc.Collection[str]: ...
105
114
  def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
106
115
  @staticmethod
107
116
  def validate_construction_mechanism(user_cls): ...
@@ -147,13 +156,19 @@ class Cls(modal.object.Object):
147
156
  _class_service_function: typing.Optional[modal.functions.Function]
148
157
  _method_functions: typing.Optional[dict[str, modal.functions.Function]]
149
158
  _options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
150
- _callables: dict[str, typing.Callable[..., typing.Any]]
159
+ _callables: dict[str, collections.abc.Callable[..., typing.Any]]
151
160
  _app: typing.Optional[modal.app.App]
161
+ _name: typing.Optional[str]
152
162
 
153
163
  def __init__(self, *args, **kwargs): ...
154
164
  def _initialize_from_empty(self): ...
155
165
  def _initialize_from_other(self, other: Cls): ...
156
166
  def _get_partial_functions(self) -> dict[str, modal.partial_function.PartialFunction]: ...
167
+ def _get_app(self) -> modal.app.App: ...
168
+ def _get_user_cls(self) -> type: ...
169
+ def _get_name(self) -> str: ...
170
+ def _get_class_service_function(self) -> modal.functions.Function: ...
171
+ def _get_method_names(self) -> collections.abc.Collection[str]: ...
157
172
  def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
158
173
  @staticmethod
159
174
  def validate_construction_mechanism(user_cls): ...
@@ -36,6 +36,8 @@ class _ContainerProcess(typing.Generic[T]):
36
36
  async def wait(self) -> int: ...
37
37
  async def attach(self, *, pty: typing.Optional[bool] = None): ...
38
38
 
39
+ SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
40
+
39
41
  class ContainerProcess(typing.Generic[T]):
40
42
  _process_id: typing.Optional[str]
41
43
  _stdout: modal.io_streams.StreamReader[T]
@@ -63,20 +65,20 @@ class ContainerProcess(typing.Generic[T]):
63
65
  @property
64
66
  def returncode(self) -> int: ...
65
67
 
66
- class __poll_spec(typing_extensions.Protocol):
68
+ class __poll_spec(typing_extensions.Protocol[SUPERSELF]):
67
69
  def __call__(self) -> typing.Optional[int]: ...
68
70
  async def aio(self) -> typing.Optional[int]: ...
69
71
 
70
- poll: __poll_spec
72
+ poll: __poll_spec[typing_extensions.Self]
71
73
 
72
- class __wait_spec(typing_extensions.Protocol):
74
+ class __wait_spec(typing_extensions.Protocol[SUPERSELF]):
73
75
  def __call__(self) -> int: ...
74
76
  async def aio(self) -> int: ...
75
77
 
76
- wait: __wait_spec
78
+ wait: __wait_spec[typing_extensions.Self]
77
79
 
78
- class __attach_spec(typing_extensions.Protocol):
80
+ class __attach_spec(typing_extensions.Protocol[SUPERSELF]):
79
81
  def __call__(self, *, pty: typing.Optional[bool] = None): ...
80
82
  async def aio(self, *, pty: typing.Optional[bool] = None): ...
81
83
 
82
- attach: __attach_spec
84
+ attach: __attach_spec[typing_extensions.Self]