modal 1.0.3.dev10__py3-none-any.whl → 1.2.3.dev7__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.

Potentially problematic release.


This version of modal might be problematic. Click here for more details.

Files changed (160) hide show
  1. modal/__init__.py +0 -2
  2. modal/__main__.py +3 -4
  3. modal/_billing.py +80 -0
  4. modal/_clustered_functions.py +7 -3
  5. modal/_clustered_functions.pyi +15 -3
  6. modal/_container_entrypoint.py +51 -69
  7. modal/_functions.py +508 -240
  8. modal/_grpc_client.py +171 -0
  9. modal/_load_context.py +105 -0
  10. modal/_object.py +81 -21
  11. modal/_output.py +58 -45
  12. modal/_partial_function.py +48 -73
  13. modal/_pty.py +7 -3
  14. modal/_resolver.py +26 -46
  15. modal/_runtime/asgi.py +4 -3
  16. modal/_runtime/container_io_manager.py +358 -220
  17. modal/_runtime/container_io_manager.pyi +296 -101
  18. modal/_runtime/execution_context.py +18 -2
  19. modal/_runtime/execution_context.pyi +64 -7
  20. modal/_runtime/gpu_memory_snapshot.py +262 -57
  21. modal/_runtime/user_code_imports.py +28 -58
  22. modal/_serialization.py +90 -6
  23. modal/_traceback.py +42 -1
  24. modal/_tunnel.pyi +380 -12
  25. modal/_utils/async_utils.py +84 -29
  26. modal/_utils/auth_token_manager.py +111 -0
  27. modal/_utils/blob_utils.py +181 -58
  28. modal/_utils/deprecation.py +19 -0
  29. modal/_utils/function_utils.py +91 -47
  30. modal/_utils/grpc_utils.py +89 -66
  31. modal/_utils/mount_utils.py +26 -1
  32. modal/_utils/name_utils.py +17 -3
  33. modal/_utils/task_command_router_client.py +536 -0
  34. modal/_utils/time_utils.py +34 -6
  35. modal/app.py +256 -88
  36. modal/app.pyi +909 -92
  37. modal/billing.py +5 -0
  38. modal/builder/2025.06.txt +18 -0
  39. modal/builder/PREVIEW.txt +18 -0
  40. modal/builder/base-images.json +58 -0
  41. modal/cli/_download.py +19 -3
  42. modal/cli/_traceback.py +3 -2
  43. modal/cli/app.py +4 -4
  44. modal/cli/cluster.py +15 -7
  45. modal/cli/config.py +5 -3
  46. modal/cli/container.py +7 -6
  47. modal/cli/dict.py +22 -16
  48. modal/cli/entry_point.py +12 -5
  49. modal/cli/environment.py +5 -4
  50. modal/cli/import_refs.py +3 -3
  51. modal/cli/launch.py +102 -5
  52. modal/cli/network_file_system.py +11 -12
  53. modal/cli/profile.py +3 -2
  54. modal/cli/programs/launch_instance_ssh.py +94 -0
  55. modal/cli/programs/run_jupyter.py +1 -1
  56. modal/cli/programs/run_marimo.py +95 -0
  57. modal/cli/programs/vscode.py +1 -1
  58. modal/cli/queues.py +57 -26
  59. modal/cli/run.py +91 -23
  60. modal/cli/secret.py +48 -22
  61. modal/cli/token.py +7 -8
  62. modal/cli/utils.py +4 -7
  63. modal/cli/volume.py +31 -25
  64. modal/client.py +15 -85
  65. modal/client.pyi +183 -62
  66. modal/cloud_bucket_mount.py +5 -3
  67. modal/cloud_bucket_mount.pyi +197 -5
  68. modal/cls.py +200 -126
  69. modal/cls.pyi +446 -68
  70. modal/config.py +29 -11
  71. modal/container_process.py +319 -19
  72. modal/container_process.pyi +190 -20
  73. modal/dict.py +290 -71
  74. modal/dict.pyi +835 -83
  75. modal/environments.py +15 -27
  76. modal/environments.pyi +46 -24
  77. modal/exception.py +14 -2
  78. modal/experimental/__init__.py +194 -40
  79. modal/experimental/flash.py +618 -0
  80. modal/experimental/flash.pyi +380 -0
  81. modal/experimental/ipython.py +11 -7
  82. modal/file_io.py +29 -36
  83. modal/file_io.pyi +251 -53
  84. modal/file_pattern_matcher.py +56 -16
  85. modal/functions.pyi +673 -92
  86. modal/gpu.py +1 -1
  87. modal/image.py +528 -176
  88. modal/image.pyi +1572 -145
  89. modal/io_streams.py +458 -128
  90. modal/io_streams.pyi +433 -52
  91. modal/mount.py +216 -151
  92. modal/mount.pyi +225 -78
  93. modal/network_file_system.py +45 -62
  94. modal/network_file_system.pyi +277 -56
  95. modal/object.pyi +93 -17
  96. modal/parallel_map.py +942 -129
  97. modal/parallel_map.pyi +294 -15
  98. modal/partial_function.py +0 -2
  99. modal/partial_function.pyi +234 -19
  100. modal/proxy.py +17 -8
  101. modal/proxy.pyi +36 -3
  102. modal/queue.py +270 -65
  103. modal/queue.pyi +817 -57
  104. modal/runner.py +115 -101
  105. modal/runner.pyi +205 -49
  106. modal/sandbox.py +512 -136
  107. modal/sandbox.pyi +845 -111
  108. modal/schedule.py +1 -1
  109. modal/secret.py +300 -70
  110. modal/secret.pyi +589 -34
  111. modal/serving.py +7 -11
  112. modal/serving.pyi +7 -8
  113. modal/snapshot.py +11 -8
  114. modal/snapshot.pyi +25 -4
  115. modal/token_flow.py +4 -4
  116. modal/token_flow.pyi +28 -8
  117. modal/volume.py +416 -158
  118. modal/volume.pyi +1117 -121
  119. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
  120. modal-1.2.3.dev7.dist-info/RECORD +195 -0
  121. modal_docs/mdmd/mdmd.py +17 -4
  122. modal_proto/api.proto +534 -79
  123. modal_proto/api_grpc.py +337 -1
  124. modal_proto/api_pb2.py +1522 -968
  125. modal_proto/api_pb2.pyi +1619 -134
  126. modal_proto/api_pb2_grpc.py +699 -4
  127. modal_proto/api_pb2_grpc.pyi +226 -14
  128. modal_proto/modal_api_grpc.py +175 -154
  129. modal_proto/sandbox_router.proto +145 -0
  130. modal_proto/sandbox_router_grpc.py +105 -0
  131. modal_proto/sandbox_router_pb2.py +149 -0
  132. modal_proto/sandbox_router_pb2.pyi +333 -0
  133. modal_proto/sandbox_router_pb2_grpc.py +203 -0
  134. modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
  135. modal_proto/task_command_router.proto +144 -0
  136. modal_proto/task_command_router_grpc.py +105 -0
  137. modal_proto/task_command_router_pb2.py +149 -0
  138. modal_proto/task_command_router_pb2.pyi +333 -0
  139. modal_proto/task_command_router_pb2_grpc.py +203 -0
  140. modal_proto/task_command_router_pb2_grpc.pyi +75 -0
  141. modal_version/__init__.py +1 -1
  142. modal/requirements/PREVIEW.txt +0 -16
  143. modal/requirements/base-images.json +0 -26
  144. modal-1.0.3.dev10.dist-info/RECORD +0 -179
  145. modal_proto/modal_options_grpc.py +0 -3
  146. modal_proto/options.proto +0 -19
  147. modal_proto/options_grpc.py +0 -3
  148. modal_proto/options_pb2.py +0 -35
  149. modal_proto/options_pb2.pyi +0 -20
  150. modal_proto/options_pb2_grpc.py +0 -4
  151. modal_proto/options_pb2_grpc.pyi +0 -7
  152. /modal/{requirements → builder}/2023.12.312.txt +0 -0
  153. /modal/{requirements → builder}/2023.12.txt +0 -0
  154. /modal/{requirements → builder}/2024.04.txt +0 -0
  155. /modal/{requirements → builder}/2024.10.txt +0 -0
  156. /modal/{requirements → builder}/README.md +0 -0
  157. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
  158. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
  159. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
  160. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/cls.py CHANGED
