modal 0.72.58__py3-none-any.whl → 0.73.1__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.
@@ -1017,7 +1017,7 @@ def check_fastapi_pydantic_compatibility(exc: ImportError) -> None:
1017
1017
  if pydantic_version >= (2, 0) and fastapi_version < (0, 100):
1018
1018
  if sys.version_info < (3, 11):
1019
1019
  # https://peps.python.org/pep-0678/
1020
- exc.__notes__ = [note]
1020
+ exc.__notes__ = [note] # type: ignore
1021
1021
  else:
1022
1022
  exc.add_note(note)
1023
1023
  except Exception:
@@ -1,5 +1,6 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import asyncio
3
+ import enum
3
4
  import inspect
4
5
  import os
5
6
  from collections.abc import AsyncGenerator
@@ -319,34 +320,32 @@ class FunctionInfo:
319
320
  These are typically local modules which are imported but not part of the running package
320
321
 
321
322
  """
322
- if self._type == FunctionInfoType.NOTEBOOK:
323
- # Don't auto-mount anything for notebooks.
323
+ if self.is_serialized():
324
+ # Don't auto-mount anything for serialized functions (including notebooks)
324
325
  return []
325
326
 
326
327
  # make sure the function's own entrypoint is included:
327
328
  if self._type == FunctionInfoType.PACKAGE:
328
- if config.get("automount"):
329
- return [_Mount._from_local_python_packages(self.module_name)]
330
- elif not self.is_serialized():
331
- # mount only relevant file and __init__.py:s
332
- return [
333
- _Mount._from_local_dir(
334
- self._base_dir,
335
- remote_path=self._remote_dir,
336
- recursive=True,
337
- condition=entrypoint_only_package_mount_condition(self._file),
338
- )
339
- ]
340
- elif not self.is_serialized():
329
+ top_level_package = self.module_name.split(".")[0]
330
+ # TODO: add deprecation warning if the following entrypoint mount
331
+ # includes non-.py files, since we'll want to migrate to .py-only
332
+ # soon to get it consistent with the `add_local_python_source()`
333
+ # defaults.
334
+ return [_Mount._from_local_python_packages(top_level_package)]
335
+ elif self._type == FunctionInfoType.FILE:
341
336
  remote_path = ROOT_DIR / Path(self._file).name
342
337
  if not _is_modal_path(remote_path):
338
+ # TODO: inspect if this file is already included as part of
339
+ # a package mount, and skip it + reference that package
340
+ # instead if that's the case. This avoids possible module
341
+ # duplication bugs
343
342
  return [
344
343
  _Mount._from_local_file(
345
344
  self._file,
346
345
  remote_path=remote_path,
347
346
  )
348
347
  ]
349
- return []
348
+ return [] # this should never be reached...
350
349
 
351
350
  def get_tag(self):
352
351
  return self.function_name
@@ -622,3 +621,33 @@ class FunctionCreationStatus:
622
621
  f"Custom domain for {method_definition.function_name} => [magenta underline]"
623
622
  f"{custom_domain.url}[/magenta underline]"
624
623
  )
624
+
625
+
626
+ class IncludeSourceMode(enum.Enum):
627
+ INCLUDE_NOTHING = False # can only be set in source, can't be set in config
628
+ INCLUDE_MAIN_PACKAGE = True # also represented by AUTOMOUNT=0 in config
629
+ INCLUDE_FIRST_PARTY = "legacy" # mounts all "local" modules in sys.modules - represented by AUTOMOUNT=1 in config
630
+
631
+
632
+ def get_include_source_mode(function_or_app_specific) -> IncludeSourceMode:
633
+ """Which "automount" behavior should a function use
634
+
635
+ function_or_app_specific: explicit value given in the @function or @cls decorator, in an App constructor, or None
636
+
637
+ If function_or_app_specific is specified, validate and return the IncludeSourceMode
638
+ If function_or_app_specific is None, infer it from config
639
+ """
640
+ if function_or_app_specific is not None:
641
+ if not isinstance(function_or_app_specific, bool):
642
+ raise ValueError(
643
+ f"Invalid `include_source` value: {function_or_app_specific}. Use one of:\n"
644
+ f"True - include function's package source\n"
645
+ f"False - include no Python source (module expected to be present in Image)\n"
646
+ )
647
+
648
+ # explicitly set in app/function
649
+ return IncludeSourceMode(function_or_app_specific)
650
+
651
+ # note that the automount config boolean isn't a 1-1 mapping with include_source!
652
+ legacy_automount_mode: bool = config.get("automount")
653
+ return IncludeSourceMode.INCLUDE_FIRST_PARTY if legacy_automount_mode else IncludeSourceMode.INCLUDE_MAIN_PACKAGE
@@ -7,7 +7,7 @@ Modal, with random seeds, and it supports oneofs, and Protobuf v4.
7
7
 
8
8
  import string
9
9
  from random import Random
10
- from typing import Any, Callable, Optional, TypeVar
10
+ from typing import Any, Callable, Optional, TypeVar, Union
11
11
 
12
12
  from google.protobuf.descriptor import Descriptor, FieldDescriptor
13
13
 
@@ -38,9 +38,9 @@ def _fill(msg, desc: Descriptor, rand: Random) -> None:
38
38
  field: FieldDescriptor
39
39
  oneof_fields: set[str] = set()
40
40
  for oneof in desc.oneofs:
41
- field = rand.choice(list(oneof.fields) + [None])
42
- if field is not None:
43
- oneof_fields.add(field.name)
41
+ oneof_field: Union[FieldDescriptor, None] = rand.choice(list(oneof.fields) + [None])
42
+ if oneof_field is not None:
43
+ oneof_fields.add(oneof_field.name)
44
44
  for field in desc.fields:
45
45
  if field.containing_oneof is not None and field.name not in oneof_fields:
46
46
  continue
modal/app.py CHANGED
@@ -170,6 +170,8 @@ class _App:
170
170
  _running_app: Optional[RunningApp] # Various app info
171
171
  _client: Optional[_Client]
172
172
 
173
+ _include_source_default: Optional[bool] = None
174
+
173
175
  def __init__(
174
176
  self,
175
177
  name: Optional[str] = None,
@@ -178,6 +180,7 @@ class _App:
178
180
  mounts: Sequence[_Mount] = [], # default mounts for all functions
179
181
  secrets: Sequence[_Secret] = [], # default secrets for all functions
180
182
  volumes: dict[Union[str, PurePosixPath], _Volume] = {}, # default volumes for all functions
183
+ include_source: Optional[bool] = None,
181
184
  ) -> None:
182
185
  """Construct a new app, optionally with default image, mounts, secrets, or volumes.
