modal 1.0.6.dev58__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 (147) hide show
  1. modal/__main__.py +3 -4
  2. modal/_billing.py +80 -0
  3. modal/_clustered_functions.py +7 -3
  4. modal/_clustered_functions.pyi +4 -2
  5. modal/_container_entrypoint.py +41 -49
  6. modal/_functions.py +424 -195
  7. modal/_grpc_client.py +171 -0
  8. modal/_load_context.py +105 -0
  9. modal/_object.py +68 -20
  10. modal/_output.py +58 -45
  11. modal/_partial_function.py +36 -11
  12. modal/_pty.py +7 -3
  13. modal/_resolver.py +21 -35
  14. modal/_runtime/asgi.py +4 -3
  15. modal/_runtime/container_io_manager.py +301 -186
  16. modal/_runtime/container_io_manager.pyi +70 -61
  17. modal/_runtime/execution_context.py +18 -2
  18. modal/_runtime/execution_context.pyi +4 -1
  19. modal/_runtime/gpu_memory_snapshot.py +170 -63
  20. modal/_runtime/user_code_imports.py +28 -58
  21. modal/_serialization.py +57 -1
  22. modal/_utils/async_utils.py +33 -12
  23. modal/_utils/auth_token_manager.py +2 -5
  24. modal/_utils/blob_utils.py +110 -53
  25. modal/_utils/function_utils.py +49 -42
  26. modal/_utils/grpc_utils.py +80 -50
  27. modal/_utils/mount_utils.py +26 -1
  28. modal/_utils/name_utils.py +17 -3
  29. modal/_utils/task_command_router_client.py +536 -0
  30. modal/_utils/time_utils.py +34 -6
  31. modal/app.py +219 -83
  32. modal/app.pyi +229 -56
  33. modal/billing.py +5 -0
  34. modal/{requirements → builder}/2025.06.txt +1 -0
  35. modal/{requirements → builder}/PREVIEW.txt +1 -0
  36. modal/cli/_download.py +19 -3
  37. modal/cli/_traceback.py +3 -2
  38. modal/cli/app.py +4 -4
  39. modal/cli/cluster.py +15 -7
  40. modal/cli/config.py +5 -3
  41. modal/cli/container.py +7 -6
  42. modal/cli/dict.py +22 -16
  43. modal/cli/entry_point.py +12 -5
  44. modal/cli/environment.py +5 -4
  45. modal/cli/import_refs.py +3 -3
  46. modal/cli/launch.py +102 -5
  47. modal/cli/network_file_system.py +9 -13
  48. modal/cli/profile.py +3 -2
  49. modal/cli/programs/launch_instance_ssh.py +94 -0
  50. modal/cli/programs/run_jupyter.py +1 -1
  51. modal/cli/programs/run_marimo.py +95 -0
  52. modal/cli/programs/vscode.py +1 -1
  53. modal/cli/queues.py +57 -26
  54. modal/cli/run.py +58 -16
  55. modal/cli/secret.py +48 -22
  56. modal/cli/utils.py +3 -4
  57. modal/cli/volume.py +28 -25
  58. modal/client.py +13 -116
  59. modal/client.pyi +9 -91
  60. modal/cloud_bucket_mount.py +5 -3
  61. modal/cloud_bucket_mount.pyi +5 -1
  62. modal/cls.py +130 -102
  63. modal/cls.pyi +45 -85
  64. modal/config.py +29 -10
  65. modal/container_process.py +291 -13
  66. modal/container_process.pyi +95 -32
  67. modal/dict.py +282 -63
  68. modal/dict.pyi +423 -73
  69. modal/environments.py +15 -27
  70. modal/environments.pyi +5 -15
  71. modal/exception.py +8 -0
  72. modal/experimental/__init__.py +143 -38
  73. modal/experimental/flash.py +247 -78
  74. modal/experimental/flash.pyi +137 -9
  75. modal/file_io.py +14 -28
  76. modal/file_io.pyi +2 -2
  77. modal/file_pattern_matcher.py +25 -16
  78. modal/functions.pyi +134 -61
  79. modal/image.py +255 -86
  80. modal/image.pyi +300 -62
  81. modal/io_streams.py +436 -126
  82. modal/io_streams.pyi +236 -171
  83. modal/mount.py +62 -157
  84. modal/mount.pyi +45 -172
  85. modal/network_file_system.py +30 -53
  86. modal/network_file_system.pyi +16 -76
  87. modal/object.pyi +42 -8
  88. modal/parallel_map.py +821 -113
  89. modal/parallel_map.pyi +134 -0
  90. modal/partial_function.pyi +4 -1
  91. modal/proxy.py +16 -7
  92. modal/proxy.pyi +10 -2
  93. modal/queue.py +263 -61
  94. modal/queue.pyi +409 -66
  95. modal/runner.py +112 -92
  96. modal/runner.pyi +45 -27
  97. modal/sandbox.py +451 -124
  98. modal/sandbox.pyi +513 -67
  99. modal/secret.py +291 -67
  100. modal/secret.pyi +425 -19
  101. modal/serving.py +7 -11
  102. modal/serving.pyi +7 -8
  103. modal/snapshot.py +11 -8
  104. modal/token_flow.py +4 -4
  105. modal/volume.py +344 -98
  106. modal/volume.pyi +464 -68
  107. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +9 -8
  108. modal-1.2.3.dev7.dist-info/RECORD +195 -0
  109. modal_docs/mdmd/mdmd.py +11 -1
  110. modal_proto/api.proto +399 -67
  111. modal_proto/api_grpc.py +241 -1
  112. modal_proto/api_pb2.py +1395 -1000
  113. modal_proto/api_pb2.pyi +1239 -79
  114. modal_proto/api_pb2_grpc.py +499 -4
  115. modal_proto/api_pb2_grpc.pyi +162 -14
  116. modal_proto/modal_api_grpc.py +175 -160
  117. modal_proto/sandbox_router.proto +145 -0
  118. modal_proto/sandbox_router_grpc.py +105 -0
  119. modal_proto/sandbox_router_pb2.py +149 -0
  120. modal_proto/sandbox_router_pb2.pyi +333 -0
  121. modal_proto/sandbox_router_pb2_grpc.py +203 -0
  122. modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
  123. modal_proto/task_command_router.proto +144 -0
  124. modal_proto/task_command_router_grpc.py +105 -0
  125. modal_proto/task_command_router_pb2.py +149 -0
  126. modal_proto/task_command_router_pb2.pyi +333 -0
  127. modal_proto/task_command_router_pb2_grpc.py +203 -0
  128. modal_proto/task_command_router_pb2_grpc.pyi +75 -0
  129. modal_version/__init__.py +1 -1
  130. modal-1.0.6.dev58.dist-info/RECORD +0 -183
  131. modal_proto/modal_options_grpc.py +0 -3
  132. modal_proto/options.proto +0 -19
  133. modal_proto/options_grpc.py +0 -3
  134. modal_proto/options_pb2.py +0 -35
  135. modal_proto/options_pb2.pyi +0 -20
  136. modal_proto/options_pb2_grpc.py +0 -4
  137. modal_proto/options_pb2_grpc.pyi +0 -7
  138. /modal/{requirements → builder}/2023.12.312.txt +0 -0
  139. /modal/{requirements → builder}/2023.12.txt +0 -0
  140. /modal/{requirements → builder}/2024.04.txt +0 -0
  141. /modal/{requirements → builder}/2024.10.txt +0 -0
  142. /modal/{requirements → builder}/README.md +0 -0
  143. /modal/{requirements → builder}/base-images.json +0 -0
  144. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
  145. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
  146. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
  147. {modal-1.0.6.dev58.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,
@@ -30,13 +31,13 @@ from ._utils.deprecation import (
30
31
  warn_if_passing_namespace,
31
32
  warn_on_renamed_autoscaler_settings,
32
33
  )
33
- from ._utils.grpc_utils import retry_transient_errors
34
34
  from ._utils.mount_utils import validate_volumes
35
35
  from .client import _Client
36
- from .config import config
36
+ from .cloud_bucket_mount import _CloudBucketMount
37
37
  from .exception import ExecutionError, InvalidError, NotFoundError
38
38
  from .gpu import GPU_T
39
39
  from .retries import Retries
40
+ from .scheduler_placement import SchedulerPlacement
40
41
  from .secret import _Secret
41
42
  from .volume import _Volume
42
43
 
@@ -80,7 +81,7 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature:
80
81
  @dataclasses.dataclass()
81
82
  class _ServiceOptions:
82
83
  # Note that default values should always be "untruthy" so we can detect when they are not set
83
- secrets: typing.Collection[_Secret] = ()
84
+ secrets: Collection[_Secret] = ()
84
85
  validated_volumes: typing.Sequence[tuple[str, _Volume]] = ()
85
86
  resources: Optional[api_pb2.Resources] = None
86
87
  retry_policy: Optional[api_pb2.FunctionRetryPolicy] = None
@@ -92,6 +93,9 @@ class _ServiceOptions:
92
93
  target_concurrent_inputs: Optional[int] = None
93
94
  batch_max_size: Optional[int] = None
94
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]] = ()
95
99
 
