modal 0.73.9__py3-none-any.whl → 0.73.10__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.
modal/cli/run.py CHANGED
@@ -7,6 +7,7 @@ import re
7
7
  import shlex
8
8
  import sys
9
9
  import time
10
+ from dataclasses import dataclass
10
11
  from functools import partial
11
12
  from typing import Any, Callable, Optional, get_type_hints
12
13
 
@@ -35,6 +36,7 @@ class ParameterMetadata(TypedDict):
35
36
  default: Any
36
37
  annotation: Any
37
38
  type_hint: Any
39
+ kind: Any
38
40
 
39
41
 
40
42
  class AnyParamType(click.ParamType):
@@ -58,7 +60,13 @@ class NoParserAvailable(InvalidError):
58
60
  pass
59
61
 
60
62
 
61
- def _get_signature(f: Callable[..., Any], is_method: bool = False) -> dict[str, ParameterMetadata]:
63
+ @dataclass
64
+ class FnSignature:
65
+ parameters: dict[str, ParameterMetadata]
66
+ has_variadic_args: bool
67
+
68
+
69
+ def _get_signature(f: Callable[..., Any], is_method: bool = False) -> FnSignature:
62
70
  try:
63
71
  type_hints = get_type_hints(f)
64
72
  except Exception as exc:
@@ -66,18 +74,29 @@ def _get_signature(f: Callable[..., Any], is_method: bool = False) -> dict[str,
66
74
  msg = "Unable to generate command line interface for app entrypoint. See traceback above for details."
67
75
  raise ExecutionError(msg) from exc
68
76
 
77
+ has_variadic_args = False
78
+
69
79
  if is_method:
70
80
  self = None # Dummy, doesn't matter
71
81
  f = functools.partial(f, self)
82
+
72
83
  signature: dict[str, ParameterMetadata] = {}
73
84
  for param in inspect.signature(f).parameters.values():
74
- signature[param.name] = {
75
- "name": param.name,
76
- "default": param.default,
77
- "annotation": param.annotation,
78
- "type_hint": type_hints.get(param.name, "Any"),
79
- }
80
- return signature
85
+ if param.kind == inspect.Parameter.VAR_POSITIONAL:
86
+ has_variadic_args = True
87
+ else:
88
+ signature[param.name] = {
89
+ "name": param.name,
90
+ "default": param.default,
91
+ "annotation": param.annotation,
92
+ "type_hint": type_hints.get(param.name, "Any"),
93
+ "kind": param.kind,
94
+ }
95
+
96
+ if has_variadic_args and len(signature) > 0:
97
+ raise InvalidError("Functions with variable-length positional arguments (*args) cannot have other parameters.")
98
+
99
+ return FnSignature(signature, has_variadic_args)
81
100
 
82
101
 
83
102
  def _get_param_type_as_str(annot: Any) -> str:
@@ -96,17 +115,18 @@ def _get_param_type_as_str(annot: Any) -> str:
96
115
  return annot_str
97
116
 
98
117
 
99
- def _add_click_options(func, signature: dict[str, ParameterMetadata]):
118
+ def _add_click_options(func, parameters: dict[str, ParameterMetadata]):
100
119
  """Adds @click.option based on function signature
101
120
 
102
121
  Kind of like typer, but using options instead of positional arguments
103
122
  """
104
- for param in signature.values():
123
+ for param in parameters.values():
105
124
  param_type_str = _get_param_type_as_str(param["type_hint"])
106
125
  param_name = param["name"].replace("_", "-")
107
126
  cli_name = "--" + param_name
108
127
  if param_type_str == "bool":
109
128
  cli_name += "/--no-" + param_name
129
+
110
130
  parser = option_parsers.get(param_type_str)
111
131
  if parser is None:
112
132
  msg = f"Parameter `{param_name}` has unparseable annotation: {param['annotation']!r}"
@@ -145,9 +165,15 @@ def _write_local_result(result_path: str, res: Any):
145
165
  fid.write(res)
146
166
 
147
167
 
148
- def _make_click_function(app, inner: Callable[[dict[str, Any]], Any]):
168
+ def _make_click_function(app, signature: FnSignature, inner: Callable[[tuple[str, ...], dict[str, Any]], Any]):
149
169
  @click.pass_context
150
170
  def f(ctx, **kwargs):
171
+ if signature.has_variadic_args:
172
+ assert len(kwargs) == 0
173
+ args = ctx.args
174
+ else:
175
+ args = ()
176
+
151
177
  show_progress: bool = ctx.obj["show_progress"]
152
178
  with enable_output(show_progress):
153
179
  with run_app(
@@ -156,7 +182,7 @@ def _make_click_function(app, inner: Callable[[dict[str, Any]], Any]):
156
182
  environment_name=ctx.obj["env"],
157
183
  interactive=ctx.obj["interactive"],
158
184
  ):
159
- res = inner(kwargs)
185
+ res = inner(args, kwargs)
160
186
 
161
187
  if result_path := ctx.obj["result_path"]:
162
188
  _write_local_result(result_path, res)
@@ -168,23 +194,33 @@ def _get_click_command_for_function(app: App, function: Function):
168
194
  if function.is_generator:
169
195
  raise InvalidError("`modal run` is not supported for generator functions")
170
196
 
171
- signature: dict[str, ParameterMetadata] = _get_signature(function.info.raw_f)
197
+ signature = _get_signature(function.info.raw_f)
198
+
199
+ def _inner(args, click_kwargs):
200
+ return function.remote(*args, **click_kwargs)
172
201
 
173
- def _inner(click_kwargs):
174
- return function.remote(**click_kwargs)
202
+ f = _make_click_function(app, signature, _inner)
175
203
 
176
- f = _make_click_function(app, _inner)
204
+ with_click_options = _add_click_options(f, signature.parameters)
177
205
 
178
- with_click_options = _add_click_options(f, signature)
179
- return click.command(with_click_options)
206
+ if signature.has_variadic_args:
207
+ return click.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})(
208
+ with_click_options
209
+ )
210
+ else:
211
+ return click.command(with_click_options)
180
212
 
