modal 0.67.22__py3-none-any.whl → 0.67.42__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.
@@ -478,7 +478,9 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
478
478
  if len(service.code_deps) != len(dep_object_ids):
479
479
  raise ExecutionError(
480
480
  f"Function has {len(service.code_deps)} dependencies"
481
- f" but container got {len(dep_object_ids)} object ids."
481
+ f" but container got {len(dep_object_ids)} object ids.\n"
482
+ f"Code deps: {service.code_deps}\n"
483
+ f"Object ids: {dep_object_ids}"
482
484
  )
483
485
  for object_id, obj in zip(dep_object_ids, service.code_deps):
484
486
  metadata: Message = container_app.object_handle_metadata[object_id]
@@ -908,7 +908,7 @@ class _ContainerIOManager:
908
908
  if self.checkpoint_id:
909
909
  logger.debug(f"Checkpoint ID: {self.checkpoint_id} (Memory Snapshot ID)")
910
910
  else:
911
- logger.debug("No checkpoint ID provided (Memory Snapshot ID)")
911
+ raise ValueError("No checkpoint ID provided for memory snapshot")
912
912
 
913
913
  # Pause heartbeats since they keep the client connection open which causes the snapshotter to crash
914
914
  async with self.heartbeat_condition:
@@ -918,7 +918,7 @@ class _ContainerIOManager:
918
918
  self.heartbeat_condition.notify_all()
919
919
 
920
920
  await self._client.stub.ContainerCheckpoint(
921
- api_pb2.ContainerCheckpointRequest(checkpoint_id=self.checkpoint_id or "")
921
+ api_pb2.ContainerCheckpointRequest(checkpoint_id=self.checkpoint_id)
922
922
  )
923
923
 
924
924
  await self._client._close(prep_for_restore=True)
@@ -248,7 +248,13 @@ class FunctionInfo:
248
248
  def get_globals(self) -> dict[str, Any]:
249
249
  from .._vendor.cloudpickle import _extract_code_globals
250
250
 
251
+ if self.raw_f is None:
252
+ return {}
253
+
251
254
  func = self.raw_f
255
+ while hasattr(func, "__wrapped__") and func is not func.__wrapped__:
256
+ # Unwrap functions decorated using functools.wrapped (potentially multiple times)
257
+ func = func.__wrapped__
252
258
  f_globals_ref = _extract_code_globals(func.__code__)
253
259
  f_globals = {k: func.__globals__[k] for k in f_globals_ref if k in func.__globals__}
254
260
  return f_globals
modal/cli/_traceback.py CHANGED
@@ -1,6 +1,7 @@
1
1
  # Copyright Modal Labs 2024
2
2
  """Helper functions related to displaying tracebacks in the CLI."""
3
3
  import functools
4
+ import re
4
5
  import warnings
5
6
  from typing import Optional
6
7
 
@@ -166,8 +167,12 @@ def highlight_modal_deprecation_warnings() -> None:
166
167
  def showwarning(warning, category, filename, lineno, file=None, line=None):
167
168
  if issubclass(category, (DeprecationError, PendingDeprecationError)):
168
169
  content = str(warning)
169
- date = content[:10]
170
- message = content[11:].strip()
170
+ if re.match(r"^\d{4}-\d{2}-\d{2}", content):
171
+ date = content[:10]
172
+ message = content[11:].strip()
173
+ else:
174
+ date = ""
175
+ message = content
171
176
  try:
172
177
  with open(filename, encoding="utf-8", errors="replace") as code_file:
173
178
  source = code_file.readlines()[lineno - 1].strip()
@@ -178,7 +183,7 @@ def highlight_modal_deprecation_warnings() -> None:
178
183
  panel = Panel(
179
184
  message,
180
185
  style="yellow",
181
- title=f"Modal Deprecation Warning ({date})",
186
+ title=f"Modal Deprecation Warning ({date})" if date else "Modal Deprecation Warning",
182
187
  title_align="left",
183
188
  )
