modAL 1.3.0__tar.gz → 1.3.1__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 (198) hide show
  1. {modal-1.3.0 → modal-1.3.1}/PKG-INFO +3 -2
  2. {modal-1.3.0 → modal-1.3.1}/modal/__init__.py +2 -0
  3. {modal-1.3.0 → modal-1.3.1}/modal/__main__.py +2 -1
  4. {modal-1.3.0 → modal-1.3.1}/modal/_billing.py +9 -5
  5. {modal-1.3.0 → modal-1.3.1}/modal/_container_entrypoint.py +13 -1
  6. {modal-1.3.0 → modal-1.3.1}/modal/_functions.py +5 -2
  7. {modal-1.3.0 → modal-1.3.1}/modal/_grpc_client.py +3 -3
  8. {modal-1.3.0 → modal-1.3.1}/modal/_load_context.py +26 -2
  9. {modal-1.3.0 → modal-1.3.1}/modal/_object.py +16 -11
  10. {modal-1.3.0 → modal-1.3.1}/modal/_output.py +2 -2
  11. {modal-1.3.0 → modal-1.3.1}/modal/_partial_function.py +1 -22
  12. {modal-1.3.0 → modal-1.3.1}/modal/_resolver.py +34 -31
  13. {modal-1.3.0 → modal-1.3.1}/modal/_runtime/user_code_imports.py +131 -63
  14. modal-1.3.1/modal/_server.py +252 -0
  15. {modal-1.3.0 → modal-1.3.1}/modal/_traceback.py +3 -12
  16. {modal-1.3.0 → modal-1.3.1}/modal/_utils/async_utils.py +51 -22
  17. {modal-1.3.0 → modal-1.3.1}/modal/_utils/blob_utils.py +3 -9
  18. {modal-1.3.0 → modal-1.3.1}/modal/_utils/function_utils.py +3 -4
  19. {modal-1.3.0 → modal-1.3.1}/modal/_utils/grpc_utils.py +7 -9
  20. {modal-1.3.0 → modal-1.3.1}/modal/_utils/task_command_router_client.py +25 -38
  21. {modal-1.3.0 → modal-1.3.1}/modal/app.py +159 -14
  22. {modal-1.3.0 → modal-1.3.1}/modal/app.pyi +121 -3
  23. {modal-1.3.0 → modal-1.3.1}/modal/builder/2025.06.txt +4 -2
  24. {modal-1.3.0 → modal-1.3.1}/modal/cli/_traceback.py +2 -1
  25. {modal-1.3.0 → modal-1.3.1}/modal/cli/container.py +5 -2
  26. {modal-1.3.0 → modal-1.3.1}/modal/cli/dict.py +4 -3
  27. {modal-1.3.0 → modal-1.3.1}/modal/cli/import_refs.py +2 -2
  28. {modal-1.3.0 → modal-1.3.1}/modal/cli/queues.py +4 -3
  29. {modal-1.3.0 → modal-1.3.1}/modal/cli/run.py +65 -13
  30. modal-1.3.1/modal/cli/token.py +108 -0
  31. {modal-1.3.0 → modal-1.3.1}/modal/client.py +2 -0
  32. {modal-1.3.0 → modal-1.3.1}/modal/client.pyi +5 -2
  33. {modal-1.3.0 → modal-1.3.1}/modal/cls.py +0 -3
  34. {modal-1.3.0 → modal-1.3.1}/modal/exception.py +1 -1
  35. {modal-1.3.0 → modal-1.3.1}/modal/experimental/__init__.py +1 -1
  36. {modal-1.3.0 → modal-1.3.1}/modal/experimental/flash.py +3 -7
  37. {modal-1.3.0 → modal-1.3.1}/modal/functions.pyi +7 -6
  38. {modal-1.3.0 → modal-1.3.1}/modal/image.py +37 -12
  39. {modal-1.3.0 → modal-1.3.1}/modal/image.pyi +4 -0
  40. {modal-1.3.0 → modal-1.3.1}/modal/mount.py +10 -4
  41. {modal-1.3.0 → modal-1.3.1}/modal/mount.pyi +3 -3
  42. {modal-1.3.0 → modal-1.3.1}/modal/output.py +2 -2
  43. {modal-1.3.0 → modal-1.3.1}/modal/partial_function.pyi +0 -1
  44. {modal-1.3.0 → modal-1.3.1}/modal/runner.py +29 -12
  45. {modal-1.3.0 → modal-1.3.1}/modal/runner.pyi +5 -1
  46. {modal-1.3.0 → modal-1.3.1}/modal/sandbox.py +12 -2
  47. {modal-1.3.0 → modal-1.3.1}/modal/sandbox.pyi +8 -0
  48. modal-1.3.1/modal/server.py +5 -0
  49. modal-1.3.1/modal/server.pyi +147 -0
  50. {modal-1.3.0 → modal-1.3.1}/modal/volume.pyi +2 -2
  51. {modal-1.3.0 → modal-1.3.1}/modal.egg-info/PKG-INFO +3 -2
  52. {modal-1.3.0 → modal-1.3.1}/modal.egg-info/SOURCES.txt +3 -0
  53. {modal-1.3.0 → modal-1.3.1}/modal.egg-info/requires.txt +6 -1
  54. {modal-1.3.0 → modal-1.3.1}/modal_proto/api.proto +56 -2
  55. {modal-1.3.0 → modal-1.3.1}/modal_proto/api_grpc.py +16 -0
  56. {modal-1.3.0 → modal-1.3.1}/modal_proto/api_pb2.py +717 -656
  57. {modal-1.3.0 → modal-1.3.1}/modal_proto/api_pb2.pyi +178 -6
  58. {modal-1.3.0 → modal-1.3.1}/modal_proto/api_pb2_grpc.py +33 -0
  59. {modal-1.3.0 → modal-1.3.1}/modal_proto/api_pb2_grpc.pyi +10 -0
  60. {modal-1.3.0 → modal-1.3.1}/modal_proto/modal_api_grpc.py +1 -0
  61. {modal-1.3.0 → modal-1.3.1}/modal_version/__init__.py +2 -2
  62. {modal-1.3.0 → modal-1.3.1}/pyproject.toml +2 -1
  63. modal-1.3.0/modal/cli/token.py +0 -60
  64. {modal-1.3.0 → modal-1.3.1}/LICENSE +0 -0
  65. {modal-1.3.0 → modal-1.3.1}/README.md +0 -0
  66. {modal-1.3.0 → modal-1.3.1}/modal/_clustered_functions.py +0 -0
  67. {modal-1.3.0 → modal-1.3.1}/modal/_clustered_functions.pyi +0 -0
  68. {modal-1.3.0 → modal-1.3.1}/modal/_ipython.py +0 -0
  69. {modal-1.3.0 → modal-1.3.1}/modal/_location.py +0 -0
  70. {modal-1.3.0 → modal-1.3.1}/modal/_pty.py +0 -0
  71. {modal-1.3.0 → modal-1.3.1}/modal/_resources.py +0 -0
  72. {modal-1.3.0 → modal-1.3.1}/modal/_runtime/__init__.py +0 -0
  73. {modal-1.3.0 → modal-1.3.1}/modal/_runtime/asgi.py +0 -0
  74. {modal-1.3.0 → modal-1.3.1}/modal/_runtime/container_io_manager.py +0 -0
  75. {modal-1.3.0 → modal-1.3.1}/modal/_runtime/container_io_manager.pyi +0 -0
  76. {modal-1.3.0 → modal-1.3.1}/modal/_runtime/execution_context.py +0 -0
  77. {modal-1.3.0 → modal-1.3.1}/modal/_runtime/execution_context.pyi +0 -0
  78. {modal-1.3.0 → modal-1.3.1}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  79. {modal-1.3.0 → modal-1.3.1}/modal/_runtime/telemetry.py +0 -0
  80. {modal-1.3.0 → modal-1.3.1}/modal/_runtime/user_code_event_loop.py +0 -0
  81. {modal-1.3.0 → modal-1.3.1}/modal/_serialization.py +0 -0
  82. {modal-1.3.0 → modal-1.3.1}/modal/_tunnel.py +0 -0
  83. {modal-1.3.0 → modal-1.3.1}/modal/_tunnel.pyi +0 -0
  84. {modal-1.3.0 → modal-1.3.1}/modal/_type_manager.py +0 -0
  85. {modal-1.3.0 → modal-1.3.1}/modal/_utils/__init__.py +0 -0
  86. {modal-1.3.0 → modal-1.3.1}/modal/_utils/app_utils.py +0 -0
  87. {modal-1.3.0 → modal-1.3.1}/modal/_utils/auth_token_manager.py +0 -0
  88. {modal-1.3.0 → modal-1.3.1}/modal/_utils/bytes_io_segment_payload.py +0 -0
  89. {modal-1.3.0 → modal-1.3.1}/modal/_utils/deprecation.py +0 -0
  90. {modal-1.3.0 → modal-1.3.1}/modal/_utils/docker_utils.py +0 -0
  91. {modal-1.3.0 → modal-1.3.1}/modal/_utils/git_utils.py +0 -0
  92. {modal-1.3.0 → modal-1.3.1}/modal/_utils/grpc_testing.py +0 -0
  93. {modal-1.3.0 → modal-1.3.1}/modal/_utils/hash_utils.py +0 -0
  94. {modal-1.3.0 → modal-1.3.1}/modal/_utils/http_utils.py +0 -0
  95. {modal-1.3.0 → modal-1.3.1}/modal/_utils/jwt_utils.py +0 -0
  96. {modal-1.3.0 → modal-1.3.1}/modal/_utils/logger.py +0 -0
  97. {modal-1.3.0 → modal-1.3.1}/modal/_utils/mount_utils.py +0 -0
  98. {modal-1.3.0 → modal-1.3.1}/modal/_utils/name_utils.py +0 -0
  99. {modal-1.3.0 → modal-1.3.1}/modal/_utils/package_utils.py +0 -0
  100. {modal-1.3.0 → modal-1.3.1}/modal/_utils/pattern_utils.py +0 -0
  101. {modal-1.3.0 → modal-1.3.1}/modal/_utils/rand_pb_testing.py +0 -0
  102. {modal-1.3.0 → modal-1.3.1}/modal/_utils/shell_utils.py +0 -0
  103. {modal-1.3.0 → modal-1.3.1}/modal/_utils/time_utils.py +0 -0
  104. {modal-1.3.0 → modal-1.3.1}/modal/_vendor/__init__.py +0 -0
  105. {modal-1.3.0 → modal-1.3.1}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  106. {modal-1.3.0 → modal-1.3.1}/modal/_vendor/cloudpickle.py +0 -0
  107. {modal-1.3.0 → modal-1.3.1}/modal/_vendor/tblib.py +0 -0
  108. {modal-1.3.0 → modal-1.3.1}/modal/_watcher.py +0 -0
  109. {modal-1.3.0 → modal-1.3.1}/modal/billing.py +0 -0
  110. {modal-1.3.0 → modal-1.3.1}/modal/builder/2023.12.312.txt +0 -0
  111. {modal-1.3.0 → modal-1.3.1}/modal/builder/2023.12.txt +0 -0
  112. {modal-1.3.0 → modal-1.3.1}/modal/builder/2024.04.txt +0 -0
  113. {modal-1.3.0 → modal-1.3.1}/modal/builder/2024.10.txt +0 -0
  114. {modal-1.3.0 → modal-1.3.1}/modal/builder/PREVIEW.txt +0 -0
  115. {modal-1.3.0 → modal-1.3.1}/modal/builder/README.md +0 -0
  116. {modal-1.3.0 → modal-1.3.1}/modal/builder/base-images.json +0 -0
  117. {modal-1.3.0 → modal-1.3.1}/modal/call_graph.py +0 -0
  118. {modal-1.3.0 → modal-1.3.1}/modal/cli/__init__.py +0 -0
  119. {modal-1.3.0 → modal-1.3.1}/modal/cli/_download.py +0 -0
  120. {modal-1.3.0 → modal-1.3.1}/modal/cli/app.py +0 -0
  121. {modal-1.3.0 → modal-1.3.1}/modal/cli/cluster.py +0 -0
  122. {modal-1.3.0 → modal-1.3.1}/modal/cli/config.py +0 -0
  123. {modal-1.3.0 → modal-1.3.1}/modal/cli/entry_point.py +0 -0
  124. {modal-1.3.0 → modal-1.3.1}/modal/cli/environment.py +0 -0
  125. {modal-1.3.0 → modal-1.3.1}/modal/cli/launch.py +0 -0
  126. {modal-1.3.0 → modal-1.3.1}/modal/cli/network_file_system.py +0 -0
  127. {modal-1.3.0 → modal-1.3.1}/modal/cli/profile.py +0 -0
  128. {modal-1.3.0 → modal-1.3.1}/modal/cli/programs/__init__.py +0 -0
  129. {modal-1.3.0 → modal-1.3.1}/modal/cli/programs/run_jupyter.py +0 -0
  130. {modal-1.3.0 → modal-1.3.1}/modal/cli/programs/vscode.py +0 -0
  131. {modal-1.3.0 → modal-1.3.1}/modal/cli/secret.py +0 -0
  132. {modal-1.3.0 → modal-1.3.1}/modal/cli/shell.py +0 -0
  133. {modal-1.3.0 → modal-1.3.1}/modal/cli/utils.py +0 -0
  134. {modal-1.3.0 → modal-1.3.1}/modal/cli/volume.py +0 -0
  135. {modal-1.3.0 → modal-1.3.1}/modal/cloud_bucket_mount.py +0 -0
  136. {modal-1.3.0 → modal-1.3.1}/modal/cloud_bucket_mount.pyi +0 -0
  137. {modal-1.3.0 → modal-1.3.1}/modal/cls.pyi +0 -0
  138. {modal-1.3.0 → modal-1.3.1}/modal/config.py +0 -0
  139. {modal-1.3.0 → modal-1.3.1}/modal/container_process.py +0 -0
  140. {modal-1.3.0 → modal-1.3.1}/modal/container_process.pyi +0 -0
  141. {modal-1.3.0 → modal-1.3.1}/modal/dict.py +0 -0
  142. {modal-1.3.0 → modal-1.3.1}/modal/dict.pyi +0 -0
  143. {modal-1.3.0 → modal-1.3.1}/modal/environments.py +0 -0
  144. {modal-1.3.0 → modal-1.3.1}/modal/environments.pyi +0 -0
  145. {modal-1.3.0 → modal-1.3.1}/modal/experimental/flash.pyi +0 -0
  146. {modal-1.3.0 → modal-1.3.1}/modal/experimental/ipython.py +0 -0
  147. {modal-1.3.0 → modal-1.3.1}/modal/file_io.py +0 -0
  148. {modal-1.3.0 → modal-1.3.1}/modal/file_io.pyi +0 -0
  149. {modal-1.3.0 → modal-1.3.1}/modal/file_pattern_matcher.py +0 -0
  150. {modal-1.3.0 → modal-1.3.1}/modal/functions.py +0 -0
  151. {modal-1.3.0 → modal-1.3.1}/modal/gpu.py +0 -0
  152. {modal-1.3.0 → modal-1.3.1}/modal/io_streams.py +0 -0
  153. {modal-1.3.0 → modal-1.3.1}/modal/io_streams.pyi +0 -0
  154. {modal-1.3.0 → modal-1.3.1}/modal/network_file_system.py +0 -0
  155. {modal-1.3.0 → modal-1.3.1}/modal/network_file_system.pyi +0 -0
  156. {modal-1.3.0 → modal-1.3.1}/modal/object.py +0 -0
  157. {modal-1.3.0 → modal-1.3.1}/modal/object.pyi +0 -0
  158. {modal-1.3.0 → modal-1.3.1}/modal/parallel_map.py +0 -0
  159. {modal-1.3.0 → modal-1.3.1}/modal/parallel_map.pyi +0 -0
  160. {modal-1.3.0 → modal-1.3.1}/modal/partial_function.py +0 -0
  161. {modal-1.3.0 → modal-1.3.1}/modal/proxy.py +0 -0
  162. {modal-1.3.0 → modal-1.3.1}/modal/proxy.pyi +0 -0
  163. {modal-1.3.0 → modal-1.3.1}/modal/py.typed +0 -0
  164. {modal-1.3.0 → modal-1.3.1}/modal/queue.py +0 -0
  165. {modal-1.3.0 → modal-1.3.1}/modal/queue.pyi +0 -0
  166. {modal-1.3.0 → modal-1.3.1}/modal/retries.py +0 -0
  167. {modal-1.3.0 → modal-1.3.1}/modal/running_app.py +0 -0
  168. {modal-1.3.0 → modal-1.3.1}/modal/schedule.py +0 -0
  169. {modal-1.3.0 → modal-1.3.1}/modal/scheduler_placement.py +0 -0
  170. {modal-1.3.0 → modal-1.3.1}/modal/secret.py +0 -0
  171. {modal-1.3.0 → modal-1.3.1}/modal/secret.pyi +0 -0
  172. {modal-1.3.0 → modal-1.3.1}/modal/serving.py +0 -0
  173. {modal-1.3.0 → modal-1.3.1}/modal/serving.pyi +0 -0
  174. {modal-1.3.0 → modal-1.3.1}/modal/snapshot.py +0 -0
  175. {modal-1.3.0 → modal-1.3.1}/modal/snapshot.pyi +0 -0
  176. {modal-1.3.0 → modal-1.3.1}/modal/stream_type.py +0 -0
  177. {modal-1.3.0 → modal-1.3.1}/modal/token_flow.py +0 -0
  178. {modal-1.3.0 → modal-1.3.1}/modal/token_flow.pyi +0 -0
  179. {modal-1.3.0 → modal-1.3.1}/modal/volume.py +0 -0
  180. {modal-1.3.0 → modal-1.3.1}/modal.egg-info/dependency_links.txt +0 -0
  181. {modal-1.3.0 → modal-1.3.1}/modal.egg-info/entry_points.txt +0 -0
  182. {modal-1.3.0 → modal-1.3.1}/modal.egg-info/top_level.txt +0 -0
  183. {modal-1.3.0 → modal-1.3.1}/modal_docs/__init__.py +0 -0
  184. {modal-1.3.0 → modal-1.3.1}/modal_docs/gen_cli_docs.py +0 -0
  185. {modal-1.3.0 → modal-1.3.1}/modal_docs/gen_reference_docs.py +0 -0
  186. {modal-1.3.0 → modal-1.3.1}/modal_docs/mdmd/__init__.py +0 -0
  187. {modal-1.3.0 → modal-1.3.1}/modal_docs/mdmd/mdmd.py +0 -0
  188. {modal-1.3.0 → modal-1.3.1}/modal_docs/mdmd/signatures.py +0 -0
  189. {modal-1.3.0 → modal-1.3.1}/modal_proto/__init__.py +0 -0
  190. {modal-1.3.0 → modal-1.3.1}/modal_proto/py.typed +0 -0
  191. {modal-1.3.0 → modal-1.3.1}/modal_proto/task_command_router.proto +0 -0
  192. {modal-1.3.0 → modal-1.3.1}/modal_proto/task_command_router_grpc.py +0 -0
  193. {modal-1.3.0 → modal-1.3.1}/modal_proto/task_command_router_pb2.py +0 -0
  194. {modal-1.3.0 → modal-1.3.1}/modal_proto/task_command_router_pb2.pyi +0 -0
  195. {modal-1.3.0 → modal-1.3.1}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  196. {modal-1.3.0 → modal-1.3.1}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  197. {modal-1.3.0 → modal-1.3.1}/modal_version/__main__.py +0 -0
  198. {modal-1.3.0 → modal-1.3.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.3.0
3
+ Version: 1.3.1
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -20,7 +20,8 @@ Requires-Dist: aiohttp
20
20
  Requires-Dist: cbor2
21
21
  Requires-Dist: certifi
22
22
  Requires-Dist: click~=8.1
23
- Requires-Dist: grpclib<0.4.10,>=0.4.7
23
+ Requires-Dist: grpclib<0.4.10,>=0.4.7; python_version < "3.14"
24
+ Requires-Dist: grpclib<0.4.10,>=0.4.9; python_version >= "3.14"
24
25
  Requires-Dist: protobuf!=4.24.0,<7.0,>=3.19
25
26
  Requires-Dist: rich>=12.0.0
26
27
  Requires-Dist: synchronicity~=0.11.1
@@ -41,6 +41,7 @@ try:
41
41
  from .schedule import Cron, Period
42
42
  from .scheduler_placement import SchedulerPlacement
43
43
  from .secret import Secret
44
+ from .server import Server
44
45
  from .snapshot import SandboxSnapshot
45
46
  from .volume import Volume
46
47
  except Exception:
@@ -75,6 +76,7 @@ __all__ = [
75
76
  "SandboxSnapshot",
76
77
  "SchedulerPlacement",
77
78
  "Secret",
79
+ "Server",
78
80
  "Tunnel",
79
81
  "Volume",
80
82
  "asgi_app",
@@ -35,6 +35,7 @@ def main():
35
35
  ):
36
36
  raise
37
37
 
38
+ from rich.markup import escape
38
39
  from rich.panel import Panel
39
40
 
40
41
  title = "Error"
@@ -43,7 +44,7 @@ def main():
43
44
  content = f"{content}\n\nNote: {' '.join(notes)}"
44
45
 
45
46
  console = make_console(stderr=True)
46
- panel = Panel(content, title=title, title_align="left", border_style="red")
47
+ panel = Panel(escape(content), title=title, title_align="left", border_style="red")
47
48
  console.print(panel, highlight=False)
48
49
  sys.exit(1)
49
50
 
@@ -52,11 +52,15 @@ async def _workspace_billing_report(
52
52
  if end is None:
53
53
  end = datetime.now(timezone.utc)
54
54
 
55
- for dt in (start, end):
56
- if dt.tzinfo is None:
57
- dt = dt.replace(tzinfo=timezone.utc)
58
- elif dt.tzinfo != timezone.utc:
59
- raise InvalidError("Timezone-aware start/end limits must be in UTC.")
55
+ if start.tzinfo is None:
56
+ start = start.replace(tzinfo=timezone.utc)
57
+ elif start.tzinfo != timezone.utc:
58
+ raise InvalidError("Timezone-aware 'start' parameter must be in UTC.")
59
+
60
+ if end.tzinfo is None:
61
+ end = end.replace(tzinfo=timezone.utc)
62
+ elif end.tzinfo != timezone.utc:
63
+ raise InvalidError("Timezone-aware 'end' parameter must be in UTC.")
60
64
 
61
65
  request = api_pb2.WorkspaceBillingReportRequest(
62
66
  resolution=resolution,
@@ -5,6 +5,7 @@ import os
5
5
  from modal._runtime.user_code_imports import (
6
6
  Service,
7
7
  import_class_service,
8
+ import_server_service,
8
9
  import_single_function_service,
9
10
  )
10
11
 
@@ -300,7 +301,18 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
300
301
  param_kwargs = {}
301
302
 
302
303
  with execution_context._import_context():
303
- if function_def.is_class:
304
+ if function_def.is_server:
305
+ service_base_function_id = container_args.app_layout.function_ids[function_def.function_name]
306
+ service_function_hydration_data = [
307
+ o for o in container_args.app_layout.objects if o.object_id == service_base_function_id
308
+ ][0]
309
+ service = import_server_service(
310
+ function_def,
311
+ service_function_hydration_data,
312
+ client,
313
+ ser_usr_cls,
314
+ )
315
+ elif function_def.is_class:
304
316
  # this is a bit ugly - match the function and class based on function name to get metadata
305
317
  # This metadata is required in order to hydrate the class in case it's not globally
306
318
  # decorated (or serialized)
@@ -689,6 +689,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
689
689
  nonpreemptible: bool = False,
690
690
  is_builder_function: bool = False,
691
691
  is_auto_snapshot: bool = False,
692
+ is_server: bool = False,
692
693
  enable_memory_snapshot: bool = False,
693
694
  block_network: bool = False,
694
695
  restrict_modal_access: bool = False,
@@ -722,7 +723,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
722
723
  f"Function {raw_f} has a schedule, so it needs to support being called with no arguments"
723
724
  )
724
725
  else:
725
- # must be a "class service function"
726
726
  assert info.user_cls
727
727
  assert not webhook_config
728
728
  assert not schedule
@@ -734,7 +734,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
734
734
  ]
735
735
 
736
736
  retry_policy = _parse_retries(
737
- retries, f"Function '{info.get_tag()}'" if info.raw_f else f"Class '{info.get_tag()}'"
737
+ retries,
738
+ f"Function '{info.get_tag()}'" if info.raw_f else f"Class '{info.get_tag()}'",
738
739
  )
739
740
 
740
741
  if retry_policy is not None:
@@ -1054,6 +1055,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1054
1055
  supported_input_formats=supported_input_formats,
1055
1056
  supported_output_formats=supported_output_formats,
1056
1057
  http_config=http_config,
1058
+ is_server=is_server,
1057
1059
  )