@@ -1,10 +1,10 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import dataclasses
3
3
  import inspect
4
- import os
5
4
  import typing
6
5
  from collections.abc import Collection
7
- from typing import Any, Callable, Optional, TypeVar, Union
6
+ from pathlib import PurePosixPath
7
+ from typing import Any, Callable, Optional, Sequence, TypeVar, Union
8
8
 
9
9
  from google.protobuf.message import Message
10
10
  from grpclib import GRPCError, Status
@@ -12,7 +12,8 @@ from grpclib import GRPCError, Status
12
12
  from modal_proto import api_pb2
13
13
 
14
14
  from ._functions import _Function, _parse_retries
15
- from ._object import _Object
15
+ from ._load_context import LoadContext
16
+ from ._object import _Object, live_method
16
17
  from ._partial_function import (
17
18
  _find_callables_for_obj,
18
19
  _find_partial_methods_for_user_cls,
@@ -25,14 +26,18 @@ from ._serialization import check_valid_cls_constructor_arg
25
26
  from ._traceback import print_server_warnings
26
27
  from ._type_manager import parameter_serde_registry
27
28
  from ._utils.async_utils import synchronize_api, synchronizer
28
- from ._utils.deprecation import deprecation_warning, warn_on_renamed_autoscaler_settings
29
- from ._utils.grpc_utils import retry_transient_errors
29
+ from ._utils.deprecation import (
30
+ deprecation_warning,
31
+ warn_if_passing_namespace,
32
+ warn_on_renamed_autoscaler_settings,
33
+ )
30
34
  from ._utils.mount_utils import validate_volumes
31
35
  from .client import _Client
32
- from .config import config
36
+ from .cloud_bucket_mount import _CloudBucketMount
33
37
  from .exception import ExecutionError, InvalidError, NotFoundError
34
38
  from .gpu import GPU_T
35
39
  from .retries import Retries
40
+ from .scheduler_placement import SchedulerPlacement
36
41
  from .secret import _Secret
37
42
  from .volume import _Volume
38
43
 
@@ -54,7 +59,7 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature:
54
59
  return inspect.signature(user_cls)
55
60
  else:
56
61
  constructor_parameters = []
57
- for name, annotation_value in user_cls.__dict__.get("__annotations__", {}).items():
62
+ for name, annotation_value in typing.get_type_hints(user_cls).items():
58
63
  if hasattr(user_cls, name):
59
64
  parameter_spec = getattr(user_cls, name)
60
65
  if is_parameter(parameter_spec):
@@ -75,7 +80,8 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature:
75
80
 
76
81
  @dataclasses.dataclass()
77
82
  class _ServiceOptions:
78
- secrets: typing.Collection[_Secret] = ()
83
+ # Note that default values should always be "untruthy" so we can detect when they are not set
84
+ secrets: Collection[_Secret] = ()
79
85
  validated_volumes: typing.Sequence[tuple[str, _Volume]] = ()
80
86
  resources: Optional[api_pb2.Resources] = None
81
87
  retry_policy: Optional[api_pb2.FunctionRetryPolicy] = None
@@ -87,6 +93,29 @@ class _ServiceOptions:
87
93
  target_concurrent_inputs: Optional[int] = None
88
94
  batch_max_size: Optional[int] = None
89
95
  batch_wait_ms: Optional[int] = None
96
+ scheduler_placement: Optional[api_pb2.SchedulerPlacement] = None
97
+ cloud: Optional[str] = None
98
+ cloud_bucket_mounts: typing.Sequence[tuple[str, _CloudBucketMount]] = ()
99
+
100
+ def merge_options(self, new_options: "_ServiceOptions") -> "_ServiceOptions":
101
+ """Implement protobuf-like MergeFrom semantics for this dataclass.
102
+
103
+ This mostly exists to support "stacking" of `.with_options()` calls.
104
+ """
105
+ # Don't use dataclasses.asdict() because it does a deepcopy(), which chokes on a hydrated object
106
+ new_options_dict = {k.name: getattr(new_options, k.name) for k in dataclasses.fields(new_options)}
107
+
108
+ # Resources needs special merge handling because individual fields are parameters in the public API
109
+ merged_resources = api_pb2.Resources()
110
+ if self.resources:
111
+ merged_resources.MergeFrom(self.resources)
112
+ if new_resources := new_options_dict.pop("resources"):
113
+ merged_resources.MergeFrom(new_resources)
114
+ self.resources = merged_resources
115
+
116
+ for key, value in new_options_dict.items():
117
+ if value: # Only overwrite data when the value was set in the new options
118
+ setattr(self, key, value)
90
119
 
91
120
 
92
121
  def _bind_instance_method(cls: "_Cls", service_function: _Function, method_name: str):
@@ -106,7 +135,7 @@ def _bind_instance_method(cls: "_Cls", service_function: _Function, method_name:
106
135
  method_metadata = cls._method_metadata[method_name]
107
136
  new_function._hydrate(service_function.object_id, service_function.client, method_metadata)
108
137
 
109
- async def _load(fun: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
138
+ async def _load(fun: "_Function", resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]):
110
139
  # there is currently no actual loading logic executed to create each method on
111
140
  # the *parametrized* instance of a class - it uses the parameter-bound service-function
112
141
  # for the instance. This load method just makes sure to set all attributes after the
@@ -125,11 +154,14 @@ def _bind_instance_method(cls: "_Cls", service_function: _Function, method_name:
125
154
 
126
155
  rep = f"Method({cls._name}.{method_name})"
127
156
 
157
+ # Bound methods should *reference* their parent Cls's LoadContext
158
+ # so that it can be modified in place on the parent and be reflected in the method
128
159
  fun = _Function._from_loader(
129
160
  _load,
130
161
  rep,
131
162
  deps=_deps,
132
163
  hydrate_lazily=True,
164
+ load_context_overrides=cls._load_context_overrides,
133
165
  )
134
166
  if service_function.is_hydrated:
135
167
  # Eager hydration (skip load) if the instance service function is already loaded
@@ -393,14 +425,13 @@ class _Obj:
393
425
 
394
426
  # Not hydrated Cls, and we don't have the class - typically a Cls.from_name that
395
427
  # has not yet been loaded. So use a special loader that loads it lazily:
396
- async def method_loader(fun, resolver: Resolver, existing_object_id):
397
- await resolver.load(self._cls) # load class so we get info about methods
428
+ async def method_loader(fun, resolver: Resolver, load_context: LoadContext, existing_object_id):
398
429
  method_function = _get_maybe_method()
399
430
  if method_function is None:
400
431
  raise NotFoundError(
401
432
  f"Class has no method {k}, and attributes can't be accessed for `Cls.from_name` instances"
402
433
  )
403
- await resolver.load(method_function) # get the appropriate method handle (lazy)
434
+ await resolver.load(method_function, load_context) # get the appropriate method handle (lazy)
404
435
  fun._hydrate_from_other(method_function)
405
436
 
406
437
  # The reason we don't *always* use this lazy loader is because it precludes attribute access
@@ -408,8 +439,9 @@ class _Obj:
408
439
  return _Function._from_loader(
409
440
  method_loader,
410
441
  rep=f"Method({self._cls._name}.{k})",
411
- deps=lambda: [], # TODO: use cls as dep instead of loading inside method_loader?
442
+ deps=lambda: [self._cls],
412
443
  hydrate_lazily=True,
444
+ load_context_overrides=self._cls._load_context_overrides,
413
445
  )
414
446
 
415
447
 
@@ -418,11 +450,11 @@ Obj = synchronize_api(_Obj)
418
450
 
419
451
  class _Cls(_Object, type_prefix="cs"):
420
452
  """
421
- Cls adds method pooling and [lifecycle hook](/docs/guide/lifecycle-functions) behavior
422
- to [modal.Function](/docs/reference/modal.Function).
453
+ Cls adds method pooling and [lifecycle hook](https://modal.com/docs/guide/lifecycle-functions) behavior
454
+ to [modal.Function](https://modal.com/docs/reference/modal.Function).
423
455
 
424
456
  Generally, you will not construct a Cls directly.
425
- Instead, use the [`@app.cls()`](/docs/reference/modal.App#cls) decorator on the App object.
457
+ Instead, use the [`@app.cls()`](https://modal.com/docs/reference/modal.App#cls) decorator on the App object.
426
458
  """
427
459
 
428
460
  _class_service_function: Optional[_Function] # The _Function (read "service") serving *all* methods of the class
@@ -456,6 +488,7 @@ class _Cls(_Object, type_prefix="cs"):
456
488
  self._callables = other._callables
457
489
  self._name = other._name
458
490
  self._method_metadata = other._method_metadata
491
+ self._load_context_overrides = other._load_context_overrides
459
492
 
460
493
  def _get_partial_functions(self) -> dict[str, _PartialFunction]:
461
494
  if not self._user_cls:
@@ -482,6 +515,11 @@ class _Cls(_Object, type_prefix="cs"):
482
515
  # returns method names for a *local* class only for now (used by cli)
483
516
  return self._method_partials.keys()
484
517
 
518
+ @live_method
519
+ async def _experimental_get_flash_urls(self) -> Optional[list[str]]:
520
+ """URL of the flash service for the class."""
521
+ return await self._get_class_service_function()._experimental_get_flash_urls()
522
+
485
523
  def _hydrate_metadata(self, metadata: Message):
486
524
  assert isinstance(metadata, api_pb2.ClassHandleMetadata)
487
525
  class_service_function = self._get_class_service_function()
@@ -543,22 +581,15 @@ More information on class parameterization can be found here: https://modal.com/
543
581
  # validate signature
544
582
  _Cls.validate_construction_mechanism(user_cls)
545
583
 
546
- method_partials: dict[str, _PartialFunction] = _find_partial_methods_for_user_cls(
547
- user_cls, _PartialFunctionFlags.interface_flags()
548
- )
549
-
550
- for method_name, partial_function in method_partials.items():
551
- if partial_function.params.webhook_config is not None:
552
- full_name = f"{user_cls.__name__}.{method_name}"
553
- app._web_endpoints.append(full_name)
554
- partial_function.registered = True
555
-
556
584
  # Disable the warning that lifecycle methods are not wrapped
557
- for partial_function in _find_partial_methods_for_user_cls(
585
+ lifecycle_method_partials = _find_partial_methods_for_user_cls(
558
586
  user_cls, ~_PartialFunctionFlags.interface_flags()
559
- ).values():
587
+ )
588
+ for partial_function in lifecycle_method_partials.values():
560
589
  partial_function.registered = True
561
590
 
591
+ method_partials = _find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.interface_flags())
592
+
562
593
  # Get all callables
563
594
  callables: dict[str, Callable] = {
564
595
  k: pf.raw_f
@@ -569,15 +600,18 @@ More information on class parameterization can be found here: https://modal.com/
569
600
  def _deps() -> list[_Function]:
570
601
  return [class_service_function]
571
602
 
572
- async def _load(self: "_Cls", resolver: Resolver, existing_object_id: Optional[str]):
603
+ async def _load(self: "_Cls", resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]):
573
604
  req = api_pb2.ClassCreateRequest(
574
- app_id=resolver.app_id, existing_class_id=existing_object_id, only_class_function=True
605
+ app_id=load_context.app_id, existing_class_id=existing_object_id, only_class_function=True
575
606
  )
576
- resp = await resolver.client.stub.ClassCreate(req)
577
- self._hydrate(resp.class_id, resolver.client, resp.handle_metadata)
607
+ resp = await load_context.client.stub.ClassCreate(req)
608
+ self._hydrate(resp.class_id, load_context.client, resp.handle_metadata)
578
609
 
579
610
  rep = f"Cls({user_cls.__name__})"
580
- cls: _Cls = _Cls._from_loader(_load, rep, deps=_deps)
611
+ # Pass a *reference* to the App's LoadContext - this is important since the App is
612
+ # the only way to infer a LoadContext for an `@app.cls`, and the App doesn't
613
+ # get its client until *after* the Cls is created.
614
+ cls: _Cls = _Cls._from_loader(_load, rep, deps=_deps, load_context_overrides=app._root_load_context)
581
615
  cls._app = app
582
616
  cls._user_cls = user_cls
583
617
  cls._class_service_function = class_service_function
@@ -592,55 +626,67 @@ More information on class parameterization can be found here: https://modal.com/
592
626
  app_name: str,
593
627
  name: str,
594
628
  *,
595
- namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
629
+ namespace: Any = None, # mdmd:line-hidden
596
630
  environment_name: Optional[str] = None,
631
+ client: Optional["_Client"] = None,
597
632
  ) -> "_Cls":
598
633
  """Reference a Cls from a deployed App by its name.
599
634
 
600
- In contrast to `modal.Cls.lookup`, this is a lazy method
601
- that defers hydrating the local object with metadata from
602
- Modal servers until the first time it is actually used.
635
+ This is a lazy method that defers hydrating the local
636
+ object with metadata from Modal servers until the first
637
+ time it is actually used.
603
638
 
604
639
  ```python
605
640
  Model = modal.Cls.from_name("other-app", "Model")
606
641
  ```
607
642
  """
608
- _environment_name = environment_name or config.get("environment")
643
+ warn_if_passing_namespace(namespace, "modal.Cls.from_name")
609
644
 
610
- async def _load_remote(self: _Cls, resolver: Resolver, existing_object_id: Optional[str]):
645
+ async def _load_remote(
646
+ self: _Cls, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
647
+ ):
611
648
  request = api_pb2.ClassGetRequest(
612
649
  app_name=app_name,
613
650
  object_tag=name,
614
- namespace=namespace,
615
- environment_name=_environment_name,
651
+ environment_name=load_context.environment_name,
616
652
  only_class_function=True,
617
653
  )
618
654
  try:
619
- response = await retry_transient_errors(resolver.client.stub.ClassGet, request)
655
+ response = await load_context.client.stub.ClassGet(request)
656
+ except NotFoundError as exc:
657
+ env_context = (
658
+ f" (in the '{load_context.environment_name}' environment)" if load_context.environment_name else ""
659
+ )
660
+ raise NotFoundError(
661
+ f"Lookup failed for Cls '{name}' from the '{app_name}' app{env_context}: {exc}."
662
+ ) from None
620
663
  except GRPCError as exc:
621
- if exc.status == Status.NOT_FOUND:
622
- env_context = f" (in the '{environment_name}' environment)" if environment_name else ""
623
- raise NotFoundError(
624
- f"Lookup failed for Cls '{name}' from the '{app_name}' app{env_context}: {exc.message}."
625
- )
626
- elif exc.status == Status.FAILED_PRECONDITION:
627
- raise InvalidError(exc.message)
664
+ if exc.status == Status.FAILED_PRECONDITION:
665
+ raise InvalidError(exc.message) from None
628
666
  else:
629
667
  raise
630
668
 
631
669
  print_server_warnings(response.server_warnings)
632
- await resolver.load(self._class_service_function)
633
- self._hydrate(response.class_id, resolver.client, response.handle_metadata)
670
+ await resolver.load(self._class_service_function, load_context)
671
+ self._hydrate(response.class_id, load_context.client, response.handle_metadata)
634
672
 
635
- rep = f"Cls.from_name({app_name!r}, {name!r})"
636
- cls = cls._from_loader(_load_remote, rep, is_another_app=True, hydrate_lazily=True)
673
+ environment_rep = f", environment_name={environment_name!r}" if environment_name else ""
674
+ rep = f"Cls.from_name({app_name!r}, {name!r}{environment_rep})"
675
+
676
+ load_context_overrides = LoadContext(client=client, environment_name=environment_name)
677
+ cls = cls._from_loader(
678
+ _load_remote,
679
+ rep,
680
+ is_another_app=True,
681
+ hydrate_lazily=True,
682
+ load_context_overrides=load_context_overrides,
683
+ )
637
684
 
638
685
  class_service_name = f"{name}.*" # special name of the base service function for the class
639
686
  cls._class_service_function = _Function._from_name(
640
687
  app_name,
641
688
  class_service_name,
642
- namespace=namespace,
643
- environment_name=_environment_name,
689
+ load_context_overrides=load_context_overrides,
644
690
  )
645
691
  cls._name = name
646
692
  return cls
@@ -652,27 +698,47 @@ More information on class parameterization can be found here: https://modal.com/
652
698
  cpu: Optional[Union[float, tuple[float, float]]] = None,
653
699
  memory: Optional[Union[int, tuple[int, int]]] = None,
654
700
  gpu: GPU_T = None,
655
- secrets: Collection[_Secret] = (),
656
- volumes: dict[Union[str, os.PathLike], _Volume] = {},
701
+ env: Optional[dict[str, Optional[str]]] = None,
702
+ secrets: Optional[Collection[_Secret]] = None,
703
+ volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]] = {},
657
704
  retries: Optional[Union[int, Retries]] = None,
