modal 0.66.39__py3-none-any.whl → 0.66.46__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.
modal/functions.py CHANGED
@@ -314,7 +314,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
314
314
  _tag: str
315
315
  _raw_f: Callable[..., Any]
316
316
  _build_args: dict
317
- _can_use_base_function: bool = False # whether we need to call FunctionBindParams
317
+
318
318
  _is_generator: Optional[bool] = None
319
319
  _cluster_size: Optional[int] = None
320
320
 
@@ -323,10 +323,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
323
323
  _use_function_id: str # The function to invoke
324
324
  _use_method_name: str = ""
325
325
 
326
- # TODO (elias): remove _parent. In case of instance functions, and methods bound on those,
327
- # this references the parent class-function and is used to infer the client for lazy-loaded methods
328
- _parent: Optional["_Function"] = None
329
-
330
326
  _class_parameter_info: Optional["api_pb2.ClassParameterInfo"] = None
331
327
  _method_handle_metadata: Optional[Dict[str, "api_pb2.FunctionHandleMetadata"]] = None
332
328
 
@@ -511,7 +507,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
511
507
  fun._info = class_bound_method._info
512
508
  fun._obj = instance_service_function._obj
513
509
  fun._is_method = True
514
- fun._parent = instance_service_function._parent
515
510
  fun._app = class_bound_method._app
516
511
  fun._spec = class_bound_method._spec
517
512
  return fun
@@ -1019,27 +1014,37 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1019
1014
  Binds a class-function to a specific instance of (init params, options) or a new workspace