96
100
  def merge_options(self, new_options: "_ServiceOptions") -> "_ServiceOptions":
97
101
  """Implement protobuf-like MergeFrom semantics for this dataclass.
@@ -131,7 +135,7 @@ def _bind_instance_method(cls: "_Cls", service_function: _Function, method_name:
131
135
  method_metadata = cls._method_metadata[method_name]
132
136
  new_function._hydrate(service_function.object_id, service_function.client, method_metadata)
133
137
 
134
- 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]):
135
139
  # there is currently no actual loading logic executed to create each method on
136
140
  # the *parametrized* instance of a class - it uses the parameter-bound service-function
137
141
  # for the instance. This load method just makes sure to set all attributes after the
@@ -150,11 +154,14 @@ def _bind_instance_method(cls: "_Cls", service_function: _Function, method_name:
150
154
 
151
155
  rep = f"Method({cls._name}.{method_name})"
152
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
153
159
  fun = _Function._from_loader(
154
160
  _load,
155
161
  rep,
156
162
  deps=_deps,
157
163
  hydrate_lazily=True,
164
+ load_context_overrides=cls._load_context_overrides,
158
165
  )
159
166
  if service_function.is_hydrated:
160
167
  # Eager hydration (skip load) if the instance service function is already loaded
@@ -418,14 +425,13 @@ class _Obj:
418
425
 
419
426
  # Not hydrated Cls, and we don't have the class - typically a Cls.from_name that
420
427
  # has not yet been loaded. So use a special loader that loads it lazily:
421
- async def method_loader(fun, resolver: Resolver, existing_object_id):
422
- 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):
423
429
  method_function = _get_maybe_method()
424
430
  if method_function is None:
425
431
  raise NotFoundError(
426
432
  f"Class has no method {k}, and attributes can't be accessed for `Cls.from_name` instances"
427
433
  )
428
- 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)
429
435
  fun._hydrate_from_other(method_function)
430
436
 
431
437
  # The reason we don't *always* use this lazy loader is because it precludes attribute access
@@ -433,8 +439,9 @@ class _Obj:
433
439
  return _Function._from_loader(
434
440
  method_loader,
435
441
  rep=f"Method({self._cls._name}.{k})",
436
- deps=lambda: [], # TODO: use cls as dep instead of loading inside method_loader?
442
+ deps=lambda: [self._cls],
437
443
  hydrate_lazily=True,
444
+ load_context_overrides=self._cls._load_context_overrides,
438
445
  )
439
446
 
440
447
 
@@ -481,6 +488,7 @@ class _Cls(_Object, type_prefix="cs"):
481
488
  self._callables = other._callables
482
489
  self._name = other._name
483
490
  self._method_metadata = other._method_metadata
491
+ self._load_context_overrides = other._load_context_overrides
484
492
 
485
493
  def _get_partial_functions(self) -> dict[str, _PartialFunction]:
486
494
  if not self._user_cls:
@@ -507,6 +515,11 @@ class _Cls(_Object, type_prefix="cs"):
507
515
  # returns method names for a *local* class only for now (used by cli)
508
516
  return self._method_partials.keys()
509
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
+
510
523
  def _hydrate_metadata(self, metadata: Message):
511
524
  assert isinstance(metadata, api_pb2.ClassHandleMetadata)
512
525
  class_service_function = self._get_class_service_function()
@@ -568,22 +581,15 @@ More information on class parameterization can be found here: https://modal.com/
568
581
  # validate signature
569
582
  _Cls.validate_construction_mechanism(user_cls)
570
583
 
571
- method_partials: dict[str, _PartialFunction] = _find_partial_methods_for_user_cls(
572
- user_cls, _PartialFunctionFlags.interface_flags()
573
- )
574
-
575
- for method_name, partial_function in method_partials.items():
576
- if partial_function.params.webhook_config is not None:
577
- full_name = f"{user_cls.__name__}.{method_name}"
578
- app._web_endpoints.append(full_name)
579
- partial_function.registered = True
580
-
581
584
  # Disable the warning that lifecycle methods are not wrapped
582
- for partial_function in _find_partial_methods_for_user_cls(
585
+ lifecycle_method_partials = _find_partial_methods_for_user_cls(
583
586
  user_cls, ~_PartialFunctionFlags.interface_flags()
584
- ).values():
587
+ )
588
+ for partial_function in lifecycle_method_partials.values():
585
589
  partial_function.registered = True
586
590
 
591
+ method_partials = _find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.interface_flags())
592
+
587
593
  # Get all callables
588
594
  callables: dict[str, Callable] = {
589
595
  k: pf.raw_f
@@ -594,15 +600,18 @@ More information on class parameterization can be found here: https://modal.com/
594
600
  def _deps() -> list[_Function]:
595
601
  return [class_service_function]
596
602
 
597
- 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]):
598
604
  req = api_pb2.ClassCreateRequest(
599
- 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
600
606
  )
601
- resp = await resolver.client.stub.ClassCreate(req)
602
- 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)
603
609
 
604
610
  rep = f"Cls({user_cls.__name__})"
605
- 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)
606
615
  cls._app = app
607
616
  cls._user_cls = user_cls
608
617
  cls._class_service_function = class_service_function
@@ -619,6 +628,7 @@ More information on class parameterization can be found here: https://modal.com/
619
628
  *,
620
629
  namespace: Any = None, # mdmd:line-hidden
621
630
  environment_name: Optional[str] = None,
631
+ client: Optional["_Client"] = None,
622
632
  ) -> "_Cls":
623
633
  """Reference a Cls from a deployed App by its name.
