modal 0.66.17__py3-none-any.whl → 0.66.44__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.
Files changed (43) hide show
  1. modal/_container_entrypoint.py +5 -342
  2. modal/_runtime/container_io_manager.py +6 -14
  3. modal/_runtime/user_code_imports.py +361 -0
  4. modal/_utils/function_utils.py +28 -8
  5. modal/_utils/grpc_testing.py +33 -26
  6. modal/app.py +13 -46
  7. modal/cli/import_refs.py +4 -38
  8. modal/client.pyi +2 -2
  9. modal/cls.py +26 -19
  10. modal/cls.pyi +4 -4
  11. modal/dict.py +0 -6
  12. modal/dict.pyi +0 -4
  13. modal/experimental.py +0 -3
  14. modal/functions.py +42 -38
  15. modal/functions.pyi +9 -13
  16. modal/gpu.py +8 -6
  17. modal/image.py +141 -7
  18. modal/image.pyi +34 -4
  19. modal/io_streams.py +40 -33
  20. modal/io_streams.pyi +13 -13
  21. modal/mount.py +5 -2
  22. modal/network_file_system.py +0 -28
  23. modal/network_file_system.pyi +0 -14
  24. modal/partial_function.py +12 -2
  25. modal/queue.py +0 -6
  26. modal/queue.pyi +0 -4
  27. modal/sandbox.py +1 -1
  28. modal/volume.py +0 -22
  29. modal/volume.pyi +0 -9
  30. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/METADATA +1 -2
  31. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/RECORD +43 -42
  32. modal_proto/api.proto +3 -20
  33. modal_proto/api_grpc.py +0 -16
  34. modal_proto/api_pb2.py +389 -413
  35. modal_proto/api_pb2.pyi +12 -58
  36. modal_proto/api_pb2_grpc.py +0 -33
  37. modal_proto/api_pb2_grpc.pyi +0 -10
  38. modal_proto/modal_api_grpc.py +0 -1
  39. modal_version/_version_generated.py +1 -1
  40. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/LICENSE +0 -0
  41. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/WHEEL +0 -0
  42. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/entry_points.txt +0 -0
  43. {modal-0.66.17.dist-info → modal-0.66.44.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,361 @@
1
+ # Copyright Modal Labs 2024
2
+ import importlib
3
+ import typing
4
+ from abc import ABCMeta, abstractmethod
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable, Dict, List, Optional
7
+
8
+ import modal._runtime.container_io_manager
9
+ import modal.cls
10
+ import modal.object
11
+ from modal import Function
12
+ from modal._runtime.asgi import (
13
+ LifespanManager,
14
+ asgi_app_wrapper,
15
+ get_ip_address,
16
+ wait_for_web_server,
17
+ web_server_proxy,
18
+ webhook_asgi_app,
19
+ wsgi_app_wrapper,
20
+ )
21
+ from modal._utils.async_utils import synchronizer
22
+ from modal._utils.function_utils import LocalFunctionError, is_async as get_is_async, is_global_object
23
+ from modal.exception import ExecutionError, InvalidError
24
+ from modal.functions import _Function
25
+ from modal.partial_function import _find_partial_methods_for_user_cls, _PartialFunctionFlags
26
+ from modal_proto import api_pb2
27
+
28
+ if typing.TYPE_CHECKING:
29
+ import modal.app
30
+ import modal.partial_function
31
+
32
+
33
+ @dataclass
34
+ class FinalizedFunction:
35
+ callable: Callable[..., Any]
36
+ is_async: bool
37
+ is_generator: bool
38
+ data_format: int # api_pb2.DataFormat
39
+ lifespan_manager: Optional[LifespanManager] = None
40
+
41
+
42
+ class Service(metaclass=ABCMeta):
43
+ """Common interface for singular functions and class-based "services"
44
+
45
+ There are differences in the importing/finalization logic, and this
46
+ "protocol"/abc basically defines a common interface for the two types
47
+ of "Services" after the point of import.
48
+ """
49
+
50
+ user_cls_instance: Any
51
+ app: Optional["modal.app._App"]
52
+ code_deps: Optional[List["modal.object._Object"]]
53
+
54
+ @abstractmethod
55
+ def get_finalized_functions(
56
+ self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
57
+ ) -> Dict[str, "FinalizedFunction"]:
58
+ ...
59
+
60
+
61
+ def construct_webhook_callable(
62
+ user_defined_callable: Callable,
63
+ webhook_config: api_pb2.WebhookConfig,
64
+ container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
65
+ ):
66
+ # For webhooks, the user function is used to construct an asgi app:
67
+ if webhook_config.type == api_pb2.WEBHOOK_TYPE_ASGI_APP:
68
+ # Function returns an asgi_app, which we can use as a callable.
69
+ return asgi_app_wrapper(user_defined_callable(), container_io_manager)
70
+
71
+ elif webhook_config.type == api_pb2.WEBHOOK_TYPE_WSGI_APP:
72
+ # Function returns an wsgi_app, which we can use as a callable.
73
+ return wsgi_app_wrapper(user_defined_callable(), container_io_manager)
74
+
75
+ elif webhook_config.type == api_pb2.WEBHOOK_TYPE_FUNCTION:
76
+ # Function is a webhook without an ASGI app. Create one for it.
77
+ return asgi_app_wrapper(
78
+ webhook_asgi_app(user_defined_callable, webhook_config.method, webhook_config.web_endpoint_docs),
79
+ container_io_manager,
80
+ )
81
+
82
+ elif webhook_config.type == api_pb2.WEBHOOK_TYPE_WEB_SERVER:
83
+ # Function spawns an HTTP web server listening at a port.
84
+ user_defined_callable()
85
+
86
+ # We intentionally try to connect to the external interface instead of the loopback
87
+ # interface here so users are forced to expose the server. This allows us to potentially
88
+ # change the implementation to use an external bridge in the future.
89
+ host = get_ip_address(b"eth0")
90
+ port = webhook_config.web_server_port
91
+ startup_timeout = webhook_config.web_server_startup_timeout
92
+ wait_for_web_server(host, port, timeout=startup_timeout)
93
+ return asgi_app_wrapper(web_server_proxy(host, port), container_io_manager)
94
+ else:
95
+ raise InvalidError(f"Unrecognized web endpoint type {webhook_config.type}")
96
+
97
+
98
+ @dataclass
99
+ class ImportedFunction(Service):
100
+ user_cls_instance: Any
101
+ app: Optional["modal.app._App"]
102
+ code_deps: Optional[List["modal.object._Object"]]
103
+
104
+ _user_defined_callable: Callable[..., Any]
105
+
106
+ def get_finalized_functions(
107
+ self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
108
+ ) -> Dict[str, "FinalizedFunction"]:
109
+ # Check this property before we turn it into a method (overriden by webhooks)
110
+ is_async = get_is_async(self._user_defined_callable)
111
+ # Use the function definition for whether this is a generator (overriden by webhooks)
112
+ is_generator = fun_def.function_type == api_pb2.Function.FUNCTION_TYPE_GENERATOR
113
+
114
+ webhook_config = fun_def.webhook_config
115
+ if not webhook_config.type:
116
+ # for non-webhooks, the runnable is straight forward:
117
+ return {
118
+ "": FinalizedFunction(
119
+ callable=self._user_defined_callable,
120
+ is_async=is_async,
121
+ is_generator=is_generator,
122
+ data_format=api_pb2.DATA_FORMAT_PICKLE,
123
+ )
124
+ }
125
+
126
+ web_callable, lifespan_manager = construct_webhook_callable(
127
+ self._user_defined_callable, fun_def.webhook_config, container_io_manager
128
+ )
129
+
130
+ return {
131
+ "": FinalizedFunction(
132
+ callable=web_callable,
133
+ lifespan_manager=lifespan_manager,
134
+ is_async=True,
135
+ is_generator=True,
136
+ data_format=api_pb2.DATA_FORMAT_ASGI,
137
+ )
138
+ }
139
+
140
+
141
+ @dataclass
142
+ class ImportedClass(Service):
143
+ user_cls_instance: Any
144
+ app: Optional["modal.app._App"]
145
+ code_deps: Optional[List["modal.object._Object"]]
146
+
147
+ _partial_functions: Dict[str, "modal.partial_function._PartialFunction"]
148
+
149
+ def get_finalized_functions(
150
+ self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
151
+ ) -> Dict[str, "FinalizedFunction"]:
152
+ finalized_functions = {}
153
+ for method_name, partial in self._partial_functions.items():
154
+ partial = synchronizer._translate_in(partial) # ugly
155
+ user_func = partial.raw_f
156
+ # Check this property before we turn it into a method (overriden by webhooks)
157
+ is_async = get_is_async(user_func)
158
+ # Use the function definition for whether this is a generator (overriden by webhooks)
159
+ is_generator = partial.is_generator
160
+ webhook_config = partial.webhook_config
161
+
162
+ bound_func = user_func.__get__(self.user_cls_instance)
163
+
164
+ if not webhook_config or webhook_config.type == api_pb2.WEBHOOK_TYPE_UNSPECIFIED:
165
+ # for non-webhooks, the runnable is straight forward:
166
+ finalized_function = FinalizedFunction(
167
+ callable=bound_func,
168
+ is_async=is_async,
169
+ is_generator=is_generator,
170
+ data_format=api_pb2.DATA_FORMAT_PICKLE,
171
+ )
172
+ else:
173
+ web_callable, lifespan_manager = construct_webhook_callable(
174
+ bound_func, webhook_config, container_io_manager
175
+ )
176
+ finalized_function = FinalizedFunction(
177
+ callable=web_callable,
178
+ lifespan_manager=lifespan_manager,
179
+ is_async=True,
180
+ is_generator=True,
181
+ data_format=api_pb2.DATA_FORMAT_ASGI,
182
+ )
183
+ finalized_functions[method_name] = finalized_function
184
+ return finalized_functions
185
+
186
+
187
+ def get_user_class_instance(
188
+ cls: typing.Union[type, modal.cls.Cls], args: typing.Tuple, kwargs: Dict[str, Any]
189
+ ) -> typing.Any:
190
+ """Returns instance of the underlying class to be used as the `self`
191
+
192
+ The input `cls` can either be the raw Python class the user has declared ("user class"),
193
+ or an @app.cls-decorated version of it which is a modal.Cls-instance wrapping the user class.
194
+ """
195
+ if isinstance(cls, modal.cls.Cls):
196
+ # globally @app.cls-decorated class
197
+ modal_obj: modal.cls.Obj = cls(*args, **kwargs)
198
+ modal_obj.entered = True # ugly but prevents .local() from triggering additional enter-logic
199
+ # TODO: unify lifecycle logic between .local() and container_entrypoint
200
+ user_cls_instance = modal_obj._cached_user_cls_instance()
201
+ else:
202
+ # undecorated class (non-global decoration or serialized)
203
+ user_cls_instance = cls(*args, **kwargs)
204
+
205
+ return user_cls_instance
206
+
207
+
208
+ def import_single_function_service(
209
+ function_def: api_pb2.Function,
210
+ ser_cls, # used only for @build functions
211
+ ser_fun,
212
+ cls_args, # used only for @build functions
213
+ cls_kwargs, # used only for @build functions
214
+ ) -> Service:
215
+ """Imports a function dynamically, and locates the app.
216
+
217
+ This is somewhat complex because we're dealing with 3 quite different type of functions:
218
+ 1. Functions defined in global scope and decorated in global scope (Function objects)
219
+ 2. Functions defined in global scope but decorated elsewhere (these will be raw callables)
220
+ 3. Serialized functions
221
+
222
+ In addition, we also need to handle
223
+ * Normal functions
224
+ * Methods on classes (in which case we need to instantiate the object)
225
+
226
+ This helper also handles web endpoints, ASGI/WSGI servers, and HTTP servers.
227
+
228
+ In order to locate the app, we try two things:
229
+ * If the function is a Function, we can get the app directly from it
230
+ * Otherwise, use the app name and look it up from a global list of apps: this
231
+ typically only happens in case 2 above, or in sometimes for case 3
232
+
233
+ Note that `import_function` is *not* synchronized, because we need it to run on the main
234
+ thread. This is so that any user code running in global scope (which executes as a part of
235
+ the import) runs on the right thread.
236
+ """
237
+ user_defined_callable: Callable
238
+ function: Optional[_Function] = None
239
+ code_deps: Optional[List["modal.object._Object"]] = None
240
+ active_app: Optional[modal.app._App] = None
241
+
242
+ if ser_fun is not None:
243
+ # This is a serialized function we already fetched from the server
244
+ cls, user_defined_callable = ser_cls, ser_fun
245
+ else:
246
+ # Load the module dynamically
247
+ module = importlib.import_module(function_def.module_name)
248
+ qual_name: str = function_def.function_name
249
+
250
+ if not is_global_object(qual_name):
251
+ raise LocalFunctionError("Attempted to load a function defined in a function scope")
252
+
253
+ parts = qual_name.split(".")
254
+ if len(parts) == 1:
255
+ # This is a function
256
+ cls = None
257
+ f = getattr(module, qual_name)
258
+ if isinstance(f, Function):
259
+ function = synchronizer._translate_in(f)
260
+ user_defined_callable = function.get_raw_f()
261
+ active_app = function._app
262
+ else:
263
+ user_defined_callable = f
264
+ elif len(parts) == 2:
265
+ # As of v0.63 - this path should only be triggered by @build class builder methods
266
+ assert not function_def.use_method_name # new "placeholder methods" should not be invoked directly!
267
+ assert function_def.is_builder_function
268
+ cls_name, fun_name = parts
269
+ cls = getattr(module, cls_name)
270
+ if isinstance(cls, modal.cls.Cls):
271
+ # The cls decorator is in global scope
272
+ _cls = synchronizer._translate_in(cls)
273
+ user_defined_callable = _cls._callables[fun_name]
274
+ function = _cls._method_functions.get(fun_name)
275
+ active_app = _cls._app
276
+ else:
277
+ # This is a raw class
278
+ user_defined_callable = getattr(cls, fun_name)
279
+ else:
280
+ raise InvalidError(f"Invalid function qualname {qual_name}")
281
+
282
+ # Instantiate the class if it's defined
283
+ if cls:
284
+ # This code is only used for @build methods on classes
285
+ user_cls_instance = get_user_class_instance(cls, cls_args, cls_kwargs)
286
+ # Bind the function to the instance as self (using the descriptor protocol!)
287
+ user_defined_callable = user_defined_callable.__get__(user_cls_instance)
288
+ else:
289
+ user_cls_instance = None
290
+
291
+ if function:
292
+ code_deps = function.deps(only_explicit_mounts=True)
293
+
294
+ return ImportedFunction(
295
+ user_cls_instance,
296
+ active_app,
297
+ code_deps,
298
+ user_defined_callable,
299
+ )
300
+
301
+
302
+ def import_class_service(
303
+ function_def: api_pb2.Function,
304
+ ser_cls,
305
+ cls_args,
306
+ cls_kwargs,
307
+ ) -> Service:
308
+ """
309
+ This imports a full class to be able to execute any @method or webhook decorated methods.
310
+
311
+ See import_function.
312
+ """
313
+ active_app: Optional["modal.app._App"]
314
+ code_deps: Optional[List["modal.object._Object"]]
315
+ cls: typing.Union[type, modal.cls.Cls]
316
+
317
+ if function_def.definition_type == api_pb2.Function.DEFINITION_TYPE_SERIALIZED:
318
+ assert ser_cls is not None
319
+ cls = ser_cls
320
+ else:
321
+ # Load the module dynamically
322
+ module = importlib.import_module(function_def.module_name)
323
+ qual_name: str = function_def.function_name
324
+
325
+ if not is_global_object(qual_name):
326
+ raise LocalFunctionError("Attempted to load a class defined in a function scope")
327
+
328
+ parts = qual_name.split(".")
329
+ if not (
330
+ len(parts) == 2 and parts[1] == "*"
331
+ ): # the "function name" of a class service "function placeholder" is expected to be "ClassName.*"
332
+ raise ExecutionError(
333
+ f"Internal error: Invalid 'service function' identifier {qual_name}. Please contact Modal support"
334
+ )
335
+
336
+ assert not function_def.use_method_name # new "placeholder methods" should not be invoked directly!
337
+ cls_name = parts[0]
338
+ cls = getattr(module, cls_name)
339
+
340
+ if isinstance(cls, modal.cls.Cls):
341
+ # The cls decorator is in global scope
342
+ _cls = synchronizer._translate_in(cls)
343
+ method_partials = _cls._get_partial_functions()
344
+ service_function: _Function = _cls._class_service_function
345
+ code_deps = service_function.deps(only_explicit_mounts=True)
346
+ active_app = service_function.app
347
+ else:
348
+ # Undecorated user class - find all methods
349
+ method_partials = _find_partial_methods_for_user_cls(cls, _PartialFunctionFlags.all())
350
+ code_deps = None
351
+ active_app = None
352
+
353
+ user_cls_instance = get_user_class_instance(cls, cls_args, cls_kwargs)
354
+
355
+ return ImportedClass(
356
+ user_cls_instance,
357
+ active_app,
358
+ code_deps,
359
+ # TODO (elias/deven): instead of using method_partials here we should use a set of api_pb2.MethodDefinition
360
+ method_partials,
361
+ )
@@ -519,6 +519,16 @@ async def _create_input(
519
519
  )
520
520
 
521
521
 
522
+ def _get_suffix_from_web_url_info(url_info: api_pb2.WebUrlInfo) -> str:
523
+ if url_info.truncated:
524
+ suffix = " [grey70](label truncated)[/grey70]"
525
+ elif url_info.label_stolen:
526
+ suffix = " [grey70](label stolen)[/grey70]"
527
+ else:
528
+ suffix = ""
529
+ return suffix
530
+
531
+
522
532
  class FunctionCreationStatus:
523
533
  # TODO(michael) this really belongs with other output-related code
524
534
  # but moving it here so we can use it when loading a function with output disabled
@@ -547,12 +557,7 @@ class FunctionCreationStatus:
547
557
  elif self.response.function.web_url:
548
558
  url_info = self.response.function.web_url_info
549
559
  # Ensure terms used here match terms used in modal.com/docs/guide/webhook-urls doc.
550
- if url_info.truncated:
551
- suffix = " [grey70](label truncated)[/grey70]"
552
- elif url_info.label_stolen:
553
- suffix = " [grey70](label stolen)[/grey70]"
554
- else:
555
- suffix = ""
560
+ suffix = _get_suffix_from_web_url_info(url_info)
556
561
  # TODO: this is only printed when we're showing progress. Maybe move this somewhere else.
557
562
  web_url = self.response.handle_metadata.web_url
558
563
  self.status_row.finish(
@@ -563,8 +568,23 @@ class FunctionCreationStatus:
563
568
  for custom_domain in self.response.function.custom_domain_info:
564
569
  custom_domain_status_row = self.resolver.add_status_row()
565
570
  custom_domain_status_row.finish(
566
- f"Custom domain for {self.tag} => [magenta underline]"
567
- f"{custom_domain.url}[/magenta underline]{suffix}"
571
+ f"Custom domain for {self.tag} => [magenta underline]" f"{custom_domain.url}[/magenta underline]"
568
572
  )
569
573
  else:
570
574
  self.status_row.finish(f"Created function {self.tag}.")
575
+ if self.response.function.method_definitions_set:
576
+ for method_definition in self.response.function.method_definitions.values():
577
+ if method_definition.web_url:
578
+ url_info = method_definition.web_url_info
579
+ suffix = _get_suffix_from_web_url_info(url_info)
580
+ class_web_endpoint_method_status_row = self.resolver.add_status_row()
581
+ class_web_endpoint_method_status_row.finish(
582
+ f"Created web endpoint for {method_definition.function_name} => [magenta underline]"
583
+ f"{method_definition.web_url}[/magenta underline]{suffix}"
584
+ )
585
+ for custom_domain in method_definition.custom_domain_info:
586
+ custom_domain_status_row = self.resolver.add_status_row()
587
+ custom_domain_status_row.finish(
588
+ f"Custom domain for {method_definition.function_name} => [magenta underline]"
589
+ f"{custom_domain.url}[/magenta underline]"
590
+ )
@@ -52,7 +52,7 @@ def patch_mock_servicer(cls):
52
52
  ctx = InterceptionContext()
53
53
  servicer.interception_context = ctx
54
54
  yield ctx
55
- ctx.assert_responses_consumed()
55
+ ctx._assert_responses_consumed()
56
56
  servicer.interception_context = None
57
57
 
58
58
  cls.intercept = intercept
@@ -64,7 +64,7 @@ def patch_mock_servicer(cls):
64
64
  ctx = servicer_self.interception_context
65
65
  if ctx:
66
66
  intercepted_stream = await InterceptedStream(ctx, method_name, stream).initialize()
67
- custom_responder = ctx.next_custom_responder(method_name, intercepted_stream.request_message)
67
+ custom_responder = ctx._next_custom_responder(method_name, intercepted_stream.request_message)
68
68
  if custom_responder:
69
69
  return await custom_responder(servicer_self, intercepted_stream)
70
70
  else:
@@ -105,19 +105,23 @@ class InterceptionContext:
105
105
  self.custom_responses: Dict[str, List[Tuple[Callable[[Any], bool], List[Any]]]] = defaultdict(list)
106
106
  self.custom_defaults: Dict[str, Callable[["MockClientServicer", grpclib.server.Stream], Awaitable[None]]] = {}
107
107
 
108
- def add_recv(self, method_name: str, msg):
109
- self.calls.append((method_name, msg))
110
-
111
108
  def add_response(
112
109
  self, method_name: str, first_payload, *, request_filter: Callable[[Any], bool] = lambda req: True
113
110
  ):
114
- # adds one response to a queue of responses for requests of the specified type
111
+ """Adds one response payload to an expected queue of responses for a method.
112
+
113
+ These responses will be used once each instead of calling the MockServicer's
114
+ implementation of the method.
115
+
116
+ The interception context will throw an exception on exit if not all of the added
117
+ responses have been consumed.
118
+ """
115
119
  self.custom_responses[method_name].append((request_filter, [first_payload]))
116
120
 
117
121
  def set_responder(
118
122
  self, method_name: str, responder: Callable[["MockClientServicer", grpclib.server.Stream], Awaitable[None]]
119
123
  ):
120
- """Replace the default responder method. E.g.
124
+ """Replace the default responder from the MockClientServicer with a custom implementation
121
125
 
122
126
  ```python notest
123
127
  def custom_responder(servicer, stream):
@@ -128,11 +132,28 @@ class InterceptionContext:
128
132
  ctx.set_responder("SomeMethod", custom_responder)
129
133
  ```
130
134
 
131
- Responses added via `.add_response()` take precedence.
135
+ Responses added via `.add_response()` take precedence over the use of this replacement
132
136
  """
133
137
  self.custom_defaults[method_name] = responder
134
138
 
135
- def next_custom_responder(self, method_name, request):
139
+ def pop_request(self, method_name):
140
+ # fast forward to the next request of type method_name
141
+ # dropping any preceding requests if there is a match
142
+ # returns the payload of the request
143
+ for i, (_method_name, msg) in enumerate(self.calls):
144
+ if _method_name == method_name:
145
+ self.calls = self.calls[i + 1 :]
146
+ return msg
147
+
148
+ raise KeyError(f"No message of that type in call list: {self.calls}")
149
+
150
+ def get_requests(self, method_name: str) -> List[Any]:
151
+ return [msg for _method_name, msg in self.calls if _method_name == method_name]
152
+
153
+ def _add_recv(self, method_name: str, msg):
154
+ self.calls.append((method_name, msg))
155
+
156
+ def _next_custom_responder(self, method_name, request):
136
157
  method_responses = self.custom_responses[method_name]
137
158
  for i, (request_filter, response_messages) in enumerate(method_responses):
138
159
  try:
@@ -159,7 +180,7 @@ class InterceptionContext:
159
180
 
160
181
  return responder
161
182
 
162
- def assert_responses_consumed(self):
183
+ def _assert_responses_consumed(self):
163
184
  unconsumed = []
164
185
  for method_name, queued_responses in self.custom_responses.items():
165
186
  unconsumed += [method_name] * len(queued_responses)
@@ -167,23 +188,9 @@ class InterceptionContext:
167
188
  if unconsumed:
168
189
  raise ResponseNotConsumed(unconsumed)
169
190
 
170
- def pop_request(self, method_name):
171
- # fast forward to the next request of type method_name
172
- # dropping any preceding requests if there is a match
173
- # returns the payload of the request
174
- for i, (_method_name, msg) in enumerate(self.calls):
175
- if _method_name == method_name:
176
- self.calls = self.calls[i + 1 :]
177
- return msg
178
-
179
- raise KeyError(f"No message of that type in call list: {self.calls}")
180
-
181
- def get_requests(self, method_name: str) -> List[Any]:
182
- return [msg for _method_name, msg in self.calls if _method_name == method_name]
183
-
184
191
 
185
192
  class InterceptedStream:
186
- def __init__(self, interception_context, method_name, stream):
193
+ def __init__(self, interception_context: InterceptionContext, method_name: str, stream):
187
194
  self.interception_context = interception_context
188
195
  self.method_name = method_name
189
196
  self.stream = stream
@@ -200,7 +207,7 @@ class InterceptedStream:
200
207
  return ret
201
208
 
202
209
  msg = await self.stream.recv_message()
203
- self.interception_context.add_recv(self.method_name, msg)
210
+ self.interception_context._add_recv(self.method_name, msg)
204
211
  return msg
205
212
 
206
213
  async def send_message(self, msg):
modal/app.py CHANGED
@@ -1,6 +1,5 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import inspect
3
- import os
4
3
  import typing
5
4
  import warnings
6
5
  from pathlib import PurePosixPath
@@ -42,7 +41,6 @@ from .image import _Image
42
41
  from .mount import _Mount
43
42
  from .network_file_system import _NetworkFileSystem
44
43
  from .object import _get_environment_name, _Object
45
- from .output import _get_output_manager, enable_output
46
44
  from .partial_function import (
47
45
  PartialFunction,
48
46
  _find_partial_methods_for_user_cls,
@@ -141,25 +139,9 @@ def f(x, y):
141
139
  ```
142
140
  """
143
141
 
144
- _enable_output_warning = """\
145
- Note that output will soon not be be printed with `app.run`.
146
-
147
- If you want to print output, use `modal.enable_output()`:
148
-
149
- ```python
150
- with modal.enable_output():
151
- with app.run():
152
- ...
153
- ```
154
-
155
- If you don't want output, and you want to to suppress this warning,
156
- use `app.run(..., show_progress=False)`.
157
- """
158
-
159
142
 
160
143
  class _App:
161
- """A Modal app (prior to April 2024 a "stub") is a group of functions and classes
162
- deployed together.
144
+ """A Modal App is a group of functions and classes that are deployed together.
163
145
 
164
146
  The app serves at least three purposes:
165
147
 
@@ -447,32 +429,16 @@ class _App:
447
429
 
448
430
  # See Github discussion here: https://github.com/modal-labs/modal-client/pull/2030#issuecomment-2237266186
449
431
 
450
- auto_enable_output = False
451
-
452
- if "MODAL_DISABLE_APP_RUN_OUTPUT_WARNING" not in os.environ:
453
- if show_progress is None:
454
- if _get_output_manager() is None:
455
- deprecation_warning((2024, 7, 18), _enable_output_warning)
456
- auto_enable_output = True
457
- elif show_progress is True:
458
- if _get_output_manager() is None:
459
- deprecation_warning((2024, 7, 18), _enable_output_warning)
460
- auto_enable_output = True
461
- else:
462
- deprecation_warning((2024, 7, 18), "`show_progress=True` is deprecated and no longer needed.")
463
- elif show_progress is False:
464
- if _get_output_manager() is not None:
465
- deprecation_warning(
466
- (2024, 7, 18), "`show_progress=False` will have no effect since output is enabled."
467
- )
432
+ if show_progress is True:
433
+ deprecation_error(
434
+ (2024, 11, 20),
435
+ "`show_progress=True` is no longer supported. Use `with modal.enable_output():` instead.",
436
+ )
437
+ elif show_progress is False:
438
+ deprecation_warning((2024, 11, 20), "`show_progress=False` is deprecated (and has no effect)")
468
439
 
469
- if auto_enable_output:
470
- with enable_output():
471
- async with _run_app(self, client=client, detach=detach, interactive=interactive):
472
- yield self
473
- else:
474
- async with _run_app(self, client=client, detach=detach, interactive=interactive):
475
- yield self
440
+ async with _run_app(self, client=client, detach=detach, interactive=interactive):
441
+ yield self
476
442
 
477
443
  def _get_default_image(self):
478
444
  if self._image:
@@ -488,7 +454,7 @@ class _App:
488
454
  *self._mounts,
489
455
  ]
490
456
  for function in self.registered_functions.values():
491
- all_mounts.extend(function._used_local_mounts)
457
+ all_mounts.extend(function._serve_mounts)
492
458
 
493
459
  return [m for m in all_mounts if m.is_local()]
494
460
 
@@ -1083,7 +1049,8 @@ App = synchronize_api(_App)
1083
1049
 
1084
1050
 
1085
1051
  class _Stub(_App):
1086
- """This enables using an "Stub" class instead of "App".
1052
+ """mdmd:hidden
1053
+ This enables using a "Stub" class instead of "App".
1087
1054
 
1088
1055
  For most of Modal's history, the app class was called "Stub", so this exists for
1089
1056
  backwards compatibility, in order to facilitate moving from "Stub" to "App".