181
213
 
182
214
  def _get_click_command_for_cls(app: App, method_ref: MethodReference):
183
- signature: dict[str, ParameterMetadata]
215
+ parameters: dict[str, ParameterMetadata]
184
216
  cls = method_ref.cls
185
217
  method_name = method_ref.method_name
186
218
 
187
219
  cls_signature = _get_signature(cls._get_user_cls())
220
+
221
+ if cls_signature.has_variadic_args:
222
+ raise InvalidError("Modal classes cannot have variable-length positional arguments (*args).")
223
+
188
224
  partial_functions = cls._get_partial_functions()
189
225
 
190
226
  if method_name in ("*", ""):
@@ -202,27 +238,35 @@ def _get_click_command_for_cls(app: App, method_ref: MethodReference):
202
238
  fun_signature = _get_signature(partial_function._get_raw_f(), is_method=True)
203
239
 
204
240
  # TODO(erikbern): assert there's no overlap?
205
- signature = dict(**cls_signature, **fun_signature) # Pool all arguments
241
+ parameters = dict(**cls_signature.parameters, **fun_signature.parameters) # Pool all arguments
206
242
 
207
- def _inner(click_kwargs):
243
+ def _inner(args, click_kwargs):
208
244
  # unpool class and method arguments
209
245
  # TODO(erikbern): this code is a bit hacky
210
- cls_kwargs = {k: click_kwargs[k] for k in cls_signature}
211
- fun_kwargs = {k: click_kwargs[k] for k in fun_signature}
246
+ cls_kwargs = {k: click_kwargs[k] for k in cls_signature.parameters}
247
+ fun_kwargs = {k: click_kwargs[k] for k in fun_signature.parameters}
212
248
 
213
249
  instance = cls(**cls_kwargs)
214
250
  method: Function = getattr(instance, method_name)
215
- return method.remote(**fun_kwargs)
251
+ return method.remote(*args, **fun_kwargs)
252
+
253
+ f = _make_click_function(app, fun_signature, _inner)
254
+ with_click_options = _add_click_options(f, parameters)
216
255
 
217
- f = _make_click_function(app, _inner)
218
- with_click_options = _add_click_options(f, signature)
219
- return click.command(with_click_options)
256
+ if fun_signature.has_variadic_args:
257
+ return click.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})(
258
+ with_click_options
259
+ )
260
+ else:
261
+ return click.command(with_click_options)
220
262
 
221
263
 
222
264
  def _get_click_command_for_local_entrypoint(app: App, entrypoint: LocalEntrypoint):
223
265
  func = entrypoint.info.raw_f
224
266
  isasync = inspect.iscoroutinefunction(func)
225
267
 
268
+ signature = _get_signature(func)
269
+
226
270
  @click.pass_context
227
271
  def f(ctx, *args, **kwargs):