658
705
  max_containers: Optional[int] = None, # Limit on the number of containers that can be concurrently running.
659
706
  buffer_containers: Optional[int] = None, # Additional containers to scale up while Function is active.
660
707
  scaledown_window: Optional[int] = None, # Max amount of time a container can remain idle before scaling down.
661
708
  timeout: Optional[int] = None,
709
+ region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
710
+ cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
662
711
  # The following parameters are deprecated
663
712
  concurrency_limit: Optional[int] = None, # Now called `max_containers`
664
713
  container_idle_timeout: Optional[int] = None, # Now called `scaledown_window`
665
714
  allow_concurrent_inputs: Optional[int] = None, # See `.with_concurrency`
666
715
  ) -> "_Cls":
667
- """Create an instance of the Cls with configuration options overridden with new values.
716
+ """Override the static Function configuration at runtime.
717
+
718
+ This method will return a new instance of the cls that will autoscale independently of the
719
+ original instance. Note that options cannot be "unset" with this method (i.e., if a GPU
720
+ is configured in the `@app.cls()` decorator, passing `gpu=None` here will not create a
721
+ CPU-only instance).
668
722
 
669
723
  **Usage:**
670
724
 
725
+ You can use this method after looking up the Cls from a deployed App or if you have a
726
+ direct reference to a Cls from another Function or local entrypoint on its App:
727
+
671
728
  ```python notest
672
729
  Model = modal.Cls.from_name("my_app", "Model")
673
730
  ModelUsingGPU = Model.with_options(gpu="A100")
674
- ModelUsingGPU().generate.remote(42) # will run with an A100 GPU
731
+ ModelUsingGPU().generate.remote(input_prompt) # Run with an A100 GPU
675
732
  ```
