modal 0.66.25__py3-none-any.whl → 0.66.35__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.
@@ -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._get_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
+ )
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,21 +139,6 @@ 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
144
  """A Modal App is a group of functions and classes that are deployed together.
@@ -446,32 +429,16 @@ class _App:
446
429
 
447
430
  # See Github discussion here: https://github.com/modal-labs/modal-client/pull/2030#issuecomment-2237266186
448
431
 
449
- auto_enable_output = False
450
-
451
- if "MODAL_DISABLE_APP_RUN_OUTPUT_WARNING" not in os.environ:
452
- if show_progress is None:
453
- if _get_output_manager() is None:
454
- deprecation_warning((2024, 7, 18), _enable_output_warning)
455
- auto_enable_output = True
456
- elif show_progress is True:
457
- if _get_output_manager() is None:
458
- deprecation_warning((2024, 7, 18), _enable_output_warning)
459
- auto_enable_output = True
460
- else:
461
- deprecation_warning((2024, 7, 18), "`show_progress=True` is deprecated and no longer needed.")
462
- elif show_progress is False:
463
- if _get_output_manager() is not None:
464
- deprecation_warning(
465
- (2024, 7, 18), "`show_progress=False` will have no effect since output is enabled."
466
- )
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)")
467
439
 
468
- if auto_enable_output:
469
- with enable_output():
470
- async with _run_app(self, client=client, detach=detach, interactive=interactive):
471
- yield self
472
- else:
473
- async with _run_app(self, client=client, detach=detach, interactive=interactive):
474
- yield self
440
+ async with _run_app(self, client=client, detach=detach, interactive=interactive):
441
+ yield self
475
442
 
476
443
  def _get_default_image(self):
477
444
  if self._image:
@@ -487,7 +454,7 @@ class _App:
487
454
  *self._mounts,
488
455
  ]
489
456
  for function in self.registered_functions.values():
490
- all_mounts.extend(function._used_local_mounts)
457
+ all_mounts.extend(function._serve_mounts)
491
458
 
492
459
  return [m for m in all_mounts if m.is_local()]
493
460
 
modal/client.pyi CHANGED
@@ -31,7 +31,7 @@ class _Client:
31
31
  server_url: str,
32
32
  client_type: int,
33
33
  credentials: typing.Optional[typing.Tuple[str, str]],
34
- version: str = "0.66.25",
34
+ version: str = "0.66.35",
35
35
  ): ...
36
36
  def is_closed(self) -> bool: ...
37
37
  @property
@@ -90,7 +90,7 @@ class Client:
90
90
  server_url: str,
91
91
  client_type: int,
92
92
  credentials: typing.Optional[typing.Tuple[str, str]],
93
- version: str = "0.66.25",
93
+ version: str = "0.66.35",
94
94
  ): ...
95
95
  def is_closed(self) -> bool: ...
96
96
  @property
modal/experimental.py CHANGED
@@ -17,7 +17,6 @@ from .partial_function import _PartialFunction, _PartialFunctionFlags
17
17
  def stop_fetching_inputs():
18
18
  """Don't fetch any more inputs from the server, after the current one.
19
19
  The container will exit gracefully after the current input is processed."""
20
-
21
20
  _ContainerIOManager.stop_fetching_inputs()
22
21
 
23
22
 
@@ -25,7 +24,6 @@ def get_local_input_concurrency():
25
24
  """Get the container's local input concurrency.
26
25
  If recently reduced to particular value, it can return a larger number than
27
26
  set due to in-progress inputs."""
28
-
29
27
  return _ContainerIOManager.get_input_concurrency()
30
28
 
31
29
 
@@ -33,7 +31,6 @@ def set_local_input_concurrency(concurrency: int):
33
31
  """Set the container's local input concurrency. Dynamic concurrency will be disabled.
34
32
  When setting to a smaller value, this method will not interrupt in-progress inputs.
