modal 0.67.1__py3-none-any.whl → 0.67.33__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 (113) hide show
  1. modal/_clustered_functions.py +2 -2
  2. modal/_clustered_functions.pyi +2 -2
  3. modal/_container_entrypoint.py +8 -5
  4. modal/_output.py +29 -28
  5. modal/_pty.py +2 -2
  6. modal/_resolver.py +6 -5
  7. modal/_resources.py +3 -3
  8. modal/_runtime/asgi.py +46 -6
  9. modal/_runtime/container_io_manager.py +22 -26
  10. modal/_runtime/execution_context.py +2 -2
  11. modal/_runtime/telemetry.py +1 -2
  12. modal/_runtime/user_code_imports.py +12 -14
  13. modal/_serialization.py +3 -7
  14. modal/_traceback.py +5 -5
  15. modal/_tunnel.py +5 -4
  16. modal/_tunnel.pyi +2 -2
  17. modal/_utils/async_utils.py +53 -17
  18. modal/_utils/blob_utils.py +22 -7
  19. modal/_utils/function_utils.py +20 -10
  20. modal/_utils/grpc_testing.py +7 -6
  21. modal/_utils/grpc_utils.py +2 -3
  22. modal/_utils/hash_utils.py +2 -2
  23. modal/_utils/mount_utils.py +5 -4
  24. modal/_utils/package_utils.py +2 -3
  25. modal/_utils/pattern_matcher.py +6 -6
  26. modal/_utils/rand_pb_testing.py +3 -3
  27. modal/_utils/shell_utils.py +2 -1
  28. modal/_vendor/a2wsgi_wsgi.py +62 -72
  29. modal/_vendor/cloudpickle.py +1 -1
  30. modal/_watcher.py +8 -7
  31. modal/app.py +68 -62
  32. modal/app.pyi +104 -99
  33. modal/call_graph.py +6 -6
  34. modal/cli/_download.py +3 -2
  35. modal/cli/_traceback.py +4 -4
  36. modal/cli/app.py +4 -4
  37. modal/cli/container.py +4 -4
  38. modal/cli/dict.py +1 -1
  39. modal/cli/environment.py +2 -3
  40. modal/cli/import_refs.py +1 -1
  41. modal/cli/launch.py +2 -2
  42. modal/cli/network_file_system.py +1 -1
  43. modal/cli/profile.py +1 -1
  44. modal/cli/programs/run_jupyter.py +2 -2
  45. modal/cli/programs/vscode.py +3 -3
  46. modal/cli/queues.py +1 -1
  47. modal/cli/run.py +6 -6
  48. modal/cli/secret.py +3 -3
  49. modal/cli/utils.py +2 -1
  50. modal/cli/volume.py +3 -3
  51. modal/client.py +6 -11
  52. modal/client.pyi +18 -27
  53. modal/cloud_bucket_mount.py +3 -3
  54. modal/cloud_bucket_mount.pyi +2 -2
  55. modal/cls.py +100 -47
  56. modal/cls.pyi +40 -40
  57. modal/config.py +3 -2
  58. modal/container_process.py +6 -2
  59. modal/dict.py +6 -3
  60. modal/dict.pyi +10 -9
  61. modal/environments.py +3 -3
  62. modal/environments.pyi +3 -3
  63. modal/exception.py +2 -3
  64. modal/functions.py +112 -104
  65. modal/functions.pyi +77 -58
  66. modal/image.py +59 -57
  67. modal/image.pyi +104 -103
  68. modal/io_streams.py +20 -12
  69. modal/io_streams.pyi +24 -14
  70. modal/mount.py +24 -24
  71. modal/mount.pyi +28 -29
  72. modal/network_file_system.py +14 -11
  73. modal/network_file_system.pyi +12 -11
  74. modal/object.py +9 -8
  75. modal/object.pyi +47 -34
  76. modal/output.py +2 -1
  77. modal/parallel_map.py +4 -4
  78. modal/partial_function.py +10 -14
  79. modal/partial_function.pyi +17 -18
  80. modal/queue.py +11 -8
  81. modal/queue.pyi +23 -22
  82. modal/retries.py +38 -0
  83. modal/runner.py +8 -7
  84. modal/runner.pyi +8 -14
  85. modal/running_app.py +3 -3
  86. modal/sandbox.py +20 -13
  87. modal/sandbox.pyi +73 -72
  88. modal/scheduler_placement.py +2 -1
  89. modal/secret.py +7 -7
  90. modal/secret.pyi +12 -12
  91. modal/serving.py +4 -3
  92. modal/serving.pyi +5 -4
  93. modal/token_flow.py +3 -2
  94. modal/token_flow.pyi +3 -3
  95. modal/volume.py +16 -23
  96. modal/volume.pyi +17 -16
  97. {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/METADATA +2 -2
  98. modal-0.67.33.dist-info/RECORD +168 -0
  99. modal_docs/mdmd/signatures.py +1 -2
  100. modal_global_objects/mounts/python_standalone.py +1 -1
  101. modal_proto/api.proto +15 -0
  102. modal_proto/api_grpc.py +32 -0
  103. modal_proto/api_pb2.py +674 -654
  104. modal_proto/api_pb2.pyi +45 -1
  105. modal_proto/api_pb2_grpc.py +66 -0
  106. modal_proto/api_pb2_grpc.pyi +20 -0
  107. modal_proto/modal_api_grpc.py +2 -0
  108. modal_version/_version_generated.py +1 -1
  109. modal-0.67.1.dist-info/RECORD +0 -168
  110. {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/LICENSE +0 -0
  111. {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/WHEEL +0 -0
  112. {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/entry_points.txt +0 -0
  113. {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/top_level.txt +0 -0
modal/exception.py CHANGED
@@ -4,7 +4,6 @@ import signal
4
4
  import sys
5
5
  import warnings
6
6
  from datetime import date
7
- from typing import Tuple
8
7
 
9
8
 
10
9
  class Error(Exception):
@@ -132,12 +131,12 @@ def _is_internal_frame(frame):
132
131
  return module in _INTERNAL_MODULES
133
132
 
134
133
 
135
- def deprecation_error(deprecated_on: Tuple[int, int, int], msg: str):
134
+ def deprecation_error(deprecated_on: tuple[int, int, int], msg: str):
136
135
  raise DeprecationError(f"Deprecated on {date(*deprecated_on)}: {msg}")
137
136
 
138
137
 
139
138
  def deprecation_warning(
140
- deprecated_on: Tuple[int, int, int], msg: str, *, pending: bool = False, show_source: bool = True
139
+ deprecated_on: tuple[int, int, int], msg: str, *, pending: bool = False, show_source: bool = True
141
140
  ) -> None:
142
141
  """Utility for getting the proper stack entry.
143
142
 
modal/functions.py CHANGED
@@ -1,24 +1,18 @@
1
1
  # Copyright Modal Labs 2023
2
+ import dataclasses
2
3
  import inspect
3
4
  import textwrap
4
5
  import time
5
6
  import typing
6
7
  import warnings
8
+ from collections.abc import AsyncGenerator, Collection, Sequence, Sized
7
9
  from dataclasses import dataclass
8
10
  from pathlib import PurePosixPath
9
11
  from typing import (
10
12
  TYPE_CHECKING,
11
13
  Any,
12
- AsyncGenerator,
13
14
  Callable,
14
- Collection,
15
- Dict,
16
- List,
17
15
  Optional,
18
- Sequence,
19
- Sized,
20
- Tuple,
21
- Type,
22
16
  Union,
23
17
  )
24
18
 
@@ -26,6 +20,7 @@ import typing_extensions
26
20
  from google.protobuf.message import Message
27
21
  from grpclib import GRPCError, Status
28
22
  from synchronicity.combined_types import MethodWithAio
23
+ from synchronicity.exceptions import UserCodeException
29
24
 
30
25
  from modal._utils.async_utils import aclosing
31
26
  from modal_proto import api_pb2
@@ -64,6 +59,7 @@ from .cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
64
59
  from .config import config
65
60
  from .exception import (
66
61
  ExecutionError,
62
+ FunctionTimeoutError,
67
63
  InvalidError,
68
64
  NotFoundError,
69
65
  OutputExpiredError,
@@ -86,7 +82,7 @@ from .parallel_map import (
86
82
  _SynchronizedQueue,
87
83
  )
88
84
  from .proxy import _Proxy
89
- from .retries import Retries
85
+ from .retries import Retries, RetryManager
90
86
  from .schedule import Schedule
91
87
  from .scheduler_placement import SchedulerPlacement
92
88
  from .secret import _Secret
@@ -98,15 +94,32 @@ if TYPE_CHECKING:
98
94
  import modal.partial_function
99
95
 
100
96
 
97
+ @dataclasses.dataclass
98
+ class _RetryContext:
99
+ function_call_invocation_type: "api_pb2.FunctionCallInvocationType.ValueType"
100
+ retry_policy: api_pb2.FunctionRetryPolicy
101
+ function_call_jwt: str
102
+ input_jwt: str
103
+ input_id: str
104
+ item: api_pb2.FunctionPutInputsItem
105
+
106
+
101
107
  class _Invocation:
102
108
  """Internal client representation of a single-input call to a Modal Function or Generator"""
103
109
 
104
110
  stub: ModalClientModal
105
111
 
106
- def __init__(self, stub: ModalClientModal, function_call_id: str, client: _Client):
112
+ def __init__(
113
+ self,
114
+ stub: ModalClientModal,
115
+ function_call_id: str,
116
+ client: _Client,
117
+ retry_context: Optional[_RetryContext] = None,
118
+ ):
107
119
  self.stub = stub
108
120
  self.client = client # Used by the deserializer.
109
121
  self.function_call_id = function_call_id # TODO: remove and use only input_id
122
+ self._retry_context = retry_context
110
123
 
111
124
  @staticmethod
112
125
  async def create(
@@ -132,7 +145,17 @@ class _Invocation:
132
145
  function_call_id = response.function_call_id
133
146
 
134
147
  if response.pipelined_inputs:
135
- return _Invocation(client.stub, function_call_id, client)
148
+ assert len(response.pipelined_inputs) == 1
149
+ input = response.pipelined_inputs[0]
150
+ retry_context = _RetryContext(
151
+ function_call_invocation_type=function_call_invocation_type,
152
+ retry_policy=response.retry_policy,
153
+ function_call_jwt=response.function_call_jwt,
154
+ input_jwt=input.input_jwt,
155
+ input_id=input.input_id,
156
+ item=item,
157
+ )
158
+ return _Invocation(client.stub, function_call_id, client, retry_context)
136
159
 
137
160
  request_put = api_pb2.FunctionPutInputsRequest(
138
161
  function_id=function_id, inputs=[item], function_call_id=function_call_id
@@ -144,7 +167,16 @@ class _Invocation:
144
167
  processed_inputs = inputs_response.inputs
145
168
  if not processed_inputs:
146
169
  raise Exception("Could not create function call - the input queue seems to be full")
147
- return _Invocation(client.stub, function_call_id, client)
170
+ input = inputs_response.inputs[0]
171
+ retry_context = _RetryContext(
172
+ function_call_invocation_type=function_call_invocation_type,
173
+ retry_policy=response.retry_policy,
174
+ function_call_jwt=response.function_call_jwt,
175
+ input_jwt=input.input_jwt,
176
+ input_id=input.input_id,
177
+ item=item,
178
+ )
179
+ return _Invocation(client.stub, function_call_id, client, retry_context)
148
180
 
149
181
  async def pop_function_call_outputs(
150
182
  self, timeout: Optional[float], clear_on_success: bool
@@ -180,13 +212,46 @@ class _Invocation:
180
212
  # return the last response to check for state of num_unfinished_inputs
181
213
  return response
182
214
 
183
- async def run_function(self) -> Any:
215
+ async def _retry_input(self) -> None:
216
+ ctx = self._retry_context
217
+ if not ctx:
218
+ raise ValueError("Cannot retry input when _retry_context is empty.")
219
+
220
+ item = api_pb2.FunctionRetryInputsItem(input_jwt=ctx.input_jwt, input=ctx.item.input)
221
+ request = api_pb2.FunctionRetryInputsRequest(function_call_jwt=ctx.function_call_jwt, inputs=[item])
222
+ await retry_transient_errors(
223
+ self.client.stub.FunctionRetryInputs,
224
+ request,
225
+ )
226
+
227
+ async def _get_single_output(self) -> Any:
184
228
  # waits indefinitely for a single result for the function, and clear the outputs buffer after
185
229
  item: api_pb2.FunctionGetOutputsItem = (
186
230
  await self.pop_function_call_outputs(timeout=None, clear_on_success=True)
187
231
  ).outputs[0]
188
232
  return await _process_result(item.result, item.data_format, self.stub, self.client)
189
233
 
234
+ async def run_function(self) -> Any:
235
+ # Use retry logic only if retry policy is specified and
236
+ ctx = self._retry_context
237
+ if (
238
+ not ctx
239
+ or not ctx.retry_policy
240
+ or ctx.retry_policy.retries == 0
241
+ or ctx.function_call_invocation_type != api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC
242
+ ):
243
+ return await self._get_single_output()
244
+
245
+ # User errors including timeouts are managed by the user specified retry policy.
246
+ user_retry_manager = RetryManager(ctx.retry_policy)
247
+
248
+ while True:
249
+ try:
250
+ return await self._get_single_output()
251
+ except (UserCodeException, FunctionTimeoutError) as exc:
252
+ await user_retry_manager.raise_or_sleep(exc)
253
+ await self._retry_input()
254
+
190
255
  async def poll_function(self, timeout: Optional[float] = None):
191
256
  """Waits up to timeout for a result from a function.
192
257
 
@@ -278,12 +343,12 @@ class _FunctionSpec:
278
343
  image: Optional[_Image]
279
344
  mounts: Sequence[_Mount]
280
345
  secrets: Sequence[_Secret]
281
- network_file_systems: Dict[Union[str, PurePosixPath], _NetworkFileSystem]
282
- volumes: Dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]]
283
- gpus: Union[GPU_T, List[GPU_T]] # TODO(irfansharif): Somehow assert that it's the first kind, in sandboxes
346
+ network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem]
347
+ volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]]
348
+ gpus: Union[GPU_T, list[GPU_T]] # TODO(irfansharif): Somehow assert that it's the first kind, in sandboxes
284
349
  cloud: Optional[str]
