modal 1.1.5.dev19__py3-none-any.whl → 1.1.5.dev21__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.

Potentially problematic release.


This version of modal might be problematic. Click here for more details.

modal/image.pyi CHANGED
@@ -166,7 +166,7 @@ class _Image(modal._object._Object):
166
166
  [typing.Literal["2023.12", "2024.04", "2024.10", "2025.06", "PREVIEW"]], DockerfileSpec
167
167
  ]
168
168
  ] = None,
169
- secrets: typing.Optional[collections.abc.Sequence[modal.secret._Secret]] = None,
169
+ secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
170
170
  gpu_config: typing.Optional[modal_proto.api_pb2.GPUConfig] = None,
171
171
  build_function: typing.Optional[modal._functions._Function] = None,
172
172
  build_function_input: typing.Optional[modal_proto.api_pb2.FunctionInput] = None,
@@ -327,7 +327,8 @@ class _Image(modal._object._Object):
327
327
  pre: bool = False,
328
328
  extra_options: str = "",
329
329
  force_build: bool = False,
330
- secrets: collections.abc.Sequence[modal.secret._Secret] = [],
330
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
331
+ secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
331
332
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
332
333
  ) -> _Image:
333
334
  """Install a list of Python packages using pip.
@@ -369,7 +370,8 @@ class _Image(modal._object._Object):
369
370
  pre: bool = False,
370
371
  extra_options: str = "",
371
372
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
372
- secrets: collections.abc.Sequence[modal.secret._Secret] = [],
373
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
374
+ secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
373
375
  force_build: bool = False,
374
376
  ) -> _Image:
375
377
  """Install a list of Python packages from private git repositories using pip.
@@ -414,7 +416,8 @@ class _Image(modal._object._Object):
414
416
  pre: bool = False,
415
417
  extra_options: str = "",
416
418
  force_build: bool = False,
417
- secrets: collections.abc.Sequence[modal.secret._Secret] = [],
419
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
420
+ secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
418
421
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
419
422
  ) -> _Image:
420
423
  """Install a list of Python packages from a local `requirements.txt` file."""
@@ -431,7 +434,8 @@ class _Image(modal._object._Object):
431
434
  pre: bool = False,
432
435
  extra_options: str = "",
433
436
  force_build: bool = False,
434
- secrets: collections.abc.Sequence[modal.secret._Secret] = [],
437
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
438
+ secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
435
439
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
436
440
  ) -> _Image:
437
441
  """Install dependencies specified by a local `pyproject.toml` file.
@@ -454,7 +458,8 @@ class _Image(modal._object._Object):
454
458
  extra_options: str = "",
455
459
  force_build: bool = False,
456
460
  uv_version: typing.Optional[str] = None,
457
- secrets: collections.abc.Sequence[modal.secret._Secret] = [],
461
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
462
+ secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
458
463
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
459
464
  ) -> _Image:
460
465
  """Install a list of Python packages using uv pip install.
@@ -487,7 +492,8 @@ class _Image(modal._object._Object):
487
492
  only: list[str] = [],
488
493
  poetry_version: typing.Optional[str] = "latest",
489
494
  old_installer: bool = False,
490
- secrets: collections.abc.Sequence[modal.secret._Secret] = [],
495
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
496
+ secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
491
497
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
492
498
  ) -> _Image:
493
499
  """Install poetry *dependencies* specified by a local `pyproject.toml` file.
@@ -514,7 +520,8 @@ class _Image(modal._object._Object):
514
520
  frozen: bool = True,
515
521
  extra_options: str = "",
516
522
  uv_version: typing.Optional[str] = None,
517
- secrets: collections.abc.Sequence[modal.secret._Secret] = [],
523
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
524
+ secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
518
525
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
519
526
  ) -> _Image:
520
527
  """Creates a virtual environment with the dependencies in a uv managed project with `uv sync`.
@@ -535,7 +542,8 @@ class _Image(modal._object._Object):
535
542
  self,
536
543
  *dockerfile_commands: typing.Union[str, list[str]],
