modal 0.67.0__py3-none-any.whl → 0.67.22__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 (113) hide show
  1. modal/_clustered_functions.py +2 -2
  2. modal/_clustered_functions.pyi +2 -2
  3. modal/_container_entrypoint.py +5 -4
  4. modal/_output.py +29 -28
  5. modal/_pty.py +2 -2
  6. modal/_resolver.py +6 -5
  7. modal/_resources.py +3 -3
  8. modal/_runtime/asgi.py +46 -6
  9. modal/_runtime/container_io_manager.py +22 -26
  10. modal/_runtime/execution_context.py +2 -2
  11. modal/_runtime/telemetry.py +1 -2
  12. modal/_runtime/user_code_imports.py +12 -14
  13. modal/_serialization.py +3 -7
  14. modal/_traceback.py +5 -5
  15. modal/_tunnel.py +5 -4
  16. modal/_tunnel.pyi +2 -2
  17. modal/_utils/async_utils.py +53 -17
  18. modal/_utils/blob_utils.py +22 -7
  19. modal/_utils/function_utils.py +14 -10
  20. modal/_utils/grpc_testing.py +7 -6
  21. modal/_utils/grpc_utils.py +2 -3
  22. modal/_utils/hash_utils.py +2 -2
  23. modal/_utils/mount_utils.py +5 -4
  24. modal/_utils/package_utils.py +2 -3
  25. modal/_utils/pattern_matcher.py +6 -6
  26. modal/_utils/rand_pb_testing.py +3 -3
  27. modal/_utils/shell_utils.py +2 -1
  28. modal/_vendor/a2wsgi_wsgi.py +62 -72
  29. modal/_vendor/cloudpickle.py +1 -1
  30. modal/_watcher.py +8 -7
  31. modal/app.py +81 -69
  32. modal/app.pyi +104 -99
  33. modal/call_graph.py +6 -6
  34. modal/cli/_download.py +3 -2
  35. modal/cli/_traceback.py +4 -4
  36. modal/cli/app.py +4 -4
  37. modal/cli/container.py +4 -4
  38. modal/cli/dict.py +1 -1
  39. modal/cli/environment.py +2 -3
  40. modal/cli/import_refs.py +1 -1
  41. modal/cli/launch.py +2 -2
  42. modal/cli/network_file_system.py +1 -1
  43. modal/cli/profile.py +1 -1
  44. modal/cli/programs/run_jupyter.py +2 -2
  45. modal/cli/programs/vscode.py +3 -3
  46. modal/cli/queues.py +1 -1
  47. modal/cli/run.py +6 -6
  48. modal/cli/secret.py +3 -3
  49. modal/cli/utils.py +2 -1
  50. modal/cli/volume.py +3 -3
  51. modal/client.py +6 -11
  52. modal/client.pyi +18 -27
  53. modal/cloud_bucket_mount.py +3 -3
  54. modal/cloud_bucket_mount.pyi +2 -2
  55. modal/cls.py +32 -32
  56. modal/cls.pyi +35 -34
  57. modal/config.py +3 -2
  58. modal/container_process.py +6 -2
  59. modal/dict.py +6 -3
  60. modal/dict.pyi +10 -9
  61. modal/environments.py +3 -3
  62. modal/environments.pyi +3 -3
  63. modal/exception.py +2 -3
  64. modal/functions.py +111 -40
  65. modal/functions.pyi +71 -48
  66. modal/image.py +46 -49
  67. modal/image.pyi +102 -101
  68. modal/io_streams.py +20 -12
  69. modal/io_streams.pyi +24 -14
  70. modal/mount.py +24 -24
  71. modal/mount.pyi +28 -29
  72. modal/network_file_system.py +14 -11
  73. modal/network_file_system.pyi +12 -11
  74. modal/object.py +9 -8
  75. modal/object.pyi +47 -34
  76. modal/output.py +2 -1
  77. modal/parallel_map.py +4 -4
  78. modal/partial_function.py +10 -14
  79. modal/partial_function.pyi +17 -18
  80. modal/queue.py +11 -8
  81. modal/queue.pyi +23 -22
  82. modal/retries.py +38 -0
  83. modal/runner.py +8 -7
  84. modal/runner.pyi +8 -14
  85. modal/running_app.py +3 -3
  86. modal/sandbox.py +20 -13
  87. modal/sandbox.pyi +73 -72
  88. modal/scheduler_placement.py +2 -1
  89. modal/secret.py +7 -7
  90. modal/secret.pyi +12 -12
  91. modal/serving.py +4 -3
  92. modal/serving.pyi +5 -4
  93. modal/token_flow.py +3 -2
  94. modal/token_flow.pyi +3 -3
  95. modal/volume.py +16 -23
  96. modal/volume.pyi +17 -16
  97. {modal-0.67.0.dist-info → modal-0.67.22.dist-info}/METADATA +2 -2
  98. modal-0.67.22.dist-info/RECORD +168 -0
  99. modal_docs/mdmd/signatures.py +1 -2
  100. modal_global_objects/mounts/python_standalone.py +1 -1
  101. modal_proto/api.proto +13 -0
  102. modal_proto/api_grpc.py +16 -0
  103. modal_proto/api_pb2.py +241 -221
  104. modal_proto/api_pb2.pyi +41 -0
  105. modal_proto/api_pb2_grpc.py +33 -0
  106. modal_proto/api_pb2_grpc.pyi +10 -0
  107. modal_proto/modal_api_grpc.py +1 -0
  108. modal_version/_version_generated.py +1 -1
  109. modal-0.67.0.dist-info/RECORD +0 -168
  110. {modal-0.67.0.dist-info → modal-0.67.22.dist-info}/LICENSE +0 -0
  111. {modal-0.67.0.dist-info → modal-0.67.22.dist-info}/WHEEL +0 -0
  112. {modal-0.67.0.dist-info → modal-0.67.22.dist-info}/entry_points.txt +0 -0
  113. {modal-0.67.0.dist-info → modal-0.67.22.dist-info}/top_level.txt +0 -0