35
33
  """
36
-
37
34
  _ContainerIOManager.set_input_concurrency(concurrency)
38
35
 
39
36
 
modal/functions.py CHANGED
@@ -304,7 +304,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
304
304
 
305
305
  # TODO: more type annotations
306
306
  _info: Optional[FunctionInfo]
307
- _used_local_mounts: typing.FrozenSet[_Mount] # set at load time, only by loader
307
+ _serve_mounts: typing.FrozenSet[_Mount] # set at load time, only by loader
308
308
  _app: Optional["modal.app._App"] = None
309
309
  _obj: Optional["modal.cls._Obj"] = None # only set for InstanceServiceFunctions and bound instance methods
310
310
  _web_url: Optional[str]
@@ -579,6 +579,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
579
579
 
580
580
  if is_local():
581
581
  entrypoint_mounts = info.get_entrypoint_mount()
582
+
582
583
  all_mounts = [
583
584
  _get_client_mount(),
584
585
  *explicit_mounts,
@@ -611,6 +612,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
611
612
  if proxy:
612
613
  # HACK: remove this once we stop using ssh tunnels for this.
613
614
  if image:
615
+ # TODO(elias): this will cause an error if users use prior `.add_local_*` commands without copy=True
614
616
  image = image.apt_install("autossh")
615
617
 
616
618
  function_spec = _FunctionSpec(
@@ -827,7 +829,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
827
829
  )
828
830
  for path, volume in validated_volumes
829
831
  ]
830
- loaded_mount_ids = {m.object_id for m in all_mounts}
832
+ loaded_mount_ids = {m.object_id for m in all_mounts} | {m.object_id for m in image._mount_layers}
831
833
 
832
834
  # Get object dependencies
833
835
  object_dependencies = []
@@ -969,9 +971,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
969
971
  raise InvalidError(f"Function {info.function_name} is too large to deploy.")
970
972
  raise
971
973
  function_creation_status.set_response(response)
972
- local_mounts = set(m for m in all_mounts if m.is_local()) # needed for modal.serve file watching
973
- local_mounts |= image._used_local_mounts
974
- obj._used_local_mounts = frozenset(local_mounts)
974
+ serve_mounts = set(m for m in all_mounts if m.is_local()) # needed for modal.serve file watching
975
+ serve_mounts |= image._serve_mounts
976
+ obj._serve_mounts = frozenset(serve_mounts)
975
977
  self._hydrate(response.function_id, resolver.client, response.handle_metadata)
976
978
 
977
979
  rep = f"Function({tag})"
@@ -1222,7 +1224,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1222
1224
  self._function_name = None
1223
1225
  self._info = None
1224
1226
  self._use_function_id = ""
1225
- self._used_local_mounts = frozenset()
1227
+ self._serve_mounts = frozenset()
1226
1228
 
1227
1229
  def _hydrate_metadata(self, metadata: Optional[Message]):
1228
1230
  # Overridden concrete implementation of base class method
modal/functions.pyi CHANGED
@@ -110,7 +110,7 @@ OriginalReturnType = typing.TypeVar("OriginalReturnType", covariant=True)
110
110
 
111
111
  class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object._Object):
112
112
  _info: typing.Optional[modal._utils.function_utils.FunctionInfo]
113
- _used_local_mounts: typing.FrozenSet[modal.mount._Mount]
113
+ _serve_mounts: typing.FrozenSet[modal.mount._Mount]
114
114
  _app: typing.Optional[modal.app._App]
115
115
  _obj: typing.Optional[modal.cls._Obj]
116
116
  _web_url: typing.Optional[str]
@@ -286,7 +286,7 @@ P_INNER = typing_extensions.ParamSpec("P_INNER")
286
286
 
287
287
  class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.Object):
288
288
  _info: typing.Optional[modal._utils.function_utils.FunctionInfo]
289
- _used_local_mounts: typing.FrozenSet[modal.mount.Mount]
289
+ _serve_mounts: typing.FrozenSet[modal.mount.Mount]
290
290
  _app: typing.Optional[modal.app.App]
291
291
  _obj: typing.Optional[modal.cls.Obj]
292
292
  _web_url: typing.Optional[str]