modal 0.67.42__py3-none-any.whl → 0.68.11__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 (41) hide show
  1. modal/_container_entrypoint.py +4 -1
  2. modal/_runtime/container_io_manager.py +3 -0
  3. modal/_runtime/user_code_imports.py +4 -2
  4. modal/_traceback.py +16 -2
  5. modal/_utils/function_utils.py +5 -1
  6. modal/_utils/grpc_testing.py +6 -2
  7. modal/_utils/hash_utils.py +14 -2
  8. modal/cli/_traceback.py +11 -4
  9. modal/cli/container.py +16 -5
  10. modal/cli/run.py +23 -21
  11. modal/cli/utils.py +4 -0
  12. modal/client.py +6 -37
  13. modal/client.pyi +2 -6
  14. modal/cls.py +132 -62
  15. modal/cls.pyi +13 -7
  16. modal/container_process.py +10 -3
  17. modal/container_process.pyi +3 -3
  18. modal/exception.py +20 -0
  19. modal/file_io.py +380 -0
  20. modal/file_io.pyi +185 -0
  21. modal/functions.py +33 -11
  22. modal/functions.pyi +11 -9
  23. modal/object.py +4 -2
  24. modal/partial_function.py +14 -10
  25. modal/partial_function.pyi +2 -2
  26. modal/runner.py +19 -7
  27. modal/runner.pyi +11 -4
  28. modal/sandbox.py +50 -3
  29. modal/sandbox.pyi +18 -0
  30. {modal-0.67.42.dist-info → modal-0.68.11.dist-info}/METADATA +2 -2
  31. {modal-0.67.42.dist-info → modal-0.68.11.dist-info}/RECORD +41 -39
  32. modal_docs/gen_reference_docs.py +1 -0
  33. modal_proto/api.proto +25 -1
  34. modal_proto/api_pb2.py +758 -718
  35. modal_proto/api_pb2.pyi +95 -10
  36. modal_version/__init__.py +1 -1
  37. modal_version/_version_generated.py +1 -1
  38. {modal-0.67.42.dist-info → modal-0.68.11.dist-info}/LICENSE +0 -0
  39. {modal-0.67.42.dist-info → modal-0.68.11.dist-info}/WHEEL +0 -0
  40. {modal-0.67.42.dist-info → modal-0.68.11.dist-info}/entry_points.txt +0 -0
  41. {modal-0.67.42.dist-info → modal-0.68.11.dist-info}/top_level.txt +0 -0
modal/cls.py CHANGED
@@ -14,15 +14,13 @@ from modal_proto import api_pb2
14
14
  from ._resolver import Resolver
15
15
  from ._resources import convert_fn_config_to_resources_config
16
16
  from ._serialization import check_valid_cls_constructor_arg
17
+ from ._traceback import print_server_warnings
17
18
  from ._utils.async_utils import synchronize_api, synchronizer
18
19
  from ._utils.grpc_utils import retry_transient_errors
19
20
  from ._utils.mount_utils import validate_volumes
20
21
  from .client import _Client
21
- from .exception import InvalidError, NotFoundError, VersionError
22
- from .functions import (
23
- _Function,
24
- _parse_retries,
25
- )
22
+ from .exception import ExecutionError, InvalidError, NotFoundError, VersionError
23
+ from .functions import _Function, _parse_retries
26
24
  from .gpu import GPU_T
27
25
  from .object import _get_environment_name, _Object