228
272
  if ctx.obj["detach"]:
@@ -231,6 +275,10 @@ def _get_click_command_for_local_entrypoint(app: App, entrypoint: LocalEntrypoin
231
275
  "triggered Modal function alive after the parent process has been killed or disconnected."
232
276
  )
233
277
 
278
+ if signature.has_variadic_args:
279
+ assert len(args) == 0 and len(kwargs) == 0
280
+ args = ctx.args
281
+
234
282
  show_progress: bool = ctx.obj["show_progress"]
235
283
  with enable_output(show_progress):
236
284
  with run_app(
@@ -250,8 +298,14 @@ def _get_click_command_for_local_entrypoint(app: App, entrypoint: LocalEntrypoin
250
298
  if result_path := ctx.obj["result_path"]:
251
299
  _write_local_result(result_path, res)
252
300
 
253
- with_click_options = _add_click_options(f, _get_signature(func))
254
- return click.command(with_click_options)
301
+ with_click_options = _add_click_options(f, signature.parameters)
302
+
303
+ if signature.has_variadic_args:
304
+ return click.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})(
305
+ with_click_options
306
+ )
307
+ else:
308
+ return click.command(with_click_options)
255
309
 
256
310
 
257
311
  def _get_runnable_list(all_usable_commands: list[CLICommand]) -> str:
modal/client.pyi CHANGED
@@ -27,7 +27,7 @@ class _Client:
27
27
  _snapshotted: bool
28
28
 
29
29
  def __init__(
30
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.73.9"
30
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.73.10"
31
31
  ): ...
32
32
  def is_closed(self) -> bool: ...
33
33
  @property
@@ -85,7 +85,7 @@ class Client:
85
85
  _snapshotted: bool
86
86
 
87
87
  def __init__(
88
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.73.9"
88
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.73.10"
89
89
  ): ...
90
90
  def is_closed(self) -> bool: ...
91
91
  @property
modal/functions.pyi CHANGED
@@ -200,11 +200,11 @@ class Function(
200
200
 
201
201
  _call_generator_nowait: ___call_generator_nowait_spec[typing_extensions.Self]
202
202
 
203
- class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
203
+ class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
204
204
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
205
205
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
206
206
 
207
- remote: __remote_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
207
+ remote: __remote_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
208
208
 
209
209
  class __remote_gen_spec(typing_extensions.Protocol[SUPERSELF]):
210
210
  def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
@@ -219,19 +219,19 @@ class Function(
219
219
  self, *args: modal._functions.P.args, **kwargs: modal._functions.P.kwargs
220
220
  ) -> modal._functions.OriginalReturnType: ...
221
221
 
222
- class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
222
+ class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
223
223
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
224
224
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
225
225
 
226
226
  _experimental_spawn: ___experimental_spawn_spec[
227
- modal._functions.P, modal._functions.ReturnType, typing_extensions.Self
227
+ modal._functions.ReturnType, modal._functions.P, typing_extensions.Self
228
228
  ]
229
229
 
230
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
230
+ class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
231
231
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
232
232
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
233
233
 
234
- spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
234
+ spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
235
235
 
236
236
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]: ...
237
237
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.73.9
3
+ Version: 0.73.10
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -21,7 +21,7 @@ modal/app.py,sha256=wRygVSrWH8iIqhDAAl2Ww_RAkz8MCJZ0Jt9qYZCF6SA,44626
21
21
  modal/app.pyi,sha256=lxiuWzE_OLb3WHg-H7Pek9DGBuCUzZ55P594VhJL5LA,26113
22
22
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
23
23
  modal/client.py,sha256=8SQawr7P1PNUCq1UmJMUQXG2jIo4Nmdcs311XqrNLRE,15276
24
- modal/client.pyi,sha256=UOfACeLizRMYzf2NDFzalr9sLhXAy447M3jSwPxPCbc,7591
24
+ modal/client.pyi,sha256=zquF7hra8lDrBysJUafRuLtmDHnUXpEU1nt9wRDDCUs,7593
25
25
  modal/cloud_bucket_mount.py,sha256=YOe9nnvSr4ZbeCn587d7_VhE9IioZYRvF9VYQTQux08,5914
26
26
  modal/cloud_bucket_mount.pyi,sha256=30T3K1a89l6wzmEJ_J9iWv9SknoGqaZDx59Xs-ZQcmk,1607
27
27
  modal/cls.py,sha256=kNnZrBYVXOhgEXU0rDWk2Hr-bQRrsZkMKDgC-TD_6Bs,31063