624
634
 
@@ -631,19 +641,22 @@ More information on class parameterization can be found here: https://modal.com/
631
641
  ```
632
642
  """
633
643
  warn_if_passing_namespace(namespace, "modal.Cls.from_name")
634
- _environment_name = environment_name or config.get("environment")
635
644
 
636
- 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
+ ):
637
648
  request = api_pb2.ClassGetRequest(
638
649
  app_name=app_name,
639
650
  object_tag=name,
640
- environment_name=_environment_name,
651
+ environment_name=load_context.environment_name,
641
652
  only_class_function=True,
642
653
  )
643
654
  try:
644
- response = await retry_transient_errors(resolver.client.stub.ClassGet, request)
655
+ response = await load_context.client.stub.ClassGet(request)
645
656
  except NotFoundError as exc:
646
- env_context = f" (in the '{environment_name}' environment)" if environment_name else ""
657
+ env_context = (
658
+ f" (in the '{load_context.environment_name}' environment)" if load_context.environment_name else ""
659
+ )
647
660
  raise NotFoundError(
648
661
  f"Lookup failed for Cls '{name}' from the '{app_name}' app{env_context}: {exc}."
649
662
  ) from None
@@ -654,18 +667,26 @@ More information on class parameterization can be found here: https://modal.com/
654
667
  raise
655
668
 
656
669
  print_server_warnings(response.server_warnings)
657
- await resolver.load(self._class_service_function)
658
- 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)
672
+
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})"
659
675
 
660
- rep = f"Cls.from_name({app_name!r}, {name!r})"
661
- cls = cls._from_loader(_load_remote, rep, is_another_app=True, hydrate_lazily=True)
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
+ )
662
684
 
663
685
  class_service_name = f"{name}.*" # special name of the base service function for the class
664
686
  cls._class_service_function = _Function._from_name(
665
687
  app_name,
666
688
  class_service_name,
667
- namespace=namespace,
668
- environment_name=_environment_name,
689
+ load_context_overrides=load_context_overrides,
669
690
  )
670
691
  cls._name = name
671
692
  return cls
@@ -677,13 +698,16 @@ More information on class parameterization can be found here: https://modal.com/
677
698
  cpu: Optional[Union[float, tuple[float, float]]] = None,
678
699
  memory: Optional[Union[int, tuple[int, int]]] = None,
679
700
  gpu: GPU_T = None,
680
- secrets: Collection[_Secret] = (),
681
- 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]] = {},
682
704
  retries: Optional[Union[int, Retries]] = None,
683
705
  max_containers: Optional[int] = None, # Limit on the number of containers that can be concurrently running.
684
706
  buffer_containers: Optional[int] = None, # Additional containers to scale up while Function is active.
685
707
  scaledown_window: Optional[int] = None, # Max amount of time a container can remain idle before scaling down.
686
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.
687
711
  # The following parameters are deprecated
688
712
  concurrency_limit: Optional[int] = None, # Now called `max_containers`
689
713
  container_idle_timeout: Optional[int] = None, # Now called `scaledown_window`
@@ -722,6 +746,8 @@ More information on class parameterization can be found here: https://modal.com/
722
746
  else:
723
747
  resources = None
724
748
 
749
+ scheduler_placement = SchedulerPlacement(region=region).proto if region else None
750
+
725
751
  if allow_concurrent_inputs is not None:
726
752
  deprecation_warning(
727
753
  (2025, 5, 9),
@@ -729,7 +755,7 @@ More information on class parameterization can be found here: https://modal.com/
729
755
  " please use the `.with_concurrency` method instead.",
730
756
  )
731
757
 
732
- 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):
733
759
  # this is a bit confusing, the cls will always have the same metadata
734
760
  # since it has the same *class* service function (i.e. "template")
735
761
  # But the (instance) service function for each Obj will be different
@@ -738,25 +764,44 @@ More information on class parameterization can be found here: https://modal.com/
738
764
  if not self.is_hydrated:
739
765
  # this should only happen for Cls.from_name instances
740
766
  # other classes should already be hydrated!
741
- await resolver.load(self)
767
+ await resolver.load(self, load_context)
742
768
 
743
769
  new_cls._initialize_from_other(self)
744
770
 
745
771
  def _deps():
746
772
  return []
747
773
 
748
- 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
+ )
749
782
  cls._initialize_from_other(self)
750
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
+
751
793
  new_options = _ServiceOptions(
752
794
  secrets=secrets,
753
- validated_volumes=validate_volumes(volumes),
795
+ validated_volumes=validated_volumes_no_cloud_buckets,
796
+ cloud_bucket_mounts=cloud_bucket_mounts,
754
797
  resources=resources,
755
798
  retry_policy=retry_policy,
756
799
  max_containers=max_containers,
757
800
  buffer_containers=buffer_containers,
758
801
  scaledown_window=scaledown_window,
759
802
  timeout_secs=timeout,
803
+ scheduler_placement=scheduler_placement,
804
+ cloud=cloud,
760
805
  # Note: set both for backwards / forwards compatibility
761
806
  # But going forward `.with_concurrency` is the preferred method with distinct parameterization
762
807
  max_concurrent_inputs=allow_concurrent_inputs,
@@ -778,16 +823,21 @@ More information on class parameterization can be found here: https://modal.com/
778
823
  ```