285
350
  cpu: Optional[float]
286
- memory: Optional[Union[int, Tuple[int, int]]]
351
+ memory: Optional[Union[int, tuple[int, int]]]
287
352
  ephemeral_disk: Optional[int]
288
353
  scheduler_placement: Optional[SchedulerPlacement]
289
354
 
@@ -304,7 +369,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
304
369
 
305
370
  # TODO: more type annotations
306
371
  _info: Optional[FunctionInfo]
307
- _serve_mounts: typing.FrozenSet[_Mount] # set at load time, only by loader
372
+ _serve_mounts: frozenset[_Mount] # set at load time, only by loader
308
373
  _app: Optional["modal.app._App"] = None
309
374
  _obj: Optional["modal.cls._Obj"] = None # only set for InstanceServiceFunctions and bound instance methods
310
375
  _web_url: Optional[str]
@@ -323,7 +388,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
323
388
  _use_method_name: str = ""
324
389
 
325
390
  _class_parameter_info: Optional["api_pb2.ClassParameterInfo"] = None
326
- _method_handle_metadata: Optional[Dict[str, "api_pb2.FunctionHandleMetadata"]] = None
391
+ _method_handle_metadata: Optional[dict[str, "api_pb2.FunctionHandleMetadata"]] = None
327
392
 