537
544
  context_files: dict[str, str] = {},
538
- secrets: collections.abc.Sequence[modal.secret._Secret] = [],
545
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
546
+ secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
539
547
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
540
548
  context_mount: typing.Optional[modal.mount._Mount] = None,
541
549
  context_dir: typing.Union[str, pathlib.Path, None] = None,
@@ -597,7 +605,8 @@ class _Image(modal._object._Object):
597
605
  def run_commands(
598
606
  self,
599
607
  *commands: typing.Union[str, list[str]],
600
- secrets: collections.abc.Sequence[modal.secret._Secret] = [],
608
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
609
+ secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
601
610
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
602
611
  force_build: bool = False,
603
612
  ) -> _Image:
@@ -615,7 +624,8 @@ class _Image(modal._object._Object):
615
624
  spec_file: typing.Optional[str] = None,
616
625
  channels: list[str] = [],
617
626
  force_build: bool = False,
618
- secrets: collections.abc.Sequence[modal.secret._Secret] = [],
627
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
628
+ secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
619
629
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
620
630
  ) -> _Image:
621
631
  """Install a list of additional packages using micromamba."""
@@ -756,7 +766,8 @@ class _Image(modal._object._Object):
756
766
  context_mount: typing.Optional[modal.mount._Mount] = None,
757
767
  force_build: bool = False,
758
768
  context_dir: typing.Union[str, pathlib.Path, None] = None,
759
- secrets: collections.abc.Sequence[modal.secret._Secret] = [],
769
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
770
+ secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
760
771
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
761
772
  add_python: typing.Optional[str] = None,
762
773
  build_args: dict[str, str] = {},
@@ -824,7 +835,8 @@ class _Image(modal._object._Object):
824
835
  self,
825
836
  *packages: typing.Union[str, list[str]],
826
837
  force_build: bool = False,
827
- secrets: collections.abc.Sequence[modal.secret._Secret] = [],
838
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
839
+ secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
828
840
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
829
841
  ) -> _Image:
830
842
  """Install a list of Debian packages using `apt`.
@@ -841,7 +853,8 @@ class _Image(modal._object._Object):
841
853
  self,
842
854
  raw_f: collections.abc.Callable[..., typing.Any],
843
855
  *,
844
- secrets: collections.abc.Sequence[modal.secret._Secret] = (),
856
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
857
+ secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
845
858
  volumes: dict[
846
859
  typing.Union[str, pathlib.PurePosixPath],
847
860
  typing.Union[modal.volume._Volume, modal.cloud_bucket_mount._CloudBucketMount],
@@ -1008,7 +1021,7 @@ class Image(modal.object.Object):
1008
1021
  [typing.Literal["2023.12", "2024.04", "2024.10", "2025.06", "PREVIEW"]], DockerfileSpec
1009
1022
  ]
1010
1023
  ] = None,
1011
- secrets: typing.Optional[collections.abc.Sequence[modal.secret.Secret]] = None,
1024
+ secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
1012
1025
  gpu_config: typing.Optional[modal_proto.api_pb2.GPUConfig] = None,
1013
1026
  build_function: typing.Optional[modal.functions.Function] = None,
1014
1027
  build_function_input: typing.Optional[modal_proto.api_pb2.FunctionInput] = None,
@@ -1178,7 +1191,8 @@ class Image(modal.object.Object):
1178
1191
  pre: bool = False,
1179
1192
  extra_options: str = "",
1180
1193
  force_build: bool = False,
1181
- secrets: collections.abc.Sequence[modal.secret.Secret] = [],
1194
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
1195
+ secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
1182
1196
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
1183
1197
  ) -> Image:
1184
1198
  """Install a list of Python packages using pip.
@@ -1220,7 +1234,8 @@ class Image(modal.object.Object):
1220
1234
  pre: bool = False,
1221
1235
  extra_options: str = "",
1222
1236
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
1223
- secrets: collections.abc.Sequence[modal.secret.Secret] = [],
1237
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
1238
+ secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
1224
1239
  force_build: bool = False,
1225
1240
  ) -> Image:
1226
1241
  """Install a list of Python packages from private git repositories using pip.
@@ -1265,7 +1280,8 @@ class Image(modal.object.Object):
1265
1280
  pre: bool = False,
1266
1281
  extra_options: str = "",
1267
1282
  force_build: bool = False,
1268
- secrets: collections.abc.Sequence[modal.secret.Secret] = [],
1283
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
1284
+ secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
1269
1285
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
1270
1286
  ) -> Image:
1271
1287
  """Install a list of Python packages from a local `requirements.txt` file."""
@@ -1282,7 +1298,8 @@ class Image(modal.object.Object):
1282
1298
  pre: bool = False,
1283
1299
  extra_options: str = "",
1284
1300
  force_build: bool = False,
1285
- secrets: collections.abc.Sequence[modal.secret.Secret] = [],
1301
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
1302
+ secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
1286
1303
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
1287
1304
  ) -> Image:
1288
1305
  """Install dependencies specified by a local `pyproject.toml` file.
@@ -1305,7 +1322,8 @@ class Image(modal.object.Object):
1305
1322
  extra_options: str = "",
1306
1323
  force_build: bool = False,
1307
1324
  uv_version: typing.Optional[str] = None,
1308
- secrets: collections.abc.Sequence[modal.secret.Secret] = [],
1325
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
1326
+ secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
1309
1327
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
1310
1328
  ) -> Image:
1311
1329
  """Install a list of Python packages using uv pip install.
@@ -1338,7 +1356,8 @@ class Image(modal.object.Object):
1338
1356
  only: list[str] = [],
1339
1357
  poetry_version: typing.Optional[str] = "latest",
1340
1358
  old_installer: bool = False,
1341
- secrets: collections.abc.Sequence[modal.secret.Secret] = [],
1359
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
1360
+ secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
1342
1361
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
1343
1362
  ) -> Image:
1344
1363
  """Install poetry *dependencies* specified by a local `pyproject.toml` file.
@@ -1365,7 +1384,8 @@ class Image(modal.object.Object):
1365
1384
  frozen: bool = True,
1366
1385
  extra_options: str = "",
1367
1386
  uv_version: typing.Optional[str] = None,
1368
- secrets: collections.abc.Sequence[modal.secret.Secret] = [],
1387
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
1388
+ secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
1369
1389
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
1370
1390
  ) -> Image:
1371
1391
  """Creates a virtual environment with the dependencies in a uv managed project with `uv sync`.
@@ -1386,7 +1406,8 @@ class Image(modal.object.Object):
1386
1406
  self,
1387
1407
  *dockerfile_commands: typing.Union[str, list[str]],
1388
1408
  context_files: dict[str, str] = {},
1389
- secrets: collections.abc.Sequence[modal.secret.Secret] = [],
1409
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
1410
+ secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
1390
1411
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
1391
1412
  context_mount: typing.Optional[modal.mount.Mount] = None,
1392
1413
  context_dir: typing.Union[str, pathlib.Path, None] = None,
@@ -1448,7 +1469,8 @@ class Image(modal.object.Object):
1448
1469
  def run_commands(
1449
1470
  self,
1450
1471
  *commands: typing.Union[str, list[str]],
1451
- secrets: collections.abc.Sequence[modal.secret.Secret] = [],
1472
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
1473
+ secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
1452
1474
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
1453
1475
  force_build: bool = False,
1454
1476
  ) -> Image:
@@ -1466,7 +1488,8 @@ class Image(modal.object.Object):
1466
1488
  spec_file: typing.Optional[str] = None,
1467
1489
  channels: list[str] = [],
1468
1490
  force_build: bool = False,
1469
- secrets: collections.abc.Sequence[modal.secret.Secret] = [],
1491
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
1492
+ secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
1470
1493
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
1471
1494
  ) -> Image:
1472
1495
  """Install a list of additional packages using micromamba."""
@@ -1607,7 +1630,8 @@ class Image(modal.object.Object):
1607
1630
  context_mount: typing.Optional[modal.mount.Mount] = None,
1608
1631
  force_build: bool = False,
1609
1632
  context_dir: typing.Union[str, pathlib.Path, None] = None,
1610
- secrets: collections.abc.Sequence[modal.secret.Secret] = [],
1633
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
1634
+ secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
1611
1635
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
1612
1636
  add_python: typing.Optional[str] = None,
1613
1637
  build_args: dict[str, str] = {},
@@ -1675,7 +1699,8 @@ class Image(modal.object.Object):
1675
1699
  self,
1676
1700
  *packages: typing.Union[str, list[str]],
1677
1701
  force_build: bool = False,
1678
- secrets: collections.abc.Sequence[modal.secret.Secret] = [],
1702
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
1703
+ secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
1679
1704
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
1680
1705
  ) -> Image:
1681
1706
  """Install a list of Debian packages using `apt`.
