modal 1.0.3.dev10__py3-none-any.whl → 1.2.3.dev7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (160) hide show
  1. modal/__init__.py +0 -2
  2. modal/__main__.py +3 -4
  3. modal/_billing.py +80 -0
  4. modal/_clustered_functions.py +7 -3
  5. modal/_clustered_functions.pyi +15 -3
  6. modal/_container_entrypoint.py +51 -69
  7. modal/_functions.py +508 -240
  8. modal/_grpc_client.py +171 -0
  9. modal/_load_context.py +105 -0
  10. modal/_object.py +81 -21
  11. modal/_output.py +58 -45
  12. modal/_partial_function.py +48 -73
  13. modal/_pty.py +7 -3
  14. modal/_resolver.py +26 -46
  15. modal/_runtime/asgi.py +4 -3
  16. modal/_runtime/container_io_manager.py +358 -220
  17. modal/_runtime/container_io_manager.pyi +296 -101
  18. modal/_runtime/execution_context.py +18 -2
  19. modal/_runtime/execution_context.pyi +64 -7
  20. modal/_runtime/gpu_memory_snapshot.py +262 -57
  21. modal/_runtime/user_code_imports.py +28 -58
  22. modal/_serialization.py +90 -6
  23. modal/_traceback.py +42 -1
  24. modal/_tunnel.pyi +380 -12
  25. modal/_utils/async_utils.py +84 -29
  26. modal/_utils/auth_token_manager.py +111 -0
  27. modal/_utils/blob_utils.py +181 -58
  28. modal/_utils/deprecation.py +19 -0
  29. modal/_utils/function_utils.py +91 -47
  30. modal/_utils/grpc_utils.py +89 -66
  31. modal/_utils/mount_utils.py +26 -1
  32. modal/_utils/name_utils.py +17 -3
  33. modal/_utils/task_command_router_client.py +536 -0
  34. modal/_utils/time_utils.py +34 -6
  35. modal/app.py +256 -88
  36. modal/app.pyi +909 -92
  37. modal/billing.py +5 -0
  38. modal/builder/2025.06.txt +18 -0
  39. modal/builder/PREVIEW.txt +18 -0
  40. modal/builder/base-images.json +58 -0
  41. modal/cli/_download.py +19 -3
  42. modal/cli/_traceback.py +3 -2
  43. modal/cli/app.py +4 -4
  44. modal/cli/cluster.py +15 -7
  45. modal/cli/config.py +5 -3
  46. modal/cli/container.py +7 -6
  47. modal/cli/dict.py +22 -16
  48. modal/cli/entry_point.py +12 -5
  49. modal/cli/environment.py +5 -4
  50. modal/cli/import_refs.py +3 -3
  51. modal/cli/launch.py +102 -5
  52. modal/cli/network_file_system.py +11 -12
  53. modal/cli/profile.py +3 -2
  54. modal/cli/programs/launch_instance_ssh.py +94 -0
  55. modal/cli/programs/run_jupyter.py +1 -1
  56. modal/cli/programs/run_marimo.py +95 -0
  57. modal/cli/programs/vscode.py +1 -1
  58. modal/cli/queues.py +57 -26
  59. modal/cli/run.py +91 -23
  60. modal/cli/secret.py +48 -22
  61. modal/cli/token.py +7 -8
  62. modal/cli/utils.py +4 -7
  63. modal/cli/volume.py +31 -25
  64. modal/client.py +15 -85
  65. modal/client.pyi +183 -62
  66. modal/cloud_bucket_mount.py +5 -3
  67. modal/cloud_bucket_mount.pyi +197 -5
  68. modal/cls.py +200 -126
  69. modal/cls.pyi +446 -68
  70. modal/config.py +29 -11
  71. modal/container_process.py +319 -19
  72. modal/container_process.pyi +190 -20
  73. modal/dict.py +290 -71
  74. modal/dict.pyi +835 -83
  75. modal/environments.py +15 -27
  76. modal/environments.pyi +46 -24
  77. modal/exception.py +14 -2
  78. modal/experimental/__init__.py +194 -40
  79. modal/experimental/flash.py +618 -0
  80. modal/experimental/flash.pyi +380 -0
  81. modal/experimental/ipython.py +11 -7
  82. modal/file_io.py +29 -36
  83. modal/file_io.pyi +251 -53
  84. modal/file_pattern_matcher.py +56 -16
  85. modal/functions.pyi +673 -92
  86. modal/gpu.py +1 -1
  87. modal/image.py +528 -176
  88. modal/image.pyi +1572 -145
  89. modal/io_streams.py +458 -128
  90. modal/io_streams.pyi +433 -52
  91. modal/mount.py +216 -151
  92. modal/mount.pyi +225 -78
  93. modal/network_file_system.py +45 -62
  94. modal/network_file_system.pyi +277 -56
  95. modal/object.pyi +93 -17
  96. modal/parallel_map.py +942 -129
  97. modal/parallel_map.pyi +294 -15
  98. modal/partial_function.py +0 -2
  99. modal/partial_function.pyi +234 -19
  100. modal/proxy.py +17 -8
  101. modal/proxy.pyi +36 -3
  102. modal/queue.py +270 -65
  103. modal/queue.pyi +817 -57
  104. modal/runner.py +115 -101
  105. modal/runner.pyi +205 -49
  106. modal/sandbox.py +512 -136
  107. modal/sandbox.pyi +845 -111
  108. modal/schedule.py +1 -1
  109. modal/secret.py +300 -70
  110. modal/secret.pyi +589 -34
  111. modal/serving.py +7 -11
  112. modal/serving.pyi +7 -8
  113. modal/snapshot.py +11 -8
  114. modal/snapshot.pyi +25 -4
  115. modal/token_flow.py +4 -4
  116. modal/token_flow.pyi +28 -8
  117. modal/volume.py +416 -158
  118. modal/volume.pyi +1117 -121
  119. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
  120. modal-1.2.3.dev7.dist-info/RECORD +195 -0
  121. modal_docs/mdmd/mdmd.py +17 -4
  122. modal_proto/api.proto +534 -79
  123. modal_proto/api_grpc.py +337 -1
  124. modal_proto/api_pb2.py +1522 -968
  125. modal_proto/api_pb2.pyi +1619 -134
  126. modal_proto/api_pb2_grpc.py +699 -4
  127. modal_proto/api_pb2_grpc.pyi +226 -14
  128. modal_proto/modal_api_grpc.py +175 -154
  129. modal_proto/sandbox_router.proto +145 -0
  130. modal_proto/sandbox_router_grpc.py +105 -0
  131. modal_proto/sandbox_router_pb2.py +149 -0
  132. modal_proto/sandbox_router_pb2.pyi +333 -0
  133. modal_proto/sandbox_router_pb2_grpc.py +203 -0
  134. modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
  135. modal_proto/task_command_router.proto +144 -0
  136. modal_proto/task_command_router_grpc.py +105 -0
  137. modal_proto/task_command_router_pb2.py +149 -0
  138. modal_proto/task_command_router_pb2.pyi +333 -0
  139. modal_proto/task_command_router_pb2_grpc.py +203 -0
  140. modal_proto/task_command_router_pb2_grpc.pyi +75 -0
  141. modal_version/__init__.py +1 -1
  142. modal/requirements/PREVIEW.txt +0 -16
  143. modal/requirements/base-images.json +0 -26
  144. modal-1.0.3.dev10.dist-info/RECORD +0 -179
  145. modal_proto/modal_options_grpc.py +0 -3
  146. modal_proto/options.proto +0 -19
  147. modal_proto/options_grpc.py +0 -3
  148. modal_proto/options_pb2.py +0 -35
  149. modal_proto/options_pb2.pyi +0 -20
  150. modal_proto/options_pb2_grpc.py +0 -4
  151. modal_proto/options_pb2_grpc.pyi +0 -7
  152. /modal/{requirements → builder}/2023.12.312.txt +0 -0
  153. /modal/{requirements → builder}/2023.12.txt +0 -0
  154. /modal/{requirements → builder}/2024.04.txt +0 -0
  155. /modal/{requirements → builder}/2024.10.txt +0 -0
  156. /modal/{requirements → builder}/README.md +0 -0
  157. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
  158. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
  159. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
  160. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/__init__.py CHANGED
