modal 1.0.4.dev10__py3-none-any.whl → 1.0.5__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 (67) hide show
  1. modal/_clustered_functions.pyi +13 -3
  2. modal/_functions.py +84 -46
  3. modal/_partial_function.py +1 -1
  4. modal/_runtime/container_io_manager.pyi +222 -40
  5. modal/_runtime/execution_context.pyi +60 -6
  6. modal/_serialization.py +25 -2
  7. modal/_tunnel.pyi +380 -12
  8. modal/_utils/async_utils.py +1 -1
  9. modal/_utils/blob_utils.py +56 -19
  10. modal/_utils/function_utils.py +33 -7
  11. modal/_utils/grpc_utils.py +11 -4
  12. modal/app.py +5 -5
  13. modal/app.pyi +658 -48
  14. modal/cli/run.py +2 -1
  15. modal/client.pyi +224 -36
  16. modal/cloud_bucket_mount.pyi +192 -4
  17. modal/cls.py +57 -16
  18. modal/cls.pyi +442 -34
  19. modal/container_process.pyi +103 -14
  20. modal/dict.py +4 -4
  21. modal/dict.pyi +453 -51
  22. modal/environments.pyi +41 -9
  23. modal/exception.py +6 -2
  24. modal/experimental/__init__.py +90 -0
  25. modal/experimental/ipython.py +11 -7
  26. modal/file_io.pyi +236 -45
  27. modal/functions.pyi +573 -65
  28. modal/gpu.py +1 -1
  29. modal/image.py +1 -1
  30. modal/image.pyi +1256 -74
  31. modal/io_streams.py +8 -4
  32. modal/io_streams.pyi +348 -38
  33. modal/mount.pyi +261 -31
  34. modal/network_file_system.py +3 -3
  35. modal/network_file_system.pyi +307 -26
  36. modal/object.pyi +48 -9
  37. modal/parallel_map.py +93 -19
  38. modal/parallel_map.pyi +160 -15
  39. modal/partial_function.pyi +255 -14
  40. modal/proxy.py +1 -1
  41. modal/proxy.pyi +28 -3
  42. modal/queue.py +4 -4
  43. modal/queue.pyi +447 -30
  44. modal/runner.pyi +160 -22
  45. modal/sandbox.py +8 -7
  46. modal/sandbox.pyi +310 -50
  47. modal/schedule.py +1 -1
  48. modal/secret.py +2 -2
  49. modal/secret.pyi +164 -15
  50. modal/snapshot.pyi +25 -4
  51. modal/token_flow.pyi +28 -8
  52. modal/volume.py +41 -4
  53. modal/volume.pyi +693 -59
  54. {modal-1.0.4.dev10.dist-info → modal-1.0.5.dist-info}/METADATA +3 -3
  55. {modal-1.0.4.dev10.dist-info → modal-1.0.5.dist-info}/RECORD +67 -67
  56. modal_proto/api.proto +57 -0
  57. modal_proto/api_grpc.py +48 -0
  58. modal_proto/api_pb2.py +874 -780
  59. modal_proto/api_pb2.pyi +198 -9
  60. modal_proto/api_pb2_grpc.py +100 -0
  61. modal_proto/api_pb2_grpc.pyi +32 -0
  62. modal_proto/modal_api_grpc.py +3 -0
  63. modal_version/__init__.py +1 -1
  64. {modal-1.0.4.dev10.dist-info → modal-1.0.5.dist-info}/WHEEL +0 -0
  65. {modal-1.0.4.dev10.dist-info → modal-1.0.5.dist-info}/entry_points.txt +0 -0
  66. {modal-1.0.4.dev10.dist-info → modal-1.0.5.dist-info}/licenses/LICENSE +0 -0
  67. {modal-1.0.4.dev10.dist-info → modal-1.0.5.dist-info}/top_level.txt +0 -0
modal/environments.pyi CHANGED
@@ -7,20 +7,42 @@ import typing
7
7
  import typing_extensions
8
8
 