183
186
 
@@ -193,6 +196,7 @@ class _App:
193
196
 
194
197
  self._name = name
195
198
  self._description = name
199
+ self._include_source_default = include_source
196
200
 
197
201
  check_sequence(mounts, _Mount, "`mounts=` has to be a list or tuple of `modal.Mount` objects")
198
202
  check_sequence(secrets, _Secret, "`secrets=` has to be a list or tuple of `modal.Secret` objects")
@@ -603,6 +607,8 @@ class _App:
603
607
  # With `max_inputs = 1`, containers will be single-use.
604
608
  max_inputs: Optional[int] = None,
605
609
  i6pn: Optional[bool] = None, # Whether to enable IPv6 container networking within the region.
610
+ # Whether the function's home package should be included in the image - defaults to True
611
+ include_source: Optional[bool] = None,
606
612
  # Parameters below here are experimental. Use with caution!
607
613
  _experimental_scheduler_placement: Optional[
608
614
  SchedulerPlacement
@@ -751,6 +757,7 @@ class _App:
751
757
  _experimental_proxy_ip=_experimental_proxy_ip,
752
758
  i6pn_enabled=i6pn_enabled,
753
759
  cluster_size=cluster_size, # Experimental: Clustered functions
760
+ include_source=include_source if include_source is not None else self._include_source_default,
754
761
  )
755
762
 
756
763
  self._add_function(function, webhook_config is not None)
@@ -800,6 +807,7 @@ class _App:
800
807
  # Limits the number of inputs a container handles before shutting down.
801
808
  # Use `max_inputs = 1` for single-use containers.
802
809
  max_inputs: Optional[int] = None,
810
+ include_source: Optional[bool] = None,
803
811
  # Parameters below here are experimental. Use with caution!
