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
modal/cls.py CHANGED
@@ -2,7 +2,8 @@
2
2
  import inspect
3
3
  import os
4
4
  import typing
5
- from typing import Any, Callable, Collection, Dict, List, Optional, Tuple, Type, TypeVar, Union
5
+ from collections.abc import Collection
6
+ from typing import Any, Callable, Optional, TypeVar, Union
6
7
 
7
8
  from google.protobuf.message import Message
8
9
  from grpclib import GRPCError, Status
@@ -71,15 +72,77 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature:
71
72
  return inspect.Signature(constructor_parameters)
72
73
 
73
74
 
75
+ def _bind_instance_method(service_function: _Function, class_bound_method: _Function):
76
+ """mdmd:hidden
77
+
78
+ Binds an "instance service function" to a specific method.
79
+ This "dummy" _Function gets no unique object_id and isn't backend-backed at the moment, since all
80
+ it does it forward invocations to the underlying instance_service_function with the specified method,
81
+ and we don't support web_config for parameterized methods at the moment.
82
+ """
83
+ # TODO(elias): refactor to not use `_from_loader()` as a crutch for lazy-loading the
84
+ # underlying instance_service_function. It's currently used in order to take advantage
85
+ # of resolver logic and get "chained" resolution of lazy loads, even though this thin
86
+ # object itself doesn't need any "loading"
87
+ assert service_function._obj
88
+ method_name = class_bound_method._use_method_name
89
+ full_function_name = f"{class_bound_method._function_name}[parameterized]"
90
+
91
+ def hydrate_from_instance_service_function(method_placeholder_fun):
92
+ method_placeholder_fun._hydrate_from_other(service_function)
93
+ method_placeholder_fun._obj = service_function._obj
94
+ method_placeholder_fun._web_url = (
95
+ class_bound_method._web_url
96
+ ) # TODO: this shouldn't be set when actual parameters are used
97
+ method_placeholder_fun._function_name = full_function_name
98
+ method_placeholder_fun._is_generator = class_bound_method._is_generator
99
+ method_placeholder_fun._cluster_size = class_bound_method._cluster_size
100
+ method_placeholder_fun._use_method_name = method_name
101
+ method_placeholder_fun._is_method = True
102
+
103
+ async def _load(fun: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
104
+ # there is currently no actual loading logic executed to create each method on
105
+ # the *parameterized* instance of a class - it uses the parameter-bound service-function
106
+ # for the instance. This load method just makes sure to set all attributes after the
107
+ # `service_function` has been loaded (it's in the `_deps`)
108
+ hydrate_from_instance_service_function(fun)
109
+
110
+ def _deps():
111
+ if service_function.is_hydrated:
112
+ # without this check, the common service_function will be reloaded by all methods
113
+ # TODO(elias): Investigate if we can fix this multi-loader in the resolver - feels like a bug?
114
+ return []
115
+ return [service_function]
116
+
117
+ rep = f"Method({full_function_name})"
118
+
119
+ fun = _Function._from_loader(
120
+ _load,
121
+ rep,
122
+ deps=_deps,
123
+ hydrate_lazily=True,
124
+ )
125
+ if service_function.is_hydrated:
126
+ # Eager hydration (skip load) if the instance service function is already loaded
127
+ hydrate_from_instance_service_function(fun)
128
+
129
+ fun._info = class_bound_method._info
130
+ fun._obj = service_function._obj
131
+ fun._is_method = True
132
+ fun._app = class_bound_method._app
133
+ fun._spec = class_bound_method._spec
134
+ return fun
135
+
136
+
74
137
  class _Obj:
75
138
  """An instance of a `Cls`, i.e. `Cls("foo", 42)` returns an `Obj`.
76
139
 
77
140
  All this class does is to return `Function` objects."""
78
141
 
79
- _functions: Dict[str, _Function]
80
- _entered: bool
142
+ _functions: dict[str, _Function]
143
+ _has_entered: bool
81
144
  _user_cls_instance: Optional[Any] = None
82
- _construction_args: Tuple[tuple, Dict[str, Any]]
145
+ _construction_args: tuple[tuple, dict[str, Any]]
83
146
 
84
147
  _instance_service_function: Optional[_Function]
85
148
 
@@ -89,10 +152,9 @@ class _Obj:
89
152
 
90
153
  def __init__(
91
154
  self,
92
- user_cls: type,
155
+ user_cls: Optional[type], # this would be None in case of lookups
93
156
  class_service_function: Optional[_Function], # only None for <v0.63 classes
94
- classbound_methods: Dict[str, _Function],
95
- from_other_workspace: bool,
157
+ classbound_methods: dict[str, _Function],
96
158
  options: Optional[api_pb2.FunctionOptions],
97
159
  args,
98
160
  kwargs,
@@ -106,22 +168,19 @@ class _Obj:
106
168
  if class_service_function:
107
169
  # >= v0.63 classes
108
170
  # first create the singular object function used by all methods on this parameterization
109
- self._instance_service_function = class_service_function._bind_parameters(
110
- self, from_other_workspace, options, args, kwargs
111
- )
171
+ self._instance_service_function = class_service_function._bind_parameters(self, options, args, kwargs)
112
172
  for method_name, class_bound_method in classbound_methods.items():
113
- method = self._instance_service_function._bind_instance_method(class_bound_method)
173
+ method = _bind_instance_method(self._instance_service_function, class_bound_method)
114
174
  self._method_functions[method_name] = method
115
175
  else:
116
176
  # looked up <v0.63 classes - bind each individual method to the new parameters
117
177
  self._instance_service_function = None
118
178
  for method_name, class_bound_method in classbound_methods.items():
119
- method = class_bound_method._bind_parameters(self, from_other_workspace, options, args, kwargs)
179
+ method = class_bound_method._bind_parameters(self, options, args, kwargs)
120
180
  self._method_functions[method_name] = method
121
181
 
122
182
  # Used for construction local object lazily
123
- self._entered = False
124
- self._local_user_cls_instance = None
183
+ self._has_entered = False
125
184
  self._user_cls = user_cls
126
185
  self._construction_args = (args, kwargs) # used for lazy construction in case of explicit constructors
127
186
 
@@ -154,7 +213,7 @@ class _Obj:
154
213
  Note that all Modal methods and web endpoints of a class share the same set
155
214
  of containers and the warm_pool_size affects that common container pool.
156
215
 
157
- ```python
216
+ ```python notest
158
217
  # Usage on a parametrized function.
159
218
  Model = modal.Cls.lookup("my-app", "Model")
160
219
  Model("fine-tuned-model").keep_warm(2)
@@ -175,8 +234,8 @@ class _Obj:
175
234
 
176
235
  return self._user_cls_instance
177
236
 
178
- def enter(self):
179
- if not self._entered:
237
+ def _enter(self):
238
+ if not self._has_entered:
180
239
  if hasattr(self._user_cls_instance, "__enter__"):
181
240
  self._user_cls_instance.__enter__()
182
241
 
@@ -187,26 +246,26 @@ class _Obj:
187
246
  for enter_method in _find_callables_for_obj(self._user_cls_instance, method_flag).values():
188
247
  enter_method()
189
248
 
190
- self._entered = True
249
+ self._has_entered = True
191
250
 
192
251
  @property
193
- def entered(self):
194
- # needed because aenter is nowrap
195
- return self._entered
252
+ def _entered(self) -> bool:
253
+ # needed because _aenter is nowrap
254
+ return self._has_entered
196
255
 
197
- @entered.setter
198
- def entered(self, val):
199
- self._entered = val
256
+ @_entered.setter
257
+ def _entered(self, val: bool):
258
+ self._has_entered = val
200
259
 
201
260
  @synchronizer.nowrap
202
- async def aenter(self):
203
- if not self.entered:
261
+ async def _aenter(self):
262
+ if not self._entered: # use the property to get at the impl class
204
263
  user_cls_instance = self._cached_user_cls_instance()
205
264
  if hasattr(user_cls_instance, "__aenter__"):
206
265
  await user_cls_instance.__aenter__()
207
266
  elif hasattr(user_cls_instance, "__enter__"):
208
267
  user_cls_instance.__enter__()
209
- self.entered = True
268
+ self._has_entered = True
210
269
 
211
270
  def __getattr__(self, k):
212
271
  if k in self._method_functions:
@@ -244,10 +303,9 @@ class _Cls(_Object, type_prefix="cs"):
244
303
  _class_service_function: Optional[
245
304
  _Function
246
305
  ] # The _Function serving *all* methods of the class, used for version >=v0.63
247
- _method_functions: Optional[Dict[str, _Function]] = None # Placeholder _Functions for each method
306
+ _method_functions: Optional[dict[str, _Function]] = None # Placeholder _Functions for each method
248
307
  _options: Optional[api_pb2.FunctionOptions]
249
- _callables: Dict[str, Callable[..., Any]]
250
- _from_other_workspace: Optional[bool] # Functions require FunctionBindParams before invocation.
308
+ _callables: dict[str, Callable[..., Any]]
251
309
  _app: Optional["modal.app._App"] = None # not set for lookups
252
310
 
253
311
  def _initialize_from_empty(self):
@@ -255,7 +313,6 @@ class _Cls(_Object, type_prefix="cs"):
255
313
  self._class_service_function = None
256
314
  self._options = None
257
315
  self._callables = {}
258
- self._from_other_workspace = None
259
316
 
260
317
  def _initialize_from_other(self, other: "_Cls"):
261
318
  self._user_cls = other._user_cls
@@ -263,9 +320,8 @@ class _Cls(_Object, type_prefix="cs"):
263
320
  self._method_functions = other._method_functions
264
321
  self._options = other._options
265
322
  self._callables = other._callables
266
- self._from_other_workspace = other._from_other_workspace
267
323
 
268
- def _get_partial_functions(self) -> Dict[str, _PartialFunction]:
324
+ def _get_partial_functions(self) -> dict[str, _PartialFunction]:
269
325
  if not self._user_cls:
270
326
  raise AttributeError("You can only get the partial functions of a local Cls instance")
271
327
  return _find_partial_methods_for_user_cls(self._user_cls, _PartialFunctionFlags.all())
@@ -277,7 +333,7 @@ class _Cls(_Object, type_prefix="cs"):
277
333
  and self._class_service_function._method_handle_metadata
278
334
  and len(self._class_service_function._method_handle_metadata)
279
335
  ):
280
- # The class only has a class service service function and no method placeholders (v0.67+)
336
+ # The class only has a class service function and no method placeholders (v0.67+)
281
337
  if self._method_functions:
282
338
  # We're here when the Cls is loaded locally (e.g. _Cls.from_local) so the _method_functions mapping is
283
339
  # populated with (un-hydrated) _Function objects
@@ -298,7 +354,7 @@ class _Cls(_Object, type_prefix="cs"):
298
354
  self._method_functions[method_name] = _Function._new_hydrated(
299
355
  self._class_service_function.object_id, self._client, method_handle_metadata
300
356
  )
301
- elif self._class_service_function:
357
+ elif self._class_service_function and self._class_service_function.object_id:
302
358
  # A class with a class service function and method placeholder functions
303
359
  self._method_functions = {}
304
360
  for method in metadata.methods:
@@ -344,8 +400,8 @@ class _Cls(_Object, type_prefix="cs"):
344
400
  # validate signature
345
401
  _Cls.validate_construction_mechanism(user_cls)
346
402
 
347
- method_functions: Dict[str, _Function] = {}
348
- partial_functions: Dict[str, _PartialFunction] = _find_partial_methods_for_user_cls(
403
+ method_functions: dict[str, _Function] = {}
404
+ partial_functions: dict[str, _PartialFunction] = _find_partial_methods_for_user_cls(
349
405
  user_cls, _PartialFunctionFlags.FUNCTION
350
406
  )
351
407
 
@@ -361,11 +417,11 @@ class _Cls(_Object, type_prefix="cs"):
361
417
  partial_function.wrapped = True
362
418
 
363
419
  # Get all callables
364
- callables: Dict[str, Callable] = {
420
+ callables: dict[str, Callable] = {
365
421
  k: pf.raw_f for k, pf in _find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.all()).items()
366
422
  }
367
423
 
368
- def _deps() -> List[_Function]:
424
+ def _deps() -> list[_Function]:
369
425
  return [class_service_function]
370
426
 
371
427
  async def _load(self: "_Cls", resolver: Resolver, existing_object_id: Optional[str]):
@@ -382,7 +438,6 @@ class _Cls(_Object, type_prefix="cs"):
382
438
  cls._class_service_function = class_service_function
383
439
  cls._method_functions = method_functions
384
440
  cls._callables = callables
385
- cls._from_other_workspace = False
386
441
  return cls
387
442
 
388
443
  def _uses_common_service_function(self):
@@ -392,7 +447,7 @@ class _Cls(_Object, type_prefix="cs"):
392
447
 
393
448
  @classmethod
394
449
  def from_name(
395
- cls: Type["_Cls"],
450
+ cls: type["_Cls"],
396
451
  app_name: str,
397
452
  tag: str,
398
453
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
@@ -449,16 +504,15 @@ class _Cls(_Object, type_prefix="cs"):
449
504
 
450
505
  rep = f"Ref({app_name})"
451
506
  cls = cls._from_loader(_load_remote, rep, is_another_app=True)
452
- cls._from_other_workspace = bool(workspace is not None)
453
507
  return cls
454
508
 
455
509
  def with_options(
456
510
  self: "_Cls",
457
- cpu: Optional[Union[float, Tuple[float, float]]] = None,
458
- memory: Optional[Union[int, Tuple[int, int]]] = None,
511
+ cpu: Optional[Union[float, tuple[float, float]]] = None,
512
+ memory: Optional[Union[int, tuple[int, int]]] = None,
459
513
  gpu: GPU_T = None,
460
514
  secrets: Collection[_Secret] = (),
461
- volumes: Dict[Union[str, os.PathLike], _Volume] = {},
515
+ volumes: dict[Union[str, os.PathLike], _Volume] = {},
462
516
  retries: Optional[Union[int, Retries]] = None,
463
517
  timeout: Optional[int] = None,
464
518
  concurrency_limit: Optional[int] = None,
@@ -524,7 +578,7 @@ class _Cls(_Object, type_prefix="cs"):
524
578
  In contrast to `modal.Cls.from_name`, this is an eager method
525
579
  that will hydrate the local object with metadata from Modal servers.
526
580
 
527
- ```python
581
+ ```python notest
528
582
  Class = modal.Cls.lookup("other-app", "Class")
529
583
  obj = Class()
530
584
  ```
@@ -543,7 +597,6 @@ class _Cls(_Object, type_prefix="cs"):
543
597
  self._user_cls,
544
598
  self._class_service_function,
545
599
  self._method_functions,
546
- self._from_other_workspace,
547
600
  self._options,
548
601
  args,
549
602
  kwargs,
modal/cls.pyi CHANGED
@@ -1,3 +1,4 @@
1
+ import collections.abc
1
2
  import google.protobuf.message
2
3
  import inspect
3
4
  import modal.app
@@ -18,21 +19,23 @@ T = typing.TypeVar("T")
18
19
 
19
20
  def _use_annotation_parameters(user_cls) -> bool: ...
20
21
  def _get_class_constructor_signature(user_cls: type) -> inspect.Signature: ...
22
+ def _bind_instance_method(
23
+ service_function: modal.functions._Function, class_bound_method: modal.functions._Function
24
+ ): ...
21
25
 
22
26
  class _Obj:
23
- _functions: typing.Dict[str, modal.functions._Function]
24
- _entered: bool
27
+ _functions: dict[str, modal.functions._Function]
28
+ _has_entered: bool
25
29
  _user_cls_instance: typing.Optional[typing.Any]
26
- _construction_args: typing.Tuple[tuple, typing.Dict[str, typing.Any]]
30
+ _construction_args: tuple[tuple, dict[str, typing.Any]]
27
31
  _instance_service_function: typing.Optional[modal.functions._Function]
28
32
 
29
33
  def _uses_common_service_function(self): ...
30
34
  def __init__(
31
35
  self,
32
- user_cls: type,
36
+ user_cls: typing.Optional[type],
33
37
  class_service_function: typing.Optional[modal.functions._Function],
34
- classbound_methods: typing.Dict[str, modal.functions._Function],
35
- from_other_workspace: bool,
38
+ classbound_methods: dict[str, modal.functions._Function],
36
39
  options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
37
40
  args,
38
41
  kwargs,
@@ -40,27 +43,26 @@ class _Obj:
40
43
  def _new_user_cls_instance(self): ...
41
44
  async def keep_warm(self, warm_pool_size: int) -> None: ...
42
45
  def _cached_user_cls_instance(self): ...
43
- def enter(self): ...
46
+ def _enter(self): ...
44
47
  @property
45
- def entered(self): ...
46
- @entered.setter
47
- def entered(self, val): ...
48
- async def aenter(self): ...
48
+ def _entered(self) -> bool: ...
49
+ @_entered.setter
50
+ def _entered(self, val: bool): ...
51
+ async def _aenter(self): ...
49
52
  def __getattr__(self, k): ...
50
53
 
51
54
  class Obj:
52
- _functions: typing.Dict[str, modal.functions.Function]
53
- _entered: bool
55
+ _functions: dict[str, modal.functions.Function]
56
+ _has_entered: bool
54
57
  _user_cls_instance: typing.Optional[typing.Any]
55
- _construction_args: typing.Tuple[tuple, typing.Dict[str, typing.Any]]
58
+ _construction_args: tuple[tuple, dict[str, typing.Any]]
56
59
  _instance_service_function: typing.Optional[modal.functions.Function]
57
60
 
58
61
  def __init__(
59
62
  self,
60
- user_cls: type,
63
+ user_cls: typing.Optional[type],
61
64
  class_service_function: typing.Optional[modal.functions.Function],
62
- classbound_methods: typing.Dict[str, modal.functions.Function],
63
- from_other_workspace: bool,
65
+ classbound_methods: dict[str, modal.functions.Function],
64
66
  options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
65
67
  args,
66
68
  kwargs,
@@ -75,26 +77,25 @@ class Obj:
75
77
  keep_warm: __keep_warm_spec
76
78
 
77
79
  def _cached_user_cls_instance(self): ...
78
- def enter(self): ...
80
+ def _enter(self): ...
79
81
  @property
80
- def entered(self): ...
81
- @entered.setter
82
- def entered(self, val): ...
83
- async def aenter(self): ...
82
+ def _entered(self) -> bool: ...
83
+ @_entered.setter
84
+ def _entered(self, val: bool): ...
85
+ async def _aenter(self): ...
84
86
  def __getattr__(self, k): ...
85
87
 
86
88
  class _Cls(modal.object._Object):
87
89
  _user_cls: typing.Optional[type]
88
90
  _class_service_function: typing.Optional[modal.functions._Function]
89
- _method_functions: typing.Optional[typing.Dict[str, modal.functions._Function]]
91
+ _method_functions: typing.Optional[dict[str, modal.functions._Function]]
90
92
  _options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
91
- _callables: typing.Dict[str, typing.Callable[..., typing.Any]]
92
- _from_other_workspace: typing.Optional[bool]
93
+ _callables: dict[str, typing.Callable[..., typing.Any]]
93
94
  _app: typing.Optional[modal.app._App]
94
95
 
95
96
  def _initialize_from_empty(self): ...
96
97
  def _initialize_from_other(self, other: _Cls): ...
97
- def _get_partial_functions(self) -> typing.Dict[str, modal.partial_function._PartialFunction]: ...
98
+ def _get_partial_functions(self) -> dict[str, modal.partial_function._PartialFunction]: ...
98
99
  def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
99
100
  @staticmethod
100
101
  def validate_construction_mechanism(user_cls): ...
@@ -103,7 +104,7 @@ class _Cls(modal.object._Object):
103
104
  def _uses_common_service_function(self): ...
104
105
  @classmethod
105
106
  def from_name(
106
- cls: typing.Type[_Cls],
107
+ cls: type[_Cls],
107
108
  app_name: str,
108
109
  tag: str,
109
110
  namespace=1,
@@ -112,11 +113,11 @@ class _Cls(modal.object._Object):
112
113
  ) -> _Cls: ...
113
114
  def with_options(
114
115
  self: _Cls,
115
- cpu: typing.Union[float, typing.Tuple[float, float], None] = None,
116
- memory: typing.Union[int, typing.Tuple[int, int], None] = None,
116
+ cpu: typing.Union[float, tuple[float, float], None] = None,
117
+ memory: typing.Union[int, tuple[int, int], None] = None,
117
118
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
118
- secrets: typing.Collection[modal.secret._Secret] = (),
119
- volumes: typing.Dict[typing.Union[str, os.PathLike], modal.volume._Volume] = {},
119
+ secrets: collections.abc.Collection[modal.secret._Secret] = (),
120
+ volumes: dict[typing.Union[str, os.PathLike], modal.volume._Volume] = {},
120
121
  retries: typing.Union[int, modal.retries.Retries, None] = None,
121
122
  timeout: typing.Optional[int] = None,
122
123
  concurrency_limit: typing.Optional[int] = None,
@@ -138,16 +139,15 @@ class _Cls(modal.object._Object):
138
139
  class Cls(modal.object.Object):
139
140
  _user_cls: typing.Optional[type]
140
141
  _class_service_function: typing.Optional[modal.functions.Function]
141
- _method_functions: typing.Optional[typing.Dict[str, modal.functions.Function]]
142
+ _method_functions: typing.Optional[dict[str, modal.functions.Function]]
142
143
  _options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
143
- _callables: typing.Dict[str, typing.Callable[..., typing.Any]]
144
- _from_other_workspace: typing.Optional[bool]
144
+ _callables: dict[str, typing.Callable[..., typing.Any]]
145
145
  _app: typing.Optional[modal.app.App]
146
146
 
147
147
  def __init__(self, *args, **kwargs): ...
148
148
  def _initialize_from_empty(self): ...
149
149
  def _initialize_from_other(self, other: Cls): ...
150
- def _get_partial_functions(self) -> typing.Dict[str, modal.partial_function.PartialFunction]: ...
150
+ def _get_partial_functions(self) -> dict[str, modal.partial_function.PartialFunction]: ...
151
151
  def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
152
152
  @staticmethod
153
153
  def validate_construction_mechanism(user_cls): ...
@@ -156,7 +156,7 @@ class Cls(modal.object.Object):
156
156
  def _uses_common_service_function(self): ...
157
157
  @classmethod
158
158
  def from_name(
159
- cls: typing.Type[Cls],
159
+ cls: type[Cls],
160
160
  app_name: str,
161
161
  tag: str,
162
162
  namespace=1,
@@ -165,11 +165,11 @@ class Cls(modal.object.Object):
165
165
  ) -> Cls: ...
166
166
  def with_options(
167
167
  self: Cls,
168
- cpu: typing.Union[float, typing.Tuple[float, float], None] = None,
169
- memory: typing.Union[int, typing.Tuple[int, int], None] = None,
168
+ cpu: typing.Union[float, tuple[float, float], None] = None,
169
+ memory: typing.Union[int, tuple[int, int], None] = None,
170
170
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
171
- secrets: typing.Collection[modal.secret.Secret] = (),
172
- volumes: typing.Dict[typing.Union[str, os.PathLike], modal.volume.Volume] = {},
171
+ secrets: collections.abc.Collection[modal.secret.Secret] = (),
172
+ volumes: dict[typing.Union[str, os.PathLike], modal.volume.Volume] = {},
173
173
  retries: typing.Union[int, modal.retries.Retries, None] = None,
174
174
  timeout: typing.Optional[int] = None,
175
175
  concurrency_limit: typing.Optional[int] = None,
modal/config.py CHANGED
@@ -80,7 +80,7 @@ import os
80
80
  import typing
81
81
  import warnings
82
82
  from textwrap import dedent
83
- from typing import Any, Dict, Optional
83
+ from typing import Any, Optional
84
84
 
85
85
  from google.protobuf.empty_pb2 import Empty
86
86
 
@@ -221,6 +221,7 @@ _SETTINGS = {
221
221
  "image_builder_version": _Setting(),
222
222
  "strict_parameters": _Setting(False, transform=_to_boolean), # For internal/experimental use
223
223
  "snapshot_debug": _Setting(False, transform=_to_boolean),
224
+ "client_retries": _Setting(False, transform=_to_boolean), # For internal testing.
224
225
  }
225
226
 
226
227
 
@@ -282,7 +283,7 @@ configure_logger(logger, config["loglevel"], config["log_format"])
282
283
 
283
284
 
284
285
  def _store_user_config(
285
- new_settings: Dict[str, Any], profile: Optional[str] = None, active_profile: Optional[str] = None
286
+ new_settings: dict[str, Any], profile: Optional[str] = None, active_profile: Optional[str] = None
286
287
  ):
287
288
  """Internal method, used by the CLI to set tokens."""
288
289
  if profile is None:
@@ -128,12 +128,16 @@ class _ContainerProcess(Generic[T]):
128
128
  on_connect = asyncio.Event()
129
129
 
130
130
  async def _write_to_fd_loop(stream: _StreamReader):
131
- async for line in stream:
131
+ # Don't skip empty messages so we can detect when the process has booted.
132
+ async for chunk in stream._get_logs(skip_empty_messages=False):
133
+ if chunk is None:
134
+ break
135
+
132
136
  if not on_connect.is_set():
133
137
  connecting_status.stop()
134
138
  on_connect.set()
135
139
 
136
- await write_to_fd(stream.file_descriptor, line.encode("utf-8"))
140
+ await write_to_fd(stream.file_descriptor, chunk)
137
141
 
138
142
  async def _handle_input(data: bytes, message_index: int):
139
143
  self.stdin.write(data)
modal/dict.py CHANGED
@@ -1,5 +1,6 @@
1
1
  # Copyright Modal Labs 2022
2
- from typing import Any, AsyncIterator, Optional, Tuple, Type
2
+ from collections.abc import AsyncIterator
3
+ from typing import Any, Optional
3
4
 
4
5
  from grpclib import GRPCError
5
6
  from synchronicity.async_wrap import asynccontextmanager
@@ -74,7 +75,7 @@ class _Dict(_Object, type_prefix="di"):
74
75
  @classmethod
75
76
  @asynccontextmanager
76
77
  async def ephemeral(
77
- cls: Type["_Dict"],
78
+ cls: type["_Dict"],
78
79
  data: Optional[dict] = None,
79
80
  client: Optional[_Client] = None,
80
81
  environment_name: Optional[str] = None,
@@ -88,7 +89,9 @@ class _Dict(_Object, type_prefix="di"):
88
89
 
89
90
  with Dict.ephemeral() as d:
90
91
  d["foo"] = "bar"
92
+ ```
91
93
 
94
+ ```python notest
92
95
  async with Dict.ephemeral() as d:
93
96
  await d.put.aio("foo", "bar")
94
97
  ```
@@ -314,7 +317,7 @@ class _Dict(_Object, type_prefix="di"):
314
317
  yield deserialize(resp.value, self._client)
315
318
 
316
319
  @live_method_gen
317
- async def items(self) -> AsyncIterator[Tuple[Any, Any]]:
320
+ async def items(self) -> AsyncIterator[tuple[Any, Any]]:
318
321
  """Return an iterator over the (key, value) tuples in this dictionary.
319
322
 
320
323
  Note that (unlike with Python dicts) the return value is a simple iterator,
modal/dict.pyi CHANGED
@@ -1,3 +1,4 @@
1
+ import collections.abc
1
2
  import modal.client
2
3
  import modal.object
3
4
  import synchronicity.combined_types
@@ -12,7 +13,7 @@ class _Dict(modal.object._Object):
12
13
  def __init__(self, data={}): ...
13
14
  @classmethod
14
15
  def ephemeral(
15
- cls: typing.Type[_Dict],
16
+ cls: type[_Dict],
16
17
  data: typing.Optional[dict] = None,
17
18
  client: typing.Optional[modal.client._Client] = None,
18
19
  environment_name: typing.Optional[str] = None,
@@ -53,9 +54,9 @@ class _Dict(modal.object._Object):
53
54
  async def pop(self, key: typing.Any) -> typing.Any: ...
54
55
  async def __delitem__(self, key: typing.Any) -> typing.Any: ...
55
56
  async def __contains__(self, key: typing.Any) -> bool: ...
56
- def keys(self) -> typing.AsyncIterator[typing.Any]: ...
57
- def values(self) -> typing.AsyncIterator[typing.Any]: ...
58
- def items(self) -> typing.AsyncIterator[typing.Tuple[typing.Any, typing.Any]]: ...
57
+ def keys(self) -> collections.abc.AsyncIterator[typing.Any]: ...
58
+ def values(self) -> collections.abc.AsyncIterator[typing.Any]: ...
59
+ def items(self) -> collections.abc.AsyncIterator[tuple[typing.Any, typing.Any]]: ...
59
60
 
60
61
  class Dict(modal.object.Object):
61
62
  def __init__(self, data={}): ...
@@ -63,7 +64,7 @@ class Dict(modal.object.Object):
63
64
  def new(data: typing.Optional[dict] = None): ...
64
65
  @classmethod
65
66
  def ephemeral(
66
- cls: typing.Type[Dict],
67
+ cls: type[Dict],
67
68
  data: typing.Optional[dict] = None,
68
69
  client: typing.Optional[modal.client.Client] = None,
69
70
  environment_name: typing.Optional[str] = None,
@@ -186,18 +187,18 @@ class Dict(modal.object.Object):
186
187
 
187
188
  class __keys_spec(typing_extensions.Protocol):
188
189
  def __call__(self) -> typing.Iterator[typing.Any]: ...
189
- def aio(self) -> typing.AsyncIterator[typing.Any]: ...
190
+ def aio(self) -> collections.abc.AsyncIterator[typing.Any]: ...
190
191
 
191
192
  keys: __keys_spec
192
193
 
193
194
  class __values_spec(typing_extensions.Protocol):
194
195
  def __call__(self) -> typing.Iterator[typing.Any]: ...
195
- def aio(self) -> typing.AsyncIterator[typing.Any]: ...
196
+ def aio(self) -> collections.abc.AsyncIterator[typing.Any]: ...
196
197
 
197
198
  values: __values_spec
198
199
 
199
200
  class __items_spec(typing_extensions.Protocol):
200
- def __call__(self) -> typing.Iterator[typing.Tuple[typing.Any, typing.Any]]: ...
201
- def aio(self) -> typing.AsyncIterator[typing.Tuple[typing.Any, typing.Any]]: ...
201
+ def __call__(self) -> typing.Iterator[tuple[typing.Any, typing.Any]]: ...
202
+ def aio(self) -> collections.abc.AsyncIterator[tuple[typing.Any, typing.Any]]: ...
202
203
 
203
204
  items: __items_spec
modal/environments.py CHANGED
@@ -1,6 +1,6 @@
1
1
  # Copyright Modal Labs 2023
2
2
  from dataclasses import dataclass
3
- from typing import Dict, List, Optional
3
+ from typing import Optional
4
4
 
5
5
  from google.protobuf.empty_pb2 import Empty
6
6
  from google.protobuf.message import Message
@@ -98,7 +98,7 @@ Environment = synchronize_api(_Environment)
98
98
 
99
99
 
100
100
  # Needs to be after definition; synchronicity interferes with forward references?
101
- ENVIRONMENT_CACHE: Dict[str, _Environment] = {}
101
+ ENVIRONMENT_CACHE: dict[str, _Environment] = {}
102
102
 
103
103
 
104
104
  async def _get_environment_cached(name: str, client: _Client) -> _Environment:
@@ -151,7 +151,7 @@ async def create_environment(name: str, client: Optional[_Client] = None):
151
151
 
152
152
 
153
153
  @synchronizer.create_blocking
154
- async def list_environments(client: Optional[_Client] = None) -> List[api_pb2.EnvironmentListItem]:
154
+ async def list_environments(client: Optional[_Client] = None) -> list[api_pb2.EnvironmentListItem]:
155
155
  if client is None:
156
156
  client = await _Client.from_env()
157
157
  resp = await client.stub.EnvironmentList(Empty())
modal/environments.pyi CHANGED
@@ -87,13 +87,13 @@ create_environment: __create_environment_spec
87
87
  class __list_environments_spec(typing_extensions.Protocol):
88
88
  def __call__(
89
89
  self, client: typing.Optional[modal.client.Client] = None
90
- ) -> typing.List[modal_proto.api_pb2.EnvironmentListItem]: ...
90
+ ) -> list[modal_proto.api_pb2.EnvironmentListItem]: ...
91
91
  async def aio(
92
92
  self, client: typing.Optional[modal.client.Client] = None
93
- ) -> typing.List[modal_proto.api_pb2.EnvironmentListItem]: ...
93
+ ) -> list[modal_proto.api_pb2.EnvironmentListItem]: ...
94
94
 
95
95
  list_environments: __list_environments_spec
96
96
 
97
97
  def ensure_env(environment_name: typing.Optional[str] = None) -> str: ...
98
98
 
99
- ENVIRONMENT_CACHE: typing.Dict[str, _Environment]
99
+ ENVIRONMENT_CACHE: dict[str, _Environment]