1058
1060
 
1059
1061
  if isinstance(gpu, list):
@@ -1093,6 +1095,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1093
1095
  supported_input_formats=supported_input_formats,
1094
1096
  supported_output_formats=supported_output_formats,
1095
1097
  http_config=http_config,
1098
+ is_server=function_definition.is_server,
1096
1099
  )
1097
1100
 
1098
1101
  ranked_functions = []
@@ -6,7 +6,7 @@ from google.protobuf.message import Message
6
6
  from grpclib import GRPCError, Status
7
7
 
8
8
  from . import exception
9
- from ._traceback import suppress_tb_frames
9
+ from ._traceback import suppress_tb_frame
10
10
  from ._utils.grpc_utils import Retry, _retry_transient_errors
11
11
  from .config import config, logger
12
12
 
@@ -50,7 +50,7 @@ class grpc_error_converter:
50
50
  def __exit__(self, exc_type, exc, traceback) -> Literal[False]:
51
51
  # skip all internal frames from grpclib
52
52
  use_full_traceback = config.get("traceback")
53
- with suppress_tb_frames(1):
53
+ with suppress_tb_frame():
54
54
  if isinstance(exc, GRPCError):
55
55
  modal_exc = _STATUS_TO_EXCEPTION[exc.status](exc.message)
