modal 1.3.1.dev28__tar.gz → 1.3.1.dev30__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 (197) hide show
  1. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/PKG-INFO +1 -1
  2. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/__main__.py +2 -1
  3. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_load_context.py +26 -2
  4. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_object.py +10 -7
  5. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_resolver.py +6 -4
  6. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/async_utils.py +51 -22
  7. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/_traceback.py +2 -1
  8. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/dict.py +4 -3
  9. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/queues.py +4 -3
  10. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/client.py +2 -0
  11. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/client.pyi +5 -2
  12. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/image.py +4 -2
  13. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/mount.py +3 -2
  14. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/runner.py +19 -12
  15. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/runner.pyi +5 -1
  16. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/sandbox.py +3 -2
  17. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal.egg-info/PKG-INFO +1 -1
  18. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_version/__init__.py +1 -1
  19. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/LICENSE +0 -0
  20. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/README.md +0 -0
  21. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/__init__.py +0 -0
  22. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_billing.py +0 -0
  23. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_clustered_functions.py +0 -0
  24. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_clustered_functions.pyi +0 -0
  25. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_container_entrypoint.py +0 -0
  26. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_functions.py +0 -0
  27. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_grpc_client.py +0 -0
  28. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_ipython.py +0 -0
  29. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_location.py +0 -0
  30. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_output.py +0 -0
  31. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_partial_function.py +0 -0
  32. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_pty.py +0 -0
  33. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_resources.py +0 -0
  34. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_runtime/__init__.py +0 -0
  35. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_runtime/asgi.py +0 -0
  36. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_runtime/container_io_manager.py +0 -0
  37. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_runtime/container_io_manager.pyi +0 -0
  38. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_runtime/execution_context.py +0 -0
  39. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_runtime/execution_context.pyi +0 -0
  40. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  41. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_runtime/telemetry.py +0 -0
  42. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_runtime/user_code_event_loop.py +0 -0
  43. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_runtime/user_code_imports.py +0 -0
  44. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_serialization.py +0 -0
  45. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_server.py +0 -0
  46. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_traceback.py +0 -0
  47. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_tunnel.py +0 -0
  48. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_tunnel.pyi +0 -0
  49. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_type_manager.py +0 -0
  50. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/__init__.py +0 -0
  51. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/app_utils.py +0 -0
  52. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/auth_token_manager.py +0 -0
  53. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/blob_utils.py +0 -0
  54. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/bytes_io_segment_payload.py +0 -0
  55. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/deprecation.py +0 -0
  56. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/docker_utils.py +0 -0
  57. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/function_utils.py +0 -0
  58. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/git_utils.py +0 -0
  59. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/grpc_testing.py +0 -0
  60. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/grpc_utils.py +0 -0
  61. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/hash_utils.py +0 -0
  62. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/http_utils.py +0 -0
  63. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/jwt_utils.py +0 -0
  64. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/logger.py +0 -0
  65. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/mount_utils.py +0 -0
  66. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/name_utils.py +0 -0
  67. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/package_utils.py +0 -0
  68. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/pattern_utils.py +0 -0
  69. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/rand_pb_testing.py +0 -0
  70. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/shell_utils.py +0 -0
  71. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/task_command_router_client.py +0 -0
  72. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_utils/time_utils.py +0 -0
  73. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_vendor/__init__.py +0 -0
  74. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  75. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_vendor/cloudpickle.py +0 -0
  76. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_vendor/tblib.py +0 -0
  77. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/_watcher.py +0 -0
  78. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/app.py +0 -0
  79. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/app.pyi +0 -0
  80. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/billing.py +0 -0
  81. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/builder/2023.12.312.txt +0 -0
  82. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/builder/2023.12.txt +0 -0
  83. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/builder/2024.04.txt +0 -0
  84. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/builder/2024.10.txt +0 -0
  85. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/builder/2025.06.txt +0 -0
  86. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/builder/PREVIEW.txt +0 -0
  87. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/builder/README.md +0 -0
  88. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/builder/base-images.json +0 -0
  89. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/call_graph.py +0 -0
  90. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/__init__.py +0 -0
  91. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/_download.py +0 -0
  92. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/app.py +0 -0
  93. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/cluster.py +0 -0
  94. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/config.py +0 -0
  95. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/container.py +0 -0
  96. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/entry_point.py +0 -0
  97. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/environment.py +0 -0
  98. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/import_refs.py +0 -0
  99. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/launch.py +0 -0
  100. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/network_file_system.py +0 -0
  101. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/profile.py +0 -0
  102. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/programs/__init__.py +0 -0
  103. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/programs/run_jupyter.py +0 -0
  104. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/programs/vscode.py +0 -0
  105. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/run.py +0 -0
  106. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/secret.py +0 -0
  107. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/shell.py +0 -0
  108. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/token.py +0 -0
  109. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/utils.py +0 -0
  110. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cli/volume.py +0 -0
  111. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cloud_bucket_mount.py +0 -0
  112. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cloud_bucket_mount.pyi +0 -0
  113. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cls.py +0 -0
  114. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/cls.pyi +0 -0
  115. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/config.py +0 -0
  116. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/container_process.py +0 -0
  117. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/container_process.pyi +0 -0
  118. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/dict.py +0 -0
  119. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/dict.pyi +0 -0
  120. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/environments.py +0 -0
  121. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/environments.pyi +0 -0
  122. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/exception.py +0 -0
  123. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/experimental/__init__.py +0 -0
  124. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/experimental/flash.py +0 -0
  125. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/experimental/flash.pyi +0 -0
  126. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/experimental/ipython.py +0 -0
  127. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/file_io.py +0 -0
  128. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/file_io.pyi +0 -0
  129. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/file_pattern_matcher.py +0 -0
  130. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/functions.py +0 -0
  131. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/functions.pyi +0 -0
  132. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/gpu.py +0 -0
  133. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/image.pyi +0 -0
  134. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/io_streams.py +0 -0
  135. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/io_streams.pyi +0 -0
  136. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/mount.pyi +0 -0
  137. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/network_file_system.py +0 -0
  138. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/network_file_system.pyi +0 -0
  139. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/object.py +0 -0
  140. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/object.pyi +0 -0
  141. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/output.py +0 -0
  142. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/parallel_map.py +0 -0
  143. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/parallel_map.pyi +0 -0
  144. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/partial_function.py +0 -0
  145. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/partial_function.pyi +0 -0
  146. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/proxy.py +0 -0
  147. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/proxy.pyi +0 -0
  148. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/py.typed +0 -0
  149. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/queue.py +0 -0
  150. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/queue.pyi +0 -0
  151. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/retries.py +0 -0
  152. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/running_app.py +0 -0
  153. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/sandbox.pyi +0 -0
  154. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/schedule.py +0 -0
  155. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/scheduler_placement.py +0 -0
  156. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/secret.py +0 -0
  157. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/secret.pyi +0 -0
  158. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/server.py +0 -0
  159. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/server.pyi +0 -0
  160. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/serving.py +0 -0
  161. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/serving.pyi +0 -0
  162. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/snapshot.py +0 -0
  163. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/snapshot.pyi +0 -0
  164. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/stream_type.py +0 -0
  165. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/token_flow.py +0 -0
  166. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/token_flow.pyi +0 -0
  167. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/volume.py +0 -0
  168. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal/volume.pyi +0 -0
  169. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal.egg-info/SOURCES.txt +0 -0
  170. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal.egg-info/dependency_links.txt +0 -0
  171. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal.egg-info/entry_points.txt +0 -0
  172. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal.egg-info/requires.txt +0 -0
  173. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal.egg-info/top_level.txt +0 -0
  174. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_docs/__init__.py +0 -0
  175. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_docs/gen_cli_docs.py +0 -0
  176. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_docs/gen_reference_docs.py +0 -0
  177. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_docs/mdmd/__init__.py +0 -0
  178. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_docs/mdmd/mdmd.py +0 -0
  179. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_docs/mdmd/signatures.py +0 -0
  180. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_proto/__init__.py +0 -0
  181. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_proto/api.proto +0 -0
  182. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_proto/api_grpc.py +0 -0
  183. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_proto/api_pb2.py +0 -0
  184. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_proto/api_pb2.pyi +0 -0
  185. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_proto/api_pb2_grpc.py +0 -0
  186. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_proto/api_pb2_grpc.pyi +0 -0
  187. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_proto/modal_api_grpc.py +0 -0
  188. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_proto/py.typed +0 -0
  189. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_proto/task_command_router.proto +0 -0
  190. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_proto/task_command_router_grpc.py +0 -0
  191. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_proto/task_command_router_pb2.py +0 -0
  192. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_proto/task_command_router_pb2.pyi +0 -0
  193. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  194. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  195. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/modal_version/__main__.py +0 -0
  196. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/pyproject.toml +0 -0
  197. {modal-1.3.1.dev28 → modal-1.3.1.dev30}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.3.1.dev28