779
824
  """
780
825
 
781
- 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):
782
827
  if not self.is_hydrated:
783
- await resolver.load(self)
828
+ await resolver.load(self, load_context)
784
829
  new_cls._initialize_from_other(self)
785
830
 
786
831
  def _deps():
787
832
  return []
788
833
 
789
834
  cls = _Cls._from_loader(
790
- _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,
791
841
  )
792
842
  cls._initialize_from_other(self)
793
843
 
@@ -807,16 +857,21 @@ More information on class parameterization can be found here: https://modal.com/
807
857
  ```
808
858
  """
809
859
 
810
- 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):
811
861
  if not self.is_hydrated:
812
- await resolver.load(self)
862
+ await resolver.load(self, load_context)
813
863
  new_cls._initialize_from_other(self)
814
864
 
815
865
  def _deps():
816
866
  return []
817
867
 
818
868
  cls = _Cls._from_loader(
819
- _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,
820
875
  )
821
876
  cls._initialize_from_other(self)
822
877
 
@@ -824,46 +879,6 @@ More information on class parameterization can be found here: https://modal.com/
824
879
  cls._options.merge_options(batching_options)
825
880
  return cls
826
881
 
827
- @staticmethod
828
- async def lookup(
829
- app_name: str,
830
- name: str,
831
- namespace=None, # mdmd:line-hidden
832
- client: Optional[_Client] = None,
833
- environment_name: Optional[str] = None,
834
- ) -> "_Cls":
835
- """mdmd:hidden
836
- Lookup a Cls from a deployed App by its name.
837
-
838
- DEPRECATED: This method is deprecated in favor of `modal.Cls.from_name`.
839
-
840
- In contrast to `modal.Cls.from_name`, this is an eager method
841
- that will hydrate the local object with metadata from Modal servers.
842
-
843
- ```python notest
844
- Model = modal.Cls.from_name("other-app", "Model")
845
- model = Model()
846
- model.inference(...)
847
- ```
848
- """
849
- deprecation_warning(
850
- (2025, 1, 27),
851
- "`modal.Cls.lookup` is deprecated and will be removed in a future release."
852
- " It can be replaced with `modal.Cls.from_name`."
853
- "\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
854
- )
855
- warn_if_passing_namespace(namespace, "modal.Cls.lookup")
856
- obj = _Cls.from_name(
857
- app_name,
858
- name,
859
- environment_name=environment_name,
860
- )
861
- if client is None:
862
- client = await _Client.from_env()
863
- resolver = Resolver(client=client)
864
- await resolver.load(obj)
865
- return obj
866
-
867
882
  @synchronizer.no_input_translation
868
883
  def __call__(self, *args, **kwargs) -> _Obj:
869
884
  """This acts as the class constructor."""
@@ -876,18 +891,31 @@ More information on class parameterization can be found here: https://modal.com/
876
891
  )
877
892
 
878
893
  def __getattr__(self, k):
879
- # TODO: remove this method - access to attributes on classes (not instances) should be discouraged
880
- if not self._is_local() or k in self._method_partials:
881
- # if not local (== k *could* be a method) or it is local and we know k is a method
882
- deprecation_warning(
883
- (2025, 1, 13),
884
- "Calling a method on an uninstantiated class will soon be deprecated; "
885
- "update your code to instantiate the class first, i.e.:\n"
886
- 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()"
887
910
  )
888
- return getattr(self(), k)
889
- # non-method attribute access on local class - arguably shouldn't be used either:
890
- 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
+ )
891
919
 
892
920
  def _is_local(self) -> bool:
893
921
  return self._user_cls is not None