@@ -25,7 +25,6 @@ try:
25
25
  from .partial_function import (
26
26
  asgi_app,
27
27
  batched,
28
- build,
29
28
  concurrent,
30
29
  enter,
31
30
  exit,
@@ -80,7 +79,6 @@ __all__ = [
80
79
  "Volume",
81
80
  "asgi_app",
82
81
  "batched",
83
- "build",
84
82
  "concurrent",
85
83
  "current_function_call_id",
86
84
  "current_input_id",
modal/__main__.py CHANGED
@@ -1,6 +1,7 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import sys
3
3
 
4
+ from ._output import make_console
4
5
  from ._traceback import reduce_traceback_to_user_code
5
6
  from .cli._traceback import highlight_modal_warnings, setup_rich_traceback
6
7
  from .cli.entry_point import entrypoint_cli
@@ -35,9 +36,7 @@ def main():
35
36
  raise
36
37
 
37
38
  from grpclib import GRPCError, Status
38
- from rich.console import Console
39
39
  from rich.panel import Panel
40
- from rich.text import Text
41
40
 
42
41
  if isinstance(exc, GRPCError):
43
42
  status_map = {
@@ -68,8 +67,8 @@ def main():
68
67
  if notes := getattr(exc, "__notes__", []):
69
68
  content = f"{content}\n\nNote: {' '.join(notes)}"
70
69
 
71
- console = Console(stderr=True)
72
- panel = Panel(Text(content), title=title, title_align="left", border_style="red")
70
+ console = make_console(stderr=True)
71
+ panel = Panel(content, title=title, title_align="left", border_style="red")
73
72
  console.print(panel, highlight=False)
74
73
  sys.exit(1)
75
74
 
modal/_billing.py ADDED
@@ -0,0 +1,80 @@
1
+ # Copyright Modal Labs 2025
2
+ from datetime import datetime, timezone
3
+ from decimal import Decimal
4
+ from typing import Any, Optional, TypedDict
5
+
6
+ from modal_proto import api_pb2
7
+
8
+ from .client import _Client
9
+ from .exception import InvalidError
10
+
11
+
12
+ class WorkspaceBillingReportItem(TypedDict):
13
+ object_id: str
14
+ description: str
15
+ environment_name: str
16
+ interval_start: datetime
17
+ cost: Decimal
18
+ tags: dict[str, str]
19
+
20
+
21
+ async def _workspace_billing_report(
22
+ *,
23
+ start: datetime, # Start of the report, inclusive
24
+ end: Optional[datetime] = None, # End of the report, exclusive
25
+ resolution: str = "d", # Resolution, e.g. "d" for daily or "h" for hourly
26
+ tag_names: Optional[list[str]] = None, # Optional additional metadata to include
27
+ client: Optional[_Client] = None,
28
+ ) -> list[dict[str, Any]]:
29
+ """Generate a tabular report of workspace usage by object and time.
30
+
31
+ The result will be a list of dictionaries for each interval (determined by `resolution`)
32
+ between the `start` and `end` limits. The dictionary represents a single Modal object
33
+ that billing can be attributed to (e.g., an App) along with metadata (including user-defined
34
+ tags) for identifying that object.
35
+
36
+ The `start` and `end` parameters are required to either have a UTC timezone or to be
37
+ timezone-naive (which will be interpreted as UTC times). The timestamps in the result will
38
+ be in UTC. Cost will be reported for full intervals, even if the provided `start` or `end`
39
+ parameters are partial: `start` will be rounded to the beginning of its interval, while
40
+ partial `end` intervals will be excluded.
41
+
42
+ Additional user-provided metadata can be included in the report if the objects have tags
43
+ and `tag_names` (i.e., keys) are specified in the request. Note that tags will be attributed
44
+ to the entire interval even if they were added or removed at some point within it.
45
+
46
+ """
47
+ if client is None:
48
+ client = await _Client.from_env()
49
+
50
+ tag_names = tag_names or []
51
+
52
+ if end is None:
53
+ end = datetime.now(timezone.utc)
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.")
60
+
61
+ request = api_pb2.WorkspaceBillingReportRequest(
62
+ resolution=resolution,
63
+ tag_names=tag_names,
64
+ )
65
+ request.start_timestamp.FromDatetime(start)
66
+ request.end_timestamp.FromDatetime(end)
67
+
68
+ rows = []
69
+ async for pb_item in client.stub.WorkspaceBillingReport.unary_stream(request):
70
+ item = {
71
+ "object_id": pb_item.object_id,
72
+ "description": pb_item.description,
73
+ "environment_name": pb_item.environment_name,
74
+ "interval_start": pb_item.interval.ToDatetime().replace(tzinfo=timezone.utc),
75
+ "cost": Decimal(pb_item.cost),
76
+ "tags": dict(pb_item.tags),
77
+ }
78
+ rows.append(item)
79
+
80
+ return rows
@@ -5,7 +5,6 @@ from dataclasses import dataclass
5
5
  from typing import Optional
6
6
 
7
7
  from modal._utils.async_utils import synchronize_api
8
- from modal._utils.grpc_utils import retry_transient_errors
9
8
  from modal.client import _Client
10
9
  from modal.exception import InvalidError
11
10
  from modal_proto import api_pb2
@@ -14,7 +13,9 @@ from modal_proto import api_pb2
14
13
  @dataclass
15
14
  class ClusterInfo:
16
15
  rank: int
16
+ cluster_id: str
17
17
  container_ips: list[str]
18
+ container_ipv4_ips: list[str]
18
19
 
19
20
 
20
21
  cluster_info: Optional[ClusterInfo] = None
@@ -59,8 +60,7 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
59
60
  os.environ["NCCL_NSOCKS_PERTHREAD"] = "1"
60
61
 
61
62
  if world_size > 1:
62
- resp: api_pb2.TaskClusterHelloResponse = await retry_transient_errors(
63
- client.stub.TaskClusterHello,
63
+ resp = await client.stub.TaskClusterHello(
64
64
  api_pb2.TaskClusterHelloRequest(
65
65
  task_id=task_id,
66
66
  container_ip=container_ip,
@@ -68,12 +68,16 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
68
68
  )
69
69
  cluster_info = ClusterInfo(
70
70
  rank=resp.cluster_rank,
71
+ cluster_id=resp.cluster_id,
71
72
  container_ips=resp.container_ips,
73
+ container_ipv4_ips=resp.container_ipv4_ips,
72
74
  )
73
75
  else:
74
76
  cluster_info = ClusterInfo(
75
77
  rank=0,
78
+ cluster_id="", # No cluster ID for single-node # TODO(irfansharif): Is this right?
76
79
  container_ips=[container_ip],
80
+ container_ipv4_ips=[], # No IPv4 IPs for single-node
77
81
  )
78
82
 
79
83
 
@@ -3,12 +3,24 @@ import typing
3
3
  import typing_extensions
4
4
 
5
5
  class ClusterInfo:
6
+ """ClusterInfo(rank: int, cluster_id: str, container_ips: list[str], container_ipv4_ips: list[str])"""
7
+
6
8
  rank: int
9
+ cluster_id: str
7
10
  container_ips: list[str]
11
+ container_ipv4_ips: list[str]
12
+
13
+ def __init__(self, rank: int, cluster_id: str, container_ips: list[str], container_ipv4_ips: list[str]) -> None:
14
+ """Initialize self. See help(type(self)) for accurate signature."""
15
+ ...
16
+
17
+ def __repr__(self):
18
+ """Return repr(self)."""
19
+ ...
8
20
 
9
- def __init__(self, rank: int, container_ips: list[str]) -> None: ...
10
- def __repr__(self): ...
11
- def __eq__(self, other): ...
21
+ def __eq__(self, other):
22
+ """Return self==value."""
23
+ ...
12
24
 
13
25
  def get_cluster_info() -> ClusterInfo: ...
14
26
  async def _initialize_clustered_function(client: modal.client._Client, task_id: str, world_size: int): ...
@@ -15,7 +15,6 @@ if telemetry_socket:
15
15
  instrument_imports(telemetry_socket)
16
16
 
17
17
  import asyncio
18
- import concurrent.futures
19
18
  import inspect
20
19
  import queue
21
20
  import signal
@@ -33,14 +32,14 @@ from modal._partial_function import (
33
32
  _PartialFunctionFlags,
34
33
  )
35
34
  from modal._serialization import deserialize, deserialize_params
36
- from modal._utils.async_utils import TaskContext, synchronizer
35
+ from modal._utils.async_utils import TaskContext, aclosing, synchronizer
37
36
  from modal._utils.function_utils import (
38
37
  callable_has_non_self_params,
39
38
  )
40
39
  from modal.app import App, _App
41
40
  from modal.client import Client, _Client
42
41
  from modal.config import logger
43
- from modal.exception import ExecutionError, InputCancellation, InvalidError
42
+ from modal.exception import ExecutionError, InputCancellation
44
43
  from modal.running_app import RunningApp, running_app_from_layout
45
44
  from modal_proto import api_pb2
46
45
 
@@ -49,7 +48,6 @@ from ._runtime.container_io_manager import (
49
48
  ContainerIOManager,
50
49
  IOContext,
51
50
  UserException,
52
- _ContainerIOManager,
53
51
  )
54
52
 
55
53
  if TYPE_CHECKING:
@@ -186,94 +184,75 @@ def call_function(
186
184
  batch_wait_ms: int,
187
185
  ):
188
186
  async def run_input_async(io_context: IOContext) -> None:
187
+ reset_context = execution_context._set_current_context_ids(
188
+ io_context.input_ids, io_context.function_call_ids, io_context.attempt_tokens
189
+ )
189
190
  started_at = time.time()
190
- input_ids, function_call_ids = io_context.input_ids, io_context.function_call_ids
191
- reset_context = execution_context._set_current_context_ids(input_ids, function_call_ids)
192
191
  async with container_io_manager.handle_input_exception.aio(io_context, started_at):
193
- res = io_context.call_finalized_function()
194
192
  # TODO(erikbern): any exception below shouldn't be considered a user exception
195
193
  if io_context.finalized_function.is_generator:
196
- if not inspect.isasyncgen(res):
197
- raise InvalidError(f"Async generator function returned value of type {type(res)}")
198
-
199
194
  # Send up to this many outputs at a time.
195
+ current_function_call_id = execution_context.current_function_call_id()
196
+ assert current_function_call_id is not None # Set above.
197
+ current_attempt_token = execution_context.current_attempt_token()
198
+ assert current_attempt_token is not None # Set above, but can be empty string.
200
199
  generator_queue: asyncio.Queue[Any] = await container_io_manager._queue_create.aio(1024)
201
- generator_output_task = asyncio.create_task(
202
- container_io_manager.generator_output_task.aio(
203
- function_call_ids[0],
204
- io_context.finalized_function.data_format,
205
- generator_queue,
206
- )
207
- )
208
-
209
- item_count = 0
210
- async for value in res:
211
- await container_io_manager._queue_put.aio(generator_queue, value)
212
- item_count += 1
213
-
214
- await container_io_manager._queue_put.aio(generator_queue, _ContainerIOManager._GENERATOR_STOP_SENTINEL)
215
- await generator_output_task # Wait to finish sending generator outputs.
216
- message = api_pb2.GeneratorDone(items_total=item_count)
217
- await container_io_manager.push_outputs.aio(
218
- io_context,
219
- started_at,
220
- message,
221
- api_pb2.DATA_FORMAT_GENERATOR_DONE,
200
+ async with container_io_manager.generator_output_sender(
201
+ current_function_call_id,
202
+ current_attempt_token,
203
+ io_context._generator_output_format(),
204
+ generator_queue,
205
+ ):
206
+ item_count = 0
207
+ async with aclosing(io_context.call_generator_async()) as gen:
208
+ async for value in gen:
209
+ await container_io_manager._queue_put.aio(generator_queue, value)
210
+ item_count += 1
211
+
212
+ await container_io_manager._send_outputs.aio(
213
+ started_at, io_context.output_items_generator_done(started_at, item_count)
222
214
  )
223
215
  else:
224
- if not inspect.iscoroutine(res) or inspect.isgenerator(res) or inspect.isasyncgen(res):
225
- raise InvalidError(
226
- f"Async (non-generator) function returned value of type {type(res)}"
227
- " You might need to use @app.function(..., is_generator=True)."
228
- )
229
- value = await res
216
+ value = await io_context.call_function_async()
230
217
  await container_io_manager.push_outputs.aio(
231
218
  io_context,
232
219
  started_at,
233
220
  value,
234
- io_context.finalized_function.data_format,
235
221
  )
236
222
  reset_context()
237
223
 
238
224
  def run_input_sync(io_context: IOContext) -> None:
239
225
  started_at = time.time()
240
- input_ids, function_call_ids = io_context.input_ids, io_context.function_call_ids
241
- reset_context = execution_context._set_current_context_ids(input_ids, function_call_ids)
226
+ reset_context = execution_context._set_current_context_ids(
227
+ io_context.input_ids, io_context.function_call_ids, io_context.attempt_tokens
228
+ )
242
229
  with container_io_manager.handle_input_exception(io_context, started_at):
243
- res = io_context.call_finalized_function()
244
-
245
230
  # TODO(erikbern): any exception below shouldn't be considered a user exception
246
231
  if io_context.finalized_function.is_generator:
247
- if not inspect.isgenerator(res):
248
- raise InvalidError(f"Generator function returned value of type {type(res)}")
249
-
232
+ gen = io_context.call_generator_sync()
250
233
  # Send up to this many outputs at a time.
234
+ current_function_call_id = execution_context.current_function_call_id()
235
+ assert current_function_call_id is not None # Set above.
236
+ current_attempt_token = execution_context.current_attempt_token()
237
+ assert current_attempt_token is not None # Set above, but can be empty string.
251
238
  generator_queue: asyncio.Queue[Any] = container_io_manager._queue_create(1024)
252
- generator_output_task: concurrent.futures.Future = container_io_manager.generator_output_task( # type: ignore
253
- function_call_ids[0],
254
- io_context.finalized_function.data_format,
239
+ with container_io_manager.generator_output_sender(
240
+ current_function_call_id,
241
+ current_attempt_token,
242
+ io_context._generator_output_format(),
255
243
  generator_queue,
256
- _future=True, # type: ignore # Synchronicity magic to return a future.
244
+ ):
245
+ item_count = 0
246
+ for value in gen:
247
+ container_io_manager._queue_put(generator_queue, value)
248
+ item_count += 1
249
+
250
+ container_io_manager._send_outputs(
251
+ started_at, io_context.output_items_generator_done(started_at, item_count)
257
252
  )
258
-
259
- item_count = 0
260
- for value in res:
261
- container_io_manager._queue_put(generator_queue, value)
262
- item_count += 1
263
-
264
- container_io_manager._queue_put(generator_queue, _ContainerIOManager._GENERATOR_STOP_SENTINEL)
265
- generator_output_task.result() # Wait to finish sending generator outputs.
266
- message = api_pb2.GeneratorDone(items_total=item_count)
267
- container_io_manager.push_outputs(io_context, started_at, message, api_pb2.DATA_FORMAT_GENERATOR_DONE)
268
253
  else:
269
- if inspect.iscoroutine(res) or inspect.isgenerator(res) or inspect.isasyncgen(res):
270
- raise InvalidError(
271
- f"Sync (non-generator) function return value of type {type(res)}."
272
- " You might need to use @app.function(..., is_generator=True)."
273
- )
274
- container_io_manager.push_outputs(
275
- io_context, started_at, res, io_context.finalized_function.data_format
276
- )
254
+ values = io_context.call_function_sync()
255
+ container_io_manager.push_outputs(io_context, started_at, values)
277
256
  reset_context()
278
257
 
279
258
  if container_io_manager.input_concurrency_enabled:
@@ -436,9 +415,9 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
436
415
  param_kwargs,
437
416
  )
438
417
  else:
418
+ assert ser_usr_cls is None
439
419
  service = import_single_function_service(
440
420
  function_def,
441
- ser_usr_cls,
442
421
  ser_fun,
443
422
  )
444
423
 
@@ -471,7 +450,10 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
471
450
  f"Function has {len(service.service_deps)} dependencies"
472
451
  f" but container got {len(dep_object_ids)} object ids.\n"
473
452
  f"Code deps: {service.service_deps}\n"
474
- f"Object ids: {dep_object_ids}"
453
+ f"Object ids: {dep_object_ids}\n"
454
+ "\n"
455
+ "This can happen if you are defining Modal objects under a conditional statement "
456
+ "that evaluates differently in the local and remote environments."
475
457
  )
476
458
  for object_id, obj in zip(dep_object_ids, service.service_deps):
477
459
  metadata: Message = container_app.object_handle_metadata[object_id]