328
393
  def _bind_method(
329
394
  self,
@@ -359,68 +424,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
359
424
  fun._is_method = True
360
425
  return fun
361
426
 
362
- def _bind_instance_method(self, class_bound_method: "_Function"):
363
- """mdmd:hidden
364
-
365
- Binds an "instance service function" to a specific method.
366
- This "dummy" _Function gets no unique object_id and isn't backend-backed at the moment, since all
367
- it does it forward invocations to the underlying instance_service_function with the specified method,
368
- and we don't support web_config for parameterized methods at the moment.
369
- """
370
- # TODO(elias): refactor to not use `_from_loader()` as a crutch for lazy-loading the
371
- # underlying instance_service_function. It's currently used in order to take advantage
372
- # of resolver logic and get "chained" resolution of lazy loads, even though this thin
373
- # object itself doesn't need any "loading"
374
- instance_service_function = self
375
- assert instance_service_function._obj
376
- method_name = class_bound_method._use_method_name
377
- full_function_name = f"{class_bound_method._function_name}[parameterized]"
378
-
379
- def hydrate_from_instance_service_function(method_placeholder_fun):
380
- method_placeholder_fun._hydrate_from_other(instance_service_function)
381
- method_placeholder_fun._obj = instance_service_function._obj
382
- method_placeholder_fun._web_url = (
383
- class_bound_method._web_url
384
- ) # TODO: this shouldn't be set when actual parameters are used
385
- method_placeholder_fun._function_name = full_function_name
386
- method_placeholder_fun._is_generator = class_bound_method._is_generator
387
- method_placeholder_fun._cluster_size = class_bound_method._cluster_size
388
- method_placeholder_fun._use_method_name = method_name
389
- method_placeholder_fun._is_method = True
390
-
391
- async def _load(fun: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
392
- # there is currently no actual loading logic executed to create each method on
393
- # the *parameterized* instance of a class - it uses the parameter-bound service-function
394
- # for the instance. This load method just makes sure to set all attributes after the
395
- # `instance_service_function` has been loaded (it's in the `_deps`)
396
- hydrate_from_instance_service_function(fun)
397
-
398
- def _deps():
399
- if instance_service_function.is_hydrated:
400
- # without this check, the common instance_service_function will be reloaded by all methods
401
- # TODO(elias): Investigate if we can fix this multi-loader in the resolver - feels like a bug?
402
- return []
403
- return [instance_service_function]
404
-
405
- rep = f"Method({full_function_name})"
406
-
407
- fun = _Function._from_loader(
408
- _load,
409
- rep,
410
- deps=_deps,
411
- hydrate_lazily=True,
412
- )
413
- if instance_service_function.is_hydrated:
414
- # Eager hydration (skip load) if the instance service function is already loaded
415
- hydrate_from_instance_service_function(fun)
416
-
417
- fun._info = class_bound_method._info
418
- fun._obj = instance_service_function._obj
419
- fun._is_method = True
420
- fun._app = class_bound_method._app
421
- fun._spec = class_bound_method._spec
422
- return fun
423
-
424
427
  @staticmethod
425
428
  def from_args(
426
429
  info: FunctionInfo,
@@ -429,14 +432,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
429
432
  secrets: Sequence[_Secret] = (),
430
433
  schedule: Optional[Schedule] = None,
431
434
  is_generator=False,
432
- gpu: Union[GPU_T, List[GPU_T]] = None,
435
+ gpu: Union[GPU_T, list[GPU_T]] = None,
433
436
  # TODO: maybe break this out into a separate decorator for notebooks.
434
437
  mounts: Collection[_Mount] = (),
435
- network_file_systems: Dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
438
+ network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
436
439
  allow_cross_region_volumes: bool = False,
437
- volumes: Dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]] = {},
440
+ volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]] = {},
438
441
  webhook_config: Optional[api_pb2.WebhookConfig] = None,