@@ -35,10 +35,8 @@ from concurrent.futures import ThreadPoolExecutor
35
35
  from types import TracebackType
36
36
  from typing import (
37
37
  Any,
38
- Awaitable,
39
38
  Callable,
40
39
  Dict,
41
- Iterable,
42
40
  List,
43
41
  Literal,
44
42
  Optional,
@@ -48,6 +46,7 @@ from typing import (
48
46
  TypedDict,
49
47
  Union,
50
48
  )
49
+ from collections.abc import Awaitable, Iterable
51
50
 
52
51
 
53
52
  ## BEGIN a2wsgi/asgi_typing.py
@@ -73,11 +72,11 @@ class HTTPScope(TypedDict):
73
72
  raw_path: NotRequired[bytes]
74
73
  query_string: bytes
75
74
  root_path: str
76
- headers: Iterable[Tuple[bytes, bytes]]
77
- client: NotRequired[Tuple[str, int]]
78
- server: NotRequired[Tuple[str, Optional[int]]]
79
- state: NotRequired[Dict[str, Any]]
80
- extensions: NotRequired[Dict[str, Dict[object, object]]]
75
+ headers: Iterable[tuple[bytes, bytes]]
76
+ client: NotRequired[tuple[str, int]]
77
+ server: NotRequired[tuple[str, Optional[int]]]
78
+ state: NotRequired[dict[str, Any]]
79
+ extensions: NotRequired[dict[str, dict[object, object]]]
81
80
 
82
81
 
83
82
  class WebSocketScope(TypedDict):
@@ -89,18 +88,18 @@ class WebSocketScope(TypedDict):
89
88
  raw_path: bytes
90
89
  query_string: bytes
91
90
  root_path: str
92
- headers: Iterable[Tuple[bytes, bytes]]
93
- client: NotRequired[Tuple[str, int]]
94
- server: NotRequired[Tuple[str, Optional[int]]]
91
+ headers: Iterable[tuple[bytes, bytes]]
92
+ client: NotRequired[tuple[str, int]]
93
+ server: NotRequired[tuple[str, Optional[int]]]
95
94
  subprotocols: Iterable[str]
96
- state: NotRequired[Dict[str, Any]]
97
- extensions: NotRequired[Dict[str, Dict[object, object]]]
95
+ state: NotRequired[dict[str, Any]]
96
+ extensions: NotRequired[dict[str, dict[object, object]]]
98
97
 
99
98
 
100
99
  class LifespanScope(TypedDict):
101
100
  type: Literal["lifespan"]
102
101
  asgi: ASGIVersions
103
- state: NotRequired[Dict[str, Any]]
102
+ state: NotRequired[dict[str, Any]]
104
103
 
105
104
 
106
105
  WWWScope = Union[HTTPScope, WebSocketScope]
@@ -116,7 +115,7 @@ class HTTPRequestEvent(TypedDict):
116
115
  class HTTPResponseStartEvent(TypedDict):
117
116
  type: Literal["http.response.start"]
118
117
  status: int
119
- headers: NotRequired[Iterable[Tuple[bytes, bytes]]]
118
+ headers: NotRequired[Iterable[tuple[bytes, bytes]]]
120
119
  trailers: NotRequired[bool]
121
120
 
122
121
 
@@ -137,7 +136,7 @@ class WebSocketConnectEvent(TypedDict):
137
136
  class WebSocketAcceptEvent(TypedDict):
138
137
  type: Literal["websocket.accept"]
139
138
  subprotocol: NotRequired[str]
140
- headers: NotRequired[Iterable[Tuple[bytes, bytes]]]
139
+ headers: NotRequired[Iterable[tuple[bytes, bytes]]]
141
140
 
142
141
 
143
142
  class WebSocketReceiveEvent(TypedDict):
@@ -223,56 +222,47 @@ ASGIApp = Callable[[Scope, Receive, Send], Awaitable[None]]
223
222
 
224
223
  ## BEGIN a2wsgi/wsgi_typing.py
225
224
 
226
- CGIRequiredDefined = TypedDict(
227
- "CGIRequiredDefined",
228
- {
229
- # The HTTP request method, such as GET or POST. This cannot ever be an
230
- # empty string, and so is always required.
231
- "REQUEST_METHOD": str,
232
- # When HTTP_HOST is not set, these variables can be combined to determine
233
- # a default.
234
- # SERVER_NAME and SERVER_PORT are required strings and must never be empty.
235
- "SERVER_NAME": str,
236
- "SERVER_PORT": str,
237
- # The version of the protocol the client used to send the request.
238
- # Typically this will be something like "HTTP/1.0" or "HTTP/1.1" and
239
- # may be used by the application to determine how to treat any HTTP
240
- # request headers. (This variable should probably be called REQUEST_PROTOCOL,
241
- # since it denotes the protocol used in the request, and is not necessarily
242
- # the protocol that will be used in the server's response. However, for
243
- # compatibility with CGI we have to keep the existing name.)
244
- "SERVER_PROTOCOL": str,
245
- },
246
- )
247
-
248
- CGIOptionalDefined = TypedDict(
249
- "CGIOptionalDefined",
250
- {
251
- "REQUEST_URI": str,
252
- "REMOTE_ADDR": str,
253
- "REMOTE_PORT": str,
254
- # The initial portion of the request URL’s “path” that corresponds to the
255
- # application object, so that the application knows its virtual “location”.
256
- # This may be an empty string, if the application corresponds to the “root”
257
- # of the server.
258
- "SCRIPT_NAME": str,
259
- # The remainder of the request URL’s “path”, designating the virtual
260
- # “location” of the request’s target within the application. This may be an
261
- # empty string, if the request URL targets the application root and does
262
- # not have a trailing slash.
263
- "PATH_INFO": str,
264
- # The portion of the request URL that follows the “?”, if any. May be empty
265
- # or absent.
266
- "QUERY_STRING": str,
267
- # The contents of any Content-Type fields in the HTTP request. May be empty
268
- # or absent.
269
- "CONTENT_TYPE": str,
270
- # The contents of any Content-Length fields in the HTTP request. May be empty
271
- # or absent.
272
- "CONTENT_LENGTH": str,
273
- },
274
- total=False,
275
- )
225
+ class CGIRequiredDefined(TypedDict):
226
+ # The HTTP request method, such as GET or POST. This cannot ever be an
227
+ # empty string, and so is always required.
228
+ REQUEST_METHOD: str
229
+ # When HTTP_HOST is not set, these variables can be combined to determine
230
+ # a default.
231
+ # SERVER_NAME and SERVER_PORT are required strings and must never be empty.
232
+ SERVER_NAME: str
233
+ SERVER_PORT: str
234
+ # The version of the protocol the client used to send the request.
235
+ # Typically this will be something like "HTTP/1.0" or "HTTP/1.1" and
236
+ # may be used by the application to determine how to treat any HTTP
237
+ # request headers. (This variable should probably be called REQUEST_PROTOCOL,
238
+ # since it denotes the protocol used in the request, and is not necessarily
239
+ # the protocol that will be used in the server's response. However, for
240
+ # compatibility with CGI we have to keep the existing name.)
241
+ SERVER_PROTOCOL: str
242
+
243
+ class CGIOptionalDefined(TypedDict, total=False):
244
+ REQUEST_URI: str
245
+ REMOTE_ADDR: str
246
+ REMOTE_PORT: str
247
+ # The initial portion of the request URL’s “path” that corresponds to the
248
+ # application object, so that the application knows its virtual “location”.
249
+ # This may be an empty string, if the application corresponds to the “root”
250
+ # of the server.
251
+ SCRIPT_NAME: str
252
+ # The remainder of the request URL’s “path”, designating the virtual
253
+ # “location” of the request’s target within the application. This may be an
254
+ # empty string, if the request URL targets the application root and does
255
+ # not have a trailing slash.
256
+ PATH_INFO: str
257
+ # The portion of the request URL that follows the “?”, if any. May be empty
258
+ # or absent.
259
+ QUERY_STRING: str
260
+ # The contents of any Content-Type fields in the HTTP request. May be empty
261
+ # or absent.
262
+ CONTENT_TYPE: str
263
+ # The contents of any Content-Length fields in the HTTP request. May be empty
264
+ # or absent.
265
+ CONTENT_LENGTH: str
276
266
 
277
267
 
278
268
  class InputStream(Protocol):
@@ -308,7 +298,7 @@ class InputStream(Protocol):
308
298
  """
309
299
  raise NotImplementedError
310
300
 
311
- def readlines(self, hint: int = -1, /) -> List[bytes]:
301
+ def readlines(self, hint: int = -1, /) -> list[bytes]:
312
302
  """
313
303
  Note that the hint argument to readlines() is optional for both caller and
314
304
  implementer. The application is free not to supply it, and the server or gateway
@@ -349,14 +339,14 @@ class ErrorStream(Protocol):
349
339
  def write(self, s: str, /) -> Any:
350
340
  raise NotImplementedError
351
341
 
352
- def writelines(self, seq: List[str], /) -> Any:
342
+ def writelines(self, seq: list[str], /) -> Any:
353
343
  raise NotImplementedError
354
344
 
355
345
 
356
346
  WSGIDefined = TypedDict(
357
347
  "WSGIDefined",
358
348
  {
359
- "wsgi.version": Tuple[int, int], # e.g. (1, 0)
349
+ "wsgi.version": tuple[int, int], # e.g. (1, 0)
360
350
  "wsgi.url_scheme": str, # e.g. "http" or "https"
361
351
  "wsgi.input": InputStream,
362
352
  "wsgi.errors": ErrorStream,
@@ -381,7 +371,7 @@ class Environ(CGIRequiredDefined, CGIOptionalDefined, WSGIDefined):
381
371
  """
382
372
 
383
373
 
384
- ExceptionInfo = Tuple[Type[BaseException], BaseException, Optional[TracebackType]]
374
+ ExceptionInfo = tuple[type[BaseException], BaseException, Optional[TracebackType]]
385
375
 
386
376
  # https://peps.python.org/pep-3333/#the-write-callable
387
377
  WriteCallable = Callable[[bytes], None]
@@ -391,7 +381,7 @@ class StartResponse(Protocol):
391
381
  def __call__(
392
382
  self,
393
383
  status: str,
394
- response_headers: List[Tuple[str, str]],
384
+ response_headers: list[tuple[str, str]],
395
385
  exc_info: Optional[ExceptionInfo] = None,
396
386
  /,
397
387
  ) -> WriteCallable:
@@ -460,7 +450,7 @@ class Body:
460
450
  self.buffer.clear()
461
451
  return result
462
452
 
463
- def readlines(self, hint: int = -1) -> typing.List[bytes]:
453
+ def readlines(self, hint: int = -1) -> list[bytes]:
464
454
  if not self.has_more:
465
455
  return []
466
456
  if hint == -1:
@@ -626,7 +616,7 @@ class WSGIResponder:
626
616
  def start_response(
627
617
  self,
628
618
  status: str,
629
- response_headers: typing.List[typing.Tuple[str, str]],
619
+ response_headers: list[tuple[str, str]],
630
620
  exc_info: typing.Optional[ExceptionInfo] = None,
631
621
  ) -> WriteCallable:
632
622
  self.exc_info = exc_info
@@ -256,7 +256,7 @@ def _should_pickle_by_reference(obj, name=None):
256
256
  return False
257
257
  return obj.__name__ in sys.modules
258
258
  else:
259
- raise TypeError("cannot check importability of {} instances".format(type(obj).__name__))
259
+ raise TypeError(f"cannot check importability of {type(obj).__name__} instances")
260
260
 
261
261
 
262
262
  def _lookup_module_and_qualname(obj, name=None):
modal/_watcher.py CHANGED
@@ -1,7 +1,8 @@
1
1
  # Copyright Modal Labs 2022
2
2
  from collections import defaultdict
3
+ from collections.abc import AsyncGenerator
3
4
  from pathlib import Path
4
- from typing import AsyncGenerator, Dict, List, Optional, Set, Tuple
5
+ from typing import Optional
5
6
 
6
7
  from rich.tree import Tree
7
8
  from watchfiles import Change, DefaultFilter, awatch
@@ -21,7 +22,7 @@ class AppFilesFilter(DefaultFilter):
21
22
  # Watching specific files is discouraged on Linux, so to watch a file we watch its
22
23
  # containing directory and then filter that directory's changes for relevant files.
23
24
  # https://github.com/notify-rs/notify/issues/394
24
- dir_filters: Dict[Path, Optional[Set[Path]]],
25
+ dir_filters: dict[Path, Optional[set[Path]]],
25
26
  ) -> None:
26
27
  self.dir_filters = dir_filters
27
28
  super().__init__()
@@ -54,7 +55,7 @@ class AppFilesFilter(DefaultFilter):
54
55
  return super().__call__(change, path)
55
56
 
56
57
 
57
- async def _watch_paths(paths: Set[Path], watch_filter: AppFilesFilter) -> AsyncGenerator[Set[str], None]:
58
+ async def _watch_paths(paths: set[Path], watch_filter: AppFilesFilter) -> AsyncGenerator[set[str], None]:
58
59
  try:
59
60
  async for changes in awatch(*paths, step=500, watch_filter=watch_filter):
60
61
  changed_paths = {stringpath for _, stringpath in changes}
@@ -64,7 +65,7 @@ async def _watch_paths(paths: Set[Path], watch_filter: AppFilesFilter) -> AsyncG
64
65
  pass
65
66
 
66
67
 
67
- def _print_watched_paths(paths: Set[Path]):
68
+ def _print_watched_paths(paths: set[Path]):
68
69
  msg = "️️⚡️ Serving... hit Ctrl-C to stop!"
69
70
 
70
71
  output_tree = Tree(msg, guide_style="gray50")
@@ -76,9 +77,9 @@ def _print_watched_paths(paths: Set[Path]):
76
77
  output_mgr.print(output_tree)
77
78
 
78
79
 
79
- def _watch_args_from_mounts(mounts: List[_Mount]) -> Tuple[Set[Path], AppFilesFilter]:
80
+ def _watch_args_from_mounts(mounts: list[_Mount]) -> tuple[set[Path], AppFilesFilter]:
80
81
  paths = set()
81
- dir_filters: Dict[Path, Optional[Set[Path]]] = defaultdict(set)
82
+ dir_filters: dict[Path, Optional[set[Path]]] = defaultdict(set)
82
83
  for mount in mounts:
83
84
  # TODO(elias): Make this part of the mount class instead, since it uses so much internals
84
85
  for entry in mount._entries:
@@ -94,7 +95,7 @@ def _watch_args_from_mounts(mounts: List[_Mount]) -> Tuple[Set[Path], AppFilesFi
94
95
  return paths, watch_filter
95
96
 
96
97
 
97
- async def watch(mounts: List[_Mount]) -> AsyncGenerator[Set[str], None]:
98
+ async def watch(mounts: list[_Mount]) -> AsyncGenerator[set[str], None]:
98
99
  paths, watch_filter = _watch_args_from_mounts(mounts)
99
100
 
100
101
  _print_watched_paths(paths)
modal/app.py CHANGED
@@ -2,19 +2,14 @@
2
2
  import inspect
3
3
  import typing
4
4
  import warnings
5
+ from collections.abc import AsyncGenerator, Coroutine, Sequence
5
6
  from pathlib import PurePosixPath
6
7
  from textwrap import dedent
7
8
  from typing import (
8
9
  Any,
9
- AsyncGenerator,
10
10
  Callable,
11
11
  ClassVar,
12
- Coroutine,
13
- Dict,
14
- List,
15
12
  Optional,
16
- Sequence,
17
- Tuple,
18
13
  Union,
19
14
  overload,
20
15
  )
@@ -87,14 +82,14 @@ class _LocalEntrypoint:
87
82
  LocalEntrypoint = synchronize_api(_LocalEntrypoint)
88
83
 
89
84
 
90
- def check_sequence(items: typing.Sequence[typing.Any], item_type: typing.Type[typing.Any], error_msg: str) -> None:
85
+ def check_sequence(items: typing.Sequence[typing.Any], item_type: type[typing.Any], error_msg: str) -> None:
91
86
  if not isinstance(items, (list, tuple)):
92
87
  raise InvalidError(error_msg)
93
88
  if not all(isinstance(v, item_type) for v in items):
94
89
  raise InvalidError(error_msg)
95
90
 
96
91
 
97
- CLS_T = typing.TypeVar("CLS_T", bound=typing.Type[Any])
92
+ CLS_T = typing.TypeVar("CLS_T", bound=type[Any])
98
93
 
99
94
 
100
95
  P = typing_extensions.ParamSpec("P")
@@ -172,20 +167,20 @@ class _App:
172
167
  In this example, the secret and schedule are registered with the app.
173
168
  """
174
169
 
175
- _all_apps: ClassVar[Dict[Optional[str], List["_App"]]] = {}
170
+ _all_apps: ClassVar[dict[Optional[str], list["_App"]]] = {}
176
171
  _container_app: ClassVar[Optional[RunningApp]] = None
177
172
 
178
173
  _name: Optional[str]
179
174
  _description: Optional[str]
180
- _functions: Dict[str, _Function]
181
- _classes: Dict[str, _Cls]
175
+ _functions: dict[str, _Function]
176
+ _classes: dict[str, _Cls]
182
177
 
183
178
  _image: Optional[_Image]
184
179
  _mounts: Sequence[_Mount]
185
180
  _secrets: Sequence[_Secret]
186
- _volumes: Dict[Union[str, PurePosixPath], _Volume]
187
- _web_endpoints: List[str] # Used by the CLI
188
- _local_entrypoints: Dict[str, _LocalEntrypoint]
181
+ _volumes: dict[Union[str, PurePosixPath], _Volume]
182
+ _web_endpoints: list[str] # Used by the CLI
183
+ _local_entrypoints: dict[str, _LocalEntrypoint]
189
184
 
190
185
  # Running apps only (container apps or running local)
191
186
  _app_id: Optional[str] # Kept after app finishes
@@ -199,7 +194,7 @@ class _App:
199
194
  image: Optional[_Image] = None, # default image for all functions (default is `modal.Image.debian_slim()`)
200
195
  mounts: Sequence[_Mount] = [], # default mounts for all functions
201
196
  secrets: Sequence[_Secret] = [], # default secrets for all functions
202
- volumes: Dict[Union[str, PurePosixPath], _Volume] = {}, # default volumes for all functions
197
+ volumes: dict[Union[str, PurePosixPath], _Volume] = {}, # default volumes for all functions
203
198
  ) -> None:
204
199
  """Construct a new app, optionally with default image, mounts, secrets, or volumes.
205
200
 
@@ -313,23 +308,6 @@ class _App:
313
308
  if not isinstance(value, _Object):
314
309
  raise InvalidError(f"App attribute `{key}` with value {value!r} is not a valid Modal object")
315
310
 
316
- def _add_object(self, tag, obj):
317
- # TODO(erikbern): replace this with _add_function and _add_class
318
- if self._running_app:
319
- # If this is inside a container, then objects can be defined after app initialization.
320
- # So we may have to initialize objects once they get bound to the app.
321
- if tag in self._running_app.tag_to_object_id:
322
- object_id: str = self._running_app.tag_to_object_id[tag]
323
- metadata: Message = self._running_app.object_handle_metadata[object_id]
324
- obj._hydrate(object_id, self._client, metadata)
325
-
326
- if isinstance(obj, _Function):
327
- self._functions[tag] = obj
328
- elif isinstance(obj, _Cls):
329
- self._classes[tag] = obj
330
- else:
331
- raise RuntimeError(f"Expected `obj` to be a _Function or _Cls (got {type(obj)}")
332
-
333
311
  def __getitem__(self, tag: str):
334
312
  deprecation_error((2024, 3, 25), _app_attr_error)
335
313
 
@@ -401,14 +379,14 @@ class _App:
401
379
 
402
380
  **Example**
403
381
 
404
- ```python
382
+ ```python notest
405
383
  with app.run():
406
384
  some_modal_function.remote()
407
385
  ```
408
386
 
409
387
  To enable output printing, use `modal.enable_output()`:
410
388
 
411
- ```python
389
+ ```python notest
412
390
  with modal.enable_output():
413
391
  with app.run():
414
392
  some_modal_function.remote()
@@ -481,10 +459,29 @@ class _App:
481
459
  if function.tag in self._classes:
482
460
  logger.warning(f"Warning: tag {function.tag} exists but is overridden by function")
483
461
 
484
- self._add_object(function.tag, function)
462
+ if self._running_app:
463
+ # If this is inside a container, then objects can be defined after app initialization.
464
+ # So we may have to initialize objects once they get bound to the app.
465
+ if function.tag in self._running_app.tag_to_object_id:
466
+ object_id: str = self._running_app.tag_to_object_id[function.tag]
467
+ metadata: Message = self._running_app.object_handle_metadata[object_id]
468
+ function._hydrate(object_id, self._client, metadata)
469
+
470
+ self._functions[function.tag] = function
485
471
  if is_web_endpoint:
486
472
  self._web_endpoints.append(function.tag)
487
473
 
474
+ def _add_class(self, tag: str, cls: _Cls):
475
+ if self._running_app:
476
+ # If this is inside a container, then objects can be defined after app initialization.
477
+ # So we may have to initialize objects once they get bound to the app.
478
+ if tag in self._running_app.tag_to_object_id:
479
+ object_id: str = self._running_app.tag_to_object_id[tag]
480
+ metadata: Message = self._running_app.object_handle_metadata[object_id]
481
+ cls._hydrate(object_id, self._client, metadata)
482
+
483
+ self._classes[tag] = cls
484
+
488
485
  def _init_container(self, client: _Client, running_app: RunningApp):
489
486
  self._app_id = running_app.app_id
490
487
  self._running_app = running_app
@@ -492,31 +489,37 @@ class _App:
492
489
 
493
490
  _App._container_app = running_app
494
491
 
495
- # Hydrate objects on app
496
- indexed_objects = dict(**self._functions, **self._classes)
497
- for tag, object_id in running_app.tag_to_object_id.items():
498
- if tag in indexed_objects:
499
- obj = indexed_objects[tag]
500
- handle_metadata = running_app.object_handle_metadata[object_id]
501
- obj._hydrate(object_id, client, handle_metadata)
492
+ # Hydrate objects on app -- hydrating functions first so that when a class is being hydrated its
493
+ # corresponding class service function is already hydrated.
494
+ def hydrate_objects(objects_dict):
495
+ for tag, object_id in running_app.tag_to_object_id.items():
496
+ if tag in objects_dict:
497
+ obj = objects_dict[tag]
498
+ handle_metadata = running_app.object_handle_metadata[object_id]
499
+ obj._hydrate(object_id, client, handle_metadata)
500
+
501
+ # Hydrate function objects
502
+ hydrate_objects(self._functions)
503
+ # Hydrate class objects
504
+ hydrate_objects(self._classes)
502
505
 
503
506
  @property
504
- def registered_functions(self) -> Dict[str, _Function]:
507
+ def registered_functions(self) -> dict[str, _Function]:
505
508
  """All modal.Function objects registered on the app."""
506
509
  return self._functions
507
510
 
508
511
  @property
509
- def registered_classes(self) -> Dict[str, _Function]:
512
+ def registered_classes(self) -> dict[str, _Function]:
510
513
  """All modal.Cls objects registered on the app."""
511
514
  return self._classes
512
515
 
513
516
  @property
514
- def registered_entrypoints(self) -> Dict[str, _LocalEntrypoint]:
517
+ def registered_entrypoints(self) -> dict[str, _LocalEntrypoint]:
515
518
  """All local CLI entrypoints registered on the app."""
516
519
  return self._local_entrypoints
517
520
 
518
521
  @property
519
- def indexed_objects(self) -> Dict[str, _Object]:
522
+ def indexed_objects(self) -> dict[str, _Object]:
520
523
  deprecation_warning(
521
524
  (2024, 11, 25),
522
525
  "`app.indexed_objects` is deprecated! Use `app.registered_functions` or `app.registered_classes` instead.",
@@ -524,7 +527,7 @@ class _App:
524
527
  return dict(**self._functions, **self._classes)
525
528
 
526
529
  @property
527
- def registered_web_endpoints(self) -> List[str]:
530
+ def registered_web_endpoints(self) -> list[str]:
528
531
  """Names of web endpoint (ie. webhook) functions registered on the app."""
529
532
  return self._web_endpoints
530
533
 
@@ -603,24 +606,24 @@ class _App:
603
606
  schedule: Optional[Schedule] = None, # An optional Modal Schedule for the function
604
607
  secrets: Sequence[_Secret] = (), # Optional Modal Secret objects with environment variables for the container
605
608
  gpu: Union[
606
- GPU_T, List[GPU_T]
609
+ GPU_T, list[GPU_T]
607
610
  ] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
608
611
  serialized: bool = False, # Whether to send the function over using cloudpickle.
609
612
  mounts: Sequence[_Mount] = (), # Modal Mounts added to the container
610
- network_file_systems: Dict[
613
+ network_file_systems: dict[
611
614
  Union[str, PurePosixPath], _NetworkFileSystem
612
615
  ] = {}, # Mountpoints for Modal NetworkFileSystems
613
- volumes: Dict[
616
+ volumes: dict[
614
617
  Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
615
618
  ] = {}, # Mount points for Modal Volumes & CloudBucketMounts
616
619
  allow_cross_region_volumes: bool = False, # Whether using network file systems from other regions is allowed.
617
620
  # Specify, in fractional CPU cores, how many CPU cores to request.
618
621
  # Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
619
622
  # CPU throttling will prevent a container from exceeding its specified limit.
620
- cpu: Optional[Union[float, Tuple[float, float]]] = None,
623
+ cpu: Optional[Union[float, tuple[float, float]]] = None,
621
624
  # Specify, in MiB, a memory request which is the minimum memory required.
622
625
  # Or, pass (request, limit) to additionally specify a hard limit in MiB.
623
- memory: Optional[Union[int, Tuple[int, int]]] = None,
626
+ memory: Optional[Union[int, tuple[int, int]]] = None,
624
627
  ephemeral_disk: Optional[int] = None, # Specify, in MiB, the ephemeral disk size for the Function.
625
628
  proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
626
629
  retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
@@ -809,24 +812,24 @@ class _App:
809
812
  image: Optional[_Image] = None, # The image to run as the container for the function
810
813
  secrets: Sequence[_Secret] = (), # Optional Modal Secret objects with environment variables for the container
811
814
  gpu: Union[
812
- GPU_T, List[GPU_T]
815
+ GPU_T, list[GPU_T]
813
816
  ] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
814
817
  serialized: bool = False, # Whether to send the function over using cloudpickle.
815
818
  mounts: Sequence[_Mount] = (),
816
- network_file_systems: Dict[
819
+ network_file_systems: dict[
817
820
  Union[str, PurePosixPath], _NetworkFileSystem
818
821
  ] = {}, # Mountpoints for Modal NetworkFileSystems
819
- volumes: Dict[
822
+ volumes: dict[
820
823
  Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
821
824
  ] = {}, # Mount points for Modal Volumes & CloudBucketMounts
822
825
  allow_cross_region_volumes: bool = False, # Whether using network file systems from other regions is allowed.
823
826
  # Specify, in fractional CPU cores, how many CPU cores to request.
824
827
  # Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
825
828
  # CPU throttling will prevent a container from exceeding its specified limit.
826
- cpu: Optional[Union[float, Tuple[float, float]]] = None,
829
+ cpu: Optional[Union[float, tuple[float, float]]] = None,
827
830
  # Specify, in MiB, a memory request which is the minimum memory required.
828
831
  # Or, pass (request, limit) to additionally specify a hard limit in MiB.
829
- memory: Optional[Union[int, Tuple[int, int]]] = None,
832
+ memory: Optional[Union[int, tuple[int, int]]] = None,
830
833
  ephemeral_disk: Optional[int] = None, # Specify, in MiB, the ephemeral disk size for the Function.
831
834
  proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
832
835
  retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
@@ -929,7 +932,7 @@ class _App:
929
932
  cls: _Cls = _Cls.from_local(user_cls, self, cls_func)
930
933
 
931
934
  tag: str = user_cls.__name__
932
- self._add_object(tag, cls)
935
+ self._add_class(tag, cls)
933
936
  return cls # type: ignore # a _Cls instance "simulates" being the user provided class
934
937
 
935
938
  return wrapper
@@ -940,7 +943,7 @@ class _App:
940
943
  image: Optional[_Image] = None, # The image to run as the container for the sandbox.
941
944
  mounts: Sequence[_Mount] = (), # Mounts to attach to the sandbox.
942
945
  secrets: Sequence[_Secret] = (), # Environment variables to inject into the sandbox.
943
- network_file_systems: Dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
946
+ network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
944
947
  timeout: Optional[int] = None, # Maximum execution time of the sandbox in seconds.
945
948
  workdir: Optional[str] = None, # Working directory of the sandbox.
946
949
  gpu: GPU_T = None,
@@ -949,12 +952,12 @@ class _App:
949
952
  # Specify, in fractional CPU cores, how many CPU cores to request.
950
953
  # Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
951
954
  # CPU throttling will prevent a container from exceeding its specified limit.
952
- cpu: Optional[Union[float, Tuple[float, float]]] = None,
955
+ cpu: Optional[Union[float, tuple[float, float]]] = None,
953
956
  # Specify, in MiB, a memory request which is the minimum memory required.
954
957
  # Or, pass (request, limit) to additionally specify a hard limit in MiB.
955
- memory: Optional[Union[int, Tuple[int, int]]] = None,
958
+ memory: Optional[Union[int, tuple[int, int]]] = None,
956
959
  block_network: bool = False, # Whether to block network access
957
- volumes: Dict[
960
+ volumes: dict[
958
961
  Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
959
962
  ] = {}, # Mount points for Modal Volumes and CloudBucketMounts
960
963
  pty_info: Optional[api_pb2.PTYInfo] = None,
@@ -1016,16 +1019,25 @@ class _App:
1016
1019
  bar.remote()
1017
1020
  ```
1018
1021
  """
1019
- indexed_objects = dict(**other_app._functions, **other_app._classes)
1020
- for tag, object in indexed_objects.items():
1021
- existing_object = indexed_objects.get(tag)
1022
- if existing_object and existing_object != object:
1022
+ for tag, function in other_app._functions.items():
1023
+ existing_function = self._functions.get(tag)
1024
+ if existing_function and existing_function != function:
1025
+ logger.warning(
1026
+ f"Named app function {tag} with existing value {existing_function} is being "
1027
+ f"overwritten by a different function {function}"
1028
+ )
1029
+
1030
+ self._add_function(function, False) # TODO(erikbern): webhook config?
1031
+
1032
+ for tag, cls in other_app._classes.items():
1033
+ existing_cls = self._classes.get(tag)
1034
+ if existing_cls and existing_cls != cls:
1023
1035
  logger.warning(
1024
- f"Named app object {tag} with existing value {existing_object} is being "
1025
- f"overwritten by a different object {object}"
1036
+ f"Named app class {tag} with existing value {existing_cls} is being "
1037
+ f"overwritten by a different class {cls}"
1026
1038
  )
1027
1039
 
1028
- self._add_object(tag, object)
1040
+ self._add_class(tag, cls)
1029
1041
 
1030
1042
  async def _logs(self, client: Optional[_Client] = None) -> AsyncGenerator[str, None]:
1031
1043
  """Stream logs from the app.