modal 0.72.15__py3-none-any.whl → 0.72.17__py3-none-any.whl

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