804
812
  _experimental_scheduler_placement: Optional[
805
813
  SchedulerPlacement
@@ -877,6 +885,7 @@ class _App:
877
885
  block_network=block_network,
878
886
  max_inputs=max_inputs,
879
887
  scheduler_placement=scheduler_placement,
888
+ include_source=include_source if include_source is not None else self._include_source_default,
880
889
  _experimental_buffer_containers=_experimental_buffer_containers,
881
890
  _experimental_proxy_ip=_experimental_proxy_ip,
882
891
  _experimental_custom_scaling_factor=_experimental_custom_scaling_factor,
modal/app.pyi CHANGED
@@ -90,6 +90,7 @@ class _App:
90
90
  _app_id: typing.Optional[str]
91
91
  _running_app: typing.Optional[modal.running_app.RunningApp]
92
92
  _client: typing.Optional[modal.client._Client]
93
+ _include_source_default: typing.Optional[bool]
93
94
 
94
95
  def __init__(
95
96
  self,
@@ -99,6 +100,7 @@ class _App:
99
100
  mounts: collections.abc.Sequence[modal.mount._Mount] = [],
100
101
  secrets: collections.abc.Sequence[modal.secret._Secret] = [],
101
102
  volumes: dict[typing.Union[str, pathlib.PurePosixPath], modal.volume._Volume] = {},
103
+ include_source: typing.Optional[bool] = None,
102
104
  ) -> None: ...
103
105
  @property
104
106
  def name(self) -> typing.Optional[str]: ...
@@ -189,6 +191,7 @@ class _App:
189
191
  block_network: bool = False,
190
192
  max_inputs: typing.Optional[int] = None,
191
193
  i6pn: typing.Optional[bool] = None,
194
+ include_source: typing.Optional[bool] = None,
192
195
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
193
196
  _experimental_buffer_containers: typing.Optional[int] = None,
194
197
  _experimental_proxy_ip: typing.Optional[str] = None,
@@ -232,6 +235,7 @@ class _App:
232
235
  enable_memory_snapshot: bool = False,
233
236
  block_network: bool = False,
234
237
  max_inputs: typing.Optional[int] = None,
238
+ include_source: typing.Optional[bool] = None,
235
239
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
236
240
  _experimental_buffer_containers: typing.Optional[int] = None,
237
241
  _experimental_proxy_ip: typing.Optional[str] = None,
@@ -288,6 +292,7 @@ class App:
288
292
  _app_id: typing.Optional[str]
289
293
  _running_app: typing.Optional[modal.running_app.RunningApp]
290
294
  _client: typing.Optional[modal.client.Client]
295
+ _include_source_default: typing.Optional[bool]
291
296
 
292
297
  def __init__(
293
298
  self,
@@ -297,6 +302,7 @@ class App:
297
302
  mounts: collections.abc.Sequence[modal.mount.Mount] = [],
298
303
  secrets: collections.abc.Sequence[modal.secret.Secret] = [],
299
304
  volumes: dict[typing.Union[str, pathlib.PurePosixPath], modal.volume.Volume] = {},
305
+ include_source: typing.Optional[bool] = None,
300
306
  ) -> None: ...
301
307
  @property
302
308
  def name(self) -> typing.Optional[str]: ...
@@ -419,6 +425,7 @@ class App:
419
425
  block_network: bool = False,
420
426
  max_inputs: typing.Optional[int] = None,
421
427
  i6pn: typing.Optional[bool] = None,
428
+ include_source: typing.Optional[bool] = None,
422
429
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
423
430
  _experimental_buffer_containers: typing.Optional[int] = None,
424
431
  _experimental_proxy_ip: typing.Optional[str] = None,
@@ -462,6 +469,7 @@ class App:
462
469
  enable_memory_snapshot: bool = False,
463
470
  block_network: bool = False,
464
471
  max_inputs: typing.Optional[int] = None,
472
+ include_source: typing.Optional[bool] = None,
465
473
  _experimental_scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
466
474
  _experimental_buffer_containers: typing.Optional[int] = None,
467
475
  _experimental_proxy_ip: typing.Optional[str] = None,
@@ -547,6 +555,7 @@ class _Stub(_App):
547
555
  mounts: collections.abc.Sequence[modal.mount._Mount] = [],
548
556
  secrets: collections.abc.Sequence[modal.secret._Secret] = [],
549
557
  volumes: dict[typing.Union[str, pathlib.PurePosixPath], modal.volume._Volume] = {},
558
+ include_source: typing.Optional[bool] = None,
550
559
  ): ...
551
560
 
552
561
  class Stub(App):
@@ -558,6 +567,7 @@ class Stub(App):
558
567
  mounts: collections.abc.Sequence[modal.mount.Mount] = [],
559
568
  secrets: collections.abc.Sequence[modal.secret.Secret] = [],
560
569
  volumes: dict[typing.Union[str, pathlib.PurePosixPath], modal.volume.Volume] = {},
570
+ include_source: typing.Optional[bool] = None,
561
571
  ) -> None: ...
562
572
 
563
573
  _default_image: modal.image._Image
modal/cli/import_refs.py CHANGED
@@ -109,6 +109,14 @@ class CLICommand:
109
109
  names: list[str]
110
110
  runnable: Runnable
111
111
  is_web_endpoint: bool
112
+ priority: int
113
+
114
+
115
+ class AutoRunPriority:
116
+ MODULE_LOCAL_ENTRYPOINT = 0
117
+ MODULE_FUNCTION = 1
118
+ APP_LOCAL_ENTRYPOINT = 2
119
+ APP_FUNCTION = 3
112
120
 
113
121
 