733
+
734
+ The method can be called multiple times to "stack" updates:
735
+
736
+ ```python notest
737
+ Model.with_options(gpu="A100").with_options(scaledown_window=300) # Use an A100 with slow scaledown
738
+ ```
739
+
740
+ Note that container arguments (i.e. `volumes` and `secrets`) passed in subsequent calls
741
+ will not be merged.
676
742
  """
677
743
  retry_policy = _parse_retries(retries, f"Class {self.__name__}" if self._user_cls else "")
678
744
  if gpu or cpu or memory:
@@ -680,6 +746,8 @@ More information on class parameterization can be found here: https://modal.com/
680
746
  else:
681
747
  resources = None
682
748
 
749
+ scheduler_placement = SchedulerPlacement(region=region).proto if region else None
750
+
683
751
  if allow_concurrent_inputs is not None:
684
752
  deprecation_warning(
685
753
  (2025, 5, 9),
@@ -687,7 +755,7 @@ More information on class parameterization can be found here: https://modal.com/
687
755
  " please use the `.with_concurrency` method instead.",
688
756
  )
689
757
 
690
- async def _load_from_base(new_cls, resolver, existing_object_id):
758
+ async def _load_from_base(new_cls, resolver, load_context, existing_object_id):
691
759
  # this is a bit confusing, the cls will always have the same metadata
692
760
  # since it has the same *class* service function (i.e. "template")
693
761
  # But the (instance) service function for each Obj will be different
@@ -696,30 +764,51 @@ More information on class parameterization can be found here: https://modal.com/
696
764
  if not self.is_hydrated:
697
765
  # this should only happen for Cls.from_name instances
698
766
  # other classes should already be hydrated!
699
- await resolver.load(self)
767
+ await resolver.load(self, load_context)
700
768
 
701
769
  new_cls._initialize_from_other(self)
702
770
 
703
771
  def _deps():
704
772
  return []
705
773
 
706
- cls = _Cls._from_loader(_load_from_base, rep=f"{self._name}.with_options(...)", is_another_app=True, deps=_deps)
774
+ cls = _Cls._from_loader(
775
+ _load_from_base,
776
+ rep=f"{self._name}.with_options(...)",
777
+ is_another_app=True,
778
+ deps=_deps,
779
+ load_context_overrides=self._load_context_overrides,
780
+ hydrate_lazily=True,
781
+ )
707
782
  cls._initialize_from_other(self)
708
- cls._options = dataclasses.replace(
709
- cls._options,
783
+
784
+ # Validate volumes
785
+ validated_volumes = validate_volumes(volumes)
786
+ cloud_bucket_mounts = [(k, v) for k, v in validated_volumes if isinstance(v, _CloudBucketMount)]
787
+ validated_volumes_no_cloud_buckets = [(k, v) for k, v in validated_volumes if isinstance(v, _Volume)]
788
+
789
+ secrets = secrets or []
790
+ if env:
791
+ secrets = [*secrets, _Secret.from_dict(env)]
792
+
793
+ new_options = _ServiceOptions(
710
794
  secrets=secrets,
795
+ validated_volumes=validated_volumes_no_cloud_buckets,
796
+ cloud_bucket_mounts=cloud_bucket_mounts,
711
797
  resources=resources,
712
798
  retry_policy=retry_policy,
713
799
  max_containers=max_containers,
714
800
  buffer_containers=buffer_containers,
715
801
  scaledown_window=scaledown_window,
716
802
  timeout_secs=timeout,
717
- validated_volumes=validate_volumes(volumes),
803
+ scheduler_placement=scheduler_placement,
804
+ cloud=cloud,
718
805
  # Note: set both for backwards / forwards compatibility
719
806
  # But going forward `.with_concurrency` is the preferred method with distinct parameterization
720
807
  max_concurrent_inputs=allow_concurrent_inputs,
721
808
  target_concurrent_inputs=allow_concurrent_inputs,
722
809
  )
810
+
811
+ cls._options.merge_options(new_options)
723
812
  return cls
724
813
 
725
814
  def with_concurrency(self: "_Cls", *, max_inputs: int, target_inputs: Optional[int] = None) -> "_Cls":
@@ -734,21 +823,26 @@ More information on class parameterization can be found here: https://modal.com/
734
823
  ```
735
824
  """