28
26
  from .partial_function import (
@@ -42,7 +40,7 @@ if typing.TYPE_CHECKING:
42
40
  import modal.app
43
41
 
44
42
 
45
- def _use_annotation_parameters(user_cls) -> bool:
43
+ def _use_annotation_parameters(user_cls: type) -> bool:
46
44
  has_parameters = any(is_parameter(cls_member) for cls_member in user_cls.__dict__.values())
47
45
  has_explicit_constructor = user_cls.__init__ != object.__init__
48
46
  return has_parameters and not has_explicit_constructor
@@ -75,7 +73,7 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature:
75
73
  def _bind_instance_method(service_function: _Function, class_bound_method: _Function):
76
74
  """mdmd:hidden
77
75
 
78
- Binds an "instance service function" to a specific method.
76
+ Binds an "instance service function" to a specific method name.
79
77
  This "dummy" _Function gets no unique object_id and isn't backend-backed at the moment, since all
80
78
  it does it forward invocations to the underlying instance_service_function with the specified method,
81
79
  and we don't support web_config for parameterized methods at the moment.
@@ -86,7 +84,6 @@ def _bind_instance_method(service_function: _Function, class_bound_method: _Func
86
84
  # object itself doesn't need any "loading"
87
85
  assert service_function._obj
88
86
  method_name = class_bound_method._use_method_name
89
- full_function_name = f"{class_bound_method._function_name}[parameterized]"
90
87
 
91
88
  def hydrate_from_instance_service_function(method_placeholder_fun):
92
89
  method_placeholder_fun._hydrate_from_other(service_function)
@@ -94,7 +91,7 @@ def _bind_instance_method(service_function: _Function, class_bound_method: _Func
94
91
  method_placeholder_fun._web_url = (
95
92
  class_bound_method._web_url
96
93
  ) # TODO: this shouldn't be set when actual parameters are used
97
- method_placeholder_fun._function_name = full_function_name
94
+ method_placeholder_fun._function_name = f"{class_bound_method._function_name}[parameterized]"
98
95
  method_placeholder_fun._is_generator = class_bound_method._is_generator
99
96
  method_placeholder_fun._cluster_size = class_bound_method._cluster_size
100
97
  method_placeholder_fun._use_method_name = method_name
@@ -114,7 +111,7 @@ def _bind_instance_method(service_function: _Function, class_bound_method: _Func
114
111
  return []
115
112
  return [service_function]
116
113
 
117
- rep = f"Method({full_function_name})"
114
+ rep = f"Method({method_name})"
118
115
 
119
116
  fun = _Function._from_loader(
120
117
  _load,
@@ -139,22 +136,23 @@ class _Obj:
139
136
 
140
137
  All this class does is to return `Function` objects."""
141
138
 
139
+ _cls: "_Cls" # parent
142
140
  _functions: dict[str, _Function]
143
141
  _has_entered: bool
144
142
  _user_cls_instance: Optional[Any] = None
145
- _construction_args: tuple[tuple, dict[str, Any]]
143
+ _args: tuple[Any, ...]
144
+ _kwargs: dict[str, Any]
146
145
 
147
- _instance_service_function: Optional[_Function]
146
+ _instance_service_function: Optional[_Function] = None # this gets set lazily
148
147
 
149
148
  def _uses_common_service_function(self):
150
149
  # Used for backwards compatibility checks with pre v0.63 classes
151
- return self._instance_service_function is not None
150
+ return self._cls._class_service_function is not None
152
151
 
153
152
  def __init__(
154
153
  self,
154
+ cls: "_Cls",
155
155
  user_cls: Optional[type], # this would be None in case of lookups
156
- class_service_function: Optional[_Function], # only None for <v0.63 classes
157
- classbound_methods: dict[str, _Function],
158
156
  options: Optional[api_pb2.FunctionOptions],
159
157
  args,
160
158
  kwargs,
@@ -163,45 +161,60 @@ class _Obj:
163
161
  check_valid_cls_constructor_arg(i + 1, arg)
164
162
  for key, kwarg in kwargs.items():
165
163
  check_valid_cls_constructor_arg(key, kwarg)
166
-
167
- self._method_functions = {}
168
- if class_service_function:
169
- # >= v0.63 classes
170
- # first create the singular object function used by all methods on this parameterization
171
- self._instance_service_function = class_service_function._bind_parameters(self, options, args, kwargs)
172
- for method_name, class_bound_method in classbound_methods.items():
173
- method = _bind_instance_method(self._instance_service_function, class_bound_method)
174
- self._method_functions[method_name] = method
175
- else:
176
- # looked up <v0.63 classes - bind each individual method to the new parameters
177
- self._instance_service_function = None
178
- for method_name, class_bound_method in classbound_methods.items():
179
- method = class_bound_method._bind_parameters(self, options, args, kwargs)
180
- self._method_functions[method_name] = method
164
+ self._cls = cls
181
165
 
182
166
  # Used for construction local object lazily
183
167
  self._has_entered = False
184
168
  self._user_cls = user_cls
185
- self._construction_args = (args, kwargs) # used for lazy construction in case of explicit constructors
169
+
170
+ # used for lazy construction in case of explicit constructors
171
+ self._args = args
172
+ self._kwargs = kwargs
173
+ self._options = options
174
+
175
+ def _cached_service_function(self) -> "modal.functions._Function":
176
+ # Returns a service function for this _Obj, serving all its methods
177
+ # In case of methods without parameters or options, this is simply proxying to the class service function
178
+
179
+ # only safe to call for 0.63+ classes (before then, all methods had their own services)
180
+ if not self._instance_service_function:
181
+ assert self._cls._class_service_function
182
+ self._instance_service_function = self._cls._class_service_function._bind_parameters(
183
+ self, self._options, self._args, self._kwargs
184
+ )
185
+ return self._instance_service_function
186
+
187
+ def _get_parameter_values(self) -> dict[str, Any]:
188
+ # binds args and kwargs according to the class constructor signature
189
+ # (implicit by parameters or explicit)
190
+ sig = _get_class_constructor_signature(self._user_cls)
191
+ bound_vars = sig.bind(*self._args, **self._kwargs)
192
+ bound_vars.apply_defaults()
193
+ return bound_vars.arguments
186
194
 
187
195
  def _new_user_cls_instance(self):
188
- args, kwargs = self._construction_args
189
196
  if not _use_annotation_parameters(self._user_cls):
190
197
  # TODO(elias): deprecate this code path eventually
191
- user_cls_instance = self._user_cls(*args, **kwargs)
198
+ user_cls_instance = self._user_cls(*self._args, **self._kwargs)
192
199
  else:
193
200
  # ignore constructor (assumes there is no custom constructor,
194
201
  # which is guaranteed by _use_annotation_parameters)
195
202
  # set the attributes on the class corresponding to annotations
196
203
  # with = parameter() specifications
197
- sig = _get_class_constructor_signature(self._user_cls)
198
- bound_vars = sig.bind(*args, **kwargs)
199
- bound_vars.apply_defaults()
204
+ param_values = self._get_parameter_values()
200
205
  user_cls_instance = self._user_cls.__new__(self._user_cls) # new instance without running __init__
201
- user_cls_instance.__dict__.update(bound_vars.arguments)
206
+ user_cls_instance.__dict__.update(param_values)
202
207
 
203
208
  # TODO: always use Obj instances instead of making modifications to user cls
204
- user_cls_instance._modal_functions = self._method_functions # Needed for PartialFunction.__get__
209
+ # TODO: OR (if simpler for now) replace all the PartialFunctions on the user cls
210
+ # with getattr(self, method_name)
211
+
212
+ # user cls instances are only created locally, so we have all partial functions available
213
+ instance_methods = {}
214
+ for method_name in _find_partial_methods_for_user_cls(self._user_cls, _PartialFunctionFlags.FUNCTION):
215
+ instance_methods[method_name] = getattr(self, method_name)
216
+
217
+ user_cls_instance._modal_functions = instance_methods
205
218
  return user_cls_instance
206
219
 
207
220
  async def keep_warm(self, warm_pool_size: int) -> None:
@@ -223,7 +236,7 @@ class _Obj:
223
236
  raise VersionError(
224
237
  "Class instance `.keep_warm(...)` can't be used on classes deployed using client version <v0.63"
225
238
  )
226
- await self._instance_service_function.keep_warm(warm_pool_size)
239
+ await self._cached_service_function().keep_warm(warm_pool_size)
227
240
 
228
241
  def _cached_user_cls_instance(self):
229
242
  """Get or construct the local object
@@ -235,18 +248,20 @@ class _Obj:
235
248
  return self._user_cls_instance
236
249
 
237
250
  def _enter(self):
251
+ assert self._user_cls
238
252
  if not self._has_entered:
239
- if hasattr(self._user_cls_instance, "__enter__"):
240
- self._user_cls_instance.__enter__()
253
+ user_cls_instance = self._cached_user_cls_instance()
254
+ if hasattr(user_cls_instance, "__enter__"):
255
+ user_cls_instance.__enter__()
241
256
 
242
257
  for method_flag in (
243
258
  _PartialFunctionFlags.ENTER_PRE_SNAPSHOT,
244
259
  _PartialFunctionFlags.ENTER_POST_SNAPSHOT,
245
260
  ):
246
- for enter_method in _find_callables_for_obj(self._user_cls_instance, method_flag).values():
261
+ for enter_method in _find_callables_for_obj(user_cls_instance, method_flag).values():
247
262
  enter_method()
248
263
 
249
- self._has_entered = True
264
+ self._has_entered = True
250
265
 
251
266
  @property
252
267
  def _entered(self) -> bool:
@@ -268,23 +283,74 @@ class _Obj:
268
283
  self._has_entered = True
269
284
 
270
285
  def __getattr__(self, k):
271
- if k in self._method_functions:
272
- # If we know the user is accessing a *method* and not another attribute,
273
- # we don't have to create an instance of the user class yet.
274
- # This is because it might just be a call to `.remote()` on it which
275
- # doesn't require a local instance.
276
- # As long as we have the service function or params, we can do remote calls
277
- # without calling the constructor of the class in the calling context.
278
- return self._method_functions[k]
286
+ # This is a bit messy and branchy because:
287
+ # * Support for pre-0.63 lookups *and* newer classes
288
+ # * Support .remote() on both hydrated (local or remote classes) or unhydrated classes (remote classes only)
289
+ # * Support .local() on both hydrated and unhydrated classes (assuming local access to code)
290
+ # * Support attribute access (when local cls is available)
291
+
292
+ def _get_method_bound_function() -> Optional["_Function"]:
293
+ """Gets _Function object for method - either for a local or a hydrated remote class
294
+
295
+ * If class is neither local or hydrated - raise exception (should never happen)
296
+ * If attribute isn't a method - return None
297
+ """
298
+ if self._cls._method_functions is None:
299
+ raise ExecutionError("Method is not local and not hydrated")
300
+
301
+ if class_bound_method := self._cls._method_functions.get(k, None):
302
+ # If we know the user is accessing a *method* and not another attribute,
303
+ # we don't have to create an instance of the user class yet.
304
+ # This is because it might just be a call to `.remote()` on it which
305
+ # doesn't require a local instance.
306
+ # As long as we have the service function or params, we can do remote calls
307
+ # without calling the constructor of the class in the calling context.
308
+ if self._cls._class_service_function is None:
309
+ # a <v0.63 lookup
310
+ return class_bound_method._bind_parameters(self, self._options, self._args, self._kwargs)
311
+ else:
312
+ return _bind_instance_method(self._cached_service_function(), class_bound_method)
313
+
314
+ return None # The attribute isn't a method
315
+
316
+ if self._cls._method_functions is not None:
317
+ # We get here with either a hydrated Cls or an unhydrated one with local definition
318
+ if method := _get_method_bound_function():
319
+ return method
320
+ elif self._user_cls:
321
+ # We have the local definition, and the attribute isn't a method
322
+ # so we instantiate if we don't have an instance, and try to get the attribute
323
+ user_cls_instance = self._cached_user_cls_instance()
324
+ return getattr(user_cls_instance, k)
325
+ else:
326
+ # This is the case for a *hydrated* class without the local definition, i.e. a lookup
327
+ # where the attribute isn't a registered method of the class
328
+ raise NotFoundError(
329
+ f"Class has no method `{k}` and attributes (or undecorated methods) can't be accessed for"
330
+ f" remote classes (`Cls.from_name` instances)"
331
+ )
279
332
 
280
- # if it's *not* a method, it *might* be an attribute of the class,
281
- # so we construct it and proxy the attribute
282
- # TODO: To get lazy loading (from_name) of classes to work, we need to avoid
283
- # this path, otherwise local initialization will happen regardless if user
284
- # only runs .remote(), since we don't know methods for the class until we
285
- # load it
286
- user_cls_instance = self._cached_user_cls_instance()
287
- return getattr(user_cls_instance, k)
333
+ # Not hydrated Cls, and we don't have the class - typically a Cls.from_name that
334
+ # has not yet been loaded. So use a special loader that loads it lazily:
335
+
336
+ async def method_loader(fun, resolver: Resolver, existing_object_id):
337
+ await resolver.load(self._cls) # load class so we get info about methods
338
+ method_function = _get_method_bound_function()
339
+ if method_function is None:
340
+ raise NotFoundError(
341
+ f"Class has no method {k}, and attributes can't be accessed for `Cls.from_name` instances"
342
+ )
343
+ await resolver.load(method_function) # get the appropriate method handle (lazy)
344
+ fun._hydrate_from_other(method_function)
345
+
346
+ # The reason we don't *always* use this lazy loader is because it precludes attribute access
347
+ # on local classes.
348
+ return _Function._from_loader(
349
+ method_loader,
350
+ repr,
351
+ deps=lambda: [], # TODO: use cls as dep instead of loading inside method_loader?
352
+ hydrate_lazily=True,
353
+ )
288
354
 
289
355
 
290
356
  Obj = synchronize_api(_Obj)
@@ -315,6 +381,7 @@ class _Cls(_Object, type_prefix="cs"):
315
381
  self._callables = {}
316
382
 
317
383
  def _initialize_from_other(self, other: "_Cls"):
384
+ super()._initialize_from_other(other)
318
385
  self._user_cls = other._user_cls
319
386
  self._class_service_function = other._class_service_function
320
387
  self._method_functions = other._method_functions
@@ -486,6 +553,8 @@ class _Cls(_Object, type_prefix="cs"):
486
553
  else:
487
554
  raise
488
555
 
556
+ print_server_warnings(response.server_warnings)
557
+
489
558
  class_function_tag = f"{tag}.*" # special name of the base service function for the class
490
559
 
491
560
  class_service_function = _Function.from_name(
@@ -503,7 +572,8 @@ class _Cls(_Object, type_prefix="cs"):
503
572
  obj._hydrate(response.class_id, resolver.client, response.handle_metadata)
504
573
 
505
574
  rep = f"Ref({app_name})"
506
- cls = cls._from_loader(_load_remote, rep, is_another_app=True)
575
+ cls = cls._from_loader(_load_remote, rep, is_another_app=True, hydrate_lazily=True)
576
+ # TODO: when pre 0.63 is phased out, we can set class_service_function here instead
507
577
  return cls
508
578
 
509
579
  def with_options(
@@ -594,9 +664,8 @@ class _Cls(_Object, type_prefix="cs"):
594
664
  def __call__(self, *args, **kwargs) -> _Obj:
595
665
  """This acts as the class constructor."""
596
666
  return _Obj(
667
+ self,
597
668
  self._user_cls,
598
- self._class_service_function,
599
- self._method_functions,
600
669
  self._options,
601
670
  args,
602
671
  kwargs,
@@ -604,6 +673,7 @@ class _Cls(_Object, type_prefix="cs"):
604
673
 
605
674
  def __getattr__(self, k):
606
675
  # Used by CLI and container entrypoint
676
+ # TODO: remove this method - access to attributes on classes should be discouraged
607
677
  if k in self._method_functions:
608
678
  return self._method_functions[k]
609
679
  return getattr(self._user_cls, k)
modal/cls.pyi CHANGED
@@ -17,29 +17,32 @@ import typing_extensions
17
17
 
18
18
  T = typing.TypeVar("T")
19
19
 
20
- def _use_annotation_parameters(user_cls) -> bool: ...
20
+ def _use_annotation_parameters(user_cls: type) -> bool: ...
21
21
  def _get_class_constructor_signature(user_cls: type) -> inspect.Signature: ...
22
22
  def _bind_instance_method(
23
23
  service_function: modal.functions._Function, class_bound_method: modal.functions._Function
24
24
  ): ...
25
25
 
26
26
  class _Obj:
27
+ _cls: _Cls
27
28
  _functions: dict[str, modal.functions._Function]
28
29
  _has_entered: bool
29
30
  _user_cls_instance: typing.Optional[typing.Any]
30
- _construction_args: tuple[tuple, dict[str, typing.Any]]
31
+ _args: tuple[typing.Any, ...]
32
+ _kwargs: dict[str, typing.Any]
31
33
  _instance_service_function: typing.Optional[modal.functions._Function]
32
34
 
33
35
  def _uses_common_service_function(self): ...
34
36
  def __init__(
35
37
  self,
38
+ cls: _Cls,
36
39
  user_cls: typing.Optional[type],
37
- class_service_function: typing.Optional[modal.functions._Function],
38
- classbound_methods: dict[str, modal.functions._Function],
39
40
  options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
40
41
  args,
41
42
  kwargs,
42
43
  ): ...
44
+ def _cached_service_function(self) -> modal.functions._Function: ...
45
+ def _get_parameter_values(self) -> dict[str, typing.Any]: ...
43
46
  def _new_user_cls_instance(self): ...
44
47
  async def keep_warm(self, warm_pool_size: int) -> None: ...
45
48
  def _cached_user_cls_instance(self): ...
@@ -52,22 +55,25 @@ class _Obj:
52
55
  def __getattr__(self, k): ...
53
56
 
54
57
  class Obj:
58
+ _cls: Cls
55
59
  _functions: dict[str, modal.functions.Function]
56
60
  _has_entered: bool
57
61
  _user_cls_instance: typing.Optional[typing.Any]
58
- _construction_args: tuple[tuple, dict[str, typing.Any]]
62
+ _args: tuple[typing.Any, ...]
63
+ _kwargs: dict[str, typing.Any]
59
64
  _instance_service_function: typing.Optional[modal.functions.Function]
60
65
 
61
66
  def __init__(
62
67
  self,
68
+ cls: Cls,
63
69
  user_cls: typing.Optional[type],
64
- class_service_function: typing.Optional[modal.functions.Function],
65
- classbound_methods: dict[str, modal.functions.Function],
66
70
  options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
67
71
  args,
68
72
  kwargs,
69
73
  ): ...
70
74
  def _uses_common_service_function(self): ...
75
+ def _cached_service_function(self) -> modal.functions.Function: ...
76
+ def _get_parameter_values(self) -> dict[str, typing.Any]: ...
71
77
  def _new_user_cls_instance(self): ...
72
78
 
73
79
  class __keep_warm_spec(typing_extensions.Protocol):
@@ -9,7 +9,7 @@ from ._utils.async_utils import TaskContext, synchronize_api
9
9
  from ._utils.grpc_utils import retry_transient_errors
10
10
  from ._utils.shell_utils import stream_from_stdin, write_to_fd
11
11
  from .client import _Client
12
- from .exception import InteractiveTimeoutError, InvalidError
12
+ from .exception import InteractiveTimeoutError, InvalidError, deprecation_error
13
13
  from .io_streams import _StreamReader, _StreamWriter
14
14
  from .stream_type import StreamType
15
15
 
@@ -114,11 +114,18 @@ class _ContainerProcess(Generic[T]):
114
114
  self._returncode = resp.exit_code
115
115
  return self._returncode
116
116
 
117
- async def attach(self, *, pty: bool):
117
+ async def attach(self, *, pty: Optional[bool] = None):
118
118
  if platform.system() == "Windows":
119
119
  print("interactive exec is not currently supported on Windows.")
120
120
  return
121
121
 
122
+ if pty is not None:
123
+ deprecation_error(
124
+ (2024, 12, 9),
125
+ "The `pty` argument to `modal.container_process.attach(pty=...)` is deprecated, "
126
+ "as only PTY mode is supported. Please remove the argument.",
127
+ )
128
+
122
129
  from rich.console import Console
123
130
 
124
131
  console = Console()
@@ -151,7 +158,7 @@ class _ContainerProcess(Generic[T]):
151
158
  # time out if we can't connect to the server fast enough
152
159
  await asyncio.wait_for(on_connect.wait(), timeout=60)
153
160
 
154
- async with stream_from_stdin(_handle_input, use_raw_terminal=pty):
161
+ async with stream_from_stdin(_handle_input, use_raw_terminal=True):
155
162
  await stdout_task
156
163
  await stderr_task
157
164
 
@@ -34,7 +34,7 @@ class _ContainerProcess(typing.Generic[T]):
34
34
  def returncode(self) -> int: ...
35
35
  async def poll(self) -> typing.Optional[int]: ...
36
36
  async def wait(self) -> int: ...
37
- async def attach(self, *, pty: bool): ...
37
+ async def attach(self, *, pty: typing.Optional[bool] = None): ...
38
38
 
39
39
  class ContainerProcess(typing.Generic[T]):
40
40
  _process_id: typing.Optional[str]
@@ -76,7 +76,7 @@ class ContainerProcess(typing.Generic[T]):
76
76
  wait: __wait_spec
77
77
 
78
78
  class __attach_spec(typing_extensions.Protocol):
79
- def __call__(self, *, pty: bool): ...
80
- async def aio(self, *, pty: bool): ...
79
+ def __call__(self, *, pty: typing.Optional[bool] = None): ...
80
+ async def aio(self, *, pty: typing.Optional[bool] = None): ...
81
81
 
82
82
  attach: __attach_spec
modal/exception.py CHANGED
@@ -4,6 +4,9 @@ import signal
4
4
  import sys
5
5
  import warnings
6
6
  from datetime import date
7
+ from typing import Iterable
8
+
9
+ from modal_proto import api_pb2
7
10
 
8
11
 
9
12
  class Error(Exception):
@@ -107,6 +110,10 @@ class PendingDeprecationError(UserWarning):
107
110
  """Soon to be deprecated feature. Only used intermittently because of multi-repo concerns."""
108
111
 
109
112
 
113
+ class ServerWarning(UserWarning):
114
+ """Warning originating from the Modal server and re-issued in client code."""
115
+
116
+
110
117
  class _CliUserExecutionError(Exception):
111
118
  """mdmd:hidden
112
119
  Private wrapper for exceptions during when importing or running stubs from the CLI.
@@ -213,3 +220,16 @@ class ModuleNotMountable(Exception):
213
220
 
214
221
  class ClientClosed(Error):
215
222
  pass
223
+
224
+
225
+ class FilesystemExecutionError(Error):
226
+ """Raised when an unknown error is thrown during a container filesystem operation."""
227
+
228
+
229
+ def print_server_warnings(server_warnings: Iterable[api_pb2.Warning]):
230
+ # TODO(erikbern): move this to modal._utils.deprecation
231
+ for warning in server_warnings:
232
+ if warning.type == api_pb2.Warning.WARNING_TYPE_CLIENT_DEPRECATION:
233
+ warnings.warn_explicit(warning.message, DeprecationError, "<unknown>", 0)
234
+ else:
235
+ warnings.warn_explicit(warning.message, UserWarning, "<unknown>", 0)