56
56
  modal_exc._grpc_message = exc.message
@@ -115,7 +115,7 @@ class UnaryUnaryWrapper(Generic[RequestType, ResponseType]):
115
115
  timeout: Optional[float] = None,
116
116
  metadata: Optional[list[tuple[str, str]]] = None,
117
117
  ) -> ResponseType:
118
- with suppress_tb_frames(1):
118
+ with suppress_tb_frame():
119
119
  if timeout is not None and retry is not None:
120
120
  raise exception.InvalidError("Retry must be None when timeout is set")
121
121
 
@@ -1,9 +1,12 @@
1
1
  # Copyright Modal Labs 2025
2
- from typing import Optional
2
+ from typing import TYPE_CHECKING, Optional
3
3
 
4
4
  from .client import _Client
5
5
  from .config import config
6
6
 
7
+ if TYPE_CHECKING:
8
+ from ._utils.async_utils import TaskContext
9
+
7
10
 
8
11
  class LoadContext:
9
12
  """Encapsulates optional metadata values used during object loading.
@@ -15,6 +18,7 @@ class LoadContext:
15
18
  _client: Optional[_Client] = None
16
19
  _environment_name: Optional[str] = None
17
20
  _app_id: Optional[str] = None
21
+ _task_context: Optional["TaskContext"] = None
18
22
 
19
23
  def __init__(
20
24
  self,
@@ -22,10 +26,12 @@ class LoadContext:
22
26
  client: Optional[_Client] = None,
23
27
  environment_name: Optional[str] = None,
24
28
  app_id: Optional[str] = None,
29
+ task_context: Optional["TaskContext"] = None,
25
30
  ):
26
31
  self._client = client
27
32
  self._environment_name = environment_name
28
33
  self._app_id = app_id
34
+ self._task_context = task_context
29
35
 
30
36
  @property
31
37
  def client(self) -> _Client:
@@ -41,6 +47,11 @@ class LoadContext:
41
47
  def app_id(self) -> Optional[str]:
42
48
  return self._app_id
43
49
 
50
+ @property
51
+ def task_context(self) -> "TaskContext":
52
+ assert self._task_context is not None, "LoadContext has no TaskContext"
53
+ return self._task_context
54
+
44
55
  @classmethod
45
56
  def empty(cls) -> "LoadContext":
46
57
  """Create an empty LoadContext with all fields set to None.