1020
1015
  """
1021
1016
 
1022
- async def _load(self: _Function, resolver: Resolver, existing_object_id: Optional[str]):
1023
- if self._parent is None:
1017
+ # In some cases, reuse the base function, i.e. not create new clones of each method or the "service function"
1018
+ can_use_parent = len(args) + len(kwargs) == 0 and not from_other_workspace and options is None
1019
+ parent = self
1020
+
1021
+ async def _load(param_bound_func: _Function, resolver: Resolver, existing_object_id: Optional[str]):
1022
+ if parent is None:
1024
1023
  raise ExecutionError("Can't find the parent class' service function")
1025
1024
  try:
1026
- identity = f"{self._parent.info.function_name} class service function"
1025
+ identity = f"{parent.info.function_name} class service function"
1027
1026
  except Exception:
1028
1027
  # Can't always look up the function name that way, so fall back to generic message
1029
1028
  identity = "class service function for a parameterized class"
1030
- if not self._parent.is_hydrated:
1031
- if self._parent.app._running_app is None:
1029
+ if not parent.is_hydrated:
1030
+ if parent.app._running_app is None:
1032
1031
  reason = ", because the App it is defined on is not running"
1033
1032
  else:
1034
1033
  reason = ""
1035
1034
  raise ExecutionError(
1036
1035
  f"The {identity} has not been hydrated with the metadata it needs to run on Modal{reason}."
1037
1036
  )
1038
- assert self._parent._client.stub
1037
+
1038
+ assert parent._client.stub
1039
+
1040
+ if can_use_parent:
1041
+ # We can end up here if parent wasn't hydrated when class was instantiated, but has been since.
1042
+ param_bound_func._hydrate_from_other(parent)
1043
+ return
1044
+
1039
1045
  if (
1040
- self._parent._class_parameter_info
1041
- and self._parent._class_parameter_info.format
1042
- == api_pb2.ClassParameterInfo.PARAM_SERIALIZATION_FORMAT_PROTO
1046
+ parent._class_parameter_info
1047
+ and parent._class_parameter_info.format == api_pb2.ClassParameterInfo.PARAM_SERIALIZATION_FORMAT_PROTO
1043
1048
  ):
1044
1049
  if args:
1045
1050
  # TODO(elias) - We could potentially support positional args as well, if we want to?
@@ -1047,34 +1052,30 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1047
1052
  "Can't use positional arguments with modal.parameter-based synthetic constructors.\n"
1048
1053
  "Use (<parameter_name>=value) keyword arguments when constructing classes instead."
1049
1054
  )
1050
- serialized_params = serialize_proto_params(kwargs, self._parent._class_parameter_info.schema)
1055
+ serialized_params = serialize_proto_params(kwargs, parent._class_parameter_info.schema)
1051
1056
  else:
1052
1057
  serialized_params = serialize((args, kwargs))
1053
1058
  environment_name = _get_environment_name(None, resolver)
1054
- assert self._parent is not None
1059
+ assert parent is not None
1055
1060
  req = api_pb2.FunctionBindParamsRequest(
1056
- function_id=self._parent._object_id,
1061
+ function_id=parent._object_id,
1057
1062
  serialized_params=serialized_params,
1058
1063
  function_options=options,
1059
1064
  environment_name=environment_name
1060
1065
  or "", # TODO: investigate shouldn't environment name always be specified here?
1061
1066
  )
1062
1067
 
1063
- response = await retry_transient_errors(self._parent._client.stub.FunctionBindParams, req)
1064
- self._hydrate(response.bound_function_id, self._parent._client, response.handle_metadata)
1068
+ response = await retry_transient_errors(parent._client.stub.FunctionBindParams, req)
1069
+ param_bound_func._hydrate(response.bound_function_id, parent._client, response.handle_metadata)
1065
1070
 
1066
1071
  fun: _Function = _Function._from_loader(_load, "Function(parametrized)", hydrate_lazily=True)
1067
1072
 
1068
- # In some cases, reuse the base function, i.e. not create new clones of each method or the "service function"
1069
- fun._can_use_base_function = len(args) + len(kwargs) == 0 and not from_other_workspace and options is None
1070
- if fun._can_use_base_function and self.is_hydrated:
1071
- # Edge case that lets us hydrate all objects right away
1072
- # if the instance didn't use explicit constructor arguments
1073
- fun._hydrate_from_other(self)
1073
+ if can_use_parent and parent.is_hydrated:
1074
+ # skip the resolver altogether:
1075
+ fun._hydrate_from_other(parent)
1074
1076
 
1075
1077
  fun._info = self._info
1076
1078
  fun._obj = obj
1077
- fun._parent = self
1078
1079
  return fun
1079
1080
 
1080
1081
  @live_method
@@ -1265,8 +1266,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1265
1266
  + f"or call it locally: {self._function_name}.local()"
1266
1267
  )
1267
1268
 
1269
+ # TODO (live_method on properties is not great, since it could be blocking the event loop from async contexts)
1268
1270
  @property
1269
- def web_url(self) -> str:
1271
+ @live_method
1272
+ async def web_url(self) -> str:
1270
1273
  """URL of a Function running as a web endpoint."""
1271
1274
  if not self._web_url:
1272
1275
  raise ValueError(
@@ -1439,7 +1442,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1439
1442
  return fun(*args, **kwargs)
1440
1443
  else:
1441
1444
  # This is a method on a class, so bind the self to the function
1442
- user_cls_instance = obj._get_user_cls_instance()
1445
+ user_cls_instance = obj._cached_user_cls_instance()
1443
1446
 
1444
1447
  fun = info.raw_f.__get__(user_cls_instance)
1445
1448
 
modal/functions.pyi CHANGED
@@ -120,12 +120,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
120
120
  _tag: str
121
121
  _raw_f: typing.Callable[..., typing.Any]
122
122
  _build_args: dict
123
- _can_use_base_function: bool
124
123
  _is_generator: typing.Optional[bool]
125
124
  _cluster_size: typing.Optional[int]
126
125
  _use_function_id: str
127
126
  _use_method_name: str
128
- _parent: typing.Optional[_Function]
129
127
  _class_parameter_info: typing.Optional[modal_proto.api_pb2.ClassParameterInfo]
130
128
  _method_handle_metadata: typing.Optional[typing.Dict[str, modal_proto.api_pb2.FunctionHandleMetadata]]
131
129
 
@@ -218,7 +216,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
218
216
  def _get_metadata(self): ...
219
217
  def _check_no_web_url(self, fn_name: str): ...
220
218
  @property
221
- def web_url(self) -> str: ...
219
+ async def web_url(self) -> str: ...
222
220
  @property
223
221
  def is_generator(self) -> bool: ...
224
222
  @property
@@ -296,12 +294,10 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
296
294
  _tag: str
297
295
  _raw_f: typing.Callable[..., typing.Any]
298
296
  _build_args: dict
299
- _can_use_base_function: bool
300
297
  _is_generator: typing.Optional[bool]
301
298
  _cluster_size: typing.Optional[int]
302
299
  _use_function_id: str
303
300
  _use_method_name: str
304
- _parent: typing.Optional[Function]
305
301
  _class_parameter_info: typing.Optional[modal_proto.api_pb2.ClassParameterInfo]
306
302
  _method_handle_metadata: typing.Optional[typing.Dict[str, modal_proto.api_pb2.FunctionHandleMetadata]]
307
303
 
modal/image.py CHANGED
@@ -344,7 +344,7 @@ class _Image(_Object, type_prefix="im"):
344
344
  "\n"
345
345
  "my_image = (\n"
346
346
  " Image.debian_slim()\n"
347
- ' .add_local_python_packages("mypak", copy=True)\n'
347
+ ' .add_local_file("data.json", copy=True)\n'
348
348
  ' .run_commands("python -m mypak") # this now works!\n'
349
349
  ")\n"
350
350
  )
@@ -601,12 +601,59 @@ class _Image(_Object, type_prefix="im"):
601
601
  context_mount=mount,
602
602
  )
603
603
 
604
+ def add_local_file(self, local_path: Union[str, Path], remote_path: str, *, copy: bool = False) -> "_Image":
605
+ """Adds a local file to the image at `remote_path` within the container
606
+
607
+ By default (`copy=False`), the files are added to containers on startup and are not built into the actual Image,
608
+ which speeds up deployment.
609
+
610
+ Set `copy=True` to copy the files into an Image layer at build time instead, similar to how
611
+ [`COPY`](https://docs.docker.com/engine/reference/builder/#copy) works in a `Dockerfile`.
612
+
613
+ copy=True can slow down iteration since it requires a rebuild of the Image and any subsequent
614
+ build steps whenever the included files change, but it is required if you want to run additional
615
+ build steps after this one.
616
+ """
617
+ if not PurePosixPath(remote_path).is_absolute():
618
+ # TODO(elias): implement relative to absolute resolution using image workdir metadata
619
+ # + make default remote_path="./"
620
+ # This requires deferring the Mount creation until after "self" (the base image) has been resolved
621
+ # so we know the workdir of the operation.
622
+ raise InvalidError("image.add_local_file() currently only supports absolute remote_path values")
623
+
624
+ if remote_path.endswith("/"):
625
+ remote_path = remote_path + Path(local_path).name
626
+
627
+ mount = _Mount.from_local_file(local_path, remote_path)
628
+ return self._add_mount_layer_or_copy(mount, copy=copy)
629
+
630
+ def add_local_dir(self, local_path: Union[str, Path], remote_path: str, *, copy: bool = False) -> "_Image":
631
+ """Adds a local directory's content to the image at `remote_path` within the container
632
+
633
+ By default (`copy=False`), the files are added to containers on startup and are not built into the actual Image,
634
+ which speeds up deployment.
635
+
636
+ Set `copy=True` to copy the files into an Image layer at build time instead, similar to how
637
+ [`COPY`](https://docs.docker.com/engine/reference/builder/#copy) works in a `Dockerfile`.
638
+
639
+ copy=True can slow down iteration since it requires a rebuild of the Image and any subsequent
640
+ build steps whenever the included files change, but it is required if you want to run additional
641
+ build steps after this one.
642
+ """
643
+ if not PurePosixPath(remote_path).is_absolute():
644
+ # TODO(elias): implement relative to absolute resolution using image workdir metadata
645
+ # + make default remote_path="./"
646
+ raise InvalidError("image.add_local_dir() currently only supports absolute remote_path values")
647
+ mount = _Mount.from_local_dir(local_path, remote_path=remote_path)
648
+ return self._add_mount_layer_or_copy(mount, copy=copy)
649
+
604
650
  def copy_local_file(self, local_path: Union[str, Path], remote_path: Union[str, Path] = "./") -> "_Image":
605
651
  """Copy a file into the image as a part of building it.
606
652
 
607
653
  This works in a similar way to [`COPY`](https://docs.docker.com/engine/reference/builder/#copy)
608
654
  works in a `Dockerfile`.
609
655
  """
656
+ # TODO(elias): add pending deprecation with suggestion to use add_* instead
610
657
  basename = str(Path(local_path).name)
611
658
  mount = _Mount.from_local_file(local_path, remote_path=f"/{basename}")
612
659
 
@@ -1637,7 +1684,7 @@ class _Image(_Object, type_prefix="im"):
1637
1684
  dockerfile_function=build_dockerfile,
1638
1685
  )
1639
1686
 
1640
- def workdir(self, path: str) -> "_Image":
1687
+ def workdir(self, path: Union[str, PurePosixPath]) -> "_Image":
1641
1688
  """Set the working directory for subsequent image build steps and function execution.
1642
1689
 
1643
1690
  **Example**
modal/image.pyi CHANGED
@@ -100,6 +100,12 @@ class _Image(modal.object._Object):
100
100
  _do_assert_no_mount_layers: bool = True,
101
101
  ) -> _Image: ...
102
102
  def copy_mount(self, mount: modal.mount._Mount, remote_path: typing.Union[str, pathlib.Path] = ".") -> _Image: ...
103
+ def add_local_file(
104
+ self, local_path: typing.Union[str, pathlib.Path], remote_path: str, *, copy: bool = False
105
+ ) -> _Image: ...
106
+ def add_local_dir(
107
+ self, local_path: typing.Union[str, pathlib.Path], remote_path: str, *, copy: bool = False
108
+ ) -> _Image: ...
103
109
  def copy_local_file(
104
110
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
105
111
  ) -> _Image: ...
@@ -301,7 +307,7 @@ class _Image(modal.object._Object):
301
307
  kwargs: typing.Dict[str, typing.Any] = {},
302
308
  ) -> _Image: ...
303
309
  def env(self, vars: typing.Dict[str, str]) -> _Image: ...
304
- def workdir(self, path: str) -> _Image: ...
310
+ def workdir(self, path: typing.Union[str, pathlib.PurePosixPath]) -> _Image: ...
305
311
  def imports(self): ...
306
312
  def _logs(self) -> typing.AsyncGenerator[str, None]: ...
307
313
 
@@ -351,6 +357,12 @@ class Image(modal.object.Object):
351
357
  _do_assert_no_mount_layers: bool = True,
352
358
  ) -> Image: ...