184
189
  Console().print(panel)
modal/cli/app.py CHANGED
@@ -115,7 +115,7 @@ def logs(
115
115
  ```
116
116
 
117
117
  """
118
- app_identifier = warn_on_name_option("stop", app_identifier, name)
118
+ app_identifier = warn_on_name_option("logs", app_identifier, name)
119
119
  app_id = get_app_id(app_identifier, env)
120
120
  stream_app_logs(app_id)
121
121
 
@@ -10,7 +10,7 @@ from grpclib import GRPCError, Status
10
10
  from rich.console import Console
11
11
  from rich.syntax import Syntax
12
12
  from rich.table import Table
13
- from typer import Typer
13
+ from typer import Argument, Typer
14
14
 
15
15
  import modal
16
16
  from modal._location import display_location
@@ -18,7 +18,7 @@ from modal._output import OutputManager, ProgressHandler
18
18
  from modal._utils.async_utils import synchronizer
19
19
  from modal._utils.grpc_utils import retry_transient_errors
20
20
  from modal.cli._download import _volume_download
21
- from modal.cli.utils import ENV_OPTION, display_table, timestamp_to_local
21
+ from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table, timestamp_to_local
22
22
  from modal.client import _Client
23
23
  from modal.environments import ensure_env
24
24
  from modal.network_file_system import _NetworkFileSystem
@@ -217,3 +217,24 @@ async def rm(
217
217
  if exc.status in (Status.NOT_FOUND, Status.INVALID_ARGUMENT):
218
218
  raise UsageError(exc.message)
219
219
  raise
220
+
221
+
222
+ @nfs_cli.command(
223
+ name="delete",
224
+ help="Delete a named, persistent modal.NetworkFileSystem.",
225
+ rich_help_panel="Management",
226
+ )
227
+ @synchronizer.create_blocking
228
+ async def delete(
229
+ nfs_name: str = Argument(help="Name of the modal.NetworkFileSystem to be deleted. Case sensitive"),
230
+ yes: bool = YES_OPTION,
231
+ env: Optional[str] = ENV_OPTION,
232
+ ):
233
+ if not yes:
234
+ typer.confirm(
235
+ f"Are you sure you want to irrevocably delete the modal.NetworkFileSystem '{nfs_name}'?",
236
+ default=False,
237
+ abort=True,
238
+ )
239
+
240
+ await _NetworkFileSystem.delete(label=nfs_name, environment_name=env)
modal/client.py CHANGED
@@ -147,7 +147,7 @@ class _Client:
147
147
  )
148
148
  if resp.warning:
149
149
  ALARM_EMOJI = chr(0x1F6A8)
150
- warnings.warn(f"{ALARM_EMOJI} {resp.warning} {ALARM_EMOJI}", DeprecationError)
150
+ warnings.warn_explicit(f"{ALARM_EMOJI} {resp.warning} {ALARM_EMOJI}", DeprecationError, "<unknown>", 0)
151
151
  except GRPCError as exc:
152
152
  if exc.status == Status.FAILED_PRECONDITION:
153
153
  raise VersionError(
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.67.22"
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.42"
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.67.22"
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.42"
85
85
  ): ...
86
86
  def is_closed(self) -> bool: ...
87
87
  @property
modal/cls.py CHANGED
@@ -72,6 +72,68 @@ 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
+ """mdmd:hidden
77
+
78
+ Binds an "instance service function" to a specific method.
79
+ This "dummy" _Function gets no unique object_id and isn't backend-backed at the moment, since all
80
+ it does it forward invocations to the underlying instance_service_function with the specified method,
81
+ and we don't support web_config for parameterized methods at the moment.
82
+ """
83
+ # TODO(elias): refactor to not use `_from_loader()` as a crutch for lazy-loading the
84
+ # underlying instance_service_function. It's currently used in order to take advantage
85
+ # of resolver logic and get "chained" resolution of lazy loads, even though this thin
86
+ # object itself doesn't need any "loading"
87
+ assert service_function._obj
88
+ method_name = class_bound_method._use_method_name
89
+ full_function_name = f"{class_bound_method._function_name}[parameterized]"
90
+
91
+ def hydrate_from_instance_service_function(method_placeholder_fun):
92
+ method_placeholder_fun._hydrate_from_other(service_function)
93
+ method_placeholder_fun._obj = service_function._obj
94
+ method_placeholder_fun._web_url = (
95
+ class_bound_method._web_url
96
+ ) # TODO: this shouldn't be set when actual parameters are used
97
+ method_placeholder_fun._function_name = full_function_name
98
+ method_placeholder_fun._is_generator = class_bound_method._is_generator
99
+ method_placeholder_fun._cluster_size = class_bound_method._cluster_size
100
+ method_placeholder_fun._use_method_name = method_name
101
+ method_placeholder_fun._is_method = True
102
+
103
+ async def _load(fun: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
104
+ # there is currently no actual loading logic executed to create each method on
105
+ # the *parameterized* instance of a class - it uses the parameter-bound service-function
106
+ # for the instance. This load method just makes sure to set all attributes after the
107
+ # `service_function` has been loaded (it's in the `_deps`)
108
+ hydrate_from_instance_service_function(fun)
109
+
110
+ def _deps():
111
+ if service_function.is_hydrated:
112
+ # without this check, the common service_function will be reloaded by all methods
113
+ # TODO(elias): Investigate if we can fix this multi-loader in the resolver - feels like a bug?
114
+ return []
115
+ return [service_function]
116
+
117
+ rep = f"Method({full_function_name})"
118
+
119
+ fun = _Function._from_loader(
120
+ _load,
121
+ rep,
122
+ deps=_deps,
123
+ hydrate_lazily=True,
124
+ )
125
+ if service_function.is_hydrated:
126
+ # Eager hydration (skip load) if the instance service function is already loaded
127
+ hydrate_from_instance_service_function(fun)
128
+
129
+ fun._info = class_bound_method._info
130
+ fun._obj = service_function._obj
131
+ fun._is_method = True
132
+ fun._app = class_bound_method._app
133
+ fun._spec = class_bound_method._spec
134
+ return fun
135
+
136
+
75
137
  class _Obj:
76
138
  """An instance of a `Cls`, i.e. `Cls("foo", 42)` returns an `Obj`.
77
139
 
@@ -90,10 +152,9 @@ class _Obj:
90
152
 
91
153
  def __init__(
92
154
  self,
93
- user_cls: type,
155
+ user_cls: Optional[type], # this would be None in case of lookups
94
156
  class_service_function: Optional[_Function], # only None for <v0.63 classes
95
157
  classbound_methods: dict[str, _Function],
96
- from_other_workspace: bool,
97
158
  options: Optional[api_pb2.FunctionOptions],
98
159
  args,
99
160
  kwargs,
@@ -107,17 +168,15 @@ class _Obj:
107
168
  if class_service_function:
108
169
  # >= v0.63 classes
109
170
  # first create the singular object function used by all methods on this parameterization
110
- self._instance_service_function = class_service_function._bind_parameters(
111
- self, from_other_workspace, options, args, kwargs
112
- )
171
+ self._instance_service_function = class_service_function._bind_parameters(self, options, args, kwargs)
113
172
  for method_name, class_bound_method in classbound_methods.items():
114
- method = self._instance_service_function._bind_instance_method(class_bound_method)
173
+ method = _bind_instance_method(self._instance_service_function, class_bound_method)
115
174
  self._method_functions[method_name] = method
116
175
  else:
117
176
  # looked up <v0.63 classes - bind each individual method to the new parameters
118
177
  self._instance_service_function = None
119
178
  for method_name, class_bound_method in classbound_methods.items():
120
- method = class_bound_method._bind_parameters(self, from_other_workspace, options, args, kwargs)
179
+ method = class_bound_method._bind_parameters(self, options, args, kwargs)
121
180
  self._method_functions[method_name] = method
122
181
 
123
182
  # Used for construction local object lazily
@@ -247,7 +306,6 @@ class _Cls(_Object, type_prefix="cs"):
247
306
  _method_functions: Optional[dict[str, _Function]] = None # Placeholder _Functions for each method
248
307
  _options: Optional[api_pb2.FunctionOptions]
249
308
  _callables: dict[str, Callable[..., Any]]
250
- _from_other_workspace: Optional[bool] # Functions require FunctionBindParams before invocation.
251
309
  _app: Optional["modal.app._App"] = None # not set for lookups
252
310
 
253
311
  def _initialize_from_empty(self):
@@ -255,7 +313,6 @@ class _Cls(_Object, type_prefix="cs"):
255
313
  self._class_service_function = None
256
314
  self._options = None
257
315
  self._callables = {}
258
- self._from_other_workspace = None
259
316
 
260
317
  def _initialize_from_other(self, other: "_Cls"):
261
318
  self._user_cls = other._user_cls
@@ -263,7 +320,6 @@ class _Cls(_Object, type_prefix="cs"):
263
320
  self._method_functions = other._method_functions
264
321
  self._options = other._options
265
322
  self._callables = other._callables
266
- self._from_other_workspace = other._from_other_workspace
267
323
 
268
324
  def _get_partial_functions(self) -> dict[str, _PartialFunction]:
269
325
  if not self._user_cls:
@@ -277,7 +333,7 @@ class _Cls(_Object, type_prefix="cs"):
277
333
  and self._class_service_function._method_handle_metadata
278
334
  and len(self._class_service_function._method_handle_metadata)
279
335
  ):
280
- # The class only has a class service service function and no method placeholders (v0.67+)
336
+ # The class only has a class service function and no method placeholders (v0.67+)
281
337
  if self._method_functions:
282
338
  # We're here when the Cls is loaded locally (e.g. _Cls.from_local) so the _method_functions mapping is
283
339
  # populated with (un-hydrated) _Function objects
@@ -298,7 +354,7 @@ class _Cls(_Object, type_prefix="cs"):
298
354
  self._method_functions[method_name] = _Function._new_hydrated(
299
355
  self._class_service_function.object_id, self._client, method_handle_metadata
300
356
  )
301
- elif self._class_service_function:
357
+ elif self._class_service_function and self._class_service_function.object_id:
302
358
  # A class with a class service function and method placeholder functions
303
359
  self._method_functions = {}
304
360
  for method in metadata.methods:
@@ -382,7 +438,6 @@ class _Cls(_Object, type_prefix="cs"):
382
438
  cls._class_service_function = class_service_function
383
439
  cls._method_functions = method_functions
384
440
  cls._callables = callables
385
- cls._from_other_workspace = False
386
441
  return cls
387
442
 
388
443
  def _uses_common_service_function(self):
@@ -449,7 +504,6 @@ class _Cls(_Object, type_prefix="cs"):
449
504
 
450
505
  rep = f"Ref({app_name})"
451
506
  cls = cls._from_loader(_load_remote, rep, is_another_app=True)
452
- cls._from_other_workspace = bool(workspace is not None)
453
507
  return cls
454
508
 
455
509
  def with_options(
@@ -543,7 +597,6 @@ class _Cls(_Object, type_prefix="cs"):
543
597
  self._user_cls,
544
598
  self._class_service_function,
545
599
  self._method_functions,
546
- self._from_other_workspace,
547
600
  self._options,
548
601
  args,
549
602
  kwargs,
modal/cls.pyi CHANGED
@@ -19,6 +19,9 @@ T = typing.TypeVar("T")
19
19
 
20
20
  def _use_annotation_parameters(user_cls) -> bool: ...
21
21
  def _get_class_constructor_signature(user_cls: type) -> inspect.Signature: ...
22
+ def _bind_instance_method(
23
+ service_function: modal.functions._Function, class_bound_method: modal.functions._Function
24
+ ): ...
22
25
 
23
26
  class _Obj:
24
27
  _functions: dict[str, modal.functions._Function]
@@ -30,10 +33,9 @@ class _Obj:
30
33
  def _uses_common_service_function(self): ...
31
34
  def __init__(
32
35
  self,
33
- user_cls: type,
36
+ user_cls: typing.Optional[type],
34
37
  class_service_function: typing.Optional[modal.functions._Function],
35
38
  classbound_methods: dict[str, modal.functions._Function],
36
- from_other_workspace: bool,
37
39
  options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
38
40
  args,
39
41
  kwargs,
@@ -58,10 +60,9 @@ class Obj:
58
60
 
59
61
  def __init__(
60
62
  self,
61
- user_cls: type,
63
+ user_cls: typing.Optional[type],
62
64
  class_service_function: typing.Optional[modal.functions.Function],
63
65
  classbound_methods: dict[str, modal.functions.Function],
64
- from_other_workspace: bool,
65
66
  options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
66
67
  args,
67
68
  kwargs,
@@ -90,7 +91,6 @@ class _Cls(modal.object._Object):
90
91
  _method_functions: typing.Optional[dict[str, modal.functions._Function]]
91
92
  _options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
92
93
  _callables: dict[str, typing.Callable[..., typing.Any]]
93
- _from_other_workspace: typing.Optional[bool]
94
94
  _app: typing.Optional[modal.app._App]
95
95
 
96
96
  def _initialize_from_empty(self): ...
@@ -142,7 +142,6 @@ class Cls(modal.object.Object):
142
142
  _method_functions: typing.Optional[dict[str, modal.functions.Function]]
143
143
  _options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
144
144
  _callables: dict[str, typing.Callable[..., typing.Any]]
145
- _from_other_workspace: typing.Optional[bool]
146
145
  _app: typing.Optional[modal.app.App]
147
146
 
148
147
  def __init__(self, *args, **kwargs): ...
modal/functions.py CHANGED
@@ -347,7 +347,7 @@ class _FunctionSpec:
347
347
  volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]]
348
348
  gpus: Union[GPU_T, list[GPU_T]] # TODO(irfansharif): Somehow assert that it's the first kind, in sandboxes
349
349
  cloud: Optional[str]
350
- cpu: Optional[float]
350
+ cpu: Optional[Union[float, tuple[float, float]]]
351
351
  memory: Optional[Union[int, tuple[int, int]]]
352
352
  ephemeral_disk: Optional[int]
353
353
  scheduler_placement: Optional[SchedulerPlacement]
@@ -424,68 +424,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
424
424
  fun._is_method = True
425
425
  return fun
426
426
 
427
- def _bind_instance_method(self, class_bound_method: "_Function"):
428
- """mdmd:hidden
429
-
430
- Binds an "instance service function" to a specific method.
431
- This "dummy" _Function gets no unique object_id and isn't backend-backed at the moment, since all
432
- it does it forward invocations to the underlying instance_service_function with the specified method,
433
- and we don't support web_config for parameterized methods at the moment.
434
- """
435
- # TODO(elias): refactor to not use `_from_loader()` as a crutch for lazy-loading the
436
- # underlying instance_service_function. It's currently used in order to take advantage
437
- # of resolver logic and get "chained" resolution of lazy loads, even though this thin
438
- # object itself doesn't need any "loading"
439
- instance_service_function = self
440
- assert instance_service_function._obj
441
- method_name = class_bound_method._use_method_name
442
- full_function_name = f"{class_bound_method._function_name}[parameterized]"
443
-
444
- def hydrate_from_instance_service_function(method_placeholder_fun):
445
- method_placeholder_fun._hydrate_from_other(instance_service_function)
446
- method_placeholder_fun._obj = instance_service_function._obj
447
- method_placeholder_fun._web_url = (
448
- class_bound_method._web_url
449
- ) # TODO: this shouldn't be set when actual parameters are used
450
- method_placeholder_fun._function_name = full_function_name
451
- method_placeholder_fun._is_generator = class_bound_method._is_generator
452
- method_placeholder_fun._cluster_size = class_bound_method._cluster_size
453
- method_placeholder_fun._use_method_name = method_name
454
- method_placeholder_fun._is_method = True
455
-
456
- async def _load(fun: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
457
- # there is currently no actual loading logic executed to create each method on
458
- # the *parameterized* instance of a class - it uses the parameter-bound service-function
459
- # for the instance. This load method just makes sure to set all attributes after the
460
- # `instance_service_function` has been loaded (it's in the `_deps`)
461
- hydrate_from_instance_service_function(fun)
462
-
463
- def _deps():
464
- if instance_service_function.is_hydrated:
465
- # without this check, the common instance_service_function will be reloaded by all methods
466
- # TODO(elias): Investigate if we can fix this multi-loader in the resolver - feels like a bug?
467
- return []
468
- return [instance_service_function]
469
-
470
- rep = f"Method({full_function_name})"
471
-
472
- fun = _Function._from_loader(
473
- _load,
474
- rep,
475
- deps=_deps,
476
- hydrate_lazily=True,
477
- )
478
- if instance_service_function.is_hydrated:
479
- # Eager hydration (skip load) if the instance service function is already loaded
480
- hydrate_from_instance_service_function(fun)
481
-
482
- fun._info = class_bound_method._info
483
- fun._obj = instance_service_function._obj
484
- fun._is_method = True
485
- fun._app = class_bound_method._app
486
- fun._spec = class_bound_method._spec
487
- return fun
488
-
489
427
  @staticmethod
490
428
  def from_args(
491
429
  info: FunctionInfo,
@@ -510,7 +448,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
510
448
  batch_max_size: Optional[int] = None,
511
449
  batch_wait_ms: Optional[int] = None,
512
450
  container_idle_timeout: Optional[int] = None,
513
- cpu: Optional[float] = None,
451
+ cpu: Optional[Union[float, tuple[float, float]]] = None,
514
452
  keep_warm: Optional[int] = None, # keep_warm=True is equivalent to keep_warm=1
515
453
  cloud: Optional[str] = None,
516
454
  scheduler_placement: Optional[SchedulerPlacement] = None,
@@ -982,7 +920,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
982
920
  def _bind_parameters(
983
921
  self,
984
922
  obj: "modal.cls._Obj",
985
- from_other_workspace: bool,
986
923
  options: Optional[api_pb2.FunctionOptions],
987
924
  args: Sized,
988
925
  kwargs: dict[str, Any],
@@ -993,7 +930,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
993
930
  """
994
931
 
995
932
  # In some cases, reuse the base function, i.e. not create new clones of each method or the "service function"
996
- can_use_parent = len(args) + len(kwargs) == 0 and not from_other_workspace and options is None
933
+ can_use_parent = len(args) + len(kwargs) == 0 and options is None
997
934
  parent = self
998
935
 
999
936
  async def _load(param_bound_func: _Function, resolver: Resolver, existing_object_id: Optional[str]):
modal/functions.pyi CHANGED
@@ -96,7 +96,7 @@ class _FunctionSpec:
96
96
  ]
97
97
  gpus: typing.Union[None, bool, str, modal.gpu._GPUConfig, list[typing.Union[None, bool, str, modal.gpu._GPUConfig]]]
98
98
  cloud: typing.Optional[str]
99
- cpu: typing.Optional[float]
99
+ cpu: typing.Union[float, tuple[float, float], None]
100
100
  memory: typing.Union[int, tuple[int, int], None]
101
101
  ephemeral_disk: typing.Optional[int]
102
102
  scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement]
@@ -117,7 +117,7 @@ class _FunctionSpec:
117
117
  None, bool, str, modal.gpu._GPUConfig, list[typing.Union[None, bool, str, modal.gpu._GPUConfig]]
118
118
  ],
119
119
  cloud: typing.Optional[str],
120
- cpu: typing.Optional[float],
120
+ cpu: typing.Union[float, tuple[float, float], None],
121
121
  memory: typing.Union[int, tuple[int, int], None],
122
122
  ephemeral_disk: typing.Optional[int],
123
123
  scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement],
@@ -150,7 +150,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
150
150
  _method_handle_metadata: typing.Optional[dict[str, modal_proto.api_pb2.FunctionHandleMetadata]]
151
151
 
152
152
  def _bind_method(self, user_cls, method_name: str, partial_function: modal.partial_function._PartialFunction): ...
153
- def _bind_instance_method(self, class_bound_method: _Function): ...
154
153
  @staticmethod
155
154
  def from_args(
156
155
  info: modal._utils.function_utils.FunctionInfo,
@@ -181,7 +180,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
181
180
  batch_max_size: typing.Optional[int] = None,
182
181
  batch_wait_ms: typing.Optional[int] = None,
183
182
  container_idle_timeout: typing.Optional[int] = None,
184
- cpu: typing.Optional[float] = None,
183
+ cpu: typing.Union[float, tuple[float, float], None] = None,
185
184
  keep_warm: typing.Optional[int] = None,
186
185
  cloud: typing.Optional[str] = None,
187
186
  scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
@@ -200,7 +199,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
200
199
  def _bind_parameters(
201
200
  self,
202
201
  obj: modal.cls._Obj,
203
- from_other_workspace: bool,
204
202
  options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
205
203
  args: collections.abc.Sized,
206
204
  kwargs: dict[str, typing.Any],
@@ -320,7 +318,6 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
320
318
 
321
319
  def __init__(self, *args, **kwargs): ...
322
320
  def _bind_method(self, user_cls, method_name: str, partial_function: modal.partial_function.PartialFunction): ...
323
- def _bind_instance_method(self, class_bound_method: Function): ...
324
321
  @staticmethod
325
322
  def from_args(
326
323
  info: modal._utils.function_utils.FunctionInfo,
@@ -351,7 +348,7 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
351
348
  batch_max_size: typing.Optional[int] = None,
352
349
  batch_wait_ms: typing.Optional[int] = None,
353
350
  container_idle_timeout: typing.Optional[int] = None,
354
- cpu: typing.Optional[float] = None,
351
+ cpu: typing.Union[float, tuple[float, float], None] = None,
355
352
  keep_warm: typing.Optional[int] = None,
356
353
  cloud: typing.Optional[str] = None,
357
354
  scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
@@ -370,7 +367,6 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
370
367
  def _bind_parameters(
371
368
  self,
372
369
  obj: modal.cls.Obj,
373
- from_other_workspace: bool,
374
370
  options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
375
371
  args: collections.abc.Sized,
376
372
  kwargs: dict[str, typing.Any],
modal/image.py CHANGED
@@ -677,13 +677,13 @@ class _Image(_Object, type_prefix="im"):
677
677
  context_mount=mount,
678
678
  )
679
679
 
680
- def _add_local_python_packages(self, *packages: str, copy: bool = False) -> "_Image":
681
- """Adds Python package files to containers
680
+ def add_local_python_source(self, *modules: str, copy: bool = False) -> "_Image":
681
+ """Adds locally available Python packages/modules to containers
682
682
 
683
- Adds all files from the specified Python packages to containers running the Image.
683
+ Adds all files from the specified Python package or module to containers running the Image.
684
684
 
685
685
  Packages are added to the `/root` directory of containers, which is on the `PYTHONPATH`
686
- of any executed Modal Functions.
686
+ of any executed Modal Functions, enabling import of the module by that name.
687
687
 
688
688
  By default (`copy=False`), the files are added to containers on startup and are not built into the actual Image,
689
689
  which speeds up deployment.
@@ -693,9 +693,14 @@ class _Image(_Object, type_prefix="im"):
693
693
  required if you want to run additional build steps after this one.
694
694
 
695
695
  **Note:** This excludes all dot-prefixed subdirectories or files and all `.pyc`/`__pycache__` files.
696
- To add full directories with finer control, use `.add_local_dir()` instead.
696
+ To add full directories with finer control, use `.add_local_dir()` instead and specify `/root` as
697
+ the destination directory.
697
698
  """
698
- mount = _Mount.from_local_python_packages(*packages)
699
+
700
+ def only_py_files(filename):
701
+ return filename.endswith(".py")
702
+
703
+ mount = _Mount.from_local_python_packages(*modules, condition=only_py_files)
699
704
  return self._add_mount_layer_or_copy(mount, copy=copy)
700
705
 
701
706
  def copy_local_dir(self, local_path: Union[str, Path], remote_path: Union[str, Path] = ".") -> "_Image":
@@ -1005,8 +1010,8 @@ class _Image(_Object, type_prefix="im"):
1005
1010
  If not provided as argument the path to the lockfile is inferred. However, the
1006
1011
  file has to exist, unless `ignore_lockfile` is set to `True`.
1007
1012
 
1008
- Note that the root project of the poetry project is not installed,
1009
- only the dependencies. For including local packages see `modal.Mount.from_local_python_packages`
1013
+ Note that the root project of the poetry project is not installed, only the dependencies.
1014
+ For including local python source files see `add_local_python_source`
1010
1015
  """
1011
1016
 
1012
1017
  def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
modal/image.pyi CHANGED
@@ -115,7 +115,7 @@ class _Image(modal.object._Object):
115
115
  def copy_local_file(
116
116
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
117
117
  ) -> _Image: ...
118
- def _add_local_python_packages(self, *module_names: str, copy: bool = False) -> _Image: ...
118
+ def add_local_python_source(self, *module_names: str, copy: bool = False) -> _Image: ...
119
119
  def copy_local_dir(
120
120
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "."
121
121
  ) -> _Image: ...
@@ -372,7 +372,7 @@ class Image(modal.object.Object):
372
372
  def copy_local_file(
373
373
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
374
374
  ) -> Image: ...
375
- def _add_local_python_packages(self, *module_names: str, copy: bool = False) -> Image: ...
375
+ def add_local_python_source(self, *module_names: str, copy: bool = False) -> Image: ...
376
376
  def copy_local_dir(
377
377
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "."
378
378
  ) -> Image: ...
modal/io_streams.py CHANGED
@@ -184,7 +184,7 @@ class _StreamReader(Generic[T]):
184
184
 
185
185
  async for message in iterator:
186
186
  if self._stream_type == StreamType.STDOUT and message:
187
- print(message, end="")
187
+ print(message.decode("utf-8"), end="")
188
188
  elif self._stream_type == StreamType.PIPE:
189
189
  self._container_process_buffer.append(message)
190
190
  if message is None:
@@ -221,6 +221,12 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
221
221
  resp = await retry_transient_errors(client.stub.SharedVolumeGetOrCreate, request)
222
222
  return resp.shared_volume_id
223
223
 
224
+ @staticmethod
225
+ async def delete(label: str, client: Optional[_Client] = None, environment_name: Optional[str] = None):
226
+ obj = await _NetworkFileSystem.lookup(label, client=client, environment_name=environment_name)
227
+ req = api_pb2.SharedVolumeDeleteRequest(shared_volume_id=obj.object_id)
228
+ await retry_transient_errors(obj._client.stub.SharedVolumeDelete, req)
229
+
224
230
  @live_method
225
231
  async def write_file(self, remote_path: str, fp: BinaryIO, progress_cb: Optional[Callable[..., Any]] = None) -> int:
226
232
  """Write from a file object to a path on the network file system, atomically.