736
825
 
737
- async def _load_from_base(new_cls, resolver, existing_object_id):
826
+ async def _load_from_base(new_cls, resolver, load_context, existing_object_id):
738
827
  if not self.is_hydrated:
739
- await resolver.load(self)
828
+ await resolver.load(self, load_context)
740
829
  new_cls._initialize_from_other(self)
741
830
 
742
831
  def _deps():
743
832
  return []
744
833
 
745
834
  cls = _Cls._from_loader(
746
- _load_from_base, rep=f"{self._name}.with_concurrency(...)", is_another_app=True, deps=_deps
835
+ _load_from_base,
836
+ rep=f"{self._name}.with_concurrency(...)",
837
+ is_another_app=True,
838
+ deps=_deps,
839
+ load_context_overrides=self._load_context_overrides,
840
+ hydrate_lazily=True,
747
841
  )
748
842
  cls._initialize_from_other(self)
749
- cls._options = dataclasses.replace(
750
- cls._options, max_concurrent_inputs=max_inputs, target_concurrent_inputs=target_inputs
751
- )
843
+
844
+ concurrency_options = _ServiceOptions(max_concurrent_inputs=max_inputs, target_concurrent_inputs=target_inputs)
845
+ cls._options.merge_options(concurrency_options)
752
846
  return cls