353
359
  def copy_mount(self, mount: modal.mount.Mount, remote_path: typing.Union[str, pathlib.Path] = ".") -> Image: ...
360
+ def add_local_file(
361
+ self, local_path: typing.Union[str, pathlib.Path], remote_path: str, *, copy: bool = False
362
+ ) -> Image: ...
363
+ def add_local_dir(
364
+ self, local_path: typing.Union[str, pathlib.Path], remote_path: str, *, copy: bool = False
365
+ ) -> Image: ...
354
366
  def copy_local_file(
355
367
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
356
368
  ) -> Image: ...
@@ -552,7 +564,7 @@ class Image(modal.object.Object):
552
564
  kwargs: typing.Dict[str, typing.Any] = {},
553
565
  ) -> Image: ...
554
566
  def env(self, vars: typing.Dict[str, str]) -> Image: ...
555
- def workdir(self, path: str) -> Image: ...
567
+ def workdir(self, path: typing.Union[str, pathlib.PurePosixPath]) -> Image: ...
556
568
  def imports(self): ...
557
569
 
558
570
  class ___logs_spec(typing_extensions.Protocol):
modal/io_streams.py CHANGED
@@ -66,9 +66,10 @@ T = TypeVar("T", str, bytes)
66
66
 
67
67
 