114
122
  def list_cli_commands(
@@ -127,17 +135,21 @@ def list_cli_commands(
127
135
  apps = cast(list[tuple[str, App]], inspect.getmembers(module, lambda x: isinstance(x, App)))
128
136
 
129
137
  all_runnables: dict[Runnable, list[str]] = defaultdict(list)
138
+ priorities: dict[Runnable, int] = defaultdict(lambda: AutoRunPriority.APP_FUNCTION)
130
139
  for app_name, app in apps:
131
140
  for name, local_entrypoint in app.registered_entrypoints.items():
132
141
  all_runnables[local_entrypoint].append(f"{app_name}.{name}")
142
+ priorities[local_entrypoint] = AutoRunPriority.APP_LOCAL_ENTRYPOINT
133
143
  for name, function in app.registered_functions.items():
134
144
  if name.endswith(".*"):
135
145
  continue
136
146
  all_runnables[function].append(f"{app_name}.{name}")
147
+ priorities[function] = AutoRunPriority.APP_FUNCTION
137
148
  for cls_name, cls in app.registered_classes.items():
138
149
  for method_name in cls._get_method_names():
139
150
  method_ref = MethodReference(cls, method_name)
140
151
  all_runnables[method_ref].append(f"{app_name}.{cls_name}.{method_name}")
152
+ priorities[method_ref] = AutoRunPriority.APP_FUNCTION
141
153
 
142
154
  # If any class or function is exported as a module level object, use that
143
155
  # as the preferred name by putting it first in the list
@@ -150,8 +162,13 @@ def list_cli_commands(
150
162
  for method_name in entity._get_method_names():
151
163
  method_ref = MethodReference(entity, method_name)
152
164
  all_runnables.setdefault(method_ref, []).insert(0, f"{name}.{method_name}")
153
- elif (isinstance(entity, Function) and entity._is_local()) or isinstance(entity, LocalEntrypoint):
165
+ priorities[method_ref] = AutoRunPriority.MODULE_FUNCTION
166
+ elif isinstance(entity, Function) and entity._is_local():
154
167
  all_runnables.setdefault(entity, []).insert(0, name)
168
+ priorities[entity] = AutoRunPriority.MODULE_FUNCTION
169
+ elif isinstance(entity, LocalEntrypoint):
170
+ all_runnables.setdefault(entity, []).insert(0, name)
171
+ priorities[entity] = AutoRunPriority.MODULE_LOCAL_ENTRYPOINT
155
172
 
156
173
  def _is_web_endpoint(runnable: Runnable) -> bool:
157
174
  if isinstance(runnable, Function) and runnable._is_web_endpoint():
@@ -164,7 +181,10 @@ def list_cli_commands(
164
181
 
165
182
  return False
166
183
 
167
- return [CLICommand(names, runnable, _is_web_endpoint(runnable)) for runnable, names in all_runnables.items()]
184
+ return [
185
+ CLICommand(names, runnable, _is_web_endpoint(runnable), priority=priorities[runnable])
186
+ for runnable, names in all_runnables.items()
187
+ ]
168
188
 
169
189
 
170
190
  def filter_cli_commands(
@@ -226,7 +246,7 @@ def import_app(app_ref: str) -> App:
226
246
  error_console = Console(stderr=True)
227
247
  error_console.print(f"[bold red]Could not find Modal app '{object_path}' in {import_path}.[/bold red]")
228
248
 
229
- if object_path is None:
249
+ if not object_path:
230
250
  guidance_msg = Markdown(
231
251
  f"Expected to find an app variable named **`{DEFAULT_APP_NAME}`** (the default app name). "
232
252
  "If your `modal.App` is assigned to a different variable name, "
@@ -308,17 +328,19 @@ def import_and_filter(
308
328
  filtered_commands = filter_cli_commands(
309
329
  cli_commands, import_ref.object_path, accept_local_entrypoint, accept_webhook
310
330
  )
331
+
311
332
  all_usable_commands = filter_cli_commands(cli_commands, "", accept_local_entrypoint, accept_webhook)
312
333
 
313
- if len(filtered_commands) == 1:
314
- cli_command = filtered_commands[0]
315
- return cli_command.runnable, all_usable_commands
334
+ if filtered_commands:
335
+ # if there is a single command with "highest run prio" - use that
336
+ filtered_commands_by_prio = defaultdict(list)
337
+ for cmd in filtered_commands:
338
+ filtered_commands_by_prio[cmd.priority].append(cmd)
316
339
 
317
- # we are here if there is more than one matching function
318
- if accept_local_entrypoint:
319
- local_entrypoint_cmds = [cmd for cmd in filtered_commands if isinstance(cmd.runnable, LocalEntrypoint)]
320
- if len(local_entrypoint_cmds) == 1:
321
- # if there is a single local entrypoint - use that
322
- return local_entrypoint_cmds[0].runnable, all_usable_commands
340
+ _, highest_prio_commands = min(filtered_commands_by_prio.items())
341
+ if len(highest_prio_commands) == 1:
342
+ cli_command = highest_prio_commands[0]
343
+ return cli_command.runnable, all_usable_commands
323
344
 
345
+ # otherwise, just return the list of all commands
324
346
  return None, all_usable_commands
modal/client.pyi CHANGED
@@ -27,7 +27,7 @@ class _Client:
27
27
  _snapshotted: bool
28
28
 
29
29
  def __init__(
30
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.58"
30
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.73.1"
31
31
  ): ...
32
32
  def is_closed(self) -> bool: ...
33
33
  @property
@@ -85,7 +85,7 @@ class Client:
85
85
  _snapshotted: bool
86
86
 
87
87
  def __init__(
88
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.58"
88
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.73.1"
89
89
  ): ...
90
90
  def is_closed(self) -> bool: ...
91
91
  @property
@@ -144,7 +144,6 @@ class FilePatternMatcher(_AbstractPatternMatcher):
144
144
  library. The reason is that `Matches()` in the original library is
145
145
  deprecated due to buggy behavior.
146
146
  """
147
-
148
147
  matched = False
149
148
  file_path = os.path.normpath(file_path)
150
149
  if file_path == ".":
modal/functions.py CHANGED
@@ -8,13 +8,7 @@ import warnings
8
8
  from collections.abc import AsyncGenerator, Collection, Sequence, Sized
9
9
  from dataclasses import dataclass
10
10
  from pathlib import PurePosixPath
11
- from typing import (
12
- TYPE_CHECKING,
13
- Any,
14
- Callable,
15
- Optional,
16
- Union,
17
- )
11
+ from typing import TYPE_CHECKING, Any, Callable, Optional, Union
18
12
 
19
13
  import typing_extensions
20
14
  from google.protobuf.message import Message
@@ -48,10 +42,12 @@ from ._utils.function_utils import (
48
42
  OUTPUTS_TIMEOUT,
49
43
  FunctionCreationStatus,
50
44
  FunctionInfo,
45
+ IncludeSourceMode,
51
46
  _create_input,
52
47
  _process_result,
53
48
  _stream_function_call_data,
54
49
  get_function_type,
50
+ get_include_source_mode,
55
51
  is_async,
56
52
  )
57
53
  from ._utils.grpc_utils import retry_transient_errors
@@ -474,6 +470,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
474
470
  cluster_size: Optional[int] = None, # Experimental: Clustered functions
475
471
  max_inputs: Optional[int] = None,
476
472
  ephemeral_disk: Optional[int] = None,
473
+ # current default: first-party, future default: main-package
474
+ include_source: Optional[bool] = None,
477
475
  _experimental_buffer_containers: Optional[int] = None,
478
476
  _experimental_proxy_ip: Optional[str] = None,
479
477
  _experimental_custom_scaling_factor: Optional[float] = None,
@@ -500,7 +498,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
500
498
  explicit_mounts = mounts
501
499
 
502
500
  if is_local():
503
- entrypoint_mounts = info.get_entrypoint_mount()
501
+ include_source_mode = get_include_source_mode(include_source)
502
+ if include_source_mode != IncludeSourceMode.INCLUDE_NOTHING:
503
+ entrypoint_mounts = info.get_entrypoint_mount()
504
+ else:
505
+ entrypoint_mounts = []
504
506
 
505
507
  all_mounts = [
506
508
  _get_client_mount(),
@@ -508,12 +510,15 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
508
510
  *entrypoint_mounts,
509
511
  ]
510
512
 
511
- if config.get("automount"):
513
+ if include_source_mode is IncludeSourceMode.INCLUDE_FIRST_PARTY:
514
+ # TODO(elias): if using INCLUDE_FIRST_PARTY *and* mounts are added that haven't already been
515
+ # added to the image via add_local_python_source
512
516
  all_mounts += get_auto_mounts()
513
517
  else:
514
518
  # skip any mount introspection/logic inside containers, since the function
515
519
  # should already be hydrated
516
- # TODO: maybe the entire constructor should be exited early if not local?
520
+ # TODO: maybe the entire from_args loader should be exited early if not local?
521
+ # since it will be hydrated
517
522
  all_mounts = []
518
523
 
519
524
  retry_policy = _parse_retries(
@@ -951,8 +956,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
951
956
  parent = self
952
957
 
953
958
  async def _load(param_bound_func: _Function, resolver: Resolver, existing_object_id: Optional[str]):
954
- if parent is None:
955
- raise ExecutionError("Can't find the parent class' service function")
956
959
  try:
957
960
  identity = f"{parent.info.function_name} class service function"
958
961
  except Exception:
modal/functions.pyi CHANGED
@@ -198,6 +198,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal._object
198
198
  cluster_size: typing.Optional[int] = None,
199
199
  max_inputs: typing.Optional[int] = None,
200
200
  ephemeral_disk: typing.Optional[int] = None,
201
+ include_source: typing.Optional[bool] = None,
201
202
  _experimental_buffer_containers: typing.Optional[int] = None,
202
203
  _experimental_proxy_ip: typing.Optional[str] = None,
203
204
  _experimental_custom_scaling_factor: typing.Optional[float] = None,
@@ -369,6 +370,7 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
369
370
  cluster_size: typing.Optional[int] = None,
370
371
  max_inputs: typing.Optional[int] = None,
371
372
  ephemeral_disk: typing.Optional[int] = None,
373
+ include_source: typing.Optional[bool] = None,
372
374
  _experimental_buffer_containers: typing.Optional[int] = None,
373
375
  _experimental_proxy_ip: typing.Optional[str] = None,
374
376
  _experimental_custom_scaling_factor: typing.Optional[float] = None,
modal/image.py CHANGED
@@ -2054,7 +2054,7 @@ class _Image(_Object, type_prefix="im"):
2054
2054
  yield
2055
2055
  except Exception as exc:
2056
2056
  if not self.is_hydrated:
2057
- # Might be hydrated later
2057
+ # Might be hydrated later (if it's the container's used image)
2058
2058
  self.inside_exceptions.append(exc)
2059
2059
  elif env_image_id == self.object_id:
2060
2060
  # Image is already hydrated (we can remove this case later
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.72.58
3
+ Version: 0.73.1
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -16,11 +16,11 @@ modal/_traceback.py,sha256=IZQzB3fVlUfMHOSyKUgw0H6qv4yHnpyq-XVCNZKfUdA,5023
16
16
  modal/_tunnel.py,sha256=zTBxBiuH1O22tS1OliAJdIsSmaZS8PlnifS_6S5z-mk,6320
17
17
  modal/_tunnel.pyi,sha256=JmmDYAy9F1FpgJ_hWx0xkom2nTOFQjn4mTPYlU3PFo4,1245
18
18
  modal/_watcher.py,sha256=K6LYnlmSGQB4tWWI9JADv-tvSvQ1j522FwT71B51CX8,3584
19
- modal/app.py,sha256=KNfzLlkI2dJPl9LY8AgW76whZpwIvYKi2E2p9u4F3N4,43659
20
- modal/app.pyi,sha256=vnQhENaQBhJO6el-ieOcw3NEeYQ314SFXRDtjij4DM8,25324
19
+ modal/app.py,sha256=BEl90AuVlcrVlXKhM0wDFn1E8hewvbia9Qdq6SiT-FE,44225
20
+ modal/app.pyi,sha256=NnUpnKq5y5fM45F5cXLMZK5zVJZA9T_UKawloOXCVXE,25858
21
21
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
22
22
  modal/client.py,sha256=8SQawr7P1PNUCq1UmJMUQXG2jIo4Nmdcs311XqrNLRE,15276
23
- modal/client.pyi,sha256=gsjGMbhZz-Uf7JXkTF8H6cHyaeEvw9FaWdLmaDufd_8,7593
23
+ modal/client.pyi,sha256=B6N3IX6_KfMiukhFzQtUJVgjMMD7wuiUHTQWphpAKaQ,7591
24
24
  modal/cloud_bucket_mount.py,sha256=YOe9nnvSr4ZbeCn587d7_VhE9IioZYRvF9VYQTQux08,5914
25
25
  modal/cloud_bucket_mount.pyi,sha256=30T3K1a89l6wzmEJ_J9iWv9SknoGqaZDx59Xs-ZQcmk,1607
26
26
  modal/cls.py,sha256=7k3_FwhPUzewRXRZP8bcVJA9AZstoxGJuHKQ5Db-YoY,32683
@@ -37,11 +37,11 @@ modal/experimental.py,sha256=H9FXT3kshbjPLovshT10DicyOkGFrPqILy-qdTX-P7s,4015
37
37
  modal/experimental.pyi,sha256=24tIYu_w9RLwFrz1cIsgYuqmDCtV8eg6-bQNz3zjhDo,939
38
38
  modal/file_io.py,sha256=lcMs_E9Xfm0YX1t9U2wNIBPnqHRxmImqjLW1GHqVmyg,20945
39
39
  modal/file_io.pyi,sha256=NTRft1tbPSWf9TlWVeZmTlgB5AZ_Zhu2srWIrWr7brk,9445
40
- modal/file_pattern_matcher.py,sha256=dSo7BMQGZBAuoBFOX-e_72HxmF3FLzjQlEtnGtJiaD4,6506
41
- modal/functions.py,sha256=VTAsXnE-tje8z4ZFgxg-j8XdzAz4iAgr0DDD8TV8yE8,69459
42
- modal/functions.pyi,sha256=m2a2ZnjqZFC6REG1_uhA5LM0O1etkFbQH3KnsLSXUKs,26533
40
+ modal/file_pattern_matcher.py,sha256=1cZ4V2wSLiaXqAqStETSwp3bzDD6QZOt6pmmjk3Okz4,6505
41
+ modal/functions.py,sha256=bD4NbWgAfo5jsiqlT5XT0nGTaQ_6zOfyNmwYu0I-aVY,69951
42
+ modal/functions.pyi,sha256=eHwUCC7wt_DkCWDnNKuL8LBc4JIRm-rzCOtdmdUgGKY,26641
43
43
  modal/gpu.py,sha256=2qZMNnoMrjU-5Bu7fx68pANUAKTtZq0EWEEeBA9OUVQ,7426
44
- modal/image.py,sha256=leeY7fLfFjS0IqTi3D4cRxIDOb80BPtb3jsQfqvVJ8c,90912
44
+ modal/image.py,sha256=c6-RsdVDCscmfOoZI26gj8GvBBw1oVC18n8WaYKBWAw,90949
45
45
  modal/image.pyi,sha256=QMKS6E3CsZr5DoyNqGpcJPBYJTJZSvtAQIsAhPVd_E4,26347
46
46
  modal/io_streams.py,sha256=QkQiizKRzd5bnbKQsap31LJgBYlAnj4-XkV_50xPYX0,15079
47
47
  modal/io_streams.pyi,sha256=bJ7ZLmSmJ0nKoa6r4FJpbqvzdUVa0lEe0Fa-MMpMezU,5071
@@ -82,7 +82,7 @@ modal/volume.py,sha256=JAWeDvoAG95tMBv-fYIERyHsJPS_X_xGpxRRmYtb6j0,30096
82
82
  modal/volume.pyi,sha256=kTsXarphjZILXci84LQy7EyC84eXUs5-7D62IM5q3eE,12491
83
83
  modal/_runtime/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
84
84
  modal/_runtime/asgi.py,sha256=c4hmaMW1pLo-cm7ouriJjieuFm4ZF6D2LMy0638sfOs,22139
85
- modal/_runtime/container_io_manager.py,sha256=drb-cY78h8P9Krzmmjex7uZlSSocniEF5cIXmUcvGnY,43145
85
+ modal/_runtime/container_io_manager.py,sha256=2WmFnErVZyfyw3P6iiv1N6FskxZGsiUHrJe7nJ_-iTs,43161
86
86
  modal/_runtime/execution_context.py,sha256=E6ofm6j1POXGPxS841X3V7JU6NheVb8OkQc7JpLq4Kg,2712
87
87
  modal/_runtime/telemetry.py,sha256=T1RoAGyjBDr1swiM6pPsGRSITm7LI5FDK18oNXxY08U,5163
88
88
  modal/_runtime/user_code_imports.py,sha256=c06azC0MtYHHCGWWRGxsVY0uMAjQKe1QEj-i0oXqbWM,14699
@@ -93,7 +93,7 @@ modal/_utils/blob_utils.py,sha256=N66LtZI8PpCkZ7maA7GLW5CAmYUoNJdG-GjaAUR4_NQ,14
93
93
  modal/_utils/bytes_io_segment_payload.py,sha256=uunxVJS4PE1LojF_UpURMzVK9GuvmYWRqQo_bxEj5TU,3385
94
94
  modal/_utils/deprecation.py,sha256=dycySRBxyZf3ITzEqPNM6MxXTk9-0VVLA8oCPQ5j_Os,3426
95
95
  modal/_utils/docker_utils.py,sha256=h1uETghR40mp_y3fSWuZAfbIASH1HMzuphJHghAL6DU,3722
96
- modal/_utils/function_utils.py,sha256=VFz3RdQc0yOKRmw5u5gkZCRzkJNqm9WoQAKg9CeUnJo,25521
96
+ modal/_utils/function_utils.py,sha256=QAQQXPhSZVPkGgbgStFp3lDCB0jnvlcwKki1N3tSSWQ,27187
97
97
  modal/_utils/grpc_testing.py,sha256=H1zHqthv19eGPJz2HKXDyWXWGSqO4BRsxah3L5Xaa8A,8619
98
98
  modal/_utils/grpc_utils.py,sha256=PPB5ay-vXencXNIWPVw5modr3EH7gfq2QPcO5YJ1lMU,7737
99
99
  modal/_utils/hash_utils.py,sha256=zg3J6OGxTFGSFri1qQ12giDz90lWk8bzaxCTUCRtiX4,3034
@@ -103,7 +103,7 @@ modal/_utils/mount_utils.py,sha256=J-FRZbPQv1i__Tob-FIpbB1oXWpFLAwZiB4OCiJpFG0,3
103
103
  modal/_utils/name_utils.py,sha256=TW1iyJedvDNPEJ5UVp93u8xuD5J2gQL_CUt1mgov_aI,1939
104
104
  modal/_utils/package_utils.py,sha256=LcL2olGN4xaUzu2Tbv-C-Ft9Qp6bsLxEfETOAVd-mjU,2073
105
105
  modal/_utils/pattern_utils.py,sha256=ZUffaECfe2iYBhH6cvCB-0-UWhmEBTZEl_TwG_So3ag,6714
106
- modal/_utils/rand_pb_testing.py,sha256=NYM8W6HF-6_bzxJCAj4ITvItZYrclacEZlBhTptOT_Q,3845
106
+ modal/_utils/rand_pb_testing.py,sha256=mmVPk1rZldHwHZx0DnHTuHQlRLAiiAYdxjwEJpxvT9c,3900
107
107
  modal/_utils/shell_utils.py,sha256=hWHzv730Br2Xyj6cGPiMZ-198Z3RZuOu3pDXhFSZ22c,2157
108
108
  modal/_vendor/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
109
109
  modal/_vendor/a2wsgi_wsgi.py,sha256=Q1AsjpV_Q_vzQsz_cSqmP9jWzsGsB-ARFU6vpQYml8k,21878
@@ -118,7 +118,7 @@ modal/cli/container.py,sha256=FYwEgjf93j4NMorAjGbSV98i1wpebqdAeNU1wfrFp1k,3668
118
118
  modal/cli/dict.py,sha256=8Wq3w-UDaywk8EVNdj-ECCNV9TYHqh4kzhUqhhulatM,4593
119
119
  modal/cli/entry_point.py,sha256=aaNxFAqZcmtSjwzkYIA_Ba9CkL4cL4_i2gy5VjoXxkM,4228
120
120
  modal/cli/environment.py,sha256=Ayddkiq9jdj3XYDJ8ZmUqFpPPH8xajYlbexRkzGtUcg,4334
121
- modal/cli/import_refs.py,sha256=-FVm5nBv8LdEelgKdVEPOoqrdzKzYH48Vvwaq5B9Vyw,11869
121
+ modal/cli/import_refs.py,sha256=YYseLJ6cU_wln7DjVWfKPgEhv77hxfA0klWAkTK_1HA,12672
122
122
  modal/cli/launch.py,sha256=pzQt2QlcrbIUU0MVzWWPAvMQ6MCyqsHZ0X9JcV-sY04,3242
123
123
  modal/cli/network_file_system.py,sha256=eq3JnwjbfFNsJodIyANHL06ByYc3BSavzdmu8C96cHA,7948
124
124
  modal/cli/profile.py,sha256=rLXfjJObfPNjaZvNfHGIKqs7y9bGYyGe-K7V0w-Ni0M,3110
@@ -167,12 +167,12 @@ modal_proto/options_pb2.pyi,sha256=l7DBrbLO7q3Ir-XDkWsajm0d0TQqqrfuX54i4BMpdQg,1
167
167
  modal_proto/options_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
168
168
  modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0yJSI,247
169
169
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
170
- modal_version/__init__.py,sha256=kGya2ZlItX2zB7oHORs-wvP4PG8lg_mtbi1QIK3G6SQ,470
170
+ modal_version/__init__.py,sha256=wiJQ53c-OMs0Xf1UeXOxQ7FwlV1VzIjnX6o-pRYZ_Pk,470
171
171
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
172
- modal_version/_version_generated.py,sha256=v1gL7H7ZV7NC7iyLvrFPw5ocRiCpg-RX1pTZBLZoArI,149
173
- modal-0.72.58.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
174
- modal-0.72.58.dist-info/METADATA,sha256=zEdu-EyhKB6_vG5I0megzcrrhGQ1JDENfvrmEU1ZbVQ,2329
175
- modal-0.72.58.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
176
- modal-0.72.58.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
177
- modal-0.72.58.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
178
- modal-0.72.58.dist-info/RECORD,,
172
+ modal_version/_version_generated.py,sha256=b5X16t-GbrJxsrM_vcD_WXLcjcejIbm7gcrXoJd5e64,148
173
+ modal-0.73.1.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
174
+ modal-0.73.1.dist-info/METADATA,sha256=qsi27dGq0I0z2jCMDbs8cfkdh_kjKztW9uR00_IPuj8,2328
175
+ modal-0.73.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
176
+ modal-0.73.1.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
177
+ modal-0.73.1.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
178
+ modal-0.73.1.dist-info/RECORD,,
modal_version/__init__.py CHANGED
@@ -7,7 +7,7 @@ from ._version_generated import build_number
7
7
  major_number = 0
8
8
 
9
9
  # Bump this manually on breaking changes, then reset the number in _version_generated.py
10
- minor_number = 72
10
+ minor_number = 73
11
11
 
12
12
  # Right now, automatically increment the patch number in CI
13
13
  __version__ = f"{major_number}.{minor_number}.{max(build_number, 0)}"
@@ -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 = 58 # git: 3d3a9d8
4
+ build_number = 1 # git: 539ddf8