439
- memory: Optional[Union[int, Tuple[int, int]]] = None,
442
+ memory: Optional[Union[int, tuple[int, int]]] = None,
440
443
  proxy: Optional[_Proxy] = None,
441
444
  retries: Optional[Union[int, Retries]] = None,
442
445
  timeout: Optional[int] = None,
@@ -623,8 +626,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
623
626
  if image is not None and not isinstance(image, _Image):
624
627
  raise InvalidError(f"Expected modal.Image object. Got {type(image)}.")
625
628
 
626
- method_definitions: Optional[Dict[str, api_pb2.MethodDefinition]] = None
627
- partial_functions: Dict[str, "modal.partial_function._PartialFunction"] = {}
629
+ method_definitions: Optional[dict[str, api_pb2.MethodDefinition]] = None
630
+ partial_functions: dict[str, "modal.partial_function._PartialFunction"] = {}
628
631
  if info.user_cls:
629
632
  method_definitions = {}
630
633
  partial_functions = _find_partial_methods_for_user_cls(info.user_cls, _PartialFunctionFlags.FUNCTION)
@@ -640,8 +643,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
640
643
 
641
644
  function_type = get_function_type(is_generator)
642
645
 
643
- def _deps(only_explicit_mounts=False) -> List[_Object]:
644
- deps: List[_Object] = list(secrets)
646
+ def _deps(only_explicit_mounts=False) -> list[_Object]:
647
+ deps: list[_Object] = list(secrets)
645
648
  if only_explicit_mounts:
646
649
  # TODO: this is a bit hacky, but all_mounts may differ in the container vs locally
647
650
  # We don't want the function dependencies to change, so we have this way to force it to
@@ -821,6 +824,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
821
824
  is_method=function_definition.is_method,
822
825
  use_function_id=function_definition.use_function_id,
823
826
  use_method_name=function_definition.use_method_name,
827
+ method_definitions=function_definition.method_definitions,
828
+ method_definitions_set=function_definition.method_definitions_set,
824
829
  _experimental_group_size=function_definition._experimental_group_size,
825
830
  _experimental_buffer_containers=function_definition._experimental_buffer_containers,
826
831
  _experimental_custom_scaling=function_definition._experimental_custom_scaling,
@@ -847,9 +852,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
847
852
  function_data.ranked_functions.extend(ranked_functions)
848
853
  function_definition = None # function_definition is not used in this case
849
854
  else:
850
- # TODO(irfansharif): Assert on this specific type once
851
- # we get rid of python 3.8.
852
- # assert isinstance(gpu, GPU_T) # includes the case where gpu==None case
855
+ # TODO(irfansharif): Assert on this specific type once we get rid of python 3.9.
856
+ # assert isinstance(gpu, GPU_T) # includes the case where gpu==None case
853
857
  function_definition.resources.CopyFrom(
854
858
  convert_fn_config_to_resources_config(
855
859
  cpu=cpu, memory=memory, gpu=gpu, ephemeral_disk=ephemeral_disk
@@ -878,7 +882,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
878
882
  raise InvalidError(f"Function {info.function_name} is too large to deploy.")
879
883
  raise
880
884
  function_creation_status.set_response(response)
881
- serve_mounts = set(m for m in all_mounts if m.is_local()) # needed for modal.serve file watching
885
+ serve_mounts = {m for m in all_mounts if m.is_local()} # needed for modal.serve file watching
882
886
  serve_mounts |= image._serve_mounts
883
887
  obj._serve_mounts = frozenset(serve_mounts)
884
888
  self._hydrate(response.function_id, resolver.client, response.handle_metadata)
@@ -897,7 +901,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
897
901
  obj._spec = function_spec # needed for modal shell
898
902
 
899
903
  # Used to check whether we should rebuild a modal.Image which uses `run_function`.
900
- gpus: List[GPU_T] = gpu if isinstance(gpu, list) else [gpu]
904
+ gpus: list[GPU_T] = gpu if isinstance(gpu, list) else [gpu]
901
905
  obj._build_args = dict( # See get_build_def
902
906
  secrets=repr(secrets),
903
907
  gpu_config=repr([parse_gpu_config(_gpu) for _gpu in gpus]),
@@ -916,10 +920,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
916
920
  def _bind_parameters(
917
921
  self,
918
922
  obj: "modal.cls._Obj",
919
- from_other_workspace: bool,
920
923
  options: Optional[api_pb2.FunctionOptions],
921
924
  args: Sized,
922
- kwargs: Dict[str, Any],
925
+ kwargs: dict[str, Any],
923
926
  ) -> "_Function":
924
927
  """mdmd:hidden
925
928
 
@@ -927,7 +930,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
927
930
  """
928
931
 
929
932
  # In some cases, reuse the base function, i.e. not create new clones of each method or the "service function"
930
- can_use_parent = len(args) + len(kwargs) == 0 and not from_other_workspace and options is None
933
+ can_use_parent = len(args) + len(kwargs) == 0 and options is None
931
934
  parent = self
932
935
 
933
936
  async def _load(param_bound_func: _Function, resolver: Resolver, existing_object_id: Optional[str]):
@@ -997,7 +1000,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
997
1000
  Please exercise care when using this advanced feature!
998
1001
  Setting and forgetting a warm pool on functions can lead to increased costs.
999
1002
 
1000
- ```python
1003
+ ```python notest
1001
1004
  # Usage on a regular function.
1002
1005
  f = modal.Function.lookup("my-app", "function")
1003
1006
  f.keep_warm(2)
@@ -1025,7 +1028,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1025
1028
 
1026
1029
  @classmethod
1027
1030
  def from_name(
1028
- cls: Type["_Function"],
1031
+ cls: type["_Function"],
1029
1032
  app_name: str,
1030
1033
  tag: str,
1031
1034
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
@@ -1076,7 +1079,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1076
1079
  In contrast to `modal.Function.from_name`, this is an eager method
1077
1080
  that will hydrate the local object with metadata from Modal servers.
1078
1081
 
1079
- ```python
1082
+ ```python notest
1080
1083
  f = modal.Function.lookup("other-app", "function")
1081
1084
  ```
1082
1085
  """
@@ -1232,13 +1235,18 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1232
1235
  yield item
1233
1236
 
1234
1237
  async def _call_function(self, args, kwargs) -> ReturnType:
1238
+ if config.get("client_retries"):
1239
+ function_call_invocation_type = api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC
1240
+ else:
1241
+ function_call_invocation_type = api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC_LEGACY
1235
1242
  invocation = await _Invocation.create(
1236
1243
  self,
1237
1244
  args,
1238
1245
  kwargs,
1239
1246
  client=self._client,
1240
- function_call_invocation_type=api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC_LEGACY,
1247
+ function_call_invocation_type=function_call_invocation_type,
1241
1248
  )
1249
+
1242
1250
  return await invocation.run_function()
1243
1251
 
1244
1252
  async def _call_function_nowait(
@@ -1355,12 +1363,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1355
1363
  if is_async(info.raw_f):
1356
1364
  # We want to run __aenter__ and fun in the same coroutine
1357
1365
  async def coro():
1358
- await obj.aenter()
1366
+ await obj._aenter()
1359
1367
  return await fun(*args, **kwargs)
1360
1368
 
1361
1369
  return coro() # type: ignore
1362
1370
  else:
1363
- obj.enter()
1371
+ obj._enter()
1364
1372
  return fun(*args, **kwargs)
1365
1373
 
1366
1374
  @synchronizer.no_input_translation
@@ -1476,7 +1484,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1476
1484
  async for res in self._invocation().run_generator():
1477
1485
  yield res
1478
1486
 
1479
- async def get_call_graph(self) -> List[InputInfo]:
1487
+ async def get_call_graph(self) -> list[InputInfo]:
1480
1488
  """Returns a structure representing the call graph from a given root
1481
1489
  call ID, along with the status of execution for each node.
1482
1490