68
68
  class _StreamReader(Generic[T]):
69
- """Provides an interface to buffer and fetch logs from a stream (`stdout` or `stderr`).
69
+ """Retrieve logs from a stream (`stdout` or `stderr`).
70
70
 
71
- As an asynchronous iterable, the object supports the async for statement.
71
+ As an asynchronous iterable, the object supports the `for` and `async for`
72
+ statements. Just loop over the object to read in chunks.
72
73
 
73
74
  **Usage**
74
75
 
@@ -140,12 +141,12 @@ class _StreamReader(Generic[T]):
140
141
  self._consume_container_process_task = asyncio.create_task(self._consume_container_process_stream())
141
142
 
142
143
  @property
143
- def file_descriptor(self):
144
+ def file_descriptor(self) -> int:
145
+ """Possible values are `1` for stdout and `2` for stderr."""
144
146
  return self._file_descriptor
145
147
 
146
148
  async def read(self) -> T:
147
- """Fetch and return contents of the entire stream. If EOF was received,
148
- return an empty string.
149
+ """Fetch the entire contents of the stream until EOF.
149
150
 
150
151
  **Usage**
151
152
 
@@ -157,7 +158,6 @@ class _StreamReader(Generic[T]):
157
158
 
158
159
  print(sandbox.stdout.read())
