modal 0.72.5__py3-none-any.whl → 0.72.48__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. modal/_container_entrypoint.py +5 -10
  2. modal/_object.py +297 -0
  3. modal/_resolver.py +7 -5
  4. modal/_runtime/container_io_manager.py +0 -11
  5. modal/_runtime/user_code_imports.py +7 -7
  6. modal/_serialization.py +4 -3
  7. modal/_tunnel.py +1 -1
  8. modal/app.py +14 -61
  9. modal/app.pyi +25 -25
  10. modal/cli/app.py +3 -2
  11. modal/cli/container.py +1 -1
  12. modal/cli/import_refs.py +185 -113
  13. modal/cli/launch.py +10 -5
  14. modal/cli/programs/run_jupyter.py +2 -2
  15. modal/cli/programs/vscode.py +3 -3
  16. modal/cli/run.py +134 -68
  17. modal/client.py +1 -0
  18. modal/client.pyi +18 -14
  19. modal/cloud_bucket_mount.py +4 -0
  20. modal/cloud_bucket_mount.pyi +4 -0
  21. modal/cls.py +33 -5
  22. modal/cls.pyi +20 -5
  23. modal/container_process.pyi +8 -6
  24. modal/dict.py +1 -1
  25. modal/dict.pyi +32 -29
  26. modal/environments.py +1 -1
  27. modal/environments.pyi +2 -1
  28. modal/experimental.py +47 -11
  29. modal/experimental.pyi +29 -0
  30. modal/file_io.pyi +30 -28
  31. modal/file_pattern_matcher.py +3 -4
  32. modal/functions.py +31 -23
  33. modal/functions.pyi +57 -50
  34. modal/gpu.py +19 -26
  35. modal/image.py +47 -19
  36. modal/image.pyi +28 -21
  37. modal/io_streams.pyi +14 -12
  38. modal/mount.py +14 -5
  39. modal/mount.pyi +28 -25
  40. modal/network_file_system.py +7 -7
  41. modal/network_file_system.pyi +27 -24
  42. modal/object.py +2 -265
  43. modal/object.pyi +46 -130
  44. modal/parallel_map.py +2 -2
  45. modal/parallel_map.pyi +10 -7
  46. modal/partial_function.py +22 -3
  47. modal/partial_function.pyi +45 -27
  48. modal/proxy.py +1 -1
  49. modal/proxy.pyi +2 -1
  50. modal/queue.py +1 -1
  51. modal/queue.pyi +26 -23
  52. modal/runner.py +14 -3
  53. modal/sandbox.py +11 -7
  54. modal/sandbox.pyi +30 -27
  55. modal/secret.py +1 -1
  56. modal/secret.pyi +2 -1
  57. modal/token_flow.pyi +6 -4
  58. modal/volume.py +1 -1
  59. modal/volume.pyi +36 -33
  60. {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/METADATA +2 -2
  61. {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/RECORD +73 -71
  62. modal_proto/api.proto +151 -4
  63. modal_proto/api_grpc.py +113 -0
  64. modal_proto/api_pb2.py +998 -795
  65. modal_proto/api_pb2.pyi +430 -11
  66. modal_proto/api_pb2_grpc.py +233 -1
  67. modal_proto/api_pb2_grpc.pyi +75 -3
  68. modal_proto/modal_api_grpc.py +7 -0
  69. modal_version/_version_generated.py +1 -1
  70. {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/LICENSE +0 -0
  71. {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/WHEEL +0 -0
  72. {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/entry_points.txt +0 -0
  73. {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/top_level.txt +0 -0
@@ -38,7 +38,7 @@ from modal.partial_function import (
38
38
  _find_callables_for_obj,
39
39
  _PartialFunctionFlags,
40
40
  )
41
- from modal.running_app import RunningApp
41
+ from modal.running_app import RunningApp, running_app_from_layout
42
42
  from modal_proto import api_pb2
43
43
 
44
44
  from ._runtime.container_io_manager import (
@@ -50,8 +50,8 @@ from ._runtime.container_io_manager import (
50
50
  from ._runtime.execution_context import _set_current_context_ids
51
51
 
52
52
  if TYPE_CHECKING:
53
+ import modal._object
53
54
  import modal._runtime.container_io_manager
54
- import modal.object
55
55
 
56
56
 
57
57
  class DaemonizedThreadPool:
@@ -468,7 +468,7 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
468
468
  batch_wait_ms = function_def.batch_linger_ms or 0
469
469
 
470
470
  # Get ids and metadata for objects (primarily functions and classes) on the app
471
- container_app: RunningApp = container_io_manager.get_app_objects(container_args.app_layout)
471
+ container_app: RunningApp = running_app_from_layout(container_args.app_id, container_args.app_layout)
472
472
 
473
473
  # Initialize objects on the app.
474
474
  # This is basically only functions and classes - anything else is deprecated and will be unsupported soon
@@ -591,15 +591,10 @@ if __name__ == "__main__":
591
591
  logger.debug("Container: starting")
592
592
 
593
593
  container_args = api_pb2.ContainerArguments()
594
-
595
594
  container_arguments_path: Optional[str] = os.environ.get("MODAL_CONTAINER_ARGUMENTS_PATH")
596
595
  if container_arguments_path is None:
597
- # TODO(erikbern): this fallback is for old workers and we can remove it very soon (days)
598
- import base64
599
-
600
- container_args.ParseFromString(base64.b64decode(sys.argv[1]))
601
- else:
602
- container_args.ParseFromString(open(container_arguments_path, "rb").read())
596
+ raise RuntimeError("No path to the container arguments file provided!")
597
+ container_args.ParseFromString(open(container_arguments_path, "rb").read())
603
598
 
604
599
  # Note that we're creating the client in a synchronous context, but it will be running in a separate thread.
605
600
  # This is good because if the function is long running then we the client can still send heartbeats
modal/_object.py ADDED
@@ -0,0 +1,297 @@
1
+ # Copyright Modal Labs 2022
2
+ import typing
3
+ import uuid
4
+ from collections.abc import Awaitable, Hashable, Sequence
5
+ from functools import wraps
6
+ from typing import Callable, ClassVar, Optional
7
+
8
+ from google.protobuf.message import Message
9
+ from typing_extensions import Self
10
+
11
+ from ._resolver import Resolver
12
+ from ._utils.async_utils import aclosing
13
+ from ._utils.deprecation import deprecation_warning
14
+ from .client import _Client
15
+ from .config import config, logger
16
+ from .exception import ExecutionError, InvalidError
17
+
18
+ EPHEMERAL_OBJECT_HEARTBEAT_SLEEP: int = 300
19
+
20
+
21
+ def _get_environment_name(environment_name: Optional[str] = None, resolver: Optional[Resolver] = None) -> Optional[str]:
22
+ if environment_name:
23
+ return environment_name
24
+ elif resolver and resolver.environment_name:
25
+ return resolver.environment_name
26
+ else:
27
+ return config.get("environment")
28
+
29
+
30
+ class _Object:
31
+ _type_prefix: ClassVar[Optional[str]] = None
32
+ _prefix_to_type: ClassVar[dict[str, type]] = {}
33
+
34
+ # For constructors
35
+ _load: Optional[Callable[[Self, Resolver, Optional[str]], Awaitable[None]]]
36
+ _preload: Optional[Callable[[Self, Resolver, Optional[str]], Awaitable[None]]]
37
+ _rep: str
38
+ _is_another_app: bool
39
+ _hydrate_lazily: bool
40
+ _deps: Optional[Callable[..., Sequence["_Object"]]]
41
+ _deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None
42
+
43
+ # For hydrated objects
44
+ _object_id: Optional[str]
45
+ _client: Optional[_Client]
46
+ _is_hydrated: bool
47
+ _is_rehydrated: bool
48
+
49
+ @classmethod
50
+ def __init_subclass__(cls, type_prefix: Optional[str] = None):
51
+ super().__init_subclass__()
52
+ if type_prefix is not None:
53
+ cls._type_prefix = type_prefix
54
+ cls._prefix_to_type[type_prefix] = cls
55
+
56
+ def __init__(self, *args, **kwargs):
57
+ raise InvalidError(f"Class {type(self).__name__} has no constructor. Use class constructor methods instead.")
58
+
59
+ def _init(
60
+ self,
61
+ rep: str,
62
+ load: Optional[Callable[[Self, Resolver, Optional[str]], Awaitable[None]]] = None,
63
+ is_another_app: bool = False,
64
+ preload: Optional[Callable[[Self, Resolver, Optional[str]], Awaitable[None]]] = None,
65
+ hydrate_lazily: bool = False,
66
+ deps: Optional[Callable[..., Sequence["_Object"]]] = None,
67
+ deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None,
68
+ ):
69
+ self._local_uuid = str(uuid.uuid4())
70
+ self._load = load
71
+ self._preload = preload
72
+ self._rep = rep
73
+ self._is_another_app = is_another_app
74
+ self._hydrate_lazily = hydrate_lazily
75
+ self._deps = deps
76
+ self._deduplication_key = deduplication_key
77
+
78
+ self._object_id = None
79
+ self._client = None
80
+ self._is_hydrated = False
81
+ self._is_rehydrated = False
82
+
83
+ self._initialize_from_empty()
84
+
85
+ def _unhydrate(self):
86
+ self._object_id = None
87
+ self._client = None
88
+ self._is_hydrated = False
89
+
90
+ def _initialize_from_empty(self):
91
+ # default implementation, can be overriden in subclasses
92
+ pass
93
+
94
+ def _initialize_from_other(self, other):
95
+ # default implementation, can be overriden in subclasses
96
+ self._object_id = other._object_id
97
+ self._is_hydrated = other._is_hydrated
98
+ self._client = other._client
99
+
100
+ def _hydrate(self, object_id: str, client: _Client, metadata: Optional[Message]):
101
+ assert isinstance(object_id, str) and self._type_prefix is not None
102
+ if not object_id.startswith(self._type_prefix):
103
+ raise ExecutionError(
104
+ f"Can not hydrate {type(self)}:"
105
+ f" it has type prefix {self._type_prefix}"
106
+ f" but the object_id starts with {object_id[:3]}"
107
+ )
108
+ self._object_id = object_id
109
+ self._client = client
110
+ self._hydrate_metadata(metadata)
111
+ self._is_hydrated = True
112
+
113
+ def _hydrate_metadata(self, metadata: Optional[Message]):
114
+ # override this is subclasses that need additional data (other than an object_id) for a functioning Handle
115
+ pass
116
+
117
+ def _get_metadata(self) -> Optional[Message]:
118
+ # return the necessary metadata from this handle to be able to re-hydrate in another context if one is needed
119
+ # used to provide a handle's handle_metadata for serializing/pickling a live handle
120
+ # the object_id is already provided by other means
121
+ return None
122
+
123
+ def _validate_is_hydrated(self):
124
+ if not self._is_hydrated:
125
+ object_type = self.__class__.__name__.strip("_")
126
+ if hasattr(self, "_app") and getattr(self._app, "_running_app", "") is None: # type: ignore
127
+ # The most common cause of this error: e.g., user called a Function without using App.run()
128
+ reason = ", because the App it is defined on is not running"
129
+ else:
130
+ # Technically possible, but with an ambiguous cause.
131
+ reason = ""
132
+ raise ExecutionError(
133
+ f"{object_type} has not been hydrated with the metadata it needs to run on Modal{reason}."
134
+ )
135
+
136
+ def clone(self) -> Self:
137
+ """mdmd:hidden Clone a given hydrated object."""
138
+
139
+ # Object to clone must already be hydrated, otherwise from_loader is more suitable.
140
+ self._validate_is_hydrated()
141
+ obj = type(self).__new__(type(self))
142
+ obj._initialize_from_other(self)
143
+ return obj
144
+
145
+ @classmethod
146
+ def _from_loader(
147
+ cls,
148
+ load: Callable[[Self, Resolver, Optional[str]], Awaitable[None]],
149
+ rep: str,
150
+ is_another_app: bool = False,
151
+ preload: Optional[Callable[[Self, Resolver, Optional[str]], Awaitable[None]]] = None,
152
+ hydrate_lazily: bool = False,
153
+ deps: Optional[Callable[..., Sequence["_Object"]]] = None,
154
+ deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None,
155
+ ):
156
+ # TODO(erikbern): flip the order of the two first arguments
157
+ obj = _Object.__new__(cls)
158
+ obj._init(rep, load, is_another_app, preload, hydrate_lazily, deps, deduplication_key)
159
+ return obj
160
+
161
+ @staticmethod
162
+ def _get_type_from_id(object_id: str) -> type["_Object"]:
163
+ parts = object_id.split("-")
164
+ if len(parts) != 2:
165
+ raise InvalidError(f"Object id {object_id} has no dash in it")
166
+ prefix = parts[0]
167
+ if prefix not in _Object._prefix_to_type:
168
+ raise InvalidError(f"Object prefix {prefix} does not correspond to a type")
169
+ return _Object._prefix_to_type[prefix]
170
+
171
+ @classmethod
172
+ def _is_id_type(cls, object_id) -> bool:
173
+ return cls._get_type_from_id(object_id) == cls
174
+
175
+ @classmethod
176
+ def _new_hydrated(
177
+ cls, object_id: str, client: _Client, handle_metadata: Optional[Message], is_another_app: bool = False
178
+ ) -> Self:
179
+ obj_cls: type[Self]
180
+ if cls._type_prefix is not None:
181
+ # This is called directly on a subclass, e.g. Secret.from_id
182
+ # validate the id matching the expected id type of the Object subclass
183
+ if not object_id.startswith(cls._type_prefix + "-"):
184
+ raise InvalidError(f"Object {object_id} does not start with {cls._type_prefix}")
185
+
186
+ obj_cls = cls
187
+ else:
188
+ # this means the method is used directly on _Object
189
+ # typically during deserialization of objects
190
+ obj_cls = typing.cast(type[Self], cls._get_type_from_id(object_id))
191
+
192
+ # Instantiate provider
193
+ obj = _Object.__new__(obj_cls)
194
+ rep = f"Object({object_id})" # TODO(erikbern): dumb
195
+ obj._init(rep, is_another_app=is_another_app)
196
+ obj._hydrate(object_id, client, handle_metadata)
197
+
198
+ return obj
199
+
200
+ def _hydrate_from_other(self, other: Self):
201
+ self._hydrate(other.object_id, other.client, other._get_metadata())
202
+
203
+ def __repr__(self):
204
+ return self._rep
205
+
206
+ @property
207
+ def local_uuid(self):
208
+ """mdmd:hidden"""
209
+ return self._local_uuid
210
+
211
+ @property
212
+ def object_id(self) -> str:
213
+ """mdmd:hidden"""
214
+ if self._object_id is None:
215
+ raise AttributeError(f"Attempting to get object_id of unhydrated {self}")
216
+ return self._object_id
217
+
218
+ @property
219
+ def client(self) -> _Client:
220
+ """mdmd:hidden"""
221
+ if self._client is None:
222
+ raise AttributeError(f"Attempting to get client of unhydrated {self}")
223
+ return self._client
224
+
225
+ @property
226
+ def is_hydrated(self) -> bool:
227
+ """mdmd:hidden"""
228
+ return self._is_hydrated
229
+
230
+ @property
231
+ def deps(self) -> Callable[..., Sequence["_Object"]]:
232
+ """mdmd:hidden"""
233
+
234
+ def default_deps(*args, **kwargs) -> Sequence["_Object"]:
235
+ return []
236
+
237
+ return self._deps if self._deps is not None else default_deps
238
+
239
+ async def resolve(self, client: Optional[_Client] = None):
240
+ """mdmd:hidden"""
241
+ obj = self.__class__.__name__.strip("_")
242
+ deprecation_warning(
243
+ (2025, 1, 16),
244
+ f"The `{obj}.resolve` method is deprecated and will be removed in a future release."
245
+ f" Please use `{obj}.hydrate()` or `await {obj}.hydrate.aio()` instead."
246
+ "\n\nNote that it is rarely necessary to explicitly hydrate objects, as most methods"
247
+ " will lazily hydrate when needed.",
248
+ show_source=False, # synchronicity interferes with attributing source correctly
249
+ )
250
+ await self.hydrate(client)
251
+
252
+ async def hydrate(self, client: Optional[_Client] = None) -> Self:
253
+ """Synchronize the local object with its identity on the Modal server.
254
+
255
+ It is rarely necessary to call this method explicitly, as most operations
256
+ will lazily hydrate when needed. The main use case is when you need to
257
+ access object metadata, such as its ID.
258
+ """
259
+ if self._is_hydrated:
260
+ if self.client._snapshotted and not self._is_rehydrated:
261
+ # memory snapshots capture references which must be rehydrated
262
+ # on restore to handle staleness.
263
+ logger.debug(f"rehydrating {self} after snapshot")
264
+ self._is_hydrated = False # un-hydrate and re-resolve
265
+ c = client if client is not None else await _Client.from_env()
266
+ resolver = Resolver(c)
267
+ await resolver.load(typing.cast(_Object, self))
268
+ self._is_rehydrated = True
269
+ logger.debug(f"rehydrated {self} with client {id(c)}")
270
+ elif not self._hydrate_lazily:
271
+ # TODO(michael) can remove _hydrate lazily? I think all objects support it now?
272
+ self._validate_is_hydrated()
273
+ else:
274
+ c = client if client is not None else await _Client.from_env()
275
+ resolver = Resolver(c)
276
+ await resolver.load(self)
277
+ return self
278
+
279
+
280
+ def live_method(method):
281
+ @wraps(method)
282
+ async def wrapped(self, *args, **kwargs):
283
+ await self.hydrate()
284
+ return await method(self, *args, **kwargs)
285
+
286
+ return wrapped
287
+
288
+
289
+ def live_method_gen(method):
290
+ @wraps(method)
291
+ async def wrapped(self, *args, **kwargs):
292
+ await self.hydrate()
293
+ async with aclosing(method(self, *args, **kwargs)) as stream:
294
+ async for item in stream:
295
+ yield item
296
+
297
+ return wrapped
modal/_resolver.py CHANGED
@@ -15,7 +15,7 @@ from .exception import NotFoundError
15
15
  if TYPE_CHECKING:
16
16
  from rich.tree import Tree
17
17
 
18
- from modal.object import _Object
18
+ import modal._object
19
19
 
20
20
 
21
21
  class StatusRow:
@@ -33,7 +33,7 @@ class StatusRow:
33
33
  self._spinner.update(text=message)
34
34
 
35
35
  def finish(self, message):
36
- if self._step_node is not None:
36
+ if self._step_node is not None and self._spinner is not None:
37
37
  from ._output import OutputManager
38
38
 
39
39
  self._spinner.update(text=message)
@@ -89,7 +89,7 @@ class Resolver:
89
89
  if obj._preload is not None:
90
90
  await obj._preload(obj, self, existing_object_id)
91
91
 
92
- async def load(self, obj: "_Object", existing_object_id: Optional[str] = None):
92
+ async def load(self, obj: "modal._object._Object", existing_object_id: Optional[str] = None):
93
93
  if obj._is_hydrated and obj._is_another_app:
94
94
  # No need to reload this, it won't typically change
95
95
  if obj.local_uuid not in self._local_uuid_to_future:
@@ -124,6 +124,8 @@ class Resolver:
124
124
  await TaskContext.gather(*[self.load(dep) for dep in obj.deps()])
125
125
 
126
126
  # Load the object itself
127
+ if not obj._load:
128
+ raise Exception(f"Object {obj} has no loader function")
127
129
  try:
128
130
  await obj._load(obj, self, existing_object_id)
129
131
  except GRPCError as exc:
@@ -154,8 +156,8 @@ class Resolver:
154
156
  # TODO(elias): print original exception/trace rather than the Resolver-internal trace
155
157
  return await cached_future
156
158
 
157
- def objects(self) -> list["_Object"]:
158
- unique_objects: dict[str, "_Object"] = {}
159
+ def objects(self) -> list["modal._object._Object"]:
160
+ unique_objects: dict[str, "modal._object._Object"] = {}
159
161
  for fut in self._local_uuid_to_future.values():
160
162
  if not fut.done():
161
163
  # this will raise an exception if not all loads have been awaited, but that *should* never happen
@@ -35,7 +35,6 @@ from modal._utils.package_utils import parse_major_minor_version
35
35
  from modal.client import HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, _Client
36
36
  from modal.config import config, logger
37
37
  from modal.exception import ClientClosed, InputCancellation, InvalidError, SerializationError
38
- from modal.running_app import RunningApp, running_app_from_layout
39
38
  from modal_proto import api_pb2
40
39
 
41
40
  if TYPE_CHECKING:
@@ -449,16 +448,6 @@ class _ContainerIOManager:
449
448
 
450
449
  await asyncio.sleep(DYNAMIC_CONCURRENCY_INTERVAL_SECS)
451
450
 
452
- async def get_app_objects(self, app_layout: api_pb2.AppLayout) -> RunningApp:
453
- if len(app_layout.objects) == 0:
454
- # TODO(erikbern): this should never happen! let's keep it in here for a short second
455
- # until we've sanity checked that this is, in fact, dead code.
456
- req = api_pb2.AppGetLayoutRequest(app_id=self.app_id)
457
- resp = await retry_transient_errors(self._client.stub.AppGetLayout, req)
458
- app_layout = resp.app_layout
459
-
460
- return running_app_from_layout(self.app_id, app_layout)
461
-
462
451
  async def get_serialized_function(self) -> tuple[Optional[Any], Optional[Callable[..., Any]]]:
463
452
  # Fetch the serialized function definition
464
453
  request = api_pb2.FunctionGetSerializedRequest(function_id=self.function_id)
@@ -3,11 +3,11 @@ import importlib
3
3
  import typing
4
4
  from abc import ABCMeta, abstractmethod
5
5
  from dataclasses import dataclass
6
- from typing import Any, Callable, Optional
6
+ from typing import Any, Callable, Optional, Sequence
7
7
 
8
+ import modal._object
8
9
  import modal._runtime.container_io_manager
9
10
  import modal.cls
10
- import modal.object
11
11
  from modal import Function
12
12
  from modal._utils.async_utils import synchronizer
13
13
  from modal._utils.function_utils import LocalFunctionError, is_async as get_is_async, is_global_object
@@ -41,7 +41,7 @@ class Service(metaclass=ABCMeta):
41
41
 
42
42
  user_cls_instance: Any
43
43
  app: Optional["modal.app._App"]
44
- code_deps: Optional[list["modal.object._Object"]]
44
+ code_deps: Optional[Sequence["modal._object._Object"]]
45
45
 
46
46
  @abstractmethod
47
47
  def get_finalized_functions(
@@ -94,7 +94,7 @@ def construct_webhook_callable(
94
94
  class ImportedFunction(Service):
95
95
  user_cls_instance: Any
96
96
  app: Optional["modal.app._App"]
97
- code_deps: Optional[list["modal.object._Object"]]
97
+ code_deps: Optional[Sequence["modal._object._Object"]]
98
98
 
99
99
  _user_defined_callable: Callable[..., Any]
100
100
 
@@ -137,7 +137,7 @@ class ImportedFunction(Service):
137
137
  class ImportedClass(Service):
138
138
  user_cls_instance: Any
139
139
  app: Optional["modal.app._App"]
140
- code_deps: Optional[list["modal.object._Object"]]
140
+ code_deps: Optional[Sequence["modal._object._Object"]]
141
141
 
142
142
  _partial_functions: dict[str, "modal.partial_function._PartialFunction"]
143
143
 
@@ -229,7 +229,7 @@ def import_single_function_service(
229
229
  """
230
230
  user_defined_callable: Callable
231
231
  function: Optional[_Function] = None
232
- code_deps: Optional[list["modal.object._Object"]] = None
232
+ code_deps: Optional[Sequence["modal._object._Object"]] = None
233
233
  active_app: Optional[modal.app._App] = None
234
234
 
235
235
  if ser_fun is not None:
@@ -306,7 +306,7 @@ def import_class_service(
306
306
  See import_function.
307
307
  """
308
308
  active_app: Optional["modal.app._App"]
309
- code_deps: Optional[list["modal.object._Object"]]
309
+ code_deps: Optional[Sequence["modal._object._Object"]]
310
310
  cls: typing.Union[type, modal.cls.Cls]
311
311
 
312
312
  if function_def.definition_type == api_pb2.Function.DEFINITION_TYPE_SERIALIZED:
modal/_serialization.py CHANGED
@@ -8,10 +8,11 @@ from typing import Any
8
8
  from modal._utils.async_utils import synchronizer
9
9
  from modal_proto import api_pb2
10
10
 
11
+ from ._object import _Object
11
12
  from ._vendor import cloudpickle
12
13
  from .config import logger
13
14
  from .exception import DeserializationError, ExecutionError, InvalidError
14
- from .object import Object, _Object
15
+ from .object import Object
15
16
 
16
17
  PICKLE_PROTOCOL = 4 # Support older Python versions.
17
18
 
@@ -48,8 +49,8 @@ class Pickler(cloudpickle.Pickler):
48
49
  return ("sync", (impl_object.__class__, attributes))
49
50
  else:
50
51
  return
51
- if not obj.object_id:
52
- raise InvalidError(f"Can't serialize object {obj} which hasn't been created.")
52
+ if not obj.is_hydrated:
53
+ raise InvalidError(f"Can't serialize object {obj} which hasn't been hydrated.")
53
54
  return (obj.object_id, flag, obj._get_metadata())
54
55
 
55
56
 
modal/_tunnel.py CHANGED
@@ -145,7 +145,7 @@ async def _forward(port: int, *, unencrypted: bool = False, client: Optional[_Cl
145
145
  modal.Image.debian_slim()
146
146
  .apt_install("openssh-server")
147
147
  .run_commands("mkdir /run/sshd")
148
- .copy_local_file("~/.ssh/id_rsa.pub", "/root/.ssh/authorized_keys")
148
+ .add_local_file("~/.ssh/id_rsa.pub", "/root/.ssh/authorized_keys", copy=True)
149
149
  )
150
150
 
151
151
 
modal/app.py CHANGED
@@ -21,6 +21,7 @@ from synchronicity.async_wrap import asynccontextmanager
21
21
  from modal_proto import api_pb2
22
22
 
23
23
  from ._ipython import is_notebook
24
+ from ._object import _get_environment_name, _Object
24
25
  from ._utils.async_utils import synchronize_api
25
26
  from ._utils.deprecation import deprecation_error, deprecation_warning, renamed_parameter
26
27
  from ._utils.function_utils import FunctionInfo, is_global_object, is_method_fn
@@ -36,7 +37,6 @@ from .gpu import GPU_T
36
37
  from .image import _Image
37
38
  from .mount import _Mount
38
39
  from .network_file_system import _NetworkFileSystem
39
- from .object import _get_environment_name, _Object
40
40
  from .partial_function import (
41
41
  PartialFunction,
42
42
  _find_partial_methods_for_user_cls,
@@ -118,23 +118,6 @@ class _FunctionDecoratorType:
118
118
  ...
119
119
 
120
120
 
121
- _app_attr_error = """\
122
- App assignments of the form `app.x` or `app["x"]` are deprecated!
123
-
124
- The only use cases for these assignments is in conjunction with `.new()`, which is now
125
- in itself deprecated. If you are constructing objects with `.from_name(...)`, there is no
126
- need to assign those objects to the app. Example:
127
-
128
- ```python
129
- d = modal.Dict.from_name("my-dict", create_if_missing=True)
130
-
131
- @app.function()
132
- def f(x, y):
133
- d[x] = y # Refer to d in global scope
134
- ```
135
- """
136
-
137
-
138
121
  class _App:
139
122
  """A Modal App is a group of functions and classes that are deployed together.
140
123
 
@@ -211,12 +194,12 @@ class _App:
211
194
  self._name = name
212
195
  self._description = name
213
196
 
214
- check_sequence(mounts, _Mount, "`mounts=` has to be a list or tuple of Mount objects")
215
- check_sequence(secrets, _Secret, "`secrets=` has to be a list or tuple of Secret objects")
197
+ check_sequence(mounts, _Mount, "`mounts=` has to be a list or tuple of `modal.Mount` objects")
198
+ check_sequence(secrets, _Secret, "`secrets=` has to be a list or tuple of `modal.Secret` objects")
216
199
  validate_volumes(volumes)
217
200
 
218
201
  if image is not None and not isinstance(image, _Image):
219
- raise InvalidError("image has to be a modal Image or AioImage object")
202
+ raise InvalidError("`image=` has to be a `modal.Image` object")
220
203
 
221
204
  self._functions = {}
222
205
  self._classes = {}
@@ -303,34 +286,6 @@ class _App:
303
286
  if not isinstance(value, _Object):
304
287
  raise InvalidError(f"App attribute `{key}` with value {value!r} is not a valid Modal object")
305
288
 
306
- def __getitem__(self, tag: str):
307
- deprecation_error((2024, 3, 25), _app_attr_error)
308
-
309
- def __setitem__(self, tag: str, obj: _Object):
310
- deprecation_error((2024, 3, 25), _app_attr_error)
311
-
312
- def __getattr__(self, tag: str):
313
- # TODO(erikbern): remove this method later
314
- assert isinstance(tag, str)
315
- if tag.startswith("__"):
316
- # Hacky way to avoid certain issues, e.g. pickle will try to look this up
317
- raise AttributeError(f"App has no member {tag}")
318
- if tag not in self._functions or tag not in self._classes:
319
- # Primarily to make hasattr work
320
- raise AttributeError(f"App has no member {tag}")
321
- deprecation_error((2024, 3, 25), _app_attr_error)
322
-
323
- def __setattr__(self, tag: str, obj: _Object):
324
- # TODO(erikbern): remove this method later
325
- # Note that only attributes defined in __annotations__ are set on the object itself,
326
- # everything else is registered on the indexed_objects
327
- if tag in self.__annotations__:
328
- object.__setattr__(self, tag, obj)
329
- elif tag == "image":
330
- self._image = obj
331
- else:
332
- deprecation_error((2024, 3, 25), _app_attr_error)
333
-
334
289
  @property
335
290
  def image(self) -> _Image:
336
291
  return self._image
@@ -365,6 +320,7 @@ class _App:
365
320
  show_progress: Optional[bool] = None,
366
321
  detach: bool = False,
367
322
  interactive: bool = False,
323
+ environment_name: Optional[str] = None,
368
324
  ) -> AsyncGenerator["_App", None]:
369
325
  """Context manager that runs an app on Modal.
370
326
 
@@ -420,7 +376,9 @@ class _App:
420
376
  elif show_progress is False:
421
377
  deprecation_warning((2024, 11, 20), "`show_progress=False` is deprecated (and has no effect)")
422
378
 
423
- async with _run_app(self, client=client, detach=detach, interactive=interactive):
379
+ async with _run_app(
380
+ self, client=client, detach=detach, interactive=interactive, environment_name=environment_name
381
+ ):
424
382
  yield self
425
383
 
426
384
  def _get_default_image(self):
@@ -442,11 +400,13 @@ class _App:
442
400
  return [m for m in all_mounts if m.is_local()]
443
401
 
444
402
  def _add_function(self, function: _Function, is_web_endpoint: bool):
445
- if function.tag in self._functions:
403
+ if old_function := self._functions.get(function.tag, None):
404
+ if old_function is function:
405
+ return # already added the same exact instance, ignore
406
+
446
407
  if not is_notebook():
447
- old_function: _Function = self._functions[function.tag]
448
408
  logger.warning(
449
- f"Warning: Tag '{function.tag}' collision!"
409
+ f"Warning: function name '{function.tag}' collision!"
450
410
  " Overriding existing function "
451
411
  f"[{old_function._info.module_name}].{old_function._info.function_name}"
452
412
  f" with new function [{function._info.module_name}].{function._info.function_name}"
@@ -504,7 +464,7 @@ class _App:
504
464
  return self._functions
505
465
 
506
466
  @property
507
- def registered_classes(self) -> dict[str, _Function]:
467
+ def registered_classes(self) -> dict[str, _Cls]:
508
468
  """All modal.Cls objects registered on the app."""
509
469
  return self._classes
510
470
 
@@ -995,13 +955,6 @@ class _App:
995
955
  ```
996
956
  """
997
957
  for tag, function in other_app._functions.items():
998
- existing_function = self._functions.get(tag)
999
- if existing_function and existing_function != function:
1000
- logger.warning(
1001
- f"Named app function {tag} with existing value {existing_function} is being "
1002
- f"overwritten by a different function {function}"
1003
- )
1004
-
1005
958
  self._add_function(function, False) # TODO(erikbern): webhook config?
1006
959
 
1007
960
  for tag, cls in other_app._classes.items():