modal 1.1.5.dev18__py3-none-any.whl → 1.1.5.dev20__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/_pty.py CHANGED
@@ -7,8 +7,11 @@ from typing import Optional
7
7
  from modal_proto import api_pb2
8
8
 
9
9
 
10
- def get_winsz(fd) -> tuple[Optional[int], Optional[int]]:
10
+ def get_winsz(fd=None) -> tuple[Optional[int], Optional[int]]:
11
11
  try:
12
+ if fd is None:
13
+ fd = sys.stdin.fileno()
14
+
12
15
  import fcntl
13
16
  import struct
14
17
  import termios
@@ -40,8 +43,8 @@ def raw_terminal():
40
43
  termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
41
44
 
42
45
 
43
- def get_pty_info(shell: bool) -> api_pb2.PTYInfo:
44
- rows, cols = get_winsz(sys.stdin.fileno())
46
+ def get_pty_info(shell: bool, no_terminate_on_idle_stdin: bool = False) -> api_pb2.PTYInfo:
47
+ rows, cols = get_winsz()
45
48
  return api_pb2.PTYInfo(
46
49
  enabled=True, # TODO(erikbern): deprecated
47
50
  winsz_rows=rows,
@@ -50,4 +53,5 @@ def get_pty_info(shell: bool) -> api_pb2.PTYInfo:
50
53
  env_colorterm=os.environ.get("COLORTERM"),
51
54
  env_term_program=os.environ.get("TERM_PROGRAM"),
52
55
  pty_type=api_pb2.PTYInfo.PTY_TYPE_SHELL if shell else api_pb2.PTYInfo.PTY_TYPE_FUNCTION,
56
+ no_terminate_on_idle_stdin=no_terminate_on_idle_stdin,
53
57
  )
modal/client.pyi CHANGED
@@ -33,7 +33,7 @@ class _Client:
33
33
  server_url: str,
34
34
  client_type: int,
35
35
  credentials: typing.Optional[tuple[str, str]],
36
- version: str = "1.1.5.dev18",
36
+ version: str = "1.1.5.dev20",
37
37
  ):
38
38
  """mdmd:hidden
39
39
  The Modal client object is not intended to be instantiated directly by users.
@@ -164,7 +164,7 @@ class Client:
164
164
  server_url: str,
165
165
  client_type: int,
166
166
  credentials: typing.Optional[tuple[str, str]],
167
- version: str = "1.1.5.dev18",
167
+ version: str = "1.1.5.dev20",
168
168
  ):
169
169
  """mdmd:hidden
170
170
  The Modal client object is not intended to be instantiated directly by users.
modal/functions.pyi CHANGED
@@ -445,7 +445,7 @@ class Function(
445
445
 
446
446
  _call_generator: ___call_generator_spec[typing_extensions.Self]
447
447
 
448
- class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
448
+ class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
449
449
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
450
450
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
451
451
  ...
@@ -454,7 +454,7 @@ class Function(
454
454
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
455
455
  ...
456
456
 
457
- remote: __remote_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
457
+ remote: __remote_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
458
458
 
459
459
  class __remote_gen_spec(typing_extensions.Protocol[SUPERSELF]):
460
460
  def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
@@ -481,7 +481,7 @@ class Function(
481
481
  """
482
482
  ...
483
483
 
484
- class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
484
+ class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
485
485
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
486
486
  """[Experimental] Calls the function with the given arguments, without waiting for the results.
487
487
 
@@ -505,7 +505,7 @@ class Function(
505
505
  ...
506
506
 
507
507
  _experimental_spawn: ___experimental_spawn_spec[
508
- modal._functions.P, modal._functions.ReturnType, typing_extensions.Self
508
+ modal._functions.ReturnType, modal._functions.P, typing_extensions.Self
509
509
  ]
510
510
 
511
511
  class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER, SUPERSELF]):
@@ -514,7 +514,7 @@ class Function(
514
514
 
515
515
  _spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P, typing_extensions.Self]
516
516
 
517
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
517
+ class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
518
518
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
519
519
  """Calls the function with the given arguments, without waiting for the results.
520
520
 
@@ -535,7 +535,7 @@ class Function(
535
535
  """
536
536
  ...
537
537
 
538
- spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
538
+ spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
539
539
 
540
540
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
541
541
  """Return the inner Python object wrapped by this Modal Function."""
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
@@ -7,6 +7,7 @@ from collections.abc import AsyncGenerator, 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,6 +125,10 @@ 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],
@@ -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:
@@ -301,7 +310,7 @@ class _Sandbox(_Object, type_prefix="sb"):
301
310
  volumes: dict[
302
311
  Union[str, os.PathLike], Union[_Volume, _CloudBucketMount]
303
312
  ] = {}, # Mount points for Modal Volumes and CloudBucketMounts
304
- pty_info: Optional[api_pb2.PTYInfo] = None,
313
+ pty: bool = False, # Enable a PTY for the Sandbox
305
314
  # List of ports to tunnel into the sandbox. Encrypted ports are tunneled with TLS.
306
315
  encrypted_ports: Sequence[int] = [],
307
316
  # List of encrypted ports to tunnel into the sandbox, using HTTP/2.
@@ -320,6 +329,7 @@ class _Sandbox(_Object, type_prefix="sb"):
320
329
  ] = None, # Experimental controls over fine-grained scheduling (alpha).