159
160
  ```
160
-
161
161
  """
162
162
  data_str = ""
163
163
  data_bytes = b""
@@ -175,9 +175,7 @@ class _StreamReader(Generic[T]):
175
175
  return cast(T, data_bytes)
176
176
 
177
177
  async def _consume_container_process_stream(self):
178
- """
179
- Consumes the container process stream and stores the messages in the buffer.
180
- """
178
+ """Consume the container process stream and store messages in the buffer."""
181
179
  if self._stream_type == StreamType.DEVNULL:
182
180
  return
183
181
 
@@ -211,9 +209,7 @@ class _StreamReader(Generic[T]):
211
209
  raise exc
212
210
 
213
211
  async def _stream_container_process(self) -> AsyncGenerator[Tuple[Optional[bytes], str], None]:
214
- """mdmd:hidden
215
- Streams the container process buffer to the reader.
216
- """
212
+ """Streams the container process buffer to the reader."""
217
213
  entry_id = 0
218
214
  if self._last_entry_id:
219
215
  entry_id = int(self._last_entry_id) + 1
@@ -232,8 +228,7 @@ class _StreamReader(Generic[T]):
232
228
  entry_id += 1
233
229
 
234
230
  async def _get_logs(self) -> AsyncGenerator[Optional[bytes], None]:
235
- """mdmd:hidden
236
- Streams sandbox or process logs from the server to the reader.
231
+ """Streams sandbox or process logs from the server to the reader.
237
232
 
238
233
  Logs returned by this method may contain partial or multiple lines at a time.
239
234
 
@@ -278,9 +273,7 @@ class _StreamReader(Generic[T]):
278
273
  raise
279
274
 
280
275
  async def _get_logs_by_line(self) -> AsyncGenerator[Optional[bytes], None]:
281
- """mdmd:hidden
282
- Processes logs from the server and yields complete lines only.
283
- """
276
+ """Process logs from the server and yield complete lines only."""
284
277
  async for message in self._get_logs():
285
278
  if message is None:
286
279
  if self._line_buffer:
@@ -325,7 +318,8 @@ MAX_BUFFER_SIZE = 2 * 1024 * 1024
325
318
  class _StreamWriter:
326
319
  """Provides an interface to buffer and write logs to a sandbox or container process stream (`stdin`)."""
327
320
 
328
- def __init__(self, object_id: str, object_type: Literal["sandbox", "container_process"], client: _Client):
321
+ def __init__(self, object_id: str, object_type: Literal["sandbox", "container_process"], client: _Client) -> None:
322
+ """mdmd:hidden"""
329
323
  self._index = 1
330
324
  self._object_id = object_id
331
325
  self._object_type = object_type
@@ -333,17 +327,16 @@ class _StreamWriter:
333
327
  self._is_closed = False
334
328
  self._buffer = bytearray()
335
329
 
336
- def get_next_index(self):
337
- """mdmd:hidden"""
330
+ def _get_next_index(self) -> int:
338
331
  index = self._index
339
332
  self._index += 1
340
333
  return index
341
334
 
342
- def write(self, data: Union[bytes, bytearray, memoryview, str]):
343
- """
344
- Writes data to stream's internal buffer, but does not drain/flush the write.
335
+ def write(self, data: Union[bytes, bytearray, memoryview, str]) -> None:
336
+ """Write data to the stream but does not send it immediately.
345
337
 
346
- This method needs to be used along with the `drain()` method which flushes the buffer.
338
+ This is non-blocking and queues the data to an internal buffer. Must be
339
+ used along with the `drain()` method, which flushes the buffer.
347
340
 
348
341
  **Usage**
349
342
 
@@ -375,22 +368,36 @@ class _StreamWriter:
375
368
  else:
376
369
  raise TypeError(f"data argument must be a bytes-like object, not {type(data).__name__}")
377
370
 
378
- def write_eof(self):
379
- """
380
- Closes the write end of the stream after the buffered write data is drained.
381
- If the process was blocked on input, it will become unblocked after `write_eof()`.
371
+ def write_eof(self) -> None:
372
+ """Close the write end of the stream after the buffered data is drained.
382
373
 
383
- This method needs to be used along with the `drain()` method which flushes the EOF to the process.
374
+ If the process was blocked on input, it will become unblocked after
375
+ `write_eof()`. This method needs to be used along with the `drain()`
376
+ method, which flushes the EOF to the process.
384
377
  """
385
378
  self._is_closed = True
386
379
 
387
- async def drain(self):
388
- """
389
- Flushes the write buffer to the running process. Flushes the EOF if the writer is closed.
380
+ async def drain(self) -> None:
381
+ """Flush the write buffer and send data to the running process.
382
+
383
+ This is a flow control method that blocks until data is sent. It returns
384
+ when it is appropriate to continue writing data to the stream.
385
+
386
+ **Usage**
387
+
388
+ ```python
389
+ # Synchronous
390
+ writer.write(data)
391
+ writer.drain()
392
+
393
+ # Async
394
+ writer.write(data)
395
+ await writer.drain.aio()
396
+ ```
390
397
  """
391
398
  data = bytes(self._buffer)
392
399
  self._buffer.clear()
393
- index = self.get_next_index()
400
+ index = self._get_next_index()
394
401
 
395
402
  try:
396
403
  if self._object_type == "sandbox":
modal/io_streams.pyi CHANGED
@@ -26,7 +26,7 @@ class _StreamReader(typing.Generic[T]):
26
26
  by_line: bool = False,
27
27
  ) -> None: ...
28
28
  @property
29
- def file_descriptor(self): ...
29
+ def file_descriptor(self) -> int: ...
30
30
  async def read(self) -> T: ...
31
31
  async def _consume_container_process_stream(self): ...
32
32
  def _stream_container_process(self) -> typing.AsyncGenerator[typing.Tuple[typing.Optional[bytes], str], None]: ...
@@ -38,11 +38,11 @@ class _StreamReader(typing.Generic[T]):
38
38
  class _StreamWriter:
39
39
  def __init__(
40
40
  self, object_id: str, object_type: typing.Literal["sandbox", "container_process"], client: modal.client._Client
41
- ): ...
42
- def get_next_index(self): ...
43
- def write(self, data: typing.Union[bytes, bytearray, memoryview, str]): ...
44
- def write_eof(self): ...
45
- async def drain(self): ...
41
+ ) -> None: ...
42
+ def _get_next_index(self) -> int: ...
43
+ def write(self, data: typing.Union[bytes, bytearray, memoryview, str]) -> None: ...
44
+ def write_eof(self) -> None: ...
45
+ async def drain(self) -> None: ...
46
46
 
47
47
  T_INNER = typing.TypeVar("T_INNER", covariant=True)
48
48
 
@@ -60,7 +60,7 @@ class StreamReader(typing.Generic[T]):
60
60
  by_line: bool = False,
61
61
  ) -> None: ...
62
62
  @property
63
- def file_descriptor(self): ...
63
+ def file_descriptor(self) -> int: ...
64
64
 
65
65
  class __read_spec(typing_extensions.Protocol[T_INNER]):
66
66
  def __call__(self) -> T_INNER: ...
@@ -100,13 +100,13 @@ class StreamReader(typing.Generic[T]):
100
100
  class StreamWriter:
101
101
  def __init__(
102
102
  self, object_id: str, object_type: typing.Literal["sandbox", "container_process"], client: modal.client.Client
103
- ): ...
104
- def get_next_index(self): ...
105
- def write(self, data: typing.Union[bytes, bytearray, memoryview, str]): ...
106
- def write_eof(self): ...
103
+ ) -> None: ...
104
+ def _get_next_index(self) -> int: ...
105
+ def write(self, data: typing.Union[bytes, bytearray, memoryview, str]) -> None: ...
106
+ def write_eof(self) -> None: ...
107
107
 
108
108
  class __drain_spec(typing_extensions.Protocol):
109
- def __call__(self): ...
110
- async def aio(self): ...
109
+ def __call__(self) -> None: ...
110
+ async def aio(self) -> None: ...
111
111
 
112
112
  drain: __drain_spec
modal/mount.py CHANGED
@@ -377,7 +377,9 @@ class _Mount(_Object, type_prefix="mo"):
377
377
  )
378
378
 
379
379
  def add_local_file(
380
- self, local_path: Union[str, Path], remote_path: Union[str, PurePosixPath, None] = None
380
+ self,
381
+ local_path: Union[str, Path],
382
+ remote_path: Union[str, PurePosixPath, None] = None,
381
383
  ) -> "_Mount":
382
384
  """
383
385
  Add a local file to the `Mount` object.
modal/partial_function.py CHANGED
@@ -91,7 +91,7 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
91
91
  if obj: # accessing the method on an instance of a class, e.g. `MyClass().fun``
92
92
  if hasattr(obj, "_modal_functions"):
93
93
  # This happens inside "local" user methods when they refer to other methods,
94
- # e.g. Foo().parent_method() doing self.local.other_method()
94
+ # e.g. Foo().parent_method.remote() calling self.other_method.remote()
95
95
  return getattr(obj, "_modal_functions")[k]
96
96
  else:
97
97
  # special edge case: referencing a method of an instance of an
modal/runner.py CHANGED
@@ -327,11 +327,13 @@ async def _run_app(
327
327
  )
328
328
 
329
329
  try:
330
+ indexed_objects = dict(**app._functions, **app._classes) # TODO(erikbern): remove
331
+
330
332
  # Create all members
331
- await _create_all_objects(client, running_app, app._indexed_objects, environment_name)
333
+ await _create_all_objects(client, running_app, indexed_objects, environment_name)
332
334
 
333
335
  # Publish the app
334
- await _publish_app(client, running_app, app_state, app._indexed_objects)
336
+ await _publish_app(client, running_app, app_state, indexed_objects)
335
337
  except asyncio.CancelledError as e:
336
338
  # this typically happens on sigint/ctrl-C during setup (the KeyboardInterrupt happens in the main thread)
337
339
  if output_mgr := _get_output_manager():
@@ -424,16 +426,18 @@ async def _serve_update(
424
426
  try:
425
427
  running_app: RunningApp = await _init_local_app_existing(client, existing_app_id, environment_name)
426
428
 
429
+ indexed_objects = dict(**app._functions, **app._classes) # TODO(erikbern): remove
430
+
427
431
  # Create objects
428
432
  await _create_all_objects(
429
433
  client,
430
434
  running_app,
431
- app._indexed_objects,
435
+ indexed_objects,
432
436
  environment_name,
433
437
  )
434
438
 
435
439
  # Publish the updated app
436
- await _publish_app(client, running_app, api_pb2.APP_STATE_UNSPECIFIED, app._indexed_objects)
440
+ await _publish_app(client, running_app, api_pb2.APP_STATE_UNSPECIFIED, indexed_objects)
437
441
 
438
442
  # Communicate to the parent process
439
443
  is_ready.set()
@@ -521,17 +525,19 @@ async def _deploy_app(
521
525
 
522
526
  tc.infinite_loop(heartbeat, sleep=HEARTBEAT_INTERVAL)
523
527
 
528
+ indexed_objects = dict(**app._functions, **app._classes) # TODO(erikbern): remove
529
+
524
530
  try:
525
531
  # Create all members
526
532
  await _create_all_objects(
527
533
  client,
528
534
  running_app,
529
- app._indexed_objects,
535
+ indexed_objects,
530
536
  environment_name=environment_name,
531
537
  )
532
538
 
533
539
  app_url, warnings = await _publish_app(
534
- client, running_app, api_pb2.APP_STATE_DEPLOYED, app._indexed_objects, name, tag
540
+ client, running_app, api_pb2.APP_STATE_DEPLOYED, indexed_objects, name, tag
535
541
  )
536
542
  except Exception as e:
537
543
  # Note that AppClientDisconnect only stops the app if it's still initializing, and is a no-op otherwise.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.66.39
3
+ Version: 0.66.46
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com