9
9
  class EnvironmentSettings:
10
+ """EnvironmentSettings(image_builder_version: str, webhook_suffix: str)"""
11
+
10
12
  image_builder_version: str
11
13
  webhook_suffix: str
12
14
 
13
- def __init__(self, image_builder_version: str, webhook_suffix: str) -> None: ...
14
- def __repr__(self): ...
15
- def __eq__(self, other): ...
16
- def __setattr__(self, name, value): ...
17
- def __delattr__(self, name): ...
18
- def __hash__(self): ...
15
+ def __init__(self, image_builder_version: str, webhook_suffix: str) -> None:
16
+ """Initialize self. See help(type(self)) for accurate signature."""
17
+ ...
18
+
19
+ def __repr__(self):
20
+ """Return repr(self)."""
21
+ ...
22
+
23
+ def __eq__(self, other):
24
+ """Return self==value."""
25
+ ...
26
+
27
+ def __setattr__(self, name, value):
28
+ """Implement setattr(self, name, value)."""
29
+ ...
30
+
31
+ def __delattr__(self, name):
32
+ """Implement delattr(self, name)."""
33
+ ...
34
+
35
+ def __hash__(self):
36
+ """Return hash(self)."""
37
+ ...
19
38
 
20
39
  class _Environment(modal._object._Object):
21
40
  _settings: EnvironmentSettings
22
41
 
23
- def __init__(self): ...
42
+ def __init__(self):
43
+ """mdmd:hidden"""
44
+ ...
45
+
24
46
  def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
25
47
  @staticmethod
26
48
  def from_name(name: str, *, create_if_missing: bool = False): ...
@@ -32,7 +54,10 @@ class _Environment(modal._object._Object):
32
54
  class Environment(modal.object.Object):
33
55
  _settings: EnvironmentSettings
34
56
 
35
- def __init__(self): ...
57
+ def __init__(self):
58
+ """mdmd:hidden"""
59
+ ...
60
+
36
61
  def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
37
62
  @staticmethod
38
63
  def from_name(name: str, *, create_if_missing: bool = False): ...
@@ -93,6 +118,13 @@ class __list_environments_spec(typing_extensions.Protocol):
93
118
 
94
119
  list_environments: __list_environments_spec
95
120
 
96
- def ensure_env(environment_name: typing.Optional[str] = None) -> str: ...
121
+ def ensure_env(environment_name: typing.Optional[str] = None) -> str:
122
+ """Override config environment with environment from environment_name
123
+
124
+ This is necessary since a cli command that runs Modal code, without explicit
125
+ environment specification wouldn't pick up the environment specified in a
126
+ command line flag otherwise, e.g. when doing `modal run --env=foo`
127
+ """
128
+ ...
97
129
 
98
130
  ENVIRONMENT_CACHE: dict[str, _Environment]
modal/exception.py CHANGED
@@ -2,11 +2,15 @@
2
2
  import random
3
3
  import signal
4
4
 
5
+ import synchronicity.exceptions
6
+
7
+ UserCodeException = synchronicity.exceptions.UserCodeException # Deprecated type used for return_exception wrapping
8
+
5
9
 
6
10
  class Error(Exception):