321
330
  client: Optional[_Client] = None,
322
331
  environment_name: Optional[str] = None, # *DEPRECATED* Optionally override the default environment
332
+ pty_info: Optional[api_pb2.PTYInfo] = None, # *DEPRECATED* Use `pty` instead. `pty` will override `pty_info`.
323
333
  ) -> "_Sandbox":
324
334
  """
325
335
  Create a new Sandbox to run untrusted, arbitrary code.
@@ -342,6 +352,13 @@ class _Sandbox(_Object, type_prefix="sb"):
342
352
  "A sandbox's environment is determined by the app it is associated with.",
343
353
  )
344
354
 
355
+ if pty_info is not None:
356
+ deprecation_warning(
357
+ (2025, 9, 12),
358
+ "The `pty_info` parameter is deprecated and will be removed in a future release. "
359
+ "Set the `pty` parameter to `True` instead.",
360
+ )
361
+
345
362
  return await _Sandbox._create(
346
363
  *args,
347
364
  app=app,
@@ -360,7 +377,7 @@ class _Sandbox(_Object, type_prefix="sb"):
360
377
  block_network=block_network,
361
378
  cidr_allowlist=cidr_allowlist,
362
379
  volumes=volumes,
363
- pty_info=pty_info,
380
+ pty=pty,
364
381
  encrypted_ports=encrypted_ports,
365
382
  h2_ports=h2_ports,
366
383
  unencrypted_ports=unencrypted_ports,
@@ -370,59 +387,51 @@ class _Sandbox(_Object, type_prefix="sb"):
370
387
  _experimental_scheduler_placement=_experimental_scheduler_placement,
371
388
  client=client,
372
389
  verbose=verbose,
390
+ pty_info=pty_info,
373
391
  )
374
392
 
375
393
  @staticmethod
376
394
  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.
395
+ *args: str,
379
396
  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.
397
+ name: Optional[str] = None,
398
+ image: Optional[_Image] = None,
399
+ secrets: Sequence[_Secret] = (),
383
400
  mounts: Sequence[_Mount] = (),
384
401
  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.
402
+ timeout: int = 300,
387
403
  idle_timeout: Optional[int] = None,
388
- workdir: Optional[str] = None, # Working directory of the sandbox.
404
+ workdir: Optional[str] = None,
389
405
  gpu: GPU_T = None,
390
406
  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.
407
+ region: Optional[Union[str, Sequence[str]]] = None,
395
408
  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
409
  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.
410
+ block_network: bool = False,
401
411
  cidr_allowlist: Optional[Sequence[str]] = None,
402
412
  volumes: dict[
403
413
  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.
414
+ ] = {},
415
+ pty: bool = False,
407
416
  encrypted_ports: Sequence[int] = [],
408
- # List of encrypted ports to tunnel into the sandbox, using HTTP/2.
409
417
  h2_ports: Sequence[int] = [],
410
- # List of ports to tunnel into the sandbox without encryption.
411
418
  unencrypted_ports: Sequence[int] = [],
412
- # Reference to a Modal Proxy to use in front of this Sandbox.
413
419
  proxy: Optional[_Proxy] = None,
414
420
  experimental_options: Optional[dict[str, bool]] = None,
415
- # Enable memory snapshots.
416
421
  _experimental_enable_snapshot: bool = False,
417
422
  _experimental_scheduler_placement: Optional[
418
423
  SchedulerPlacement
419
- ] = None, # Experimental controls over fine-grained scheduling (alpha).
424
+ ] = None,
420
425
  client: Optional[_Client] = None,
421
426
  verbose: bool = False,
427
+ pty_info: Optional[api_pb2.PTYInfo] = None,
422
428
  ):
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
429
+ """Private method used internally.
430
+
431
+ This method exposes some internal arguments (currently `mounts`) which are not in the public API.
432
+ `mounts` is currently only used by modal shell (cli) to provide a function's mounts to the
433
+ sandbox that runs the shell session.
434
+ """
426
435
  from .app import _App
427
436
 
428
437
  _validate_exec_args(args)
@@ -451,6 +460,7 @@ class _Sandbox(_Object, type_prefix="sb"):
451
460
  block_network=block_network,
452
461
  cidr_allowlist=cidr_allowlist,
453
462
  volumes=volumes,
463
+ pty=pty,
454
464
  pty_info=pty_info,
455
465
  encrypted_ports=encrypted_ports,
456
466
  h2_ports=h2_ports,
@@ -703,7 +713,6 @@ class _Sandbox(_Object, type_prefix="sb"):
703
713
  async def exec(
704
714
  self,
705
715
  *args: str,
706
- pty_info: Optional[api_pb2.PTYInfo] = None,
707
716
  stdout: StreamType = StreamType.PIPE,
708
717
  stderr: StreamType = StreamType.PIPE,
709
718
  timeout: Optional[int] = None,
@@ -711,6 +720,8 @@ class _Sandbox(_Object, type_prefix="sb"):
711
720
  secrets: Sequence[_Secret] = (),
712
721
  text: Literal[True] = True,
713
722
  bufsize: Literal[-1, 1] = -1,
723
+ pty: bool = False,
724
+ pty_info: Optional[api_pb2.PTYInfo] = None,
714
725
  _pty_info: Optional[api_pb2.PTYInfo] = None,
715
726
  ) -> _ContainerProcess[str]: ...
716
727
 
@@ -718,7 +729,6 @@ class _Sandbox(_Object, type_prefix="sb"):
718
729
  async def exec(
719
730
  self,
720
731
  *args: str,
721
- pty_info: Optional[api_pb2.PTYInfo] = None,
722
732
  stdout: StreamType = StreamType.PIPE,
723
733
  stderr: StreamType = StreamType.PIPE,
724
734
  timeout: Optional[int] = None,
@@ -726,13 +736,14 @@ class _Sandbox(_Object, type_prefix="sb"):
726
736
  secrets: Sequence[_Secret] = (),
727
737
  text: Literal[False] = False,
728
738
  bufsize: Literal[-1, 1] = -1,
739
+ pty: bool = False,
740
+ pty_info: Optional[api_pb2.PTYInfo] = None,
729
741
  _pty_info: Optional[api_pb2.PTYInfo] = None,
730
742
  ) -> _ContainerProcess[bytes]: ...
731
743
 
732
744
  async def exec(
733
745
  self,
734
746
  *args: str,
735
- pty_info: Optional[api_pb2.PTYInfo] = None, # Deprecated: internal use only
736
747
  stdout: StreamType = StreamType.PIPE,
737
748
  stderr: StreamType = StreamType.PIPE,
738
749
  timeout: Optional[int] = None,
@@ -743,8 +754,9 @@ class _Sandbox(_Object, type_prefix="sb"):
743
754
  # Control line-buffered output.
744
755
  # -1 means unbuffered, 1 means line-buffered (only available if `text=True`).
745
756
  bufsize: Literal[-1, 1] = -1,
746
- # Internal option to set terminal size and metadata
747
- _pty_info: Optional[api_pb2.PTYInfo] = None,
757
+ pty: bool = False, # Enable a PTY for the command
758
+ _pty_info: Optional[api_pb2.PTYInfo] = None, # *DEPRECATED* Use `pty` instead. `pty` will override `pty_info`.
759
+ pty_info: Optional[api_pb2.PTYInfo] = None, # *DEPRECATED* Use `pty` instead. `pty` will override `pty_info`.
748
760
  ):
749
761
  """Execute a command in the Sandbox and return a ContainerProcess handle.