@@ -1692,7 +1717,8 @@ class Image(modal.object.Object):
1692
1717
  self,
1693
1718
  raw_f: collections.abc.Callable[..., typing.Any],
1694
1719
  *,
1695
- secrets: collections.abc.Sequence[modal.secret.Secret] = (),
1720
+ env: typing.Optional[dict[str, typing.Optional[str]]] = None,
1721
+ secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
1696
1722
  volumes: dict[
1697
1723
  typing.Union[str, pathlib.PurePosixPath],
1698
1724
  typing.Union[modal.volume.Volume, modal.cloud_bucket_mount.CloudBucketMount],
modal/runner.py CHANGED
@@ -607,12 +607,12 @@ async def _interactive_shell(
607
607
 
608
608
  try:
609
609
  if pty:
610
- container_process = await sandbox.exec(
610
+ container_process = await sandbox._exec(
611
611
  *sandbox_cmds, pty_info=get_pty_info(shell=True) if pty else None
612
612
  )
613
613
  await container_process.attach()
614
614
  else:
615
- container_process = await sandbox.exec(
615
+ container_process = await sandbox._exec(
616
616
  *sandbox_cmds, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
617
617
  )
618
618
  await container_process.wait()
modal/sandbox.py CHANGED
@@ -3,10 +3,11 @@ import asyncio
3
3
  import json
4
4
  import os
5
5
  import time
6
- from collections.abc import AsyncGenerator, Sequence
6
+ from collections.abc import AsyncGenerator, Collection, Sequence
7
7
  from dataclasses import dataclass
8
8
  from typing import TYPE_CHECKING, Any, AsyncIterator, Literal, Optional, Union, overload
9
9
 
10
+ from ._pty import get_pty_info
10
11
  from .config import config, logger
11
12
 
12
13
  if TYPE_CHECKING:
@@ -124,11 +125,15 @@ class _Sandbox(_Object, type_prefix="sb"):
124
125
  _tunnels: Optional[dict[int, Tunnel]] = None
125
126
  _enable_snapshot: bool = False
126
127
 
128
+ @staticmethod
129
+ def _default_pty_info() -> api_pb2.PTYInfo:
130
+ return get_pty_info(shell=True, no_terminate_on_idle_stdin=True)
131
+
127
132
  @staticmethod
128
133
  def _new(
129
134
  args: Sequence[str],
130
135
  image: _Image,
131
- secrets: Sequence[_Secret],
136
+ secrets: Collection[_Secret],
132
137
  name: Optional[str] = None,
133
138
  timeout: int = 300,
134
139
  idle_timeout: Optional[int] = None,
@@ -143,7 +148,8 @@ class _Sandbox(_Object, type_prefix="sb"):
143
148
  block_network: bool = False,
144
149
  cidr_allowlist: Optional[Sequence[str]] = None,
145
150
  volumes: dict[Union[str, os.PathLike], Union[_Volume, _CloudBucketMount]] = {},
146
- pty_info: Optional[api_pb2.PTYInfo] = None,
151
+ pty: bool = False,
152
+ pty_info: Optional[api_pb2.PTYInfo] = None, # deprecated
147
153
  encrypted_ports: Sequence[int] = [],
148
154
  h2_ports: Sequence[int] = [],
149
155
  unencrypted_ports: Sequence[int] = [],
@@ -177,6 +183,9 @@ class _Sandbox(_Object, type_prefix="sb"):
177
183
  cloud_bucket_mounts = [(k, v) for k, v in validated_volumes if isinstance(v, _CloudBucketMount)]
178
184
  validated_volumes = [(k, v) for k, v in validated_volumes if isinstance(v, _Volume)]
179
185
 
186
+ if pty:
187
+ pty_info = _Sandbox._default_pty_info()
188
+
180
189
  def _deps() -> list[_Object]:
181
190
  deps: list[_Object] = [image] + list(mounts) + list(secrets)
182
191
  for _, vol in validated_network_file_systems:
@@ -279,7 +288,8 @@ class _Sandbox(_Object, type_prefix="sb"):
279
288
  app: Optional["modal.app._App"] = None,
280
289
  name: Optional[str] = None, # Optionally give the sandbox a name. Unique within an app.
281
290
  image: Optional[_Image] = None, # The image to run as the container for the sandbox.
282
- secrets: Sequence[_Secret] = (), # Environment variables to inject into the sandbox.
291
+ env: Optional[dict[str, Optional[str]]] = None, # Environment variables to set in the Sandbox.
292
+ secrets: Optional[Collection[_Secret]] = None, # Secrets to inject into the Sandbox as environment variables.
283
293
  network_file_systems: dict[Union[str, os.PathLike], _NetworkFileSystem] = {},
284
294
  timeout: int = 300, # Maximum lifetime of the sandbox in seconds.
285
295
  # The amount of time in seconds that a sandbox can be idle before being terminated.
@@ -301,7 +311,7 @@ class _Sandbox(_Object, type_prefix="sb"):
301
311
  volumes: dict[
302
312
  Union[str, os.PathLike], Union[_Volume, _CloudBucketMount]
303
313
  ] = {}, # Mount points for Modal Volumes and CloudBucketMounts
304
- pty_info: Optional[api_pb2.PTYInfo] = None,
314
+ pty: bool = False, # Enable a PTY for the Sandbox
305
315
  # List of ports to tunnel into the sandbox. Encrypted ports are tunneled with TLS.
306
316
  encrypted_ports: Sequence[int] = [],
307
317
  # List of encrypted ports to tunnel into the sandbox, using HTTP/2.
@@ -320,6 +330,7 @@ class _Sandbox(_Object, type_prefix="sb"):
320
330
  ] = None, # Experimental controls over fine-grained scheduling (alpha).
321
331
  client: Optional[_Client] = None,
322
332
  environment_name: Optional[str] = None, # *DEPRECATED* Optionally override the default environment
333
+ pty_info: Optional[api_pb2.PTYInfo] = None, # *DEPRECATED* Use `pty` instead. `pty` will override `pty_info`.
323
334
  ) -> "_Sandbox":
324
335
  """
325
336
  Create a new Sandbox to run untrusted, arbitrary code.
@@ -342,6 +353,17 @@ class _Sandbox(_Object, type_prefix="sb"):
342
353
  "A sandbox's environment is determined by the app it is associated with.",
343
354
  )
344
355
 
356
+ if pty_info is not None:
357
+ deprecation_warning(
358
+ (2025, 9, 12),
359
+ "The `pty_info` parameter is deprecated and will be removed in a future release. "
360
+ "Set the `pty` parameter to `True` instead.",
361
+ )
362
+
363
+ secrets = secrets or []
364
+ if env:
365
+ secrets = [*secrets, _Secret.from_dict(env)]
366
+
345
367
  return await _Sandbox._create(
346
368
  *args,
347
369
  app=app,
@@ -360,7 +382,7 @@ class _Sandbox(_Object, type_prefix="sb"):
360
382
  block_network=block_network,
361
383
  cidr_allowlist=cidr_allowlist,
362
384
  volumes=volumes,
363
- pty_info=pty_info,
385
+ pty=pty,
364
386
  encrypted_ports=encrypted_ports,
365
387
  h2_ports=h2_ports,
366
388
  unencrypted_ports=unencrypted_ports,
@@ -370,59 +392,48 @@ class _Sandbox(_Object, type_prefix="sb"):
370
392
  _experimental_scheduler_placement=_experimental_scheduler_placement,
371
393
  client=client,
372
394
  verbose=verbose,
395
+ pty_info=pty_info,
373
396
  )
374
397
 
375
398
  @staticmethod
376
399
  async def _create(
377
- *args: str, # Set the CMD of the Sandbox, overriding any CMD of the container image.
378
- # Associate the sandbox with an app. Required unless creating from a container.
400
+ *args: str,
379
401
  app: Optional["modal.app._App"] = None,
380
- name: Optional[str] = None, # Optionally give the sandbox a name. Unique within an app.
381
- image: Optional[_Image] = None, # The image to run as the container for the sandbox.
382
- secrets: Sequence[_Secret] = (), # Environment variables to inject into the sandbox.
402
+ name: Optional[str] = None,
403
+ image: Optional[_Image] = None,
404
+ env: Optional[dict[str, Optional[str]]] = None,
405
+ secrets: Optional[Collection[_Secret]] = None,
383
406
  mounts: Sequence[_Mount] = (),
384
407
  network_file_systems: dict[Union[str, os.PathLike], _NetworkFileSystem] = {},
385
- timeout: int = 300, # Maximum lifetime of the sandbox in seconds.
386
- # The amount of time in seconds that a sandbox can be idle before being terminated.
408
+ timeout: int = 300,
387
409
  idle_timeout: Optional[int] = None,
388
- workdir: Optional[str] = None, # Working directory of the sandbox.
410
+ workdir: Optional[str] = None,
389
411
  gpu: GPU_T = None,
390
412
  cloud: Optional[str] = None,
391
- region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the sandbox on.
392
- # Specify, in fractional CPU cores, how many CPU cores to request.
393
- # Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
394
- # CPU throttling will prevent a container from exceeding its specified limit.
413
+ region: Optional[Union[str, Sequence[str]]] = None,
395
414
  cpu: Optional[Union[float, tuple[float, float]]] = None,
396
- # Specify, in MiB, a memory request which is the minimum memory required.
397
- # Or, pass (request, limit) to additionally specify a hard limit in MiB.
398
415
  memory: Optional[Union[int, tuple[int, int]]] = None,
399
- block_network: bool = False, # Whether to block network access
400
- # List of CIDRs the sandbox is allowed to access. If None, all CIDRs are allowed.
416
+ block_network: bool = False,
401
417
  cidr_allowlist: Optional[Sequence[str]] = None,
402
- volumes: dict[
403
- Union[str, os.PathLike], Union[_Volume, _CloudBucketMount]
404
- ] = {}, # Mount points for Modal Volumes and CloudBucketMounts
405
- pty_info: Optional[api_pb2.PTYInfo] = None,
406
- # List of ports to tunnel into the sandbox. Encrypted ports are tunneled with TLS.
418
+ volumes: dict[Union[str, os.PathLike], Union[_Volume, _CloudBucketMount]] = {},
419
+ pty: bool = False,
407
420
  encrypted_ports: Sequence[int] = [],
408
- # List of encrypted ports to tunnel into the sandbox, using HTTP/2.
409
421
  h2_ports: Sequence[int] = [],
410
- # List of ports to tunnel into the sandbox without encryption.
411
422
  unencrypted_ports: Sequence[int] = [],
412
- # Reference to a Modal Proxy to use in front of this Sandbox.
413
423
  proxy: Optional[_Proxy] = None,
414
424
  experimental_options: Optional[dict[str, bool]] = None,
415
- # Enable memory snapshots.
416
425
  _experimental_enable_snapshot: bool = False,
417
- _experimental_scheduler_placement: Optional[
418
- SchedulerPlacement
419
- ] = None, # Experimental controls over fine-grained scheduling (alpha).
426
+ _experimental_scheduler_placement: Optional[SchedulerPlacement] = None,
420
427
  client: Optional[_Client] = None,
421
428
  verbose: bool = False,
429
+ pty_info: Optional[api_pb2.PTYInfo] = None,
422
430
  ):
423
- # This method exposes some internal arguments (currently `mounts`) which are not in the public API
424
- # `mounts` is currently only used by modal shell (cli) to provide a function's mounts to the
425
- # sandbox that runs the shell session
431
+ """Private method used internally.
432
+
433
+ This method exposes some internal arguments (currently `mounts`) which are not in the public API.
434
+ `mounts` is currently only used by modal shell (cli) to provide a function's mounts to the
435
+ sandbox that runs the shell session.
436
+ """
426
437
  from .app import _App
427
438
 
428
439
  _validate_exec_args(args)
@@ -432,6 +443,10 @@ class _Sandbox(_Object, type_prefix="sb"):
432
443
  if block_network and (encrypted_ports or h2_ports or unencrypted_ports):
433
444
  raise InvalidError("Cannot specify open ports when `block_network` is enabled")
434
445
 
446
+ secrets = secrets or []
447
+ if env:
448
+ secrets = [*secrets, _Secret.from_dict(env)]
449
+
435
450
  # TODO(erikbern): Get rid of the `_new` method and create an already-hydrated object
436
451
  obj = _Sandbox._new(
437
452
  args,
@@ -451,6 +466,7 @@ class _Sandbox(_Object, type_prefix="sb"):
451
466
  block_network=block_network,
452
467
  cidr_allowlist=cidr_allowlist,
453
468
  volumes=volumes,
469
+ pty=pty,
454
470
  pty_info=pty_info,
455
471
  encrypted_ports=encrypted_ports,
456
472
  h2_ports=h2_ports,
@@ -703,14 +719,16 @@ class _Sandbox(_Object, type_prefix="sb"):
703
719
  async def exec(
704
720
  self,
705
721
  *args: str,
706
- pty_info: Optional[api_pb2.PTYInfo] = None,
707
722
  stdout: StreamType = StreamType.PIPE,
708
723
  stderr: StreamType = StreamType.PIPE,
709
724
  timeout: Optional[int] = None,
710
725
  workdir: Optional[str] = None,
711
- secrets: Sequence[_Secret] = (),
726
+ env: Optional[dict[str, Optional[str]]] = None,
727
+ secrets: Optional[Collection[_Secret]] = None,
712
728
  text: Literal[True] = True,
713
729
  bufsize: Literal[-1, 1] = -1,
730
+ pty: bool = False,
731
+ pty_info: Optional[api_pb2.PTYInfo] = None,
714
732
  _pty_info: Optional[api_pb2.PTYInfo] = None,
715
733
  ) -> _ContainerProcess[str]: ...
716
734
 
@@ -718,33 +736,38 @@ class _Sandbox(_Object, type_prefix="sb"):
718
736
  async def exec(
719
737
  self,
720
738
  *args: str,
721
- pty_info: Optional[api_pb2.PTYInfo] = None,
722
739
  stdout: StreamType = StreamType.PIPE,
723
740
  stderr: StreamType = StreamType.PIPE,
724
741
  timeout: Optional[int] = None,
725
742
  workdir: Optional[str] = None,
726
- secrets: Sequence[_Secret] = (),
743
+ env: Optional[dict[str, Optional[str]]] = None,
744
+ secrets: Optional[Collection[_Secret]] = None,
727
745
  text: Literal[False] = False,
728
746
  bufsize: Literal[-1, 1] = -1,
747
+ pty: bool = False,
748
+ pty_info: Optional[api_pb2.PTYInfo] = None,
729
749
  _pty_info: Optional[api_pb2.PTYInfo] = None,
730
750
  ) -> _ContainerProcess[bytes]: ...
731
751
 
732
752
  async def exec(
733
753
  self,
734
754
  *args: str,
735
- pty_info: Optional[api_pb2.PTYInfo] = None, # Deprecated: internal use only
736
755
  stdout: StreamType = StreamType.PIPE,
737
756
  stderr: StreamType = StreamType.PIPE,
738
757
  timeout: Optional[int] = None,
739
758
  workdir: Optional[str] = None,
740
- secrets: Sequence[_Secret] = (),
759
+ env: Optional[dict[str, Optional[str]]] = None, # Environment variables to set during command execution.
760
+ secrets: Optional[
761
+ Collection[_Secret]
762
+ ] = None, # Secrets to inject as environment variables during command execution.
741
763
  # Encode output as text.
742
764
  text: bool = True,
743
765
  # Control line-buffered output.
744
766
  # -1 means unbuffered, 1 means line-buffered (only available if `text=True`).
745
767
  bufsize: Literal[-1, 1] = -1,
746
- # Internal option to set terminal size and metadata
747
- _pty_info: Optional[api_pb2.PTYInfo] = None,
768
+ pty: bool = False, # Enable a PTY for the command
769
+ _pty_info: Optional[api_pb2.PTYInfo] = None, # *DEPRECATED* Use `pty` instead. `pty` will override `pty_info`.
770
+ pty_info: Optional[api_pb2.PTYInfo] = None, # *DEPRECATED* Use `pty` instead. `pty` will override `pty_info`.
748
771
  ):
749
772
  """Execute a command in the Sandbox and return a ContainerProcess handle.
750
773
 
@@ -764,11 +787,54 @@ class _Sandbox(_Object, type_prefix="sb"):
764
787
  print(line)
765
788
  ```
766
789
  """
790
+ if pty_info is not None or _pty_info is not None:
791
+ deprecation_warning(
792
+ (2025, 9, 12),
793
+ "The `_pty_info` and `pty_info` parameters are deprecated and will be removed in a future release. "
794
+ "Set the `pty` parameter to `True` instead.",
795
+ )
796
+ pty_info = _pty_info or pty_info
797
+ if pty:
798
+ pty_info = self._default_pty_info()
799
+
800
+ return await self._exec(
801
+ *args,
802
+ pty_info=pty_info,
803
+ stdout=stdout,
804
+ stderr=stderr,
805
+ timeout=timeout,
806
+ workdir=workdir,
807
+ env=env,
808
+ secrets=secrets,
809
+ text=text,
810
+ bufsize=bufsize,
811
+ )
812
+
813
+ async def _exec(
814
+ self,
815
+ *args: str,
816
+ pty_info: Optional[api_pb2.PTYInfo] = None,
817
+ stdout: StreamType = StreamType.PIPE,
818
+ stderr: StreamType = StreamType.PIPE,
819
+ timeout: Optional[int] = None,
820
+ workdir: Optional[str] = None,
821
+ env: Optional[dict[str, Optional[str]]] = None,
822
+ secrets: Optional[Collection[_Secret]] = None,
823
+ text: bool = True,
824
+ bufsize: Literal[-1, 1] = -1,
825
+ ) -> Union[_ContainerProcess[bytes], _ContainerProcess[str]]:
826
+ """Private method used internally.
767
827
 
828
+ This method exposes some internal arguments (currently `pty_info`) which are not in the public API.
829
+ """
768
830
  if workdir is not None and not workdir.startswith("/"):
769
831
  raise InvalidError(f"workdir must be an absolute path, got: {workdir}")
770
832
  _validate_exec_args(args)
771
833
 
834
+ secrets = secrets or []
835
+ if env:
836
+ secrets = [*secrets, _Secret.from_dict(env)]
837
+
772
838
  # Force secret resolution so we can pass the secret IDs to the backend.
773
839
  secret_coros = [secret.hydrate(client=self._client) for secret in secrets]
774
840
  await TaskContext.gather(*secret_coros)
@@ -777,7 +843,7 @@ class _Sandbox(_Object, type_prefix="sb"):
777
843
  req = api_pb2.ContainerExecRequest(
778
844
  task_id=task_id,
779
845
  command=args,
780
- pty_info=_pty_info or pty_info,
846
+ pty_info=pty_info,
781
847
  runtime_debug=config.get("function_runtime_debug"),
782
848
  timeout_secs=timeout or 0,
783
849
  workdir=workdir,