@@ -40,7 +40,7 @@ modal/file_io.py,sha256=lcMs_E9Xfm0YX1t9U2wNIBPnqHRxmImqjLW1GHqVmyg,20945
40
40
  modal/file_io.pyi,sha256=NTRft1tbPSWf9TlWVeZmTlgB5AZ_Zhu2srWIrWr7brk,9445
41
41
  modal/file_pattern_matcher.py,sha256=1cZ4V2wSLiaXqAqStETSwp3bzDD6QZOt6pmmjk3Okz4,6505
42
42
  modal/functions.py,sha256=kcNHvqeGBxPI7Cgd57NIBBghkfbeFJzXO44WW0jSmao,325
43
- modal/functions.pyi,sha256=QYZy3BCjA2y3UC217e3YG-omyG0E1Jx-Uc-sonsyQsE,14288
43
+ modal/functions.pyi,sha256=mtngzj8VlzMOQATe6muBN5oH_Gw9zGKxMKZ56Z-41kU,14288
44
44
  modal/gpu.py,sha256=2qZMNnoMrjU-5Bu7fx68pANUAKTtZq0EWEEeBA9OUVQ,7426
45
45
  modal/image.py,sha256=Vjsi7wS9dEcoj-7m7_LmvbK5iqEuFz-SHKl2K-qWcew,90952
46
46
  modal/image.pyi,sha256=A5mW2dBguEhmRo815Ax1rBIMXTCriu7PqLMHoUPsez8,26372
@@ -125,7 +125,7 @@ modal/cli/launch.py,sha256=pzQt2QlcrbIUU0MVzWWPAvMQ6MCyqsHZ0X9JcV-sY04,3242
125
125
  modal/cli/network_file_system.py,sha256=eq3JnwjbfFNsJodIyANHL06ByYc3BSavzdmu8C96cHA,7948
126
126
  modal/cli/profile.py,sha256=rLXfjJObfPNjaZvNfHGIKqs7y9bGYyGe-K7V0w-Ni0M,3110
127
127
  modal/cli/queues.py,sha256=6gTu76dzBtPN5eQVsLrvQpuru5jI9ZCWK5Eh8J8XhaM,4498
128
- modal/cli/run.py,sha256=_hYvRwBkrbakN3oAuoiaMdYytBtN4BoryrYxPAVIIvc,20065
128
+ modal/cli/run.py,sha256=hXCvqQM6w7dXz0254q5Sr0UNR5JC6cEA6fBdrcshCcg,21784
129
129
  modal/cli/secret.py,sha256=uQpwYrMY98iMCWeZOQTcktOYhPTZ8IHnyealDc2CZqo,4206
130
130
  modal/cli/token.py,sha256=mxSgOWakXG6N71hQb1ko61XAR9ZGkTMZD-Txn7gmTac,1924
131
131
  modal/cli/utils.py,sha256=hZmjyzcPjDnQSkLvycZD2LhGdcsfdZshs_rOU78EpvI,3717
@@ -171,10 +171,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
171
171
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
172
172
  modal_version/__init__.py,sha256=wiJQ53c-OMs0Xf1UeXOxQ7FwlV1VzIjnX6o-pRYZ_Pk,470
173
173
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
174
- modal_version/_version_generated.py,sha256=RgDn253uA_DeV6d6P40AOPpDJqMMVupp6gXgLMcak24,148
175
- modal-0.73.9.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
176
- modal-0.73.9.dist-info/METADATA,sha256=BYeidDOAvKaxgE_yMfAun3xjsRB6Q0jGKR8fcs9a0jU,2329
177
- modal-0.73.9.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
178
- modal-0.73.9.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
179
- modal-0.73.9.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
180
- modal-0.73.9.dist-info/RECORD,,
174
+ modal_version/_version_generated.py,sha256=bRP1kitLENe-C1R8SFEB0RNXUvVdmynlJRPg8FgsxXA,149
175
+ modal-0.73.10.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
176
+ modal-0.73.10.dist-info/METADATA,sha256=o2yy7RC_d68S0vZZyIem-3gJG0H7EDGGXtmHHcPzmEE,2330
177
+ modal-0.73.10.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
178
+ modal-0.73.10.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
179
+ modal-0.73.10.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
180
+ modal-0.73.10.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
 
3
3
  # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client.
4
- build_number = 9 # git: 76a3f59
4
+ build_number = 10 # git: 6951dc1