modal 0.67.1__py3-none-any.whl → 0.67.33__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 +8 -5
  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 +20 -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 +68 -62
  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 +100 -47
  56. modal/cls.pyi +40 -40
  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 +112 -104
  65. modal/functions.pyi +77 -58
  66. modal/image.py +59 -57
  67. modal/image.pyi +104 -103
  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.1.dist-info → modal-0.67.33.dist-info}/METADATA +2 -2
  98. modal-0.67.33.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 +15 -0
  102. modal_proto/api_grpc.py +32 -0
  103. modal_proto/api_pb2.py +674 -654
  104. modal_proto/api_pb2.pyi +45 -1
  105. modal_proto/api_pb2_grpc.py +66 -0
  106. modal_proto/api_pb2_grpc.pyi +20 -0
  107. modal_proto/modal_api_grpc.py +2 -0
  108. modal_version/_version_generated.py +1 -1
  109. modal-0.67.1.dist-info/RECORD +0 -168
  110. {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/LICENSE +0 -0
  111. {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/WHEEL +0 -0
  112. {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/entry_points.txt +0 -0
  113. {modal-0.67.1.dist-info → modal-0.67.33.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
@@ -507,22 +504,22 @@ class _App:
507
504
  hydrate_objects(self._classes)
508
505
 
509
506
  @property
510
- def registered_functions(self) -> Dict[str, _Function]:
507
+ def registered_functions(self) -> dict[str, _Function]:
511
508
  """All modal.Function objects registered on the app."""
512
509
  return self._functions
513
510
 
514
511
  @property
515
- def registered_classes(self) -> Dict[str, _Function]:
512
+ def registered_classes(self) -> dict[str, _Function]:
516
513
  """All modal.Cls objects registered on the app."""
517
514
  return self._classes
518
515
 
519
516
  @property
520
- def registered_entrypoints(self) -> Dict[str, _LocalEntrypoint]:
517
+ def registered_entrypoints(self) -> dict[str, _LocalEntrypoint]:
521
518
  """All local CLI entrypoints registered on the app."""
522
519
  return self._local_entrypoints
523
520
 
524
521
  @property
525
- def indexed_objects(self) -> Dict[str, _Object]:
522
+ def indexed_objects(self) -> dict[str, _Object]:
526
523
  deprecation_warning(
527
524
  (2024, 11, 25),
528
525
  "`app.indexed_objects` is deprecated! Use `app.registered_functions` or `app.registered_classes` instead.",
@@ -530,7 +527,7 @@ class _App:
530
527
  return dict(**self._functions, **self._classes)
531
528
 
532
529
  @property
533
- def registered_web_endpoints(self) -> List[str]:
530
+ def registered_web_endpoints(self) -> list[str]:
534
531
  """Names of web endpoint (ie. webhook) functions registered on the app."""
535
532
  return self._web_endpoints
536
533
 
@@ -609,24 +606,24 @@ class _App:
609
606
  schedule: Optional[Schedule] = None, # An optional Modal Schedule for the function
610
607
  secrets: Sequence[_Secret] = (), # Optional Modal Secret objects with environment variables for the container
611
608
  gpu: Union[
612
- GPU_T, List[GPU_T]
609
+ GPU_T, list[GPU_T]
613
610
  ] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
614
611
  serialized: bool = False, # Whether to send the function over using cloudpickle.
615
612
  mounts: Sequence[_Mount] = (), # Modal Mounts added to the container
616
- network_file_systems: Dict[
613
+ network_file_systems: dict[
617
614
  Union[str, PurePosixPath], _NetworkFileSystem
618
615
  ] = {}, # Mountpoints for Modal NetworkFileSystems
619
- volumes: Dict[
616
+ volumes: dict[
620
617
  Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
621
618
  ] = {}, # Mount points for Modal Volumes & CloudBucketMounts
622
619
  allow_cross_region_volumes: bool = False, # Whether using network file systems from other regions is allowed.
623
620
  # Specify, in fractional CPU cores, how many CPU cores to request.
624
621
  # Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
625
622
  # CPU throttling will prevent a container from exceeding its specified limit.
626
- cpu: Optional[Union[float, Tuple[float, float]]] = None,
623
+ cpu: Optional[Union[float, tuple[float, float]]] = None,
627
624
  # Specify, in MiB, a memory request which is the minimum memory required.
628
625
  # Or, pass (request, limit) to additionally specify a hard limit in MiB.
629
- memory: Optional[Union[int, Tuple[int, int]]] = None,
626
+ memory: Optional[Union[int, tuple[int, int]]] = None,
630
627
  ephemeral_disk: Optional[int] = None, # Specify, in MiB, the ephemeral disk size for the Function.
631
628
  proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
632
629
  retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
@@ -815,24 +812,24 @@ class _App:
815
812
  image: Optional[_Image] = None, # The image to run as the container for the function
816
813
  secrets: Sequence[_Secret] = (), # Optional Modal Secret objects with environment variables for the container
817
814
  gpu: Union[
818
- GPU_T, List[GPU_T]
815
+ GPU_T, list[GPU_T]
819
816
  ] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
820
817
  serialized: bool = False, # Whether to send the function over using cloudpickle.
821
818
  mounts: Sequence[_Mount] = (),
822
- network_file_systems: Dict[
819
+ network_file_systems: dict[
823
820
  Union[str, PurePosixPath], _NetworkFileSystem
824
821
  ] = {}, # Mountpoints for Modal NetworkFileSystems
825
- volumes: Dict[
822
+ volumes: dict[
826
823
  Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
827
824
  ] = {}, # Mount points for Modal Volumes & CloudBucketMounts
828
825
  allow_cross_region_volumes: bool = False, # Whether using network file systems from other regions is allowed.
829
826
  # Specify, in fractional CPU cores, how many CPU cores to request.
830
827
  # Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
831
828
  # CPU throttling will prevent a container from exceeding its specified limit.
832
- cpu: Optional[Union[float, Tuple[float, float]]] = None,
829
+ cpu: Optional[Union[float, tuple[float, float]]] = None,
833
830
  # Specify, in MiB, a memory request which is the minimum memory required.
834
831
  # Or, pass (request, limit) to additionally specify a hard limit in MiB.
835
- memory: Optional[Union[int, Tuple[int, int]]] = None,
832
+ memory: Optional[Union[int, tuple[int, int]]] = None,
836
833
  ephemeral_disk: Optional[int] = None, # Specify, in MiB, the ephemeral disk size for the Function.
837
834
  proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
838
835
  retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
@@ -935,7 +932,7 @@ class _App:
935
932
  cls: _Cls = _Cls.from_local(user_cls, self, cls_func)
936
933
 
937
934
  tag: str = user_cls.__name__
938
- self._add_object(tag, cls)
935
+ self._add_class(tag, cls)
939
936
  return cls # type: ignore # a _Cls instance "simulates" being the user provided class
940
937
 
941
938
  return wrapper
@@ -946,7 +943,7 @@ class _App:
946
943
  image: Optional[_Image] = None, # The image to run as the container for the sandbox.
947
944
  mounts: Sequence[_Mount] = (), # Mounts to attach to the sandbox.
948
945
  secrets: Sequence[_Secret] = (), # Environment variables to inject into the sandbox.
949
- network_file_systems: Dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
946
+ network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
950
947
  timeout: Optional[int] = None, # Maximum execution time of the sandbox in seconds.
951
948
  workdir: Optional[str] = None, # Working directory of the sandbox.
952
949
  gpu: GPU_T = None,
@@ -955,12 +952,12 @@ class _App:
955
952
  # Specify, in fractional CPU cores, how many CPU cores to request.
956
953
  # Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
957
954
  # CPU throttling will prevent a container from exceeding its specified limit.
958
- cpu: Optional[Union[float, Tuple[float, float]]] = None,
955
+ cpu: Optional[Union[float, tuple[float, float]]] = None,
959
956
  # Specify, in MiB, a memory request which is the minimum memory required.
960
957
  # Or, pass (request, limit) to additionally specify a hard limit in MiB.
961
- memory: Optional[Union[int, Tuple[int, int]]] = None,
958
+ memory: Optional[Union[int, tuple[int, int]]] = None,
962
959
  block_network: bool = False, # Whether to block network access
963
- volumes: Dict[
960
+ volumes: dict[
964
961
  Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
965
962
  ] = {}, # Mount points for Modal Volumes and CloudBucketMounts
966
963
  pty_info: Optional[api_pb2.PTYInfo] = None,
@@ -1022,16 +1019,25 @@ class _App:
1022
1019
  bar.remote()
1023
1020
  ```
1024
1021
  """
1025
- indexed_objects = dict(**other_app._functions, **other_app._classes)
1026
- for tag, object in indexed_objects.items():
1027
- existing_object = indexed_objects.get(tag)
1028
- 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:
1029
1035
  logger.warning(
1030
- f"Named app object {tag} with existing value {existing_object} is being "
1031
- 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}"
1032
1038
  )
1033
1039
 
1034
- self._add_object(tag, object)
1040
+ self._add_class(tag, cls)
1035
1041
 
1036
1042
  async def _logs(self, client: Optional[_Client] = None) -> AsyncGenerator[str, None]:
1037
1043
  """Stream logs from the app.