3
+ Version: 1.3.1.dev30
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -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
 
@@ -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
@@ -13,7 +13,7 @@ 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
@@ -314,9 +314,10 @@ class _Object:
314
314
  self._is_hydrated = False # un-hydrate and re-resolve
315
315
  # we don't set an explicit Client here, relying on the default
316
316
  # env client to be applied by LoadContext.apply_default
317
- root_load_context = LoadContext.empty()
318
317
  resolver = Resolver()
319
- await resolver.load(typing.cast(_Object, self), root_load_context)
318
+ async with TaskContext() as tc:
319
+ root_load_context = LoadContext(task_context=tc)
320
+ await resolver.load(typing.cast(_Object, self), root_load_context)
320
321
  else:
321
322
  logger.debug(f"reloading non-lazy {self} by replacing client")
322
323
  self._client = client or await _Client.from_env()
@@ -325,11 +326,13 @@ class _Object:
325
326
  elif not self._hydrate_lazily:
326
327
  self._validate_is_hydrated()
327
328
  else:
328
- # Set the client on LoadContext before loading
329
- root_load_context = LoadContext(client=client)
329
+ # Set the client on LoadContext before loading, with a TaskContext for proper
330
+ # exception handling when loading shared dependencies
330
331
  resolver = Resolver()
331
- with suppress_tb_frame(): # skip this frame by default
332
- await resolver.load(self, root_load_context)
332
+ async with TaskContext() as tc:
333
+ root_load_context = LoadContext(client=client, task_context=tc)
334
+ with suppress_tb_frame(): # skip this frame by default
335
+ await resolver.load(self, root_load_context)
333
336
  return self
334
337
 
335
338
 
@@ -13,7 +13,6 @@ 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
@@ -123,8 +122,10 @@ class Resolver:
123
122
  with suppress_tb_frame():
124
123
  load_context = await obj._load_context_overrides.merged_with(parent_load_context).apply_defaults()
125
124
 
126
- # TODO(erikbern): do we need existing_object_id for those?
127
- await TaskContext.gather(*[self.load(dep, load_context) for dep in obj.deps()])
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()])
128
129
 
129
130
  # Load the object itself
130
131
  if not obj._load:
@@ -147,7 +148,8 @@ class Resolver:
147
148
 
148
149
  return obj
149
150
 
150
- cached_future = asyncio.create_task(loader())
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())
151
153
  self._local_uuid_to_future[obj.local_uuid] = cached_future
152
154
  if deduplication_key is not None:
153
155
  self._deduplication_cache[deduplication_key] = cached_future
@@ -402,26 +402,32 @@ def retry(direct_fn=None, *, n_attempts=3, base_delay=0, delay_factor=2, timeout
402
402
  class TaskContext:
403
403
  """A structured group that helps manage stray tasks.
404
404
 
405
- This differs from the standard library `asyncio.TaskGroup` in that it cancels all tasks still
405
+ This differs from the standard library `asyncio.TaskGroup` in that it *cancels* tasks still
406
406
  running after exiting the context manager, rather than waiting for them to finish.
407
407
 
408
- A `TaskContext` can have an optional `grace` period in seconds, which will wait for a certain
409
- amount of time before cancelling all remaining tasks. This is useful for allowing tasks to
410
- gracefully exit when they determine that the context is shutting down.
408
+ Arguments:
409
+ `grace: float`: period in seconds, which will wait for a certain amount of time before cancelling
410
+ all remaining tasks. This is useful for allowing tasks to finish after the context exits.
411
+
412
+ `cancellation_grace: float = 1.0`: period in seconds that cancelled tasks are allowed to stall before
413
+ they exit once they get cancelled (e.g. if they do async handling of the CancelledError). If tasks
414
+ take longer than this to exit the tasks are left dangling when the context exits.
411
415
 
412
416
  Usage:
413
417
 
414
418
  ```python notest
415
- async with TaskContext() as task_context:
419
+ async with TaskContext(grace=1.0) as task_context:
416
420
  task = task_context.create_task(coro())
417
421
  ```
418
422
  """
419
423
 
420
424
  _loops: set[asyncio.Task]
421
425
 
422
- def __init__(self, grace: Optional[float] = None):
426
+ def __init__(self, grace: Optional[float] = None, *, cancellation_grace: float = 1.0):
423
427
  self._grace = grace # grace is the time we want for tasks to finish before cancelling them
424
- self._cancellation_grace: float = 1.0 # extra graceperiod for the cancellation itself to "bubble up"
428
+ self._cancellation_grace: float = (
429
+ cancellation_grace # extra graceperiod for the cancellation itself to "bubble up"
430
+ )
425
431
  self._loops = set()
426
432
 
427
433
  async def start(self):
@@ -438,10 +444,19 @@ class TaskContext:
438
444
  return self
439
445
 
440
446
  async def stop(self):
447
+ """This is called when exiting the TaskContext
448
+
449
+ Two important properties that we need to maintain here:
450
+ * Should never raise exceptions as a result
451
+ of exceptions (incl. cancellations) in the contained tasks
452
+ * Should not have an open-ended runtime, even if
453
+ the contained tasks are uncooperative with cancellations.
454
+ """
441
455
  self._exited.set()
442
456
  await asyncio.sleep(0) # Causes any just-created tasks to get started
443
457
  unfinished_tasks = [t for t in self._tasks if not t.done()]
444
458
  gather_future = None
459
+
445
460
  try:
446
461
  if self._grace is not None and unfinished_tasks:
447
462
  gather_future = asyncio.gather(*unfinished_tasks, return_exceptions=True)
@@ -458,17 +473,16 @@ class TaskContext:
458
473
 
459
474
  cancelled_tasks: list[asyncio.Task] = []
460
475
  for task in self._tasks:
461
- if task.done() and not task.cancelled():
462
- # Raise any exceptions if they happened.
463
- # Only tasks without a done_callback will still be present in self._tasks
464
- task.result()
465
-
466
476
  if task.done():
467
- continue
468
-
469
- # Cancel any remaining unfinished tasks.
470
- task.cancel()
471
- cancelled_tasks.append(task)
477
+ # consume potential exceptions so we don't get warnings
478
+ # not that this is not supposed to reraise exceptions
479
+ # since those are expected to be reraised by aexit anyway
480
+ with contextlib.suppress(BaseException):
481
+ task.result()
482
+ else:
483
+ # Cancel any remaining unfinished tasks.
484
+ task.cancel()
485
+ cancelled_tasks.append(task)
472
486
 
473
487
  cancellation_gather = asyncio.gather(*cancelled_tasks, return_exceptions=True)
474
488
  try:
@@ -479,7 +493,22 @@ class TaskContext:
479
493
  await asyncio.sleep(0) # wake up coroutines waiting for cancellations
480
494
 
481
495
  async def __aexit__(self, exc_type, value, tb):
482
- await self.stop()
496
+ """
497
+ This is a bit involved:
498
+ * If there is an exception within the "context", we typically always want to reraise that
499
+ * If a cancellation comes in *during* aexit/stop execution itself, we don't actually cancel
500
+ the exit logic (it's already performing cancellation logic of sorts), but we do reraise
501
+ the CancelledError to prevent muting cancellation chains
502
+ """
503
+ stop_task = asyncio.ensure_future(self.stop())
504
+ try:
505
+ await asyncio.shield(stop_task)
506
+ except asyncio.CancelledError:
507
+ if not stop_task.done():
508
+ # External cancellation - wait for stop() to finish, then propagate
509
+ with contextlib.suppress(asyncio.CancelledError):
510
+ await stop_task # always run stop_task to completion
511
+ raise
483
512
 
484
513
  def create_task(self, coro_or_task) -> asyncio.Task:
485
514
  if isinstance(coro_or_task, asyncio.Task):
@@ -537,9 +566,9 @@ class TaskContext:
537
566
  For example, if you use `asyncio.gather(t1, t2, t3)` and t2 raises an exception, then t1 and
538
567
  t3 would continue running. With `TaskContext.gather(t1, t2, t3)`, they are cancelled.
539
568
 
540
- (It's still acceptable to use `asyncio.gather()` if you don't need cancellation — for
569
+ It's still useful to use `asyncio.gather()` if you don't need cancellation — for
541
570
  example, if you're just gathering quick coroutines with no side-effects. Or if you're
542
- gathering the tasks with `return_exceptions=True`.)
571
+ gathering the tasks with `return_exceptions=True`.
543
572
 
544
573
  Usage:
545
574
 
@@ -557,8 +586,8 @@ class TaskContext:
557
586
  ```
558
587
  """
559
588
  async with TaskContext() as tc:
560
- results = await asyncio.gather(*(tc.create_task(coro) for coro in coros))
561
- return results
589
+ tasks = [tc.create_task(coro) for coro in coros]
590
+ return await asyncio.gather(*tasks)
562
591
 
563
592
 
564
593
  def run_coro_blocking(coro):
@@ -7,6 +7,7 @@ import warnings
7
7
  from typing import Optional
8
8
 
9
9
  from rich.console import RenderResult, group
10
+ from rich.markup import escape
10
11
  from rich.panel import Panel
11
12
  from rich.syntax import Syntax
12
13
  from rich.text import Text
@@ -189,7 +190,7 @@ def highlight_modal_warnings() -> None:
189
190
  if date:
190
191
  title += f" ({date})"
191
192
  panel = Panel(
192
- message,
193
+ escape(message),
193
194
  border_style="yellow",
194
195
  title=title,
195
196
  title_align="left",
@@ -7,7 +7,7 @@ from typer import Argument, Option, Typer
7
7
  from modal._load_context import LoadContext
8
8
  from modal._output import make_console
9
9
  from modal._resolver import Resolver
10
- from modal._utils.async_utils import synchronizer
10
+ from modal._utils.async_utils import TaskContext, synchronizer
11
11
  from modal._utils.time_utils import timestamp_to_localized_str
12
12
  from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
13
13
  from modal.client import _Client
@@ -32,8 +32,9 @@ async def create(name: str, *, env: Optional[str] = ENV_OPTION):
32
32
  client = await _Client.from_env()
33
33
  resolver = Resolver()
34
34
 
35
- load_context = LoadContext(client=client, environment_name=env)
36
- await resolver.load(d, load_context)
35
+ async with TaskContext() as tc:
36
+ load_context = LoadContext(client=client, environment_name=env, task_context=tc)
37
+ await resolver.load(d, load_context)
37
38
 
38
39
 
39
40
  @dict_cli.command(name="list", rich_help_panel="Management")
@@ -8,7 +8,7 @@ from typer import Argument, Option, Typer
8
8
  from modal._load_context import LoadContext
9
9
  from modal._output import make_console
10
10
  from modal._resolver import Resolver
11
- from modal._utils.async_utils import synchronizer
11
+ from modal._utils.async_utils import TaskContext, synchronizer
12
12
  from modal._utils.time_utils import timestamp_to_localized_str
13
13
  from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
14
14
  from modal.client import _Client
@@ -40,8 +40,9 @@ async def create(name: str, *, env: Optional[str] = ENV_OPTION):
40
40
  q = _Queue.from_name(name, environment_name=env, create_if_missing=True)
41
41
  client = await _Client.from_env()
42
42
  resolver = Resolver()
43
- load_context = LoadContext(client=client, environment_name=env)
44
- await resolver.load(q, load_context)
43
+ async with TaskContext() as tc:
44
+ load_context = LoadContext(client=client, environment_name=env, task_context=tc)
45
+ await resolver.load(q, load_context)
45
46
 
46
47
 
47
48
  @queue_cli.command(name="delete", rich_help_panel="Management")
@@ -74,6 +74,7 @@ class _Client:
74
74
  _stub: Optional[modal_api_grpc.ModalClientModal] = None
75
75
  _auth_token_manager: Optional[_AuthTokenManager] = None
76
76
  _snapshotted: bool = False
77
+ _connection_manager: Optional[ConnectionManager] = None
77
78
  client_type: "api_pb2.ClientType.ValueType"
78
79
 
79
80
  def __init__(
@@ -315,6 +316,7 @@ class _Client:
315
316
  # Get a valid grpclib channel, reusing existing channels if possible.
316
317
  # This prevents usage of stale channels across forks of processes.
317
318
  await self._reset_on_pid_change()
319
+ assert self._connection_manager # invariant: ._open() should always have been called before this
318
320
  return await self._connection_manager.get_or_create_channel(server_url)
319
321
 
320
322
  @synchronizer.nowrap
@@ -5,6 +5,7 @@ import google.protobuf.message
5
5
  import grpclib.client
6
6
  import modal._utils.async_utils
7
7
  import modal._utils.auth_token_manager
8
+ import modal._utils.grpc_utils
8
9
  import modal_proto.modal_api_grpc
9
10
  import synchronicity.combined_types
10
11
  import typing
@@ -26,6 +27,7 @@ class _Client:
26
27
  _stub: typing.Optional[modal_proto.modal_api_grpc.ModalClientModal]
27
28
  _auth_token_manager: typing.Optional[modal._utils.auth_token_manager._AuthTokenManager]
28
29
  _snapshotted: bool
30
+ _connection_manager: typing.Optional[modal._utils.grpc_utils.ConnectionManager]
29
31
  client_type: int
30
32
 
31
33
  def __init__(
@@ -33,7 +35,7 @@ class _Client:
33
35
  server_url: str,
34
36
  client_type: int,
35
37
  credentials: typing.Optional[tuple[str, str]],
36
- version: str = "1.3.1.dev28",
38
+ version: str = "1.3.1.dev30",
37
39
  ):
38
40
  """mdmd:hidden
39
41
  The Modal client object is not intended to be instantiated directly by users.
@@ -156,6 +158,7 @@ class Client:
156
158
  _stub: typing.Optional[modal_proto.modal_api_grpc.ModalClientModal]
157
159
  _auth_token_manager: typing.Optional[modal._utils.auth_token_manager._AuthTokenManager]
158
160
  _snapshotted: bool
161
+ _connection_manager: typing.Optional[modal._utils.grpc_utils.ConnectionManager]
159
162
  client_type: int
160
163
 
161
164
  def __init__(
@@ -163,7 +166,7 @@ class Client:
163
166
  server_url: str,
164
167
  client_type: int,
165
168
  credentials: typing.Optional[tuple[str, str]],
166
- version: str = "1.3.1.dev28",
169
+ version: str = "1.3.1.dev30",
167
170
  ):
168
171
  """mdmd:hidden
169
172
  The Modal client object is not intended to be instantiated directly by users.
@@ -33,7 +33,7 @@ from ._load_context import LoadContext
33
33
  from ._object import _Object, live_method_gen
34
34
  from ._resolver import Resolver
35
35
  from ._serialization import get_preferred_payload_format, serialize
36
- from ._utils.async_utils import deprecate_aio_usage, synchronize_api, synchronizer
36
+ from ._utils.async_utils import TaskContext, deprecate_aio_usage, synchronize_api, synchronizer
37
37
  from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
38
38
  from ._utils.docker_utils import (
39
39
  extract_copy_command_patterns,
@@ -959,7 +959,9 @@ class _Image(_Object, type_prefix="im"):
959
959
  raise InvalidError("App has not been initialized yet. Use the content manager `app.run()` or `App.lookup`")
960
960
 
961
961
  resolver = Resolver()
962
- await resolver.load(self, app._root_load_context)
962
+ async with TaskContext() as tc:
963
+ load_context = LoadContext(task_context=tc).merged_with(app._root_load_context)
964
+ await resolver.load(self, load_context)
963
965
  return self
964
966
 
965
967
  def pip_install(
@@ -679,8 +679,9 @@ class _Mount(_Object, type_prefix="mo"):
679
679
  self._namespace = namespace
680
680
  self._allow_overwrite = allow_overwrite
681
681
  resolver = Resolver()
682
- root_metadata = LoadContext(client=client, environment_name=environment_name)
683
- await resolver.load(self, root_metadata)
682
+ async with TaskContext() as tc:
683
+ load_context = LoadContext(client=client, environment_name=environment_name, task_context=tc)
684
+ await resolver.load(self, load_context)
684
685
 
685
686
  def _get_metadata(self) -> api_pb2.MountHandleMetadata:
686
687
  if self._content_checksum_sha256_hex is None:
@@ -128,8 +128,13 @@ async def _create_all_objects(
128
128
  local_app_state: "modal.app._LocalAppState",
129
129
  load_context: LoadContext,
130
130
  ) -> None:
131
- """Create objects that have been defined but not created on the server."""
131
+ """Create objects that have been defined but not created on the server.
132
+
133
+ The load_context must have a task_context set for proper exception handling
134
+ when loading shared dependencies.
135
+ """
132
136
  indexed_objects: dict[str, _Object] = {**local_app_state.functions, **local_app_state.classes}
137
+ tc = load_context.task_context
133
138
 
134
139
  resolver = Resolver()
135
140
  with resolver.display():
@@ -158,8 +163,6 @@ async def _create_all_objects(
158
163
  if obj.is_hydrated:
159
164
  tag_to_object_id[tag] = obj.object_id
160
165
 
161
- await TaskContext.gather(*(_preload(tag, obj) for tag, obj in indexed_objects.items()))
162
-
163
166
  async def _load(tag, obj):
164
167
  existing_object_id = tag_to_object_id.get(tag)
165
168
  # Pass load_context so dependencies can inherit app_id, client, etc.
@@ -171,7 +174,8 @@ async def _create_all_objects(
171
174
  else:
172
175
  raise RuntimeError(f"Unexpected object {obj.object_id}")
173
176
 
174
- await TaskContext.gather(*(_load(tag, obj) for tag, obj in indexed_objects.items()))
177
+ await asyncio.gather(*[tc.create_task(_preload(tag, obj)) for tag, obj in indexed_objects.items()])
178
+ await asyncio.gather(*[tc.create_task(_load(tag, obj)) for tag, obj in indexed_objects.items()])
175
179
 
176
180
 
177
181
  async def _publish_app(
@@ -296,10 +300,12 @@ async def _run_app(
296
300
  app_state=app_state,
297
301
  interactive=interactive,
298
302
  )
299
- await load_context.in_place_upgrade(app_id=running_app.app_id)
300
303
 
301
304
  logs_timeout = config["logs_timeout"]
302
305
  async with app._set_local_app(load_context.client, running_app), TaskContext(grace=logs_timeout) as tc:
306
+ # Inject TaskContext into load_context for proper exception handling when loading shared dependencies
307
+ await load_context.in_place_upgrade(task_context=tc, app_id=running_app.app_id)
308
+
303
309
  # Start heartbeats loop to keep the client alive
304
310
  # we don't log heartbeat exceptions in detached mode
305
311
  # as losing the local connection will not affect the running app
@@ -443,10 +449,12 @@ async def _serve_update(
443
449
  load_context = await app._root_load_context.reset().in_place_upgrade(environment_name=environment_name)
444
450
  try:
445
451
  running_app: RunningApp = await _init_local_app_existing(load_context.client, existing_app_id, environment_name)
446
- await load_context.in_place_upgrade(app_id=running_app.app_id)
447
452
  local_app_state = app._local_state
448
- # Create objects
449
- await _create_all_objects(running_app, local_app_state, load_context)
453
+
454
+ # Create objects with a TaskContext for proper exception handling
455
+ async with TaskContext() as tc:
456
+ await load_context.in_place_upgrade(task_context=tc, app_id=running_app.app_id)
457
+ await _create_all_objects(running_app, local_app_state, load_context)
450
458
 
451
459
  # Publish the updated app
452
460
  await _publish_app(
@@ -526,11 +534,10 @@ async def _deploy_app(
526
534
  root_load_context.client, name, local_app_state.tags, environment_name=root_load_context.environment_name
527
535
  )
528
536
 
529
- await root_load_context.in_place_upgrade(
530
- app_id=running_app.app_id,
531
- )
532
-
533
537
  async with TaskContext(0) as tc:
538
+ # Inject TaskContext into load_context for proper exception handling when loading shared dependencies
539
+ await root_load_context.in_place_upgrade(task_context=tc, app_id=running_app.app_id)
540
+
534
541
  # Start heartbeats loop to keep the client alive
535
542
  def heartbeat():
536
543
  return _heartbeat(client, running_app.app_id)
@@ -30,7 +30,11 @@ async def _create_all_objects(
30
30
  local_app_state: modal.app._LocalAppState,
31
31
  load_context: modal._load_context.LoadContext,
32
32
  ) -> None:
33
- """Create objects that have been defined but not created on the server."""
33
+ """Create objects that have been defined but not created on the server.
34
+
35
+ The load_context must have a task_context set for proper exception handling
36
+ when loading shared dependencies.
37
+ """
34
38
  ...
35
39
 
36
40
  async def _publish_app(
@@ -491,8 +491,9 @@ class _Sandbox(_Object, type_prefix="sb"):
491
491
  client = client or app_client
492
492
 
493
493
  resolver = Resolver()
494
- load_context = LoadContext(client=client, app_id=app_id)
495
- await resolver.load(obj, load_context)
494
+ async with TaskContext() as tc:
495
+ load_context = LoadContext(client=client, app_id=app_id, task_context=tc)
496
+ await resolver.load(obj, load_context)
496
497
  return obj
497
498
 
498
499
  def _hydrate_metadata(self, handle_metadata: Optional[Message]):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.3.1.dev28
3
+ Version: 1.3.1.dev30
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
  """Supplies the current version of the modal client library."""
3
3
 
4
- __version__ = "1.3.1.dev28"
4
+ __version__ = "1.3.1.dev30"
File without changes
File without changes
File without changes