7
11
  """
8
- Base class for all Modal errors. See [`modal.exception`](/docs/reference/modal.exception) for the specialized
9
- error classes.
12
+ Base class for all Modal errors. See [`modal.exception`](https://modal.com/docs/reference/modal.exception)
13
+ for the specialized error classes.
10
14
 
11
15
  **Usage**
12
16
 
@@ -1,5 +1,7 @@
1
1
  # Copyright Modal Labs 2025
2
+ import asyncio
2
3
  import os
4
+ import sys
3
5
  from dataclasses import dataclass
4
6
  from pathlib import Path
5
7
  from typing import Literal, Optional, Union
@@ -11,11 +13,13 @@ from .._functions import _Function
11
13
  from .._object import _get_environment_name
12
14
  from .._partial_function import _clustered
13
15
  from .._runtime.container_io_manager import _ContainerIOManager
16
+ from .._tunnel import _forward as _forward_tunnel
14
17
  from .._utils.async_utils import synchronize_api, synchronizer
15
18
  from .._utils.deprecation import deprecation_warning
16
19
  from .._utils.grpc_utils import retry_transient_errors
17
20
  from ..client import _Client
18
21
  from ..cls import _Obj
22
+ from ..config import logger
19
23
  from ..exception import InvalidError
20
24
  from ..image import DockerfileSpec, ImageBuilderVersion, _Image, _ImageRegistryConfig
21
25
  from ..secret import _Secret
@@ -209,3 +213,89 @@ async def update_autoscaler(
209
213
 
210
214
  request = api_pb2.FunctionUpdateSchedulingParamsRequest(function_id=f.object_id, settings=settings)
211
215
  await retry_transient_errors(client.stub.FunctionUpdateSchedulingParams, request)
216
+
217
+
218
+ class _FlashManager:
219
+ def __init__(self, client: _Client, port: int):
220
+ self.client = client
221
+ self.port = port
222
+ self.tunnel_manager = _forward_tunnel(port, client=client)
223
+ self.stopped = False
224
+
225
+ async def _start(self):
226
+ self.tunnel = await self.tunnel_manager.__aenter__()
227
+
228
+ hostname = self.tunnel.url.split("://")[1]
229
+ if ":" in hostname:
230
+ host, port = hostname.split(":")
231
+ else:
232
+ host = hostname
233
+ port = "443"
234
+
235
+ self.heartbeat_task = asyncio.create_task(self._run_heartbeat(host, int(port)))
236
+
237
+ async def _run_heartbeat(self, host: str, port: int):
238
+ first_registration = True
239
+ while True:
240
+ try:
241
+ resp = await self.client.stub.FlashContainerRegister(
242
+ api_pb2.FlashContainerRegisterRequest(
243
+ priority=10,
244
+ weight=5,
245
+ host=host,
246
+ port=port,
247
+ ),
248
+ timeout=10,
249
+ )
250
+ if first_registration:
251
+ logger.warning(f"[Modal Flash] Listening at {resp.url}")
252
+ first_registration = False
253
+ except asyncio.CancelledError:
254
+ logger.warning("[Modal Flash] Shutting down...")
255
+ break
256
+ except Exception as e:
257
+ logger.error(f"[Modal Flash] Heartbeat failed: {e}")
258
+
259
+ try:
260
+ await asyncio.sleep(1)
261
+ except asyncio.CancelledError:
262
+ logger.warning("[Modal Flash] Shutting down...")
263
+ break
264
+
265
+ async def stop(self):
266
+ self.heartbeat_task.cancel()
267
+ await retry_transient_errors(
268
+ self.client.stub.FlashContainerDeregister,
269
+ api_pb2.FlashContainerDeregisterRequest(),
270
+ )
271
+
272
+ self.stopped = True
273
+ logger.warning(f"[Modal Flash] No longer accepting new requests on {self.tunnel.url}.")
274
+
275
+ # NOTE(gongy): We skip calling TunnelStop to avoid interrupting in-flight requests.
276
+ # It is up to the user to wait after calling .stop() to drain in-flight requests.
277
+
278
+ async def close(self):
279
+ if not self.stopped:
280
+ await self.stop()
281
+
282
+ logger.warning(f"[Modal Flash] Closing tunnel on {self.tunnel.url}.")
283
+ await self.tunnel_manager.__aexit__(*sys.exc_info())
284
+
285
+
286
+ FlashManager = synchronize_api(_FlashManager)
287
+
288
+
289
+ @synchronizer.create_blocking
290
+ async def flash_forward(port: int) -> _FlashManager:
291
+ """
292
+ Forward a port to the Modal Flash service, exposing that port as a stable web endpoint.
293
+
294
+ This is a highly experimental method that can break or be removed at any time without warning.
295
+ Do not use this method unless explicitly instructed to do so by Modal support.
296
+ """
297
+ client = await _Client.from_env()
298
+
299
+ manager = _FlashManager(client, port)
300
+ await manager._start()
301
+ return manager
@@ -21,7 +21,7 @@ class ModalMagics(Magics):
21
21
  **Example:**
22
22
 
23
23
  ```python notest
24
- %modal from main/my-app import my_function, MyClass as Foo
24
+ %modal from my-app import my_function, MyClass as Foo
25
25
 
26
26
  # Now you can call my_function() and Foo from your notebook.
27
27
  my_function.remote()
@@ -30,7 +30,7 @@ class ModalMagics(Magics):
30
30
  """
31
31
  line = line.strip()
32
32
  if not line.startswith("from "):
33
- print("Invalid syntax. Use: %modal from <env>/<app> import <function|Class>[, <function|Class> [as alias]]")
33
+ print("Invalid syntax. Use: %modal from [env/]<app> import <function|Class>[, <function|Class> [as alias]]")
34
34
  return
35
35
 
36
36
  # Remove the initial "from "
@@ -40,11 +40,12 @@ class ModalMagics(Magics):
40
40
  print("Invalid syntax. Missing 'import' keyword.")
41
41
  return
42
42
 
43
- # Parse environment and app from "env/app"
43
+ # Parse environment and app from "[env/]app"
44
+ environment: str | None
44
45
  if "/" not in env_app_part:
45
- print("Invalid app specification. Expected format: <env>/<app>")
46
- return
47
- environment, app = env_app_part.split("/", 1)
46
+ environment, app = None, env_app_part
47
+ else:
48
+ environment, app = env_app_part.split("/", 1)
48
49
 
49
50
  # Parse the import items (multiple imports separated by commas)
50
51
  import_items = [item.strip() for item in import_part.split(",")]
@@ -73,7 +74,10 @@ class ModalMagics(Magics):
73
74
 
74
75
  # Set the loaded object in the notebook namespace
75
76
  self.shell.user_ns[alias] = obj # type: ignore
76
- print(f"Loaded {alias!r} from environment {environment!r} and app {app!r}.")
77
+ if environment:
78
+ print(f"Loaded {alias!r} from environment {environment!r} and app {app!r}.")
79
+ else:
80
+ print(f"Loaded {alias!r} from app {app!r}.")
77
81
 
78
82
 
79
83
  def load_ipython_extension(ipython):
modal/file_io.pyi CHANGED
@@ -6,12 +6,24 @@ import typing_extensions
6
6
 
7
7
  T = typing.TypeVar("T")
8
8
 
9
- async def _delete_bytes(
10
- file: _FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None
11
- ) -> None: ...
9
+ async def _delete_bytes(file: _FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None) -> None:
10
+ """Delete a range of bytes from the file.
11
+
12
+ `start` and `end` are byte offsets. `start` is inclusive, `end` is exclusive.
13
+ If either is None, the start or end of the file is used, respectively.
14
+ """
15
+ ...
16
+
12
17
  async def _replace_bytes(
13
18
  file: _FileIO, data: bytes, start: typing.Optional[int] = None, end: typing.Optional[int] = None
14
- ) -> None: ...
19
+ ) -> None:
20
+ """Replace a range of bytes in the file with new data. The length of the data does not
21
+ have to be the same as the length of the range being replaced.
22
+
23
+ `start` and `end` are byte offsets. `start` is inclusive, `end` is exclusive.
24
+ If either is None, the start or end of the file is used, respectively.
25
+ """
26
+ ...
15
27
 
16
28
  class FileWatchEventType(enum.Enum):
17
29
  Unknown = "Unknown"
@@ -21,20 +33,51 @@ class FileWatchEventType(enum.Enum):
21
33
  Remove = "Remove"
22
34
 
23
35
  class FileWatchEvent:
36
+ """FileWatchEvent(paths: list[str], type: modal.file_io.FileWatchEventType)"""
37
+
24
38
  paths: list[str]
25
39
  type: FileWatchEventType
26
40
 
27
- def __init__(self, paths: list[str], type: FileWatchEventType) -> None: ...
28
- def __repr__(self): ...
29
- def __eq__(self, other): ...
41
+ def __init__(self, paths: list[str], type: FileWatchEventType) -> None:
42
+ """Initialize self. See help(type(self)) for accurate signature."""
43
+ ...
44
+
45
+ def __repr__(self):
46
+ """Return repr(self)."""
47
+ ...
48
+
49
+ def __eq__(self, other):
50
+ """Return self==value."""
51
+ ...
30
52
 
31
53
  class _FileIO(typing.Generic[T]):
54
+ """FileIO handle, used in the Sandbox filesystem API.
55
+
56
+ The API is designed to mimic Python's io.FileIO.
57
+
58
+ **Usage**
59
+
60
+ ```python
61
+ import modal
62
+
63
+ app = modal.App.lookup("my-app", create_if_missing=True)
64
+
65
+ sb = modal.Sandbox.create(app=app)
66
+ f = sb.open("/tmp/foo.txt", "w")
67
+ f.write("hello")
68
+ f.close()
69
+ ```
70
+ """
71
+
32
72
  _task_id: str
33
73
  _file_descriptor: str
34
74
  _client: modal.client._Client
35
75
  _watch_output_buffer: list[typing.Union[bytes, None, Exception]]
36
76
 
37
- def __init__(self, client: modal.client._Client, task_id: str) -> None: ...
77
+ def __init__(self, client: modal.client._Client, task_id: str) -> None:
78
+ """Initialize self. See help(type(self)) for accurate signature."""
79
+ ...
80
+
38
81
  def _validate_mode(self, mode: str) -> None: ...
39
82
  def _consume_output(self, exec_id: str) -> typing.AsyncIterator[typing.Union[bytes, None, Exception]]: ...
40
83
  async def _consume_watch_output(self, exec_id: str) -> None: ...
@@ -49,21 +92,60 @@ class _FileIO(typing.Generic[T]):
49
92
  mode: typing.Union[_typeshed.OpenTextMode, _typeshed.OpenBinaryMode],
50
93
  client: modal.client._Client,
51
94
  task_id: str,
52
- ) -> _FileIO: ...
95
+ ) -> _FileIO:
96
+ """Create a new FileIO handle."""
97
+ ...
98
+
53
99
  async def _make_read_request(self, n: typing.Optional[int]) -> bytes: ...
54
- async def read(self, n: typing.Optional[int] = None) -> T: ...
55
- async def readline(self) -> T: ...
56
- async def readlines(self) -> typing.Sequence[T]: ...
57
- async def write(self, data: typing.Union[bytes, str]) -> None: ...
58
- async def flush(self) -> None: ...
100
+ async def read(self, n: typing.Optional[int] = None) -> T:
101
+ """Read n bytes from the current position, or the entire remaining file if n is None."""
102
+ ...
103
+
104
+ async def readline(self) -> T:
105
+ """Read a single line from the current position."""
106
+ ...
107
+
108
+ async def readlines(self) -> typing.Sequence[T]:
109
+ """Read all lines from the current position."""
110
+ ...
111
+
112
+ async def write(self, data: typing.Union[bytes, str]) -> None:
113
+ """Write data to the current position.
114
+
115
+ Writes may not appear until the entire buffer is flushed, which
116
+ can be done manually with `flush()` or automatically when the file is
117
+ closed.
118
+ """
119
+ ...
120
+
121
+ async def flush(self) -> None:
122
+ """Flush the buffer to disk."""
123
+ ...
124
+
59
125
  def _get_whence(self, whence: int): ...
60
- async def seek(self, offset: int, whence: int = 0) -> None: ...
126
+ async def seek(self, offset: int, whence: int = 0) -> None:
127
+ """Move to a new position in the file.
128
+
129
+ `whence` defaults to 0 (absolute file positioning); other values are 1
130
+ (relative to the current position) and 2 (relative to the file's end).
131
+ """
132
+ ...
133
+
61
134
  @classmethod
62
- async def ls(cls, path: str, client: modal.client._Client, task_id: str) -> list[str]: ...
135
+ async def ls(cls, path: str, client: modal.client._Client, task_id: str) -> list[str]:
136
+ """List the contents of the provided directory."""
137
+ ...
138
+
63
139
  @classmethod
64
- async def mkdir(cls, path: str, client: modal.client._Client, task_id: str, parents: bool = False) -> None: ...
140
+ async def mkdir(cls, path: str, client: modal.client._Client, task_id: str, parents: bool = False) -> None:
141
+ """Create a new directory."""
142
+ ...
143
+
65
144
  @classmethod
66
- async def rm(cls, path: str, client: modal.client._Client, task_id: str, recursive: bool = False) -> None: ...
145
+ async def rm(cls, path: str, client: modal.client._Client, task_id: str, recursive: bool = False) -> None:
146
+ """Remove a file or directory in the Sandbox."""
147
+ ...
148
+
67
149
  @classmethod
68
150
  def watch(
69
151
  cls,
@@ -75,7 +157,10 @@ class _FileIO(typing.Generic[T]):
75
157
  timeout: typing.Optional[int] = None,
76
158
  ) -> typing.AsyncIterator[FileWatchEvent]: ...
77
159
  async def _close(self) -> None: ...
78
- async def close(self) -> None: ...
160
+ async def close(self) -> None:
161
+ """Flush the buffer and close the file."""
162
+ ...
163
+
79
164
  def _check_writable(self) -> None: ...
80
165
  def _check_readable(self) -> None: ...
81
166
  def _check_closed(self) -> None: ...
@@ -83,22 +168,46 @@ class _FileIO(typing.Generic[T]):
83
168
  async def __aexit__(self, exc_type, exc_value, traceback) -> None: ...
84
169
 
85
170
  class __delete_bytes_spec(typing_extensions.Protocol):
86
- def __call__(
87
- self, /, file: FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None
88
- ) -> None: ...
89
- async def aio(
90
- self, /, file: FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None
91
- ) -> None: ...
171
+ def __call__(self, /, file: FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None) -> None:
172
+ """Delete a range of bytes from the file.
173
+
174
+ `start` and `end` are byte offsets. `start` is inclusive, `end` is exclusive.
175
+ If either is None, the start or end of the file is used, respectively.
176
+ """
177
+ ...
178
+
179
+ async def aio(self, /, file: FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None) -> None:
180
+ """Delete a range of bytes from the file.
181
+
182
+ `start` and `end` are byte offsets. `start` is inclusive, `end` is exclusive.
183
+ If either is None, the start or end of the file is used, respectively.
184
+ """
185
+ ...
92
186
 
93
187
  delete_bytes: __delete_bytes_spec
94
188
 
95
189
  class __replace_bytes_spec(typing_extensions.Protocol):
96
190
  def __call__(
97
191
  self, /, file: FileIO, data: bytes, start: typing.Optional[int] = None, end: typing.Optional[int] = None
98
- ) -> None: ...
192
+ ) -> None:
193
+ """Replace a range of bytes in the file with new data. The length of the data does not
194
+ have to be the same as the length of the range being replaced.
195
+
196
+ `start` and `end` are byte offsets. `start` is inclusive, `end` is exclusive.
197
+ If either is None, the start or end of the file is used, respectively.
198
+ """
199
+ ...
200
+
99
201
  async def aio(
100
202
  self, /, file: FileIO, data: bytes, start: typing.Optional[int] = None, end: typing.Optional[int] = None
101
- ) -> None: ...
203
+ ) -> None:
204
+ """Replace a range of bytes in the file with new data. The length of the data does not
205
+ have to be the same as the length of the range being replaced.
206
+
207
+ `start` and `end` are byte offsets. `start` is inclusive, `end` is exclusive.
208
+ If either is None, the start or end of the file is used, respectively.
209
+ """
210
+ ...
102
211
 
103
212
  replace_bytes: __replace_bytes_spec
104
213
 
@@ -107,6 +216,24 @@ SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
107
216
  T_INNER = typing.TypeVar("T_INNER", covariant=True)
108
217
 
109
218
  class FileIO(typing.Generic[T]):
219
+ """FileIO handle, used in the Sandbox filesystem API.
220
+
221
+ The API is designed to mimic Python's io.FileIO.
222
+
223
+ **Usage**
224
+
225
+ ```python
226
+ import modal
227
+
228
+ app = modal.App.lookup("my-app", create_if_missing=True)
229
+
230
+ sb = modal.Sandbox.create(app=app)
231
+ f = sb.open("/tmp/foo.txt", "w")
232
+ f.write("hello")
233
+ f.close()
234
+ ```
235
+ """
236
+
110
237
  _task_id: str
111
238
  _file_descriptor: str
112
239
  _client: modal.client.Client
@@ -154,7 +281,9 @@ class FileIO(typing.Generic[T]):
154
281
  mode: typing.Union[_typeshed.OpenTextMode, _typeshed.OpenBinaryMode],
155
282
  client: modal.client.Client,
156
283
  task_id: str,
157
- ) -> FileIO: ...
284
+ ) -> FileIO:
285
+ """Create a new FileIO handle."""
286
+ ...
158
287
 
159
288
  class ___make_read_request_spec(typing_extensions.Protocol[SUPERSELF]):
160
289
  def __call__(self, /, n: typing.Optional[int]) -> bytes: ...
@@ -163,49 +292,106 @@ class FileIO(typing.Generic[T]):
163
292
  _make_read_request: ___make_read_request_spec[typing_extensions.Self]
164
293
 
165
294
  class __read_spec(typing_extensions.Protocol[T_INNER, SUPERSELF]):
166
- def __call__(self, /, n: typing.Optional[int] = None) -> T_INNER: ...
167
- async def aio(self, /, n: typing.Optional[int] = None) -> T_INNER: ...
295
+ def __call__(self, /, n: typing.Optional[int] = None) -> T_INNER:
296
+ """Read n bytes from the current position, or the entire remaining file if n is None."""
297
+ ...
298
+
299
+ async def aio(self, /, n: typing.Optional[int] = None) -> T_INNER:
300
+ """Read n bytes from the current position, or the entire remaining file if n is None."""
301
+ ...
168
302
 
169
303
  read: __read_spec[T, typing_extensions.Self]
170
304
 
171
305
  class __readline_spec(typing_extensions.Protocol[T_INNER, SUPERSELF]):
172
- def __call__(self, /) -> T_INNER: ...
173
- async def aio(self, /) -> T_INNER: ...
306
+ def __call__(self, /) -> T_INNER:
307
+ """Read a single line from the current position."""
308
+ ...
309
+
310
+ async def aio(self, /) -> T_INNER:
311
+ """Read a single line from the current position."""
312
+ ...
174
313
 
175
314
  readline: __readline_spec[T, typing_extensions.Self]
176
315
 
177
316
  class __readlines_spec(typing_extensions.Protocol[T_INNER, SUPERSELF]):
178
- def __call__(self, /) -> typing.Sequence[T_INNER]: ...
179
- async def aio(self, /) -> typing.Sequence[T_INNER]: ...
317
+ def __call__(self, /) -> typing.Sequence[T_INNER]:
318
+ """Read all lines from the current position."""
319
+ ...
320
+
321
+ async def aio(self, /) -> typing.Sequence[T_INNER]:
322
+ """Read all lines from the current position."""
323
+ ...
180
324
 
181
325
  readlines: __readlines_spec[T, typing_extensions.Self]
182
326
 
183
327
  class __write_spec(typing_extensions.Protocol[SUPERSELF]):
184
- def __call__(self, /, data: typing.Union[bytes, str]) -> None: ...
185
- async def aio(self, /, data: typing.Union[bytes, str]) -> None: ...
328
+ def __call__(self, /, data: typing.Union[bytes, str]) -> None:
329
+ """Write data to the current position.
330
+
331
+ Writes may not appear until the entire buffer is flushed, which
332
+ can be done manually with `flush()` or automatically when the file is
333
+ closed.
334
+ """
335
+ ...
336
+
337
+ async def aio(self, /, data: typing.Union[bytes, str]) -> None:
338
+ """Write data to the current position.
339
+
340
+ Writes may not appear until the entire buffer is flushed, which
341
+ can be done manually with `flush()` or automatically when the file is
342
+ closed.
343
+ """
344
+ ...
186
345
 
187
346
  write: __write_spec[typing_extensions.Self]
188
347
 
189
348
  class __flush_spec(typing_extensions.Protocol[SUPERSELF]):
190
- def __call__(self, /) -> None: ...
191
- async def aio(self, /) -> None: ...
349
+ def __call__(self, /) -> None:
350
+ """Flush the buffer to disk."""
351
+ ...
352
+
353
+ async def aio(self, /) -> None:
354
+ """Flush the buffer to disk."""
355
+ ...
192
356
 
193
357
  flush: __flush_spec[typing_extensions.Self]
194
358
 
195
359
  def _get_whence(self, whence: int): ...
196
360
 
197
361
  class __seek_spec(typing_extensions.Protocol[SUPERSELF]):
198
- def __call__(self, /, offset: int, whence: int = 0) -> None: ...
199
- async def aio(self, /, offset: int, whence: int = 0) -> None: ...
362
+ def __call__(self, /, offset: int, whence: int = 0) -> None:
363
+ """Move to a new position in the file.
364
+
365
+ `whence` defaults to 0 (absolute file positioning); other values are 1
366
+ (relative to the current position) and 2 (relative to the file's end).
367
+ """
368
+ ...
369
+
370
+ async def aio(self, /, offset: int, whence: int = 0) -> None:
371
+ """Move to a new position in the file.
372
+
373
+ `whence` defaults to 0 (absolute file positioning); other values are 1
374
+ (relative to the current position) and 2 (relative to the file's end).
375
+ """
376
+ ...
200
377
 
201
378
  seek: __seek_spec[typing_extensions.Self]
202
379
 
203
380
  @classmethod
204
- def ls(cls, path: str, client: modal.client.Client, task_id: str) -> list[str]: ...
381
+ def ls(cls, path: str, client: modal.client.Client, task_id: str) -> list[str]:
382
+ """List the contents of the provided directory."""
383
+ ...
384
+
205
385
  @classmethod
206
- def mkdir(cls, path: str, client: modal.client.Client, task_id: str, parents: bool = False) -> None: ...
386
+ def mkdir(cls, path: str, client: modal.client.Client, task_id: str, parents: bool = False) -> None:
387
+ """Create a new directory."""
388
+ ...
389
+
207
390
  @classmethod
208
- def rm(cls, path: str, client: modal.client.Client, task_id: str, recursive: bool = False) -> None: ...
391
+ def rm(cls, path: str, client: modal.client.Client, task_id: str, recursive: bool = False) -> None:
392
+ """Remove a file or directory in the Sandbox."""
393
+ ...
394
+
209
395
  @classmethod
210
396
  def watch(
211
397
  cls,
@@ -224,8 +410,13 @@ class FileIO(typing.Generic[T]):
224
410
  _close: ___close_spec[typing_extensions.Self]
225
411
 
226
412
  class __close_spec(typing_extensions.Protocol[SUPERSELF]):
227
- def __call__(self, /) -> None: ...
228
- async def aio(self, /) -> None: ...
413
+ def __call__(self, /) -> None:
414
+ """Flush the buffer and close the file."""
415
+ ...
416
+
417
+ async def aio(self, /) -> None:
418
+ """Flush the buffer and close the file."""
419
+ ...
229
420
 
230
421
  close: __close_spec[typing_extensions.Self]
231
422