750
762
 
@@ -764,7 +776,44 @@ class _Sandbox(_Object, type_prefix="sb"):
764
776
  print(line)
765
777
  ```
766
778
  """
779
+ if pty_info is not None or _pty_info is not None:
780
+ deprecation_warning(
781
+ (2025, 9, 12),
782
+ "The `_pty_info` and `pty_info` parameters are deprecated and will be removed in a future release. "
783
+ "Set the `pty` parameter to `True` instead.",
784
+ )
785
+ pty_info = _pty_info or pty_info
786
+ if pty:
787
+ pty_info = self._default_pty_info()
767
788
 
789
+ return await self._exec(
790
+ *args,
791
+ pty_info=pty_info,
792
+ stdout=stdout,
793
+ stderr=stderr,
794
+ timeout=timeout,
795
+ workdir=workdir,
796
+ secrets=secrets,
797
+ text=text,
798
+ bufsize=bufsize,
799
+ )
800
+
801
+ async def _exec(
802
+ self,
803
+ *args: str,
804
+ pty_info: Optional[api_pb2.PTYInfo] = None,
805
+ stdout: StreamType = StreamType.PIPE,
806
+ stderr: StreamType = StreamType.PIPE,
807
+ timeout: Optional[int] = None,
808
+ workdir: Optional[str] = None,
809
+ secrets: Sequence[_Secret] = (),
810
+ text: bool = True,
811
+ bufsize: Literal[-1, 1] = -1,
812
+ ) -> Union[_ContainerProcess[bytes], _ContainerProcess[str]]:
813
+ """Private method used internally.
814
+
815
+ This method exposes some internal arguments (currently `pty_info`) which are not in the public API.
816
+ """
768
817
  if workdir is not None and not workdir.startswith("/"):
769
818
  raise InvalidError(f"workdir must be an absolute path, got: {workdir}")
770
819
  _validate_exec_args(args)
@@ -777,7 +826,7 @@ class _Sandbox(_Object, type_prefix="sb"):
777
826
  req = api_pb2.ContainerExecRequest(
778
827
  task_id=task_id,
779
828
  command=args,
780
- pty_info=_pty_info or pty_info,
829
+ pty_info=pty_info,
781
830
  runtime_debug=config.get("function_runtime_debug"),
782
831
  timeout_secs=timeout or 0,
783
832
  workdir=workdir,