753
847
 
754
848
  def with_batching(self: "_Cls", *, max_batch_size: int, wait_ms: int) -> "_Cls":
@@ -763,60 +857,27 @@ More information on class parameterization can be found here: https://modal.com/
763
857
  ```
764
858
  """
765
859
 
766
- async def _load_from_base(new_cls, resolver, existing_object_id):
860
+ async def _load_from_base(new_cls, resolver, load_context, existing_object_id):
767
861
  if not self.is_hydrated:
768
- await resolver.load(self)
862
+ await resolver.load(self, load_context)
769
863
  new_cls._initialize_from_other(self)
770
864
 
771
865
  def _deps():
772
866
  return []
773
867
 
774
868
  cls = _Cls._from_loader(
775
- _load_from_base, rep=f"{self._name}.with_concurrency(...)", is_another_app=True, deps=_deps
869
+ _load_from_base,
870
+ rep=f"{self._name}.with_concurrency(...)",
871
+ is_another_app=True,
872
+ deps=_deps,
873
+ load_context_overrides=self._load_context_overrides,
874
+ hydrate_lazily=True,
776
875
  )
777
876
  cls._initialize_from_other(self)
778
- cls._options = dataclasses.replace(cls._options, batch_max_size=max_batch_size, batch_wait_ms=wait_ms)
779
- return cls
780
-
781
- @staticmethod
782
- async def lookup(
783
- app_name: str,
784
- name: str,
785
- namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
786
- client: Optional[_Client] = None,
787
- environment_name: Optional[str] = None,
788
- ) -> "_Cls":
789
- """mdmd:hidden
790
- Lookup a Cls from a deployed App by its name.
791
-
792
- DEPRECATED: This method is deprecated in favor of `modal.Cls.from_name`.
793
877
 
794
- In contrast to `modal.Cls.from_name`, this is an eager method
795
- that will hydrate the local object with metadata from Modal servers.
796
-
797
- ```python notest
798
- Model = modal.Cls.from_name("other-app", "Model")
799
- model = Model()
800
- model.inference(...)
801
- ```
802
- """
803
- deprecation_warning(
804
- (2025, 1, 27),
805
- "`modal.Cls.lookup` is deprecated and will be removed in a future release."
806
- " It can be replaced with `modal.Cls.from_name`."
807
- "\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
808
- )
809
- obj = _Cls.from_name(
810
- app_name,
811
- name,
812
- namespace=namespace,
813
- environment_name=environment_name,
814
- )
815
- if client is None:
816
- client = await _Client.from_env()
817
- resolver = Resolver(client=client)
818
- await resolver.load(obj)
819
- return obj
878
+ batching_options = _ServiceOptions(batch_max_size=max_batch_size, batch_wait_ms=wait_ms)
879
+ cls._options.merge_options(batching_options)
880
+ return cls
820
881
 
821
882
  @synchronizer.no_input_translation
822
883
  def __call__(self, *args, **kwargs) -> _Obj:
@@ -830,18 +891,31 @@ More information on class parameterization can be found here: https://modal.com/
830
891
  )
831
892
 
832
893
  def __getattr__(self, k):
833
- # TODO: remove this method - access to attributes on classes (not instances) should be discouraged
834
- if not self._is_local() or k in self._method_partials:
835
- # if not local (== k *could* be a method) or it is local and we know k is a method
836
- deprecation_warning(
837
- (2025, 1, 13),
838
- "Calling a method on an uninstantiated class will soon be deprecated; "
839
- "update your code to instantiate the class first, i.e.:\n"
840
- f"{self._name}().{k} instead of {self._name}.{k}",
894
+ if self._user_cls is not None:
895
+ # local class, we can check if there are static attributes and let the user access them
896
+ # except if they are PartialFunction (i.e. methods)
897
+ v = getattr(self._user_cls, k)
898
+ if not isinstance(v, modal.partial_function.PartialFunction):
899
+ return v
900
+
901
+ # We create a synthetic dummy Function that is guaranteed to raise an AttributeError when
902
+ # a user tries to use any of its "live methods" - this lets us raise exceptions for users
903
+ # only if they try to access methods on a Cls as if they were methods on the instance.
904
+ async def error_loader(
905
+ fun: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
906
+ ):
907
+ raise AttributeError(
908
+ "You can't access methods on a Cls directly - Did you forget to instantiate the class first?\n"
909
+ "e.g. instead of MyClass.method.remote(), do MyClass().method.remote()"
841
910
  )
842
- return getattr(self(), k)
843
- # non-method attribute access on local class - arguably shouldn't be used either:
844
- return getattr(self._user_cls, k)
911
+
912
+ return _Function._from_loader(
913
+ error_loader,
914
+ rep=f"UnboundMethod({self._name}.{k})",
915
+ deps=lambda: [],
916
+ hydrate_lazily=True,
917
+ load_context_overrides=self._load_context_overrides,
918
+ )
845
919
 
846
920
  def _is_local(self) -> bool:
847
921
  return self._user_cls is not None