modAL 1.0.0__tar.gz → 1.0.0.dev2__tar.gz

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 (183) hide show
  1. {modal-1.0.0 → modal-1.0.0.dev2}/PKG-INFO +1 -1
  2. {modal-1.0.0 → modal-1.0.0.dev2}/modal/__init__.py +4 -10
  3. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_functions.py +88 -15
  4. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_object.py +14 -0
  5. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_partial_function.py +14 -4
  6. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_serialization.py +2 -2
  7. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/function_utils.py +6 -3
  8. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/grpc_utils.py +1 -6
  9. {modal-1.0.0 → modal-1.0.0.dev2}/modal/app.py +102 -1
  10. {modal-1.0.0 → modal-1.0.0.dev2}/modal/app.pyi +127 -0
  11. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/app.py +19 -0
  12. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/programs/run_jupyter.py +1 -1
  13. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/programs/vscode.py +1 -1
  14. {modal-1.0.0 → modal-1.0.0.dev2}/modal/client.pyi +10 -2
  15. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cls.py +11 -8
  16. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cls.pyi +17 -2
  17. {modal-1.0.0 → modal-1.0.0.dev2}/modal/config.py +16 -5
  18. {modal-1.0.0 → modal-1.0.0.dev2}/modal/container_process.py +9 -1
  19. {modal-1.0.0 → modal-1.0.0.dev2}/modal/container_process.pyi +3 -3
  20. {modal-1.0.0 → modal-1.0.0.dev2}/modal/dict.py +1 -2
  21. {modal-1.0.0 → modal-1.0.0.dev2}/modal/exception.py +1 -1
  22. {modal-1.0.0 → modal-1.0.0.dev2}/modal/functions.pyi +29 -8
  23. {modal-1.0.0 → modal-1.0.0.dev2}/modal/image.py +36 -12
  24. {modal-1.0.0 → modal-1.0.0.dev2}/modal/image.pyi +5 -2
  25. {modal-1.0.0 → modal-1.0.0.dev2}/modal/mount.py +62 -1
  26. {modal-1.0.0 → modal-1.0.0.dev2}/modal/mount.pyi +1 -0
  27. {modal-1.0.0 → modal-1.0.0.dev2}/modal/network_file_system.py +1 -2
  28. {modal-1.0.0 → modal-1.0.0.dev2}/modal/object.pyi +6 -0
  29. {modal-1.0.0 → modal-1.0.0.dev2}/modal/queue.py +1 -2
  30. {modal-1.0.0 → modal-1.0.0.dev2}/modal/runner.py +19 -2
  31. {modal-1.0.0 → modal-1.0.0.dev2}/modal/runner.pyi +5 -0
  32. {modal-1.0.0 → modal-1.0.0.dev2}/modal/sandbox.py +32 -78
  33. {modal-1.0.0 → modal-1.0.0.dev2}/modal/sandbox.pyi +7 -102
  34. {modal-1.0.0 → modal-1.0.0.dev2}/modal/serving.py +6 -0
  35. {modal-1.0.0 → modal-1.0.0.dev2}/modal/serving.pyi +3 -0
  36. {modal-1.0.0 → modal-1.0.0.dev2}/modal/volume.py +14 -8
  37. {modal-1.0.0 → modal-1.0.0.dev2}/modal.egg-info/PKG-INFO +1 -1
  38. {modal-1.0.0 → modal-1.0.0.dev2}/modal_proto/api.proto +2 -29
  39. {modal-1.0.0 → modal-1.0.0.dev2}/modal_proto/api_grpc.py +0 -32
  40. {modal-1.0.0 → modal-1.0.0.dev2}/modal_proto/api_pb2.py +756 -788
  41. {modal-1.0.0 → modal-1.0.0.dev2}/modal_proto/api_pb2.pyi +12 -86
  42. {modal-1.0.0 → modal-1.0.0.dev2}/modal_proto/api_pb2_grpc.py +0 -66
  43. {modal-1.0.0 → modal-1.0.0.dev2}/modal_proto/api_pb2_grpc.pyi +0 -20
  44. {modal-1.0.0 → modal-1.0.0.dev2}/modal_proto/modal_api_grpc.py +0 -2
  45. {modal-1.0.0 → modal-1.0.0.dev2}/modal_version/__init__.py +1 -1
  46. {modal-1.0.0 → modal-1.0.0.dev2}/LICENSE +0 -0
  47. {modal-1.0.0 → modal-1.0.0.dev2}/README.md +0 -0
  48. {modal-1.0.0 → modal-1.0.0.dev2}/modal/__main__.py +0 -0
  49. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_clustered_functions.py +0 -0
  50. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_clustered_functions.pyi +0 -0
  51. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_container_entrypoint.py +0 -0
  52. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_ipython.py +0 -0
  53. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_location.py +0 -0
  54. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_output.py +0 -0
  55. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_pty.py +0 -0
  56. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_resolver.py +0 -0
  57. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_resources.py +0 -0
  58. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_runtime/__init__.py +0 -0
  59. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_runtime/asgi.py +0 -0
  60. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_runtime/container_io_manager.py +0 -0
  61. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_runtime/container_io_manager.pyi +0 -0
  62. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_runtime/execution_context.py +0 -0
  63. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_runtime/execution_context.pyi +0 -0
  64. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  65. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_runtime/telemetry.py +0 -0
  66. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_runtime/user_code_imports.py +0 -0
  67. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_traceback.py +0 -0
  68. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_tunnel.py +0 -0
  69. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_tunnel.pyi +0 -0
  70. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_type_manager.py +0 -0
  71. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/__init__.py +0 -0
  72. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/app_utils.py +0 -0
  73. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/async_utils.py +0 -0
  74. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/blob_utils.py +0 -0
  75. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/bytes_io_segment_payload.py +0 -0
  76. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/deprecation.py +0 -0
  77. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/docker_utils.py +0 -0
  78. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/git_utils.py +0 -0
  79. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/grpc_testing.py +0 -0
  80. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/hash_utils.py +0 -0
  81. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/http_utils.py +0 -0
  82. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/jwt_utils.py +0 -0
  83. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/logger.py +0 -0
  84. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/mount_utils.py +0 -0
  85. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/name_utils.py +0 -0
  86. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/package_utils.py +0 -0
  87. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/pattern_utils.py +0 -0
  88. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/rand_pb_testing.py +0 -0
  89. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_utils/shell_utils.py +0 -0
  90. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_vendor/__init__.py +0 -0
  91. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  92. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_vendor/cloudpickle.py +0 -0
  93. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_vendor/tblib.py +0 -0
  94. {modal-1.0.0 → modal-1.0.0.dev2}/modal/_watcher.py +0 -0
  95. {modal-1.0.0 → modal-1.0.0.dev2}/modal/call_graph.py +0 -0
  96. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/__init__.py +0 -0
  97. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/_download.py +0 -0
  98. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/_traceback.py +0 -0
  99. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/cluster.py +0 -0
  100. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/config.py +0 -0
  101. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/container.py +0 -0
  102. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/dict.py +0 -0
  103. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/entry_point.py +0 -0
  104. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/environment.py +0 -0
  105. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/import_refs.py +0 -0
  106. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/launch.py +0 -0
  107. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/network_file_system.py +0 -0
  108. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/profile.py +0 -0
  109. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/programs/__init__.py +0 -0
  110. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/queues.py +0 -0
  111. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/run.py +0 -0
  112. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/secret.py +0 -0
  113. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/token.py +0 -0
  114. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/utils.py +0 -0
  115. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cli/volume.py +0 -0
  116. {modal-1.0.0 → modal-1.0.0.dev2}/modal/client.py +0 -0
  117. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cloud_bucket_mount.py +0 -0
  118. {modal-1.0.0 → modal-1.0.0.dev2}/modal/cloud_bucket_mount.pyi +0 -0
  119. {modal-1.0.0 → modal-1.0.0.dev2}/modal/dict.pyi +0 -0
  120. {modal-1.0.0 → modal-1.0.0.dev2}/modal/environments.py +0 -0
  121. {modal-1.0.0 → modal-1.0.0.dev2}/modal/environments.pyi +0 -0
  122. {modal-1.0.0 → modal-1.0.0.dev2}/modal/experimental/__init__.py +0 -0
  123. {modal-1.0.0 → modal-1.0.0.dev2}/modal/experimental/ipython.py +0 -0
  124. {modal-1.0.0 → modal-1.0.0.dev2}/modal/file_io.py +0 -0
  125. {modal-1.0.0 → modal-1.0.0.dev2}/modal/file_io.pyi +0 -0
  126. {modal-1.0.0 → modal-1.0.0.dev2}/modal/file_pattern_matcher.py +0 -0
  127. {modal-1.0.0 → modal-1.0.0.dev2}/modal/functions.py +0 -0
  128. {modal-1.0.0 → modal-1.0.0.dev2}/modal/gpu.py +0 -0
  129. {modal-1.0.0 → modal-1.0.0.dev2}/modal/io_streams.py +0 -0
  130. {modal-1.0.0 → modal-1.0.0.dev2}/modal/io_streams.pyi +0 -0
  131. {modal-1.0.0 → modal-1.0.0.dev2}/modal/network_file_system.pyi +0 -0
  132. {modal-1.0.0 → modal-1.0.0.dev2}/modal/object.py +0 -0
  133. {modal-1.0.0 → modal-1.0.0.dev2}/modal/output.py +0 -0
  134. {modal-1.0.0 → modal-1.0.0.dev2}/modal/parallel_map.py +0 -0
  135. {modal-1.0.0 → modal-1.0.0.dev2}/modal/parallel_map.pyi +0 -0
  136. {modal-1.0.0 → modal-1.0.0.dev2}/modal/partial_function.py +0 -0
  137. {modal-1.0.0 → modal-1.0.0.dev2}/modal/partial_function.pyi +0 -0
  138. {modal-1.0.0 → modal-1.0.0.dev2}/modal/proxy.py +0 -0
  139. {modal-1.0.0 → modal-1.0.0.dev2}/modal/proxy.pyi +0 -0
  140. {modal-1.0.0 → modal-1.0.0.dev2}/modal/py.typed +0 -0
  141. {modal-1.0.0 → modal-1.0.0.dev2}/modal/queue.pyi +0 -0
  142. {modal-1.0.0 → modal-1.0.0.dev2}/modal/requirements/2023.12.312.txt +0 -0
  143. {modal-1.0.0 → modal-1.0.0.dev2}/modal/requirements/2023.12.txt +0 -0
  144. {modal-1.0.0 → modal-1.0.0.dev2}/modal/requirements/2024.04.txt +0 -0
  145. {modal-1.0.0 → modal-1.0.0.dev2}/modal/requirements/2024.10.txt +0 -0
  146. {modal-1.0.0 → modal-1.0.0.dev2}/modal/requirements/PREVIEW.txt +0 -0
  147. {modal-1.0.0 → modal-1.0.0.dev2}/modal/requirements/README.md +0 -0
  148. {modal-1.0.0 → modal-1.0.0.dev2}/modal/requirements/base-images.json +0 -0
  149. {modal-1.0.0 → modal-1.0.0.dev2}/modal/retries.py +0 -0
  150. {modal-1.0.0 → modal-1.0.0.dev2}/modal/running_app.py +0 -0
  151. {modal-1.0.0 → modal-1.0.0.dev2}/modal/schedule.py +0 -0
  152. {modal-1.0.0 → modal-1.0.0.dev2}/modal/scheduler_placement.py +0 -0
  153. {modal-1.0.0 → modal-1.0.0.dev2}/modal/secret.py +0 -0
  154. {modal-1.0.0 → modal-1.0.0.dev2}/modal/secret.pyi +0 -0
  155. {modal-1.0.0 → modal-1.0.0.dev2}/modal/snapshot.py +0 -0
  156. {modal-1.0.0 → modal-1.0.0.dev2}/modal/snapshot.pyi +0 -0
  157. {modal-1.0.0 → modal-1.0.0.dev2}/modal/stream_type.py +0 -0
  158. {modal-1.0.0 → modal-1.0.0.dev2}/modal/token_flow.py +0 -0
  159. {modal-1.0.0 → modal-1.0.0.dev2}/modal/token_flow.pyi +0 -0
  160. {modal-1.0.0 → modal-1.0.0.dev2}/modal/volume.pyi +0 -0
  161. {modal-1.0.0 → modal-1.0.0.dev2}/modal.egg-info/SOURCES.txt +0 -0
  162. {modal-1.0.0 → modal-1.0.0.dev2}/modal.egg-info/dependency_links.txt +0 -0
  163. {modal-1.0.0 → modal-1.0.0.dev2}/modal.egg-info/entry_points.txt +0 -0
  164. {modal-1.0.0 → modal-1.0.0.dev2}/modal.egg-info/requires.txt +0 -0
  165. {modal-1.0.0 → modal-1.0.0.dev2}/modal.egg-info/top_level.txt +0 -0
  166. {modal-1.0.0 → modal-1.0.0.dev2}/modal_docs/__init__.py +0 -0
  167. {modal-1.0.0 → modal-1.0.0.dev2}/modal_docs/gen_cli_docs.py +0 -0
  168. {modal-1.0.0 → modal-1.0.0.dev2}/modal_docs/gen_reference_docs.py +0 -0
  169. {modal-1.0.0 → modal-1.0.0.dev2}/modal_docs/mdmd/__init__.py +0 -0
  170. {modal-1.0.0 → modal-1.0.0.dev2}/modal_docs/mdmd/mdmd.py +0 -0
  171. {modal-1.0.0 → modal-1.0.0.dev2}/modal_docs/mdmd/signatures.py +0 -0
  172. {modal-1.0.0 → modal-1.0.0.dev2}/modal_proto/__init__.py +0 -0
  173. {modal-1.0.0 → modal-1.0.0.dev2}/modal_proto/modal_options_grpc.py +0 -0
  174. {modal-1.0.0 → modal-1.0.0.dev2}/modal_proto/options.proto +0 -0
  175. {modal-1.0.0 → modal-1.0.0.dev2}/modal_proto/options_grpc.py +0 -0
  176. {modal-1.0.0 → modal-1.0.0.dev2}/modal_proto/options_pb2.py +0 -0
  177. {modal-1.0.0 → modal-1.0.0.dev2}/modal_proto/options_pb2.pyi +0 -0
  178. {modal-1.0.0 → modal-1.0.0.dev2}/modal_proto/options_pb2_grpc.py +0 -0
  179. {modal-1.0.0 → modal-1.0.0.dev2}/modal_proto/options_pb2_grpc.pyi +0 -0
  180. {modal-1.0.0 → modal-1.0.0.dev2}/modal_proto/py.typed +0 -0
  181. {modal-1.0.0 → modal-1.0.0.dev2}/modal_version/__main__.py +0 -0
  182. {modal-1.0.0 → modal-1.0.0.dev2}/pyproject.toml +0 -0
  183. {modal-1.0.0 → modal-1.0.0.dev2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.0.0
3
+ Version: 1.0.0.dev2
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -11,7 +11,7 @@ from modal_version import __version__
11
11
  try:
12
12
  from ._runtime.execution_context import current_function_call_id, current_input_id, interact, is_local
13
13
  from ._tunnel import Tunnel, forward
14
- from .app import App
14
+ from .app import App, Stub
15
15
  from .client import Client
16
16
  from .cloud_bucket_mount import CloudBucketMount
17
17
  from .cls import Cls, parameter
@@ -20,6 +20,7 @@ try:
20
20
  from .file_pattern_matcher import FilePatternMatcher
21
21
  from .functions import Function, FunctionCall
22
22
  from .image import Image
23
+ from .mount import Mount
23
24
  from .network_file_system import NetworkFileSystem
24
25
  from .output import enable_output
25
26
  from .partial_function import (
@@ -53,7 +54,6 @@ except Exception:
53
54
  print()
54
55
  raise
55
56
 
56
-
57
57
  __all__ = [
58
58
  "__version__",
59
59
  "App",
@@ -66,6 +66,7 @@ __all__ = [
66
66
  "Function",
67
67
  "FunctionCall",
68
68
  "Image",
69
+ "Mount",
69
70
  "NetworkFileSystem",
70
71
  "Period",
71
72
  "Proxy",
@@ -76,6 +77,7 @@ __all__ = [
76
77
  "SandboxSnapshot",
77
78
  "SchedulerPlacement",
78
79
  "Secret",
80
+ "Stub",
79
81
  "Tunnel",
80
82
  "Volume",
81
83
  "asgi_app",
@@ -97,11 +99,3 @@ __all__ = [
97
99
  "web_server",
98
100
  "wsgi_app",
99
101
  ]
100
-
101
-
102
- def __getattr__(name):
103
- if name == "Stub":
104
- raise AttributeError(
105
- "Module 'modal' has no attribute 'Stub'. Use `modal.App` instead. This is a simple name change."
106
- )
107
- raise AttributeError(f"module 'modal' has no attribute '{name}'")
@@ -6,7 +6,7 @@ import textwrap
6
6
  import time
7
7
  import typing
8
8
  import warnings
9
- from collections.abc import AsyncGenerator, Sequence, Sized
9
+ from collections.abc import AsyncGenerator, Collection, Sequence, Sized
10
10
  from dataclasses import dataclass
11
11
  from pathlib import PurePosixPath
12
12
  from typing import TYPE_CHECKING, Any, Callable, Optional, Union
@@ -41,7 +41,7 @@ from ._utils.async_utils import (
41
41
  synchronizer,
42
42
  warn_if_generator_is_not_consumed,
43
43
  )
44
- from ._utils.deprecation import deprecation_warning
44
+ from ._utils.deprecation import deprecation_error, deprecation_warning
45
45
  from ._utils.function_utils import (
46
46
  ATTEMPT_TIMEOUT_GRACE_PERIOD,
47
47
  OUTPUTS_TIMEOUT,
@@ -71,7 +71,7 @@ from .exception import (
71
71
  )
72
72
  from .gpu import GPU_T, parse_gpu_config
73
73
  from .image import _Image
74
- from .mount import _get_client_mount, _Mount
74
+ from .mount import _get_client_mount, _Mount, get_sys_modules_mounts
75
75
  from .network_file_system import _NetworkFileSystem, network_file_system_mount_protos
76
76
  from .output import _get_output_manager
77
77
  from .parallel_map import (
@@ -405,6 +405,12 @@ class FunctionStats:
405
405
  backlog: int
406
406
  num_total_runners: int
407
407
 
408
+ def __getattr__(self, name):
409
+ if name == "num_active_runners":
410
+ msg = "'FunctionStats.num_active_runners' is no longer available."
411
+ deprecation_error((2024, 6, 14), msg)
412
+ raise AttributeError(f"'FunctionStats' object has no attribute '{name}'")
413
+
408
414
 
409
415
  def _parse_retries(
410
416
  retries: Optional[Union[int, Retries]],
@@ -505,6 +511,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
505
511
  is_generator: bool = False,
506
512
  gpu: Union[GPU_T, list[GPU_T]] = None,
507
513
  # TODO: maybe break this out into a separate decorator for notebooks.
514
+ mounts: Collection[_Mount] = (),
508
515
  network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
509
516
  volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]] = {},
510
517
  webhook_config: Optional[api_pb2.WebhookConfig] = None,
@@ -560,6 +567,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
560
567
  assert not webhook_config
561
568
  assert not schedule
562
569
 
570
+ explicit_mounts = mounts
571
+
563
572
  include_source_mode = get_include_source_mode(include_source)
564
573
  if include_source_mode != IncludeSourceMode.INCLUDE_NOTHING:
565
574
  entrypoint_mounts = info.get_entrypoint_mount()
@@ -568,9 +577,34 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
568
577
 
569
578
  all_mounts = [
570
579
  _get_client_mount(),
580
+ *explicit_mounts,
571
581
  *entrypoint_mounts.values(),
572
582
  ]
573
583
 
584
+ if include_source_mode is IncludeSourceMode.INCLUDE_FIRST_PARTY and is_local():
585
+ auto_mounts = get_sys_modules_mounts()
586
+ # don't need to add entrypoint modules to automounts:
587
+ for entrypoint_module in entrypoint_mounts:
588
+ auto_mounts.pop(entrypoint_module, None)
589
+
590
+ warn_missing_modules = set(auto_mounts.keys()) - image._added_python_source_set
591
+
592
+ if warn_missing_modules:
593
+ python_stringified_modules = ", ".join(f'"{mod}"' for mod in sorted(warn_missing_modules))
594
+ deprecation_warning(
595
+ (2025, 2, 3),
596
+ (
597
+ 'Modal will stop implicitly adding local Python modules to the Image ("automounting") in a '
598
+ "future update. The following modules need to be explicitly added for future "
599
+ "compatibility:\n"
600
+ )
601
+ + "\n".join(sorted([f"* {m}" for m in warn_missing_modules]))
602
+ + "\n\n"
603
+ + (f"e.g.:\nimage_with_source = my_image.add_local_python_source({python_stringified_modules})\n\n")
604
+ + "For more information, see https://modal.com/docs/guide/modal-1-0-migration",
605
+ )
606
+ all_mounts += auto_mounts.values()
607
+
574
608
  retry_policy = _parse_retries(
575
609
  retries, f"Function '{info.get_tag()}'" if info.raw_f else f"Class '{info.get_tag()}'"
576
610
  )
@@ -607,6 +641,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
607
641
  image=image,
608
642
  secrets=secrets,
609
643
  gpu=gpu,
644
+ mounts=mounts,
610
645
  network_file_systems=network_file_systems,
611
646
  volumes=volumes,
612
647
  memory=memory,
@@ -714,7 +749,16 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
714
749
 
715
750
  def _deps(only_explicit_mounts=False) -> list[_Object]:
716
751
  deps: list[_Object] = list(secrets)
717
- if not only_explicit_mounts:
752
+ if only_explicit_mounts:
753
+ # TODO: this is a bit hacky, but all_mounts may differ in the container vs locally
754
+ # We don't want the function dependencies to change, so we have this way to force it to
755
+ # only include its declared dependencies.
756
+ # Only objects that need interaction within a user's container actually need to be
757
+ # included when only_explicit_mounts=True, so omitting auto mounts here
758
+ # wouldn't be a problem as long as Mounts are "passive" and only loaded by the
759
+ # worker runtime
760
+ deps += list(explicit_mounts)
761
+ else:
718
762
  deps += list(all_mounts)
719
763
  if proxy:
720
764
  deps.append(proxy)
@@ -982,6 +1026,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
982
1026
  obj._build_args = dict( # See get_build_def
983
1027
  secrets=repr(secrets),
984
1028
  gpu_config=repr([parse_gpu_config(_gpu) for _gpu in gpus]),
1029
+ mounts=repr(mounts),
985
1030
  network_file_systems=repr(network_file_systems),
986
1031
  )
987
1032
  # these key are excluded if empty to avoid rebuilds on client upgrade
@@ -1145,8 +1190,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1145
1190
 
1146
1191
  @live_method
1147
1192
  async def keep_warm(self, warm_pool_size: int) -> None:
1148
- """mdmd:hidden
1149
- Set the warm pool size for the Function.
1193
+ """Set the warm pool size for the Function.
1150
1194
 
1151
1195
  DEPRECATED: Please adapt your code to use the more general `update_autoscaler` method instead:
1152
1196
 
@@ -1250,8 +1294,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1250
1294
  client: Optional[_Client] = None,
1251
1295
  environment_name: Optional[str] = None,
1252
1296
  ) -> "_Function":
1253
- """mdmd:hidden
1254
- Lookup a Function from a deployed App by its name.
1297
+ """Lookup a Function from a deployed App by its name.
1255
1298
 
1256
1299
  DEPRECATED: This method is deprecated in favor of `modal.Function.from_name`.
1257
1300
 
@@ -1375,8 +1418,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1375
1418
  @property
1376
1419
  @live_method
1377
1420
  async def web_url(self) -> Optional[str]:
1378
- """mdmd:hidden
1379
- Deprecated. Use the `Function.get_web_url()` method instead.
1421
+ """Deprecated. Use the `Function.get_web_url()` method instead.
1380
1422
 
1381
1423
  URL of a Function running as a web endpoint.
1382
1424
  """
@@ -1493,6 +1535,20 @@ Use the `Function.get_web_url()` method instead.
1493
1535
  async for res in invocation.run_generator():
1494
1536
  yield res
1495
1537
 
1538
+ @synchronizer.no_io_translation
1539
+ async def _call_generator_nowait(self, args, kwargs):
1540
+ deprecation_warning(
1541
+ (2024, 12, 11),
1542
+ "Calling spawn on a generator function is deprecated and will soon raise an exception.",
1543
+ )
1544
+ return await _Invocation.create(
1545
+ self,
1546
+ args,
1547
+ kwargs,
1548
+ client=self.client,
1549
+ function_call_invocation_type=api_pb2.FUNCTION_CALL_INVOCATION_TYPE_ASYNC,
1550
+ )
1551
+
1496
1552
  @synchronizer.no_io_translation
1497
1553
  @live_method
1498
1554
  async def remote(self, *args: P.args, **kwargs: P.kwargs) -> ReturnType:
@@ -1605,7 +1661,7 @@ Use the `Function.get_web_url()` method instead.
1605
1661
  """
1606
1662
  self._check_no_web_url("_experimental_spawn")
1607
1663
  if self._is_generator:
1608
- raise InvalidError("Cannot `spawn` a generator function.")
1664
+ invocation = await self._call_generator_nowait(args, kwargs)
1609
1665
  else:
1610
1666
  invocation = await self._call_function_nowait(
1611
1667
  args, kwargs, function_call_invocation_type=api_pb2.FUNCTION_CALL_INVOCATION_TYPE_ASYNC
@@ -1637,13 +1693,14 @@ Use the `Function.get_web_url()` method instead.
1637
1693
  """
1638
1694
  self._check_no_web_url("spawn")
1639
1695
  if self._is_generator:
1640
- raise InvalidError("Cannot `spawn` a generator function.")
1696
+ invocation = await self._call_generator_nowait(args, kwargs)
1641
1697
  else:
1642
1698
  invocation = await self._call_function_nowait(args, kwargs, api_pb2.FUNCTION_CALL_INVOCATION_TYPE_ASYNC)
1643
1699
 
1644
1700
  fc: _FunctionCall[ReturnType] = _FunctionCall._new_hydrated(
1645
1701
  invocation.function_call_id, invocation.client, None
1646
1702
  )
1703
+ fc._is_generator = self._is_generator if self._is_generator else False
1647
1704
  return fc
1648
1705
 
1649
1706
  def get_raw_f(self) -> Callable[..., Any]:
@@ -1702,8 +1759,22 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1702
1759
 
1703
1760
  The returned coroutine is not cancellation-safe.
1704
1761
  """
1762
+
1763
+ if self._is_generator:
1764
+ raise Exception("Cannot get the result of a generator function call. Use `get_gen` instead.")
1765
+
1705
1766
  return await self._invocation().poll_function(timeout=timeout)
1706
1767
 
1768
+ async def get_gen(self) -> AsyncGenerator[Any, None]:
1769
+ """
1770
+ Calls the generator remotely, executing it with the given arguments and returning the execution's result.
1771
+ """
1772
+ if not self._is_generator:
1773
+ raise Exception("Cannot iterate over a non-generator function call. Use `get` instead.")
1774
+
1775
+ async for res in self._invocation().run_generator():
1776
+ yield res
1777
+
1707
1778
  async def get_call_graph(self) -> list[InputInfo]:
1708
1779
  """Returns a structure representing the call graph from a given root
1709
1780
  call ID, along with the status of execution for each node.
@@ -1734,7 +1805,9 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1734
1805
  await retry_transient_errors(self._client.stub.FunctionCallCancel, request)
1735
1806
 
1736
1807
  @staticmethod
1737
- async def from_id(function_call_id: str, client: Optional[_Client] = None) -> "_FunctionCall[Any]":
1808
+ async def from_id(
1809
+ function_call_id: str, client: Optional[_Client] = None, is_generator: bool = False
1810
+ ) -> "_FunctionCall[Any]":
1738
1811
  """Instantiate a FunctionCall object from an existing ID.
1739
1812
 
1740
1813
  Examples:
@@ -1757,6 +1830,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1757
1830
  client = await _Client.from_env()
1758
1831
 
1759
1832
  fc: _FunctionCall[Any] = _FunctionCall._new_hydrated(function_call_id, client, None)
1833
+ fc._is_generator = is_generator
1760
1834
  return fc
1761
1835
 
1762
1836
  @staticmethod
@@ -1787,8 +1861,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1787
1861
 
1788
1862
 
1789
1863
  async def _gather(*function_calls: _FunctionCall[T]) -> typing.Sequence[T]:
1790
- """mdmd:hidden
1791
- Deprecated: Please use `modal.FunctionCall.gather()` instead."""
1864
+ """Deprecated: Please use `modal.FunctionCall.gather()` instead."""
1792
1865
  deprecation_warning(
1793
1866
  (2025, 2, 24),
1794
1867
  "`modal.functions.gather()` is deprecated; please use `modal.FunctionCall.gather()` instead.",
@@ -10,6 +10,7 @@ from typing_extensions import Self
10
10
 
11
11
  from ._resolver import Resolver
12
12
  from ._utils.async_utils import aclosing
13
+ from ._utils.deprecation import deprecation_warning
13
14
  from .client import _Client
14
15
  from .config import config, logger
15
16
  from .exception import ExecutionError, InvalidError
@@ -236,6 +237,19 @@ class _Object:
236
237
 
237
238
  return self._deps if self._deps is not None else default_deps
238
239
 
240
+ async def resolve(self, client: Optional[_Client] = None):
241
+ """mdmd:hidden"""
242
+ obj = self.__class__.__name__.strip("_")
243
+ deprecation_warning(
244
+ (2025, 1, 16),
245
+ f"The `{obj}.resolve` method is deprecated and will be removed in a future release."
246
+ f" Please use `{obj}.hydrate()` or `await {obj}.hydrate.aio()` instead."
247
+ "\n\nNote that it is rarely necessary to explicitly hydrate objects, as most methods"
248
+ " will lazily hydrate when needed.",
249
+ show_source=False, # synchronicity interferes with attributing source correctly
250
+ )
251
+ await self.hydrate(client)
252
+
239
253
  async def hydrate(self, client: Optional[_Client] = None) -> Self:
240
254
  """Synchronize the local object with its identity on the Modal server.
241
255
 
@@ -18,7 +18,7 @@ from modal_proto import api_pb2
18
18
  from ._functions import _Function
19
19
  from ._utils.async_utils import synchronizer
20
20
  from ._utils.deprecation import deprecation_warning
21
- from ._utils.function_utils import callable_has_non_self_params
21
+ from ._utils.function_utils import callable_has_non_self_non_default_params, callable_has_non_self_params
22
22
  from .config import logger
23
23
  from .exception import InvalidError
24
24
 
@@ -181,11 +181,21 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
181
181
 
182
182
  if require_sync and inspect.iscoroutinefunction(self.raw_f):
183
183
  self.registered = True # Hacky, avoid false-positive warning
184
- raise InvalidError(f"The `@modal.{decorator_name}` decorator can't be applied to an async function.")
184
+ raise InvalidError(f"`@modal.{decorator_name}` can't be applied to an async function.")
185
185
 
186
186
  if require_nullary and callable_has_non_self_params(self.raw_f):
187
187
  self.registered = True # Hacky, avoid false-positive warning
188
- raise InvalidError(f"Functions decorated by `@modal.{decorator_name}` can't have parameters.")
188
+ if callable_has_non_self_non_default_params(self.raw_f):
189
+ raise InvalidError(f"Functions obj by `@modal.{decorator_name}` can't have parameters.")
190
+ else:
191
+ # TODO(michael): probably fine to just make this an error at this point
192
+ # but best to do it in a separate PR
193
+ deprecation_warning(
194
+ (2024, 9, 4),
195
+ f"The function obj by `@modal.{decorator_name}` has default parameters, "
196
+ "but shouldn't have any parameters - Modal will drop support for "
197
+ "default parameters in a future release.",
198
+ )
189
199
 
190
200
  def _get_raw_f(self) -> Callable[P, ReturnType]:
191
201
  assert self.raw_f is not None
@@ -649,7 +659,7 @@ def _web_server(
649
659
  def _build(
650
660
  _warn_parentheses_missing=None, *, force: bool = False, timeout: int = 86400
651
661
  ) -> Callable[[Union[_PartialFunction, NullaryMethod]], _PartialFunction]:
652
- """mdmd:hidden
662
+ """
653
663
  Decorator for methods that execute at _build time_ to create a new Image layer.
654
664
 
655
665
  **Deprecated**: This function is deprecated. We recommend using `modal.Volume`
@@ -324,8 +324,8 @@ def _deserialize_asgi(asgi: api_pb2.Asgi) -> Any:
324
324
  elif msg_type == "websocket_send":
325
325
  return {
326
326
  "type": "websocket.send",
327
- **({"bytes": asgi.websocket_send.bytes} if asgi.websocket_send.HasField("bytes") else {}),
328
- **({"text": asgi.websocket_send.text} if asgi.websocket_send.HasField("text") else {}),
327
+ "bytes": asgi.websocket_send.bytes if asgi.websocket_send.HasField("bytes") else None,
328
+ "text": asgi.websocket_send.text if asgi.websocket_send.HasField("text") else None,
329
329
  }
330
330
  elif msg_type == "websocket_disconnect":
331
331
  return {
@@ -23,7 +23,7 @@ from .._serialization import (
23
23
  signature_to_parameter_specs,
24
24
  )
25
25
  from .._traceback import append_modal_tb
26
- from ..config import logger
26
+ from ..config import config, logger
27
27
  from ..exception import (
28
28
  DeserializationError,
29
29
  ExecutionError,
@@ -627,7 +627,8 @@ class FunctionCreationStatus:
627
627
 
628
628
  class IncludeSourceMode(enum.Enum):
629
629
  INCLUDE_NOTHING = False # can only be set in source, can't be set in config
630
- INCLUDE_MAIN_PACKAGE = True # Default behavior
630
+ INCLUDE_MAIN_PACKAGE = True # also represented by AUTOMOUNT=0 in config
631
+ INCLUDE_FIRST_PARTY = "legacy" # mounts all "local" modules in sys.modules - represented by AUTOMOUNT=1 in config
631
632
 
632
633
 
633
634
  def get_include_source_mode(function_or_app_specific) -> IncludeSourceMode:
@@ -649,4 +650,6 @@ def get_include_source_mode(function_or_app_specific) -> IncludeSourceMode:
649
650
  # explicitly set in app/function
650
651
  return IncludeSourceMode(function_or_app_specific)
651
652
 
652
- return IncludeSourceMode.INCLUDE_MAIN_PACKAGE
653
+ # note that the automount config boolean isn't a 1-1 mapping with include_source!
654
+ legacy_automount_mode: bool = config.get("automount")
655
+ return IncludeSourceMode.INCLUDE_FIRST_PARTY if legacy_automount_mode else IncludeSourceMode.INCLUDE_MAIN_PACKAGE
@@ -148,20 +148,15 @@ def create_channel(
148
148
 
149
149
  logger.debug(f"Sending request to {event.method_name}")
150
150
 
151
- async def recv_initial_metadata(initial_metadata: grpclib.events.RecvInitialMetadata) -> None:
151
+ async def recv_trailing_metadata(trailing_metadata: grpclib.events.RecvTrailingMetadata) -> None:
152
152
  # If we receive an auth token from the server, include it in all future requests.
153
153
  # TODO(nathan): This isn't perfect because the metadata isn't propagated when the
154
154
  # process is forked and a new channel is created. This is OK for now since this
155
155
  # token is only used by the experimental input plane
156
- if token := initial_metadata.metadata.get("x-modal-auth-token"):
157
- metadata["x-modal-auth-token"] = str(token)
158
-
159
- async def recv_trailing_metadata(trailing_metadata: grpclib.events.RecvTrailingMetadata) -> None:
160
156
  if token := trailing_metadata.metadata.get("x-modal-auth-token"):
161
157
  metadata["x-modal-auth-token"] = str(token)
162
158
 
163
159
  grpclib.events.listen(channel, grpclib.events.SendRequest, send_request)
164
- grpclib.events.listen(channel, grpclib.events.RecvInitialMetadata, recv_initial_metadata)
165
160
  grpclib.events.listen(channel, grpclib.events.RecvTrailingMetadata, recv_trailing_metadata)
166
161
 
167
162
  return channel
@@ -1,6 +1,7 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import inspect
3
3
  import typing
4
+ import warnings
4
5
  from collections.abc import AsyncGenerator, Coroutine, Sequence
5
6
  from pathlib import PurePosixPath
6
7
  from textwrap import dedent
@@ -29,6 +30,7 @@ from ._partial_function import (
29
30
  )
30
31
  from ._utils.async_utils import synchronize_api
31
32
  from ._utils.deprecation import (
33
+ deprecation_error,
32
34
  deprecation_warning,
33
35
  warn_on_renamed_autoscaler_settings,
34
36
  )
@@ -44,6 +46,7 @@ from .exception import ExecutionError, InvalidError
44
46
  from .functions import Function
45
47
  from .gpu import GPU_T
46
48
  from .image import _Image
49
+ from .mount import _Mount
47
50
  from .network_file_system import _NetworkFileSystem
48
51
  from .partial_function import PartialFunction
49
52
  from .proxy import _Proxy
@@ -76,6 +79,11 @@ class _LocalEntrypoint:
76
79
  def app(self) -> "_App":
77
80
  return self._app
78
81
 
82
+ @property
83
+ def stub(self) -> "_App":
84
+ # Deprecated soon, only for backwards compatibility
85
+ return self._app
86
+
79
87
 
80
88
  LocalEntrypoint = synchronize_api(_LocalEntrypoint)
81
89
 
@@ -155,6 +163,7 @@ class _App:
155
163
  _classes: dict[str, _Cls]
156
164
 
157
165
  _image: Optional[_Image]
166
+ _mounts: Sequence[_Mount]
158
167
  _secrets: Sequence[_Secret]
159
168
  _volumes: dict[Union[str, PurePosixPath], _Volume]
160
169
  _web_endpoints: list[str] # Used by the CLI
@@ -172,6 +181,7 @@ class _App:
172
181
  name: Optional[str] = None,
173
182
  *,
174
183
  image: Optional[_Image] = None, # default image for all functions (default is `modal.Image.debian_slim()`)
184
+ mounts: Sequence[_Mount] = [], # default mounts for all functions
175
185
  secrets: Sequence[_Secret] = [], # default secrets for all functions
176
186
  volumes: dict[Union[str, PurePosixPath], _Volume] = {}, # default volumes for all functions
177
187
  include_source: Optional[bool] = None,
@@ -192,6 +202,7 @@ class _App:
192
202
  self._description = name
193
203
  self._include_source_default = include_source
194
204
 
205
+ check_sequence(mounts, _Mount, "`mounts=` has to be a list or tuple of `modal.Mount` objects")
195
206
  check_sequence(secrets, _Secret, "`secrets=` has to be a list or tuple of `modal.Secret` objects")
196
207
  validate_volumes(volumes)
197
208
 
@@ -201,6 +212,7 @@ class _App:
201
212
  self._functions = {}
202
213
  self._classes = {}
203
214
  self._image = image
215
+ self._mounts = mounts
204
216
  self._secrets = secrets
205
217
  self._volumes = volumes
206
218
  self._local_entrypoints = {}
@@ -316,6 +328,7 @@ class _App:
316
328
  self,
317
329
  *,
318
330
  client: Optional[_Client] = None,
331
+ show_progress: Optional[bool] = None,
319
332
  detach: bool = False,
320
333
  interactive: bool = False,
321
334
  environment_name: Optional[str] = None,
@@ -361,6 +374,16 @@ class _App:
361
374
  """
362
375
  from .runner import _run_app # Defer import of runner.py, which imports a lot from Rich
363
376
 
377
+ # See Github discussion here: https://github.com/modal-labs/modal-client/pull/2030#issuecomment-2237266186
378
+
379
+ if show_progress is True:
380
+ deprecation_error(
381
+ (2024, 11, 20),
382
+ "`show_progress=True` is no longer supported. Use `with modal.enable_output():` instead.",
383
+ )
384
+ elif show_progress is False:
385
+ deprecation_warning((2024, 11, 20), "`show_progress=False` is deprecated (and has no effect)")
386
+
364
387
  async with _run_app(
365
388
  self, client=client, detach=detach, interactive=interactive, environment_name=environment_name
366
389
  ):
@@ -441,7 +464,9 @@ class _App:
441
464
  if not self._running_app:
442
465
  raise ExecutionError("`_get_watch_mounts` requires a running app.")
443
466
 
444
- all_mounts = []
467
+ all_mounts = [
468
+ *self._mounts,
469
+ ]
445
470
  for function in self.registered_functions.values():
446
471
  all_mounts.extend(function._serve_mounts)
447
472
 
@@ -521,6 +546,14 @@ class _App:
521
546
  """All local CLI entrypoints registered on the app."""
522
547
  return self._local_entrypoints
523
548
 
549
+ @property
550
+ def indexed_objects(self) -> dict[str, _Object]:
551
+ deprecation_warning(
552
+ (2024, 11, 25),
553
+ "`app.indexed_objects` is deprecated! Use `app.registered_functions` or `app.registered_classes` instead.",
554
+ )
555
+ return dict(**self._functions, **self._classes)
556
+
524
557
  @property
525
558
  def registered_web_endpoints(self) -> list[str]:
526
559
  """Names of web endpoint (ie. webhook) functions registered on the app."""
@@ -605,6 +638,7 @@ class _App:
605
638
  GPU_T, list[GPU_T]
606
639
  ] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
607
640
  serialized: bool = False, # Whether to send the function over using cloudpickle.
641
+ mounts: Sequence[_Mount] = (), # Modal Mounts added to the container
608
642
  network_file_systems: dict[
609
643
  Union[str, PurePosixPath], _NetworkFileSystem
610
644
  ] = {}, # Mountpoints for Modal NetworkFileSystems
@@ -767,6 +801,12 @@ class _App:
767
801
  rdma = None
768
802
  i6pn_enabled = i6pn
769
803
 
804
+ if info.function_name.endswith(".app"):
805
+ warnings.warn(
806
+ "Beware: the function name is `app`. Modal will soon rename `Stub` to `App`, "
807
+ "so you might run into issues if you have code like `app = modal.App()` in the same scope"
808
+ )
809
+
770
810
  if is_generator is None:
771
811
  is_generator = inspect.isgeneratorfunction(raw_f) or inspect.isasyncgenfunction(raw_f)
772
812
 
@@ -784,6 +824,7 @@ class _App:
784
824
  schedule=schedule,
785
825
  is_generator=is_generator,
786
826
  gpu=gpu,
827
+ mounts=[*self._mounts, *mounts],
787
828
  network_file_systems=network_file_systems,
788
829
  volumes={**self._volumes, **volumes},
789
830
  cpu=cpu,
@@ -834,6 +875,7 @@ class _App:
834
875
  GPU_T, list[GPU_T]
835
876
  ] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
836
877
  serialized: bool = False, # Whether to send the function over using cloudpickle.
878
+ mounts: Sequence[_Mount] = (),
837
879
  network_file_systems: dict[
838
880
  Union[str, PurePosixPath], _NetworkFileSystem
839
881
  ] = {}, # Mountpoints for Modal NetworkFileSystems
@@ -955,6 +997,7 @@ class _App:
955
997
  image=image or self._get_default_image(),
956
998
  secrets=[*self._secrets, *secrets],
957
999
  gpu=gpu,
1000
+ mounts=[*self._mounts, *mounts],
958
1001
  network_file_systems=network_file_systems,
959
1002
  volumes={**self._volumes, **volumes},
960
1003
  cpu=cpu,
@@ -994,6 +1037,44 @@ class _App:
994
1037
 
995
1038
  return wrapper
996
1039
 
1040
+ async def spawn_sandbox(
1041
+ self,
1042
+ *entrypoint_args: str,
1043
+ image: Optional[_Image] = None, # The image to run as the container for the sandbox.
1044
+ mounts: Sequence[_Mount] = (), # Mounts to attach to the sandbox.
1045
+ secrets: Sequence[_Secret] = (), # Environment variables to inject into the sandbox.
1046
+ network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
1047
+ timeout: Optional[int] = None, # Maximum execution time of the sandbox in seconds.
1048
+ workdir: Optional[str] = None, # Working directory of the sandbox.
1049
+ gpu: GPU_T = None,
1050
+ cloud: Optional[str] = None,
1051
+ region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the sandbox on.
1052
+ # Specify, in fractional CPU cores, how many CPU cores to request.
1053
+ # Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
1054
+ # CPU throttling will prevent a container from exceeding its specified limit.
1055
+ cpu: Optional[Union[float, tuple[float, float]]] = None,
1056
+ # Specify, in MiB, a memory request which is the minimum memory required.
1057
+ # Or, pass (request, limit) to additionally specify a hard limit in MiB.
1058
+ memory: Optional[Union[int, tuple[int, int]]] = None,
1059
+ block_network: bool = False, # Whether to block network access
1060
+ volumes: dict[
1061
+ Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
1062
+ ] = {}, # Mount points for Modal Volumes and CloudBucketMounts
1063
+ pty_info: Optional[api_pb2.PTYInfo] = None,
1064
+ _experimental_scheduler_placement: Optional[
1065
+ SchedulerPlacement
1066
+ ] = None, # Experimental controls over fine-grained scheduling (alpha).
1067
+ ) -> None:
1068
+ """mdmd:hidden"""
1069
+ arglist = ", ".join(repr(s) for s in entrypoint_args)
1070
+ message = (
1071
+ "`App.spawn_sandbox` is deprecated.\n\n"
1072
+ "Sandboxes can be created using the `Sandbox` object:\n\n"
1073
+ f"```\nsb = Sandbox.create({arglist}, app=app)\n```\n\n"
1074
+ "See https://modal.com/docs/guide/sandbox for more info on working with sandboxes."
1075
+ )
1076
+ deprecation_error((2024, 7, 5), message)
1077
+
997
1078
  def include(self, /, other_app: "_App") -> typing_extensions.Self:
998
1079
  """Include another App's objects in this one.
999
1080
 
@@ -1073,3 +1154,23 @@ class _App:
1073
1154
 
1074
1155
 
1075
1156
  App = synchronize_api(_App)
1157
+
1158
+
1159
+ class _Stub(_App):
1160
+ """mdmd:hidden
1161
+ This enables using a "Stub" class instead of "App".
1162
+
1163
+ For most of Modal's history, the app class was called "Stub", so this exists for
1164
+ backwards compatibility, in order to facilitate moving from "Stub" to "App".
1165
+ """
1166
+
1167
+ def __new__(cls, *args, **kwargs):
1168
+ deprecation_warning(
1169
+ (2024, 4, 29),
1170
+ 'The use of "Stub" has been deprecated in favor of "App".'
1171
+ " This is a pure name change with no other implications.",
1172
+ )
1173
+ return _App(*args, **kwargs)
1174
+
1175
+
1176
+ Stub = synchronize_api(_Stub)