modal 0.66.44__py3-none-any.whl → 0.67.0__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/_resolver.py CHANGED
@@ -130,9 +130,14 @@ class Resolver:
130
130
  raise NotFoundError(exc.message)
131
131
  raise
132
132
 
133
- # Check that the id of functions and classes didn't change
133
+ # Check that the id of functions didn't change
134
134
  # Persisted refs are ignored because their life cycle is managed independently.
135
- if not obj._is_another_app and existing_object_id is not None and obj.object_id != existing_object_id:
135
+ if (
136
+ not obj._is_another_app
137
+ and existing_object_id is not None
138
+ and existing_object_id.startswith("fu-")
139
+ and obj.object_id != existing_object_id
140
+ ):
136
141
  raise Exception(
137
142
  f"Tried creating an object using existing id {existing_object_id}"
138
143
  f" but it has id {obj.object_id}"
@@ -452,7 +452,7 @@ class _ContainerIOManager:
452
452
  await asyncio.sleep(DYNAMIC_CONCURRENCY_INTERVAL_SECS)
453
453
 
454
454
  async def get_app_objects(self) -> RunningApp:
455
- req = api_pb2.AppGetObjectsRequest(app_id=self.app_id, include_unindexed=True)
455
+ req = api_pb2.AppGetObjectsRequest(app_id=self.app_id, include_unindexed=True, only_class_function=True)
456
456
  resp = await retry_transient_errors(self._client.stub.AppGetObjects, req)
457
457
  logger.debug(f"AppGetObjects received {len(resp.items)} objects for app {self.app_id}")
458
458
 
modal/app.py CHANGED
@@ -177,7 +177,8 @@ class _App:
177
177
 
178
178
  _name: Optional[str]
179
179
  _description: Optional[str]
180
- _indexed_objects: Dict[str, _Object]
180
+ _functions: Dict[str, _Function]
181
+ _classes: Dict[str, _Cls]
181
182
 
182
183
  _image: Optional[_Image]
183
184
  _mounts: Sequence[_Mount]
@@ -223,7 +224,8 @@ class _App:
223
224
  if image is not None and not isinstance(image, _Image):
224
225
  raise InvalidError("image has to be a modal Image or AioImage object")
225
226
 
226
- self._indexed_objects = {}
227
+ self._functions = {}
228
+ self._classes = {}
227
229
  self._image = image
228
230
  self._mounts = mounts
229
231
  self._secrets = secrets
@@ -312,6 +314,7 @@ class _App:
312
314
  raise InvalidError(f"App attribute `{key}` with value {value!r} is not a valid Modal object")
313
315
 
314
316
  def _add_object(self, tag, obj):
317
+ # TODO(erikbern): replace this with _add_function and _add_class
315
318
  if self._running_app:
316
319
  # If this is inside a container, then objects can be defined after app initialization.
317
320
  # So we may have to initialize objects once they get bound to the app.
@@ -320,7 +323,12 @@ class _App:
320
323
  metadata: Message = self._running_app.object_handle_metadata[object_id]
321
324
  obj._hydrate(object_id, self._client, metadata)
322
325
 
323
- self._indexed_objects[tag] = obj
326
+ if isinstance(obj, _Function):
327
+ self._functions[tag] = obj
328
+ elif isinstance(obj, _Cls):
329
+ self._classes[tag] = obj
330
+ else:
331
+ raise RuntimeError(f"Expected `obj` to be a _Function or _Cls (got {type(obj)}")
324
332
 
325
333
  def __getitem__(self, tag: str):
326
334
  deprecation_error((2024, 3, 25), _app_attr_error)
@@ -334,7 +342,7 @@ class _App:
334
342
  if tag.startswith("__"):
335
343
  # Hacky way to avoid certain issues, e.g. pickle will try to look this up
336
344
  raise AttributeError(f"App has no member {tag}")
337
- if tag not in self._indexed_objects:
345
+ if tag not in self._functions or tag not in self._classes:
338
346
  # Primarily to make hasattr work
339
347
  raise AttributeError(f"App has no member {tag}")
340
348
  deprecation_error((2024, 3, 25), _app_attr_error)
@@ -360,7 +368,9 @@ class _App:
360
368
 
361
369
  def _uncreate_all_objects(self):
362
370
  # TODO(erikbern): this doesn't unhydrate objects that aren't tagged
363
- for obj in self._indexed_objects.values():
371
+ for obj in self._functions.values():
372
+ obj._unhydrate()
373
+ for obj in self._classes.values():
364
374
  obj._unhydrate()
365
375
 
366
376
  @asynccontextmanager
@@ -459,18 +469,17 @@ class _App:
459
469
  return [m for m in all_mounts if m.is_local()]
460
470
 
461
471
  def _add_function(self, function: _Function, is_web_endpoint: bool):
462
- if function.tag in self._indexed_objects:
463
- old_function = self._indexed_objects[function.tag]
464
- if isinstance(old_function, _Function):
465
- if not is_notebook():
466
- logger.warning(
467
- f"Warning: Tag '{function.tag}' collision!"
468
- " Overriding existing function "
469
- f"[{old_function._info.module_name}].{old_function._info.function_name}"
470
- f" with new function [{function._info.module_name}].{function._info.function_name}"
471
- )
472
- else:
473
- logger.warning(f"Warning: tag {function.tag} exists but is overridden by function")
472
+ if function.tag in self._functions:
473
+ if not is_notebook():
474
+ old_function: _Function = self._functions[function.tag]
475
+ logger.warning(
476
+ f"Warning: Tag '{function.tag}' collision!"
477
+ " Overriding existing function "
478
+ f"[{old_function._info.module_name}].{old_function._info.function_name}"
479
+ f" with new function [{function._info.module_name}].{function._info.function_name}"
480
+ )
481
+ if function.tag in self._classes:
482
+ logger.warning(f"Warning: tag {function.tag} exists but is overridden by function")
474
483
 
475
484
  self._add_object(function.tag, function)
476
485
  if is_web_endpoint:
@@ -484,21 +493,22 @@ class _App:
484
493
  _App._container_app = running_app
485
494
 
486
495
  # Hydrate objects on app
496
+ indexed_objects = dict(**self._functions, **self._classes)
487
497
  for tag, object_id in running_app.tag_to_object_id.items():
488
- if tag in self._indexed_objects:
489
- obj = self._indexed_objects[tag]
498
+ if tag in indexed_objects:
499
+ obj = indexed_objects[tag]
490
500
  handle_metadata = running_app.object_handle_metadata[object_id]
491
501
  obj._hydrate(object_id, client, handle_metadata)
492
502
 
493
503
  @property
494
504
  def registered_functions(self) -> Dict[str, _Function]:
495
505
  """All modal.Function objects registered on the app."""
496
- return {tag: obj for tag, obj in self._indexed_objects.items() if isinstance(obj, _Function)}
506
+ return self._functions
497
507
 
498
508
  @property
499
509
  def registered_classes(self) -> Dict[str, _Function]:
500
510
  """All modal.Cls objects registered on the app."""
501
- return {tag: obj for tag, obj in self._indexed_objects.items() if isinstance(obj, _Cls)}
511
+ return self._classes
502
512
 
503
513
  @property
504
514
  def registered_entrypoints(self) -> Dict[str, _LocalEntrypoint]:
@@ -507,7 +517,11 @@ class _App:
507
517
 
508
518
  @property
509
519
  def indexed_objects(self) -> Dict[str, _Object]:
510
- return self._indexed_objects
520
+ deprecation_warning(
521
+ (2024, 11, 25),
522
+ "`app.indexed_objects` is deprecated! Use `app.registered_functions` or `app.registered_classes` instead.",
523
+ )
524
+ return dict(**self._functions, **self._classes)
511
525
 
512
526
  @property
513
527
  def registered_web_endpoints(self) -> List[str]:
@@ -1002,8 +1016,9 @@ class _App:
1002
1016
  bar.remote()
1003
1017
  ```
1004
1018
  """
1005
- for tag, object in other_app._indexed_objects.items():
1006
- existing_object = self._indexed_objects.get(tag)
1019
+ indexed_objects = dict(**other_app._functions, **other_app._classes)
1020
+ for tag, object in indexed_objects.items():
1021
+ existing_object = indexed_objects.get(tag)
1007
1022
  if existing_object and existing_object != object:
1008
1023
  logger.warning(
1009
1024
  f"Named app object {tag} with existing value {existing_object} is being "
modal/app.pyi CHANGED
@@ -76,7 +76,8 @@ class _App:
76
76
  _container_app: typing.ClassVar[typing.Optional[modal.running_app.RunningApp]]
77
77
  _name: typing.Optional[str]
78
78
  _description: typing.Optional[str]
79
- _indexed_objects: typing.Dict[str, modal.object._Object]
79
+ _functions: typing.Dict[str, modal.functions._Function]
80
+ _classes: typing.Dict[str, modal.cls._Cls]
80
81
  _image: typing.Optional[modal.image._Image]
81
82
  _mounts: typing.Sequence[modal.mount._Mount]
82
83
  _secrets: typing.Sequence[modal.secret._Secret]
@@ -270,7 +271,8 @@ class App:
270
271
  _container_app: typing.ClassVar[typing.Optional[modal.running_app.RunningApp]]
271
272
  _name: typing.Optional[str]
272
273
  _description: typing.Optional[str]
273
- _indexed_objects: typing.Dict[str, modal.object.Object]
274
+ _functions: typing.Dict[str, modal.functions.Function]
275
+ _classes: typing.Dict[str, modal.cls.Cls]
274
276
  _image: typing.Optional[modal.image.Image]
275
277
  _mounts: typing.Sequence[modal.mount.Mount]
276
278
  _secrets: typing.Sequence[modal.secret.Secret]
modal/cli/config.py CHANGED
@@ -3,6 +3,7 @@ import typer
3
3
  from rich.console import Console
4
4
 
5
5
  from modal.config import _profile, _store_user_config, config
6
+ from modal.environments import Environment
6
7
 
7
8
  config_cli = typer.Typer(
8
9
  name="config",
@@ -38,6 +39,8 @@ when running a command that requires an environment.
38
39
 
39
40
  @config_cli.command(help=SET_DEFAULT_ENV_HELP)
40
41
  def set_environment(environment_name: str):
42
+ # Confirm that the environment exists by looking it up
43
+ Environment.lookup(environment_name)
41
44
  _store_user_config({"environment": environment_name})
42
45
  typer.echo(f"New default environment for profile {_profile}: {environment_name}")
43
46
 
modal/cli/import_refs.py CHANGED
@@ -110,7 +110,7 @@ def get_by_object_path(obj: Any, obj_path: str) -> Optional[Any]:
110
110
  def _infer_function_or_help(
111
111
  app: App, module, accept_local_entrypoint: bool, accept_webhook: bool
112
112
  ) -> Union[Function, LocalEntrypoint]:
113
- function_choices = set(tag for tag, func in app.registered_functions.items() if not func.info.is_service_class())
113
+ function_choices = set(app.registered_functions)
114
114
  if not accept_webhook:
115
115
  function_choices -= set(app.registered_web_endpoints)
116
116
  if accept_local_entrypoint:
@@ -154,7 +154,7 @@ Registered functions and local entrypoints on the selected app are:
154
154
  # entrypoint is in entrypoint registry, for now
155
155
  return app.registered_entrypoints[function_name]
156
156
 
157
- function = app.indexed_objects[function_name] # functions are in blueprint
157
+ function = app.registered_functions[function_name]
158
158
  assert isinstance(function, Function)
159
159
  return function
160
160
 
modal/cli/launch.py CHANGED
@@ -25,7 +25,7 @@ launch_cli = Typer(
25
25
  )
26
26
 
27
27
 
28
- def _launch_program(name: str, filename: str, args: Dict[str, Any]) -> None:
28
+ def _launch_program(name: str, filename: str, detach: bool, args: Dict[str, Any]) -> None:
29
29
  os.environ["MODAL_LAUNCH_ARGS"] = json.dumps(args)
30
30
 
31
31
  program_path = str(Path(__file__).parent / "programs" / filename)
@@ -37,7 +37,7 @@ def _launch_program(name: str, filename: str, args: Dict[str, Any]) -> None:
37
37
  func = entrypoint.info.raw_f
38
38
  isasync = inspect.iscoroutinefunction(func)
39
39
  with enable_output():
40
- with run_app(app):
40
+ with run_app(app, detach=detach):
41
41
  try:
42
42
  if isasync:
43
43
  asyncio.run(func())
@@ -57,6 +57,7 @@ def jupyter(
57
57
  add_python: Optional[str] = "3.11",
58
58
  mount: Optional[str] = None, # Create a `modal.Mount` from a local directory.
59
59
  volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
60
+ detach: bool = False, # Run the app in "detached" mode to persist after local client disconnects
60
61
  ):
61
62
  args = {
62
63
  "cpu": cpu,
@@ -68,7 +69,7 @@ def jupyter(
68
69
  "mount": mount,
69
70
  "volume": volume,
70
71
  }
71
- _launch_program("jupyter", "run_jupyter.py", args)
72
+ _launch_program("jupyter", "run_jupyter.py", detach, args)
72
73
 
73
74
 
74
75
  @launch_cli.command(name="vscode", help="Start Visual Studio Code on Modal.")
@@ -79,6 +80,7 @@ def vscode(
79
80
  timeout: int = 3600,
80
81
  mount: Optional[str] = None, # Create a `modal.Mount` from a local directory.
81
82
  volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
83
+ detach: bool = False, # Run the app in "detached" mode to persist after local client disconnects
82
84
  ):
83
85
  args = {
84
86
  "cpu": cpu,
@@ -88,4 +90,4 @@ def vscode(
88
90
  "mount": mount,
89
91
  "volume": volume,
90
92
  }
91
- _launch_program("vscode", "vscode.py", args)
93
+ _launch_program("vscode", "vscode.py", detach, args)
modal/cli/run.py CHANGED
@@ -136,7 +136,13 @@ def _get_clean_app_description(func_ref: str) -> str:
136
136
 
137
137
 
138
138
  def _get_click_command_for_function(app: App, function_tag):
139
- function = app.indexed_objects[function_tag]
139
+ function = app.registered_functions.get(function_tag)
140
+ if not function or (isinstance(function, Function) and function.info.user_cls is not None):
141
+ # This is either a function_tag for a class method function (e.g MyClass.foo) or a function tag for a
142
+ # class service function (MyClass.*)
143
+ class_name, method_name = function_tag.rsplit(".", 1)
144
+ if not function:
145
+ function = app.registered_functions.get(f"{class_name}.*")
140
146
  assert isinstance(function, Function)
141
147
  function = typing.cast(Function, function)
142
148
  if function.is_generator:
@@ -144,12 +150,19 @@ def _get_click_command_for_function(app: App, function_tag):
144
150
 
145
151
  signature: Dict[str, ParameterMetadata]
146
152
  cls: Optional[Cls] = None
147
- method_name: Optional[str] = None
148
153
  if function.info.user_cls is not None:
149
- class_name, method_name = function_tag.rsplit(".", 1)
150
- cls = typing.cast(Cls, app.indexed_objects[class_name])
154
+ cls = typing.cast(Cls, app.registered_classes[class_name])
151
155
  cls_signature = _get_signature(function.info.user_cls)
152
- fun_signature = _get_signature(function.info.raw_f, is_method=True)
156
+ if method_name == "*":
157
+ method_names = list(cls._get_partial_functions().keys())
158
+ if len(method_names) == 1:
159
+ method_name = method_names[0]
160
+ else:
161
+ class_name = function.info.user_cls.__name__
162
+ raise click.UsageError(
163
+ f"Please specify a specific method of {class_name} to run, e.g. `modal run foo.py::MyClass.bar`" # noqa: E501
164
+ )
165
+ fun_signature = _get_signature(getattr(cls, method_name).info.raw_f, is_method=True)
153
166
  signature = dict(**cls_signature, **fun_signature) # Pool all arguments
154
167
  # TODO(erikbern): assert there's no overlap?
155
168
  else:
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.44",
34
+ version: str = "0.67.0",
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.44",
93
+ version: str = "0.67.0",
94
94
  ): ...
95
95
  def is_closed(self) -> bool: ...
96
96
  @property
modal/cls.py CHANGED
@@ -244,7 +244,7 @@ class _Cls(_Object, type_prefix="cs"):
244
244
  _class_service_function: Optional[
245
245
  _Function
246
246
  ] # The _Function serving *all* methods of the class, used for version >=v0.63
247
- _method_functions: Dict[str, _Function] # Placeholder _Functions for each method
247
+ _method_functions: Optional[Dict[str, _Function]] = None # Placeholder _Functions for each method
248
248
  _options: Optional[api_pb2.FunctionOptions]
249
249
  _callables: Dict[str, Callable[..., Any]]
250
250
  _from_other_workspace: Optional[bool] # Functions require FunctionBindParams before invocation.
@@ -253,7 +253,6 @@ class _Cls(_Object, type_prefix="cs"):
253
253
  def _initialize_from_empty(self):
254
254
  self._user_cls = None
255
255
  self._class_service_function = None
256
- self._method_functions = {}
257
256
  self._options = None
258
257
  self._callables = {}
259
258
  self._from_other_workspace = None
@@ -273,28 +272,46 @@ class _Cls(_Object, type_prefix="cs"):
273
272
 
274
273
  def _hydrate_metadata(self, metadata: Message):
275
274
  assert isinstance(metadata, api_pb2.ClassHandleMetadata)
276
-
277
- for method in metadata.methods:
278
- if method.function_name in self._method_functions:
279
- # This happens when the class is loaded locally
280
- # since each function will already be a loaded dependency _Function
281
- self._method_functions[method.function_name]._hydrate(
282
- method.function_id, self._client, method.function_handle_metadata
283
- )
275
+ if (
276
+ self._class_service_function
277
+ and self._class_service_function._method_handle_metadata
278
+ and len(self._class_service_function._method_handle_metadata)
279
+ ):
280
+ # The class only has a class service service function and no method placeholders (v0.67+)
281
+ if self._method_functions:
282
+ # We're here when the Cls is loaded locally (e.g. _Cls.from_local) so the _method_functions mapping is
283
+ # populated with (un-hydrated) _Function objects
284
+ for (
285
+ method_name,
286
+ method_handle_metadata,
287
+ ) in self._class_service_function._method_handle_metadata.items():
288
+ self._method_functions[method_name]._hydrate(
289
+ self._class_service_function.object_id, self._client, method_handle_metadata
290
+ )
284
291
  else:
292
+ # We're here when the function is loaded remotely (e.g. _Cls.from_name)
293
+ self._method_functions = {}
294
+ for (
295
+ method_name,
296
+ method_handle_metadata,
297
+ ) in self._class_service_function._method_handle_metadata.items():
298
+ self._method_functions[method_name] = _Function._new_hydrated(
299
+ self._class_service_function.object_id, self._client, method_handle_metadata
300
+ )
301
+ elif self._class_service_function:
302
+ # A class with a class service function and method placeholder functions
303
+ self._method_functions = {}
304
+ for method in metadata.methods:
285
305
  self._method_functions[method.function_name] = _Function._new_hydrated(
286
- method.function_id, self._client, method.function_handle_metadata
306
+ self._class_service_function.object_id, self._client, method.function_handle_metadata
287
307
  )
288
-
289
- def _get_metadata(self) -> api_pb2.ClassHandleMetadata:
290
- class_handle_metadata = api_pb2.ClassHandleMetadata()
291
- for f_name, f in self._method_functions.items():
292
- class_handle_metadata.methods.append(
293
- api_pb2.ClassMethod(
294
- function_name=f_name, function_id=f.object_id, function_handle_metadata=f._get_metadata()
308
+ else:
309
+ # pre 0.63 class that does not have a class service function and only method functions
310
+ self._method_functions = {}
311
+ for method in metadata.methods:
312
+ self._method_functions[method.function_name] = _Function._new_hydrated(
313
+ method.function_id, self._client, method.function_handle_metadata
295
314
  )
296
- )
297
- return class_handle_metadata
298
315
 
299
316
  @staticmethod
300
317
  def validate_construction_mechanism(user_cls):
@@ -327,16 +344,17 @@ class _Cls(_Object, type_prefix="cs"):
327
344
  # validate signature
328
345
  _Cls.validate_construction_mechanism(user_cls)
329
346
 
330
- functions: Dict[str, _Function] = {}
347
+ method_functions: Dict[str, _Function] = {}
331
348
  partial_functions: Dict[str, _PartialFunction] = _find_partial_methods_for_user_cls(
332
349
  user_cls, _PartialFunctionFlags.FUNCTION
333
350
  )
334
351
 
335
352
  for method_name, partial_function in partial_functions.items():
336
- method_function = class_service_function._bind_method_old(user_cls, method_name, partial_function)
337
- app._add_function(method_function, is_web_endpoint=partial_function.webhook_config is not None)
353
+ method_function = class_service_function._bind_method(user_cls, method_name, partial_function)
354
+ if partial_function.webhook_config is not None:
355
+ app._web_endpoints.append(method_function.tag)
338
356
  partial_function.wrapped = True
339
- functions[method_name] = method_function
357
+ method_functions[method_name] = method_function
340
358
 
341
359
  # Disable the warning that these are not wrapped
342
360
  for partial_function in _find_partial_methods_for_user_cls(user_cls, ~_PartialFunctionFlags.FUNCTION).values():
@@ -344,31 +362,17 @@ class _Cls(_Object, type_prefix="cs"):
344
362
 
345
363
  # Get all callables
346
364
  callables: Dict[str, Callable] = {
347
- k: pf.raw_f for k, pf in _find_partial_methods_for_user_cls(user_cls, ~_PartialFunctionFlags(0)).items()
365
+ k: pf.raw_f for k, pf in _find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.all()).items()
348
366
  }
349
367
 
350
368
  def _deps() -> List[_Function]:
351
- return [class_service_function] + list(functions.values())
369
+ return [class_service_function]
352
370
 
353
371
  async def _load(self: "_Cls", resolver: Resolver, existing_object_id: Optional[str]):
354
- req = api_pb2.ClassCreateRequest(app_id=resolver.app_id, existing_class_id=existing_object_id)
355
- for f_name, f in self._method_functions.items():
356
- req.methods.append(
357
- api_pb2.ClassMethod(
358
- function_name=f_name, function_id=f.object_id, function_handle_metadata=f._get_metadata()
359
- )
360
- )
372
+ req = api_pb2.ClassCreateRequest(
373
+ app_id=resolver.app_id, existing_class_id=existing_object_id, only_class_function=True
374
+ )
361
375
  resp = await resolver.client.stub.ClassCreate(req)
362
- # Even though we already have the function_handle_metadata for this method locally,
363
- # The RPC is going to replace it with function_handle_metadata derived from the server.
364
- # We need to overwrite the definition_id sent back from the server here with the definition_id
365
- # previously stored in function metadata, which may have been sent back from FunctionCreate.
366
- # The problem is that this metadata propagates back and overwrites the metadata on the Function
367
- # object itself. This is really messy. Maybe better to exclusively populate the method metadata
368
- # from the function metadata we already have locally? Really a lot to clean up here...
369
- for method in resp.handle_metadata.methods:
370
- f_metadata = self._method_functions[method.function_name]._get_metadata()
371
- method.function_handle_metadata.definition_id = f_metadata.definition_id
372
376
  self._hydrate(resp.class_id, resolver.client, resp.handle_metadata)
373
377
 
374
378
  rep = f"Cls({user_cls.__name__})"
@@ -376,7 +380,7 @@ class _Cls(_Object, type_prefix="cs"):
376
380
  cls._app = app
377
381
  cls._user_cls = user_cls
378
382
  cls._class_service_function = class_service_function
379
- cls._method_functions = functions
383
+ cls._method_functions = method_functions
380
384
  cls._callables = callables
381
385
  cls._from_other_workspace = False
382
386
  return cls
@@ -415,6 +419,7 @@ class _Cls(_Object, type_prefix="cs"):
415
419
  environment_name=_environment_name,
416
420
  lookup_published=workspace is not None,
417
421
  workspace_name=workspace,
422
+ only_class_function=True,
418
423
  )
419
424
  try:
420
425
  response = await retry_transient_errors(resolver.client.stub.ClassGet, request)
modal/cls.pyi CHANGED
@@ -86,7 +86,7 @@ class Obj:
86
86
  class _Cls(modal.object._Object):
87
87
  _user_cls: typing.Optional[type]
88
88
  _class_service_function: typing.Optional[modal.functions._Function]
89
- _method_functions: typing.Dict[str, modal.functions._Function]
89
+ _method_functions: typing.Optional[typing.Dict[str, modal.functions._Function]]
90
90
  _options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
91
91
  _callables: typing.Dict[str, typing.Callable[..., typing.Any]]
92
92
  _from_other_workspace: typing.Optional[bool]
@@ -96,7 +96,6 @@ class _Cls(modal.object._Object):
96
96
  def _initialize_from_other(self, other: _Cls): ...
97
97
  def _get_partial_functions(self) -> typing.Dict[str, modal.partial_function._PartialFunction]: ...
98
98
  def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
99
- def _get_metadata(self) -> modal_proto.api_pb2.ClassHandleMetadata: ...
100
99
  @staticmethod
101
100
  def validate_construction_mechanism(user_cls): ...
102
101
  @staticmethod
@@ -139,7 +138,7 @@ class _Cls(modal.object._Object):
139
138
  class Cls(modal.object.Object):
140
139
  _user_cls: typing.Optional[type]
141
140
  _class_service_function: typing.Optional[modal.functions.Function]
142
- _method_functions: typing.Dict[str, modal.functions.Function]
141
+ _method_functions: typing.Optional[typing.Dict[str, modal.functions.Function]]
143
142
  _options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
144
143
  _callables: typing.Dict[str, typing.Callable[..., typing.Any]]
145
144
  _from_other_workspace: typing.Optional[bool]
@@ -150,7 +149,6 @@ class Cls(modal.object.Object):
150
149
  def _initialize_from_other(self, other: Cls): ...
151
150
  def _get_partial_functions(self) -> typing.Dict[str, modal.partial_function.PartialFunction]: ...
152
151
  def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
153
- def _get_metadata(self) -> modal_proto.api_pb2.ClassHandleMetadata: ...
154
152
  @staticmethod
155
153
  def validate_construction_mechanism(user_cls): ...
156
154
  @staticmethod
modal/config.py CHANGED
@@ -268,7 +268,7 @@ class Config:
268
268
  return repr(self.to_dict())
269
269
 
270
270
  def to_dict(self):
271
- return {key: self.get(key) for key in _SETTINGS.keys()}
271
+ return {key: self.get(key) for key in sorted(_SETTINGS)}
272
272
 
273
273
 
274
274
  config = Config()