@@ -59,6 +70,7 @@ class LoadContext:
59
70
  client=self._client if self._client is not None else parent._client,
60
71
  environment_name=self._environment_name if self._environment_name is not None else parent._environment_name,
61
72
  app_id=self._app_id if self._app_id is not None else parent._app_id,
73
+ task_context=self._task_context if self._task_context is not None else parent._task_context,
62
74
  ) # TODO (elias): apply_defaults?
63
75
 
64
76
  async def apply_defaults(self) -> "LoadContext":
@@ -71,16 +83,27 @@ class LoadContext:
71
83
  client=self.client if is_valid_client else await _Client.from_env(),
72
84
  environment_name=self._environment_name or config.get("environment") or "",
73
85
  app_id=self._app_id,
86
+ task_context=self._task_context,
74
87
  )
75
88
 
76
89
  def reset(self) -> "LoadContext":
90
+ """In-place replace all values with None, such that any inferred values/upgrades
91
+ will work. This is useful in cases where a load context reference may have leaked
92
+ into objects and used/upgraded but you want to make a fresh re-load, e.g. when doing
93
+ multiple `app.run()` calls in the same interpreter session.
94
+ """
77
95
  self._client = None
78
96
  self._environment_name = None
79
97
  self._app_id = None
98
+ self._task_context = None
80
99
  return self
81
100
 
82
101
  async def in_place_upgrade(
83
- self, client: Optional[_Client] = None, environment_name: Optional[str] = None, app_id: Optional[str] = None
102
+ self,
103
+ client: Optional[_Client] = None,
104
+ environment_name: Optional[str] = None,
105
+ app_id: Optional[str] = None,
106
+ task_context: Optional["TaskContext"] = None,
84
107
  ) -> "LoadContext":
85
108
  """In-place set values if they aren't already set, or set default values
86
109
 
@@ -103,4 +126,5 @@ class LoadContext:
103
126
  self._client = self._client or client or await _Client.from_env()
104
127
  self._environment_name = self._environment_name or environment_name or config.get("environment") or ""
105
128
  self._app_id = self._app_id or app_id
129
+ self._task_context = self._task_context or task_context
106
130
  return self
@@ -9,11 +9,11 @@ from typing import Callable, ClassVar, Optional
9
9
  from google.protobuf.message import Message
10
10
  from typing_extensions import Self
11
11
 
12
- from modal._traceback import suppress_tb_frames
12
+ from modal._traceback import suppress_tb_frame
13
13
 
14
14
  from ._load_context import LoadContext
15
15
  from ._resolver import Resolver
16
- from ._utils.async_utils import aclosing
16
+ from ._utils.async_utils import TaskContext, aclosing
17
17
  from ._utils.deprecation import deprecation_warning
18
18
  from .client import _Client
19
19
  from .config import config, logger
@@ -127,9 +127,11 @@ class _Object:
127
127
  assert isinstance(object_id, str) and self._type_prefix is not None
128
128
  if not object_id.startswith(self._type_prefix):
129
129
  raise ExecutionError(
130
- f"Can not hydrate {type(self)}:"
130
+ f"Can not hydrate {type(self)}: "
131
131
  f" it has type prefix {self._type_prefix}"
132
- f" but the object_id starts with {object_id[:3]}"
132
+ f" but the object_id starts with {object_id[:3]}. "
133
+ "This usually means the object name was previously used for a different type. "
134
+ "Rename the object/app or stop the previous deployment and redeploy."
133
135
  )
134
136
  self._object_id = object_id
135
137
  self._client = client
@@ -137,7 +139,7 @@ class _Object:
137
139
  self._is_hydrated = True
138
140
 
139
141
  def _hydrate_metadata(self, metadata: Optional[Message]):
140
- # override this is subclasses that need additional data (other than an object_id) for a functioning Handle
142
+ # override this if it's a subclass that needs additional data (other than an object_id) for a functioning Handle
141
143
  pass
142
144
 
143
145
  def _get_metadata(self) -> Optional[Message]:
@@ -314,9 +316,10 @@ class _Object:
314
316
  self._is_hydrated = False # un-hydrate and re-resolve
315
317
  # we don't set an explicit Client here, relying on the default
316
318
  # env client to be applied by LoadContext.apply_default
317
- root_load_context = LoadContext.empty()
318
319
  resolver = Resolver()
319
- await resolver.load(typing.cast(_Object, self), root_load_context)
320
+ async with TaskContext() as tc:
321
+ root_load_context = LoadContext(task_context=tc)
322
+ await resolver.load(typing.cast(_Object, self), root_load_context)
320
323
  else:
321
324
  logger.debug(f"reloading non-lazy {self} by replacing client")
322
325
  self._client = client or await _Client.from_env()
@@ -325,11 +328,13 @@ class _Object:
325
328
  elif not self._hydrate_lazily:
326
329
  self._validate_is_hydrated()
327
330
  else:
328
- # Set the client on LoadContext before loading
329
- root_load_context = LoadContext(client=client)
331
+ # Set the client on LoadContext before loading, with a TaskContext for proper
332
+ # exception handling when loading shared dependencies
330
333
  resolver = Resolver()
331
- with suppress_tb_frames(1): # skip this frame by default
332
- await resolver.load(self, root_load_context)
334
+ async with TaskContext() as tc:
335
+ root_load_context = LoadContext(client=client, task_context=tc)
336
+ with suppress_tb_frame(): # skip this frame by default
337
+ await resolver.load(self, root_load_context)
333
338
  return self
334
339
 
335
340
 
@@ -169,9 +169,9 @@ class OutputManager:
169
169
 
170
170
  @classmethod
171
171
  @contextlib.contextmanager
172
- def enable_output(cls, show_progress: bool = True) -> Generator[None]:
172
+ def enable_output(cls, show_progress: bool = True, show_timestamps: bool = False) -> Generator[None]:
173
173
  if show_progress:
174
- cls._instance = OutputManager()
174
+ cls._instance = OutputManager(show_timestamps=show_timestamps)
175
175
  try:
176
176
  yield
177
177
  finally:
@@ -19,7 +19,7 @@ from ._functions import _Function
19
19
  from ._utils.async_utils import synchronizer
20
20
  from ._utils.deprecation import deprecation_warning
21
21
  from ._utils.function_utils import callable_has_non_self_params
22
- from .config import config, logger
22
+ from .config import config
23
23
  from .exception import InvalidError
24
24
 
25
25
  MAX_MAX_BATCH_SIZE = 1000
@@ -125,7 +125,6 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
125
125
  user_cls: Optional[type] = None # class
126
126
  flags: _PartialFunctionFlags
127
127
  params: _PartialFunctionParams
128
- registered: bool
129
128
 
130
129
  def __init__(
131
130
  self,
@@ -141,7 +140,6 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
141
140
  self.user_cls = None
142
141
  self.flags = flags
143
142
  self.params = params
144
- self.registered = False
145
143
  self.validate_flag_composition()
146
144
 
147
145
  def stack(self, flags: _PartialFunctionFlags, params: _PartialFunctionParams) -> typing_extensions.Self:
@@ -156,13 +154,11 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
156
154
  uses_interface_flags = self.flags & _PartialFunctionFlags.interface_flags()
157
155
  uses_lifecycle_flags = self.flags & _PartialFunctionFlags.lifecycle_flags()
158
156
  if uses_interface_flags and uses_lifecycle_flags:
159
- self.registered = True # Hacky, avoid false-positive warning
160
157
  raise InvalidError("Interface decorators cannot be combined with lifecycle decorators.")
161
158
 
162
159
  has_web_interface = self.flags & _PartialFunctionFlags.WEB_INTERFACE
163
160
  has_callable_interface = self.flags & _PartialFunctionFlags.CALLABLE_INTERFACE
164
161
  if has_web_interface and has_callable_interface:
165
- self.registered = True # Hacky, avoid false-positive warning
166
162
  raise InvalidError("Callable decorators cannot be combined with web interface decorators.")
167
163
 
168
164
  def validate_obj_compatibility(
@@ -174,20 +170,17 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
174
170
  uses_lifecycle_flags = self.flags & _PartialFunctionFlags.lifecycle_flags()
175
171
  uses_interface_flags = self.flags & _PartialFunctionFlags.interface_flags()
176
172
  if self.user_cls is not None and (uses_lifecycle_flags or uses_interface_flags):
177
- self.registered = True # Hacky, avoid false-positive warning
178
173
  raise InvalidError(
179
174
  f"Cannot apply `@modal.{decorator_name}` to a class. Hint: consider applying to a method instead."
180
175
  )
181
176
 
182
177
  wrapped_object = self.raw_f or self.user_cls
183
178
  if isinstance(wrapped_object, _Function):
184
- self.registered = True # Hacky, avoid false-positive warning
185
179
  raise InvalidError(
186
180
  f"Cannot stack `@modal.{decorator_name}` on top of `@app.function`."
187
181
  " Hint: swap the order of the decorators."
188
182
  )
189
183
  elif isinstance(wrapped_object, _Cls):
190
- self.registered = True # Hacky, avoid false-positive warning
191
184
  raise InvalidError(
192
185
  f"Cannot stack `@modal.{decorator_name}` on top of `@app.cls()`."
193
186
  " Hint: swap the order of the decorators."
@@ -196,15 +189,12 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
196
189
  # Run some assertions about a callable wrappee defined by the specific decorator used
197
190
  if self.raw_f is not None:
198
191
  if not callable(self.raw_f):
199
- self.registered = True # Hacky, avoid false-positive warning
200
192
  raise InvalidError(f"The object wrapped by `@modal.{decorator_name}` must be callable.")
201
193
 
202
194
  if require_sync and inspect.iscoroutinefunction(self.raw_f):
203
- self.registered = True # Hacky, avoid false-positive warning
204
195
  raise InvalidError(f"The `@modal.{decorator_name}` decorator can't be applied to an async function.")
205
196
 
206
197
  if require_nullary and callable_has_non_self_params(self.raw_f):
207
- self.registered = True # Hacky, avoid false-positive warning
208
198
  raise InvalidError(f"Functions decorated by `@modal.{decorator_name}` can't have parameters.")
209
199
 
210
200
  def _get_raw_f(self) -> Callable[P, ReturnType]:
@@ -244,17 +234,6 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
244
234
  # to let it be bound to a variable and become a Function later on
245
235
  return self # type: ignore # this returns a PartialFunction in a special internal case
246
236
 
247
- def __del__(self):
248
- if self.registered is False:
249
- if self.raw_f is not None:
250
- name, object_type, suggestion = self.raw_f.__name__, "function", "@app.function or @app.cls"
251
- elif self.user_cls is not None:
252
- name, object_type, suggestion = self.user_cls.__name__, "class", "@app.cls"
253
- logger.warning(
254
- f"The `{name}` {object_type} was never registered with the App."
255
- f" Did you forget an {suggestion} decorator?"
256
- )
257
-
258
237
 
259
238
  def _find_partial_methods_for_user_cls(user_cls: type[Any], flags: int) -> dict[str, _PartialFunction]:
260
239
  """Grabs all method on a user class, and returns partials. Includes legacy methods."""
@@ -9,11 +9,10 @@ from collections.abc import Hashable
9
9
  from typing import TYPE_CHECKING, Optional
10
10
 
11
11
  import modal._object
12
- from modal._traceback import suppress_tb_frames
12
+ from modal._traceback import suppress_tb_frame
13
13
  from modal_proto import api_pb2
14
14
 
15
15
  from ._load_context import LoadContext
16
- from ._utils.async_utils import TaskContext
17
16
 
18
17
  if TYPE_CHECKING:
19
18
  from rich.tree import Tree
@@ -35,7 +34,7 @@ class StatusRow:
35
34
 
36
35
  def warning(self, warning: api_pb2.Warning):
37
36
  if self._step_node is not None:
38
- self._step_node.add(f"[yellow]:warning:[/yellow] {warning.message}")
37
+ self._step_node.add(f"⚠️ {warning.message}")
39
38
 
40
39
  def finish(self, message):
41
40
  if self._step_node is not None and self._spinner is not None:
@@ -120,37 +119,41 @@ class Resolver:
120
119
  if not cached_future:
121
120
  # don't run any awaits within this if-block to prevent race conditions
122
121
  async def loader():
123
- load_context = await obj._load_context_overrides.merged_with(parent_load_context).apply_defaults()
124
-
125
- # TODO(erikbern): do we need existing_object_id for those?
126
- await TaskContext.gather(*[self.load(dep, load_context) for dep in obj.deps()])
127
-
128
- # Load the object itself
129
- if not obj._load:
130
- raise Exception(f"Object {obj} has no loader function")
131
-
132
- await obj._load(obj, self, load_context, existing_object_id)
133
-
134
- # Check that the id of functions didn't change
135
- # Persisted refs are ignored because their life cycle is managed independently.
136
- if (
137
- not obj._is_another_app
138
- and existing_object_id is not None
139
- and existing_object_id.startswith("fu-")
140
- and obj.object_id != existing_object_id
141
- ):
142
- raise Exception(
143
- f"Tried creating an object using existing id {existing_object_id} but it has id {obj.object_id}"
144
- )
145
-
146
- return obj
147
-
148
- cached_future = asyncio.create_task(loader())
122
+ with suppress_tb_frame():
123
+ load_context = await obj._load_context_overrides.merged_with(parent_load_context).apply_defaults()
124
+
125
+ # Use asyncio.gather here (not TaskContext.gather) - the shared TaskContext
126
+ # in load_context handles cancellation at the top level, preventing premature
127
+ # cancellation of shared dependencies when sibling tasks fail.
128
+ await asyncio.gather(*[self.load(dep, load_context) for dep in obj.deps()])
129
+
130
+ # Load the object itself
131
+ if not obj._load:
132
+ raise Exception(f"Object {obj} has no loader function")
133
+
134
+ await obj._load(obj, self, load_context, existing_object_id)
135
+
136
+ # Check that the id of functions didn't change
137
+ # Persisted refs are ignored because their life cycle is managed independently.
138
+ if (
139
+ not obj._is_another_app
140
+ and existing_object_id is not None
141
+ and existing_object_id.startswith("fu-")
142
+ and obj.object_id != existing_object_id
143
+ ):
144
+ raise Exception(
145
+ f"Tried creating an object using existing id {existing_object_id} "
146
+ f"but it has id {obj.object_id}"
147
+ )
148
+
149
+ return obj
150
+
151
+ # use task_context from load_context to make sure tasks are cleaned up eventually
152
+ cached_future = parent_load_context.task_context.create_task(loader())
149
153
  self._local_uuid_to_future[obj.local_uuid] = cached_future
150
154
  if deduplication_key is not None:
151
155
  self._deduplication_cache[deduplication_key] = cached_future
152
- with suppress_tb_frames(2):
153
- # skip current frame + `loader()` closure frame from above
156
+ with suppress_tb_frame():
154
157
  return await cached_future
155
158
 
156
159
  def objects(self) -> list["modal._object._Object"]: