modal 0.62.115__py3-none-any.whl → 0.72.13__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/__init__.py +13 -9
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +402 -398
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -60
- modal/_resources.py +26 -7
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1025 -0
- modal/{execution_context.py → _runtime/execution_context.py} +11 -2
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +123 -6
- modal/_traceback.py +47 -187
- modal/_tunnel.py +50 -14
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +386 -104
- modal/_utils/blob_utils.py +157 -186
- modal/_utils/bytes_io_segment_payload.py +97 -0
- modal/_utils/deprecation.py +89 -0
- modal/_utils/docker_utils.py +98 -0
- modal/_utils/function_utils.py +299 -98
- modal/_utils/grpc_testing.py +47 -34
- modal/_utils/grpc_utils.py +54 -21
- modal/_utils/hash_utils.py +51 -10
- modal/_utils/http_utils.py +39 -9
- modal/_utils/logger.py +2 -1
- modal/_utils/mount_utils.py +34 -16
- modal/_utils/name_utils.py +58 -0
- modal/_utils/package_utils.py +14 -1
- modal/_utils/pattern_utils.py +205 -0
- modal/_utils/rand_pb_testing.py +3 -3
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +12 -10
- modal/app.py +561 -323
- modal/app.pyi +474 -262
- modal/call_graph.py +7 -6
- modal/cli/_download.py +22 -6
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +203 -42
- modal/cli/config.py +12 -5
- modal/cli/container.py +61 -13
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +21 -48
- modal/cli/launch.py +28 -14
- modal/cli/network_file_system.py +57 -21
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +34 -9
- modal/cli/programs/vscode.py +58 -8
- modal/cli/queues.py +131 -0
- modal/cli/run.py +199 -96
- modal/cli/secret.py +5 -4
- modal/cli/token.py +7 -2
- modal/cli/utils.py +74 -8
- modal/cli/volume.py +97 -56
- modal/client.py +248 -144
- modal/client.pyi +156 -124
- modal/cloud_bucket_mount.py +43 -30
- modal/cloud_bucket_mount.pyi +32 -25
- modal/cls.py +528 -141
- modal/cls.pyi +189 -145
- modal/config.py +32 -15
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +50 -54
- modal/dict.pyi +120 -164
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +30 -43
- modal/experimental.py +62 -2
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +196 -0
- modal/functions.py +846 -428
- modal/functions.pyi +446 -387
- modal/gpu.py +57 -44
- modal/image.py +943 -417
- modal/image.pyi +584 -245
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +223 -90
- modal/mount.pyi +241 -243
- modal/network_file_system.py +85 -86
- modal/network_file_system.pyi +151 -110
- modal/object.py +66 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +73 -47
- modal/parallel_map.pyi +51 -63
- modal/partial_function.py +272 -107
- modal/partial_function.pyi +219 -120
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +96 -72
- modal/queue.pyi +210 -135
- modal/requirements/2024.04.txt +2 -1
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +45 -4
- modal/runner.py +325 -203
- modal/runner.pyi +124 -110
- modal/running_app.py +27 -4
- modal/sandbox.py +509 -231
- modal/sandbox.pyi +396 -169
- modal/schedule.py +2 -2
- modal/scheduler_placement.py +20 -3
- modal/secret.py +41 -25
- modal/secret.pyi +62 -42
- modal/serving.py +39 -49
- modal/serving.pyi +37 -43
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +123 -137
- modal/volume.pyi +228 -221
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
- modal-0.72.13.dist-info/RECORD +174 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
- modal_docs/gen_reference_docs.py +3 -1
- modal_docs/mdmd/mdmd.py +0 -1
- modal_docs/mdmd/signatures.py +1 -2
- modal_global_objects/images/base_images.py +28 -0
- modal_global_objects/mounts/python_standalone.py +2 -2
- modal_proto/__init__.py +1 -1
- modal_proto/api.proto +1231 -531
- modal_proto/api_grpc.py +750 -430
- modal_proto/api_pb2.py +2102 -1176
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1329 -675
- modal_proto/api_pb2_grpc.pyi +1416 -0
- modal_proto/modal_api_grpc.py +149 -0
- modal_proto/modal_options_grpc.py +3 -0
- modal_proto/options_pb2.pyi +20 -0
- modal_proto/options_pb2_grpc.pyi +7 -0
- modal_proto/py.typed +0 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +2 -2
- modal/_asgi.py +0 -370
- modal/_container_exec.py +0 -128
- modal/_container_io_manager.py +0 -646
- modal/_container_io_manager.pyi +0 -412
- modal/_sandbox_shell.py +0 -49
- modal/app_utils.py +0 -20
- modal/app_utils.pyi +0 -17
- modal/execution_context.pyi +0 -37
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal-0.62.115.dist-info/RECORD +0 -207
- modal_global_objects/images/conda.py +0 -15
- modal_global_objects/images/debian_slim.py +0 -15
- modal_global_objects/images/micromamba.py +0 -15
- test/__init__.py +0 -1
- test/aio_test.py +0 -12
- test/async_utils_test.py +0 -279
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -674
- test/client_test.py +0 -203
- test/cloud_bucket_mount_test.py +0 -22
- test/cls_test.py +0 -636
- test/config_test.py +0 -149
- test/conftest.py +0 -1485
- test/container_app_test.py +0 -50
- test/container_test.py +0 -1405
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -51
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -791
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -82
- test/helpers.py +0 -47
- test/image_test.py +0 -814
- test/live_reload_test.py +0 -80
- test/lookup_test.py +0 -70
- test/mdmd_test.py +0 -329
- test/mount_test.py +0 -162
- test/mounted_files_test.py +0 -327
- test/network_file_system_test.py +0 -188
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -115
- test/resolver_test.py +0 -59
- test/retries_test.py +0 -67
- test/runner_test.py +0 -85
- test/sandbox_test.py +0 -191
- test/schedule_test.py +0 -15
- test/scheduler_placement_test.py +0 -57
- test/secret_test.py +0 -89
- test/serialization_test.py +0 -50
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -361
- test/test_asgi_wrapper.py +0 -234
- test/token_flow_test.py +0 -18
- test/traceback_test.py +0 -135
- test/tunnel_test.py +0 -29
- test/utils_test.py +0 -88
- test/version_test.py +0 -14
- test/volume_test.py +0 -397
- test/watcher_test.py +0 -58
- test/webhook_test.py +0 -145
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/entry_points.txt +0 -0
modal/cls.py
CHANGED
@@ -1,32 +1,33 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
|
+
import inspect
|
2
3
|
import os
|
3
4
|
import typing
|
4
|
-
from
|
5
|
+
from collections.abc import Collection
|
6
|
+
from typing import Any, Callable, Optional, TypeVar, Union
|
5
7
|
|
6
8
|
from google.protobuf.message import Message
|
7
9
|
from grpclib import GRPCError, Status
|
8
10
|
|
11
|
+
from modal._utils.function_utils import CLASS_PARAM_TYPE_MAP
|
9
12
|
from modal_proto import api_pb2
|
10
13
|
|
11
|
-
from ._output import OutputManager
|
12
14
|
from ._resolver import Resolver
|
13
15
|
from ._resources import convert_fn_config_to_resources_config
|
14
16
|
from ._serialization import check_valid_cls_constructor_arg
|
17
|
+
from ._traceback import print_server_warnings
|
15
18
|
from ._utils.async_utils import synchronize_api, synchronizer
|
19
|
+
from ._utils.deprecation import deprecation_warning, renamed_parameter
|
16
20
|
from ._utils.grpc_utils import retry_transient_errors
|
17
21
|
from ._utils.mount_utils import validate_volumes
|
18
22
|
from .client import _Client
|
19
|
-
from .exception import InvalidError, NotFoundError
|
20
|
-
from .functions import
|
21
|
-
_parse_retries,
|
22
|
-
)
|
23
|
+
from .exception import ExecutionError, InvalidError, NotFoundError, VersionError
|
24
|
+
from .functions import _Function, _parse_retries
|
23
25
|
from .gpu import GPU_T
|
24
26
|
from .object import _get_environment_name, _Object
|
25
27
|
from .partial_function import (
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
_Function,
|
28
|
+
_find_callables_for_obj,
|
29
|
+
_find_partial_methods_for_user_cls,
|
30
|
+
_PartialFunction,
|
30
31
|
_PartialFunctionFlags,
|
31
32
|
)
|
32
33
|
from .retries import Retries
|
@@ -40,23 +41,119 @@ if typing.TYPE_CHECKING:
|
|
40
41
|
import modal.app
|
41
42
|
|
42
43
|
|
44
|
+
def _use_annotation_parameters(user_cls: type) -> bool:
|
45
|
+
has_parameters = any(is_parameter(cls_member) for cls_member in user_cls.__dict__.values())
|
46
|
+
has_explicit_constructor = user_cls.__init__ != object.__init__
|
47
|
+
return has_parameters and not has_explicit_constructor
|
48
|
+
|
49
|
+
|
50
|
+
def _get_class_constructor_signature(user_cls: type) -> inspect.Signature:
|
51
|
+
if not _use_annotation_parameters(user_cls):
|
52
|
+
return inspect.signature(user_cls)
|
53
|
+
else:
|
54
|
+
constructor_parameters = []
|
55
|
+
for name, annotation_value in user_cls.__dict__.get("__annotations__", {}).items():
|
56
|
+
if hasattr(user_cls, name):
|
57
|
+
parameter_spec = getattr(user_cls, name)
|
58
|
+
if is_parameter(parameter_spec):
|
59
|
+
maybe_default = {}
|
60
|
+
if not isinstance(parameter_spec.default, _NO_DEFAULT):
|
61
|
+
maybe_default["default"] = parameter_spec.default
|
62
|
+
|
63
|
+
param = inspect.Parameter(
|
64
|
+
name=name,
|
65
|
+
annotation=annotation_value,
|
66
|
+
kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
67
|
+
**maybe_default,
|
68
|
+
)
|
69
|
+
constructor_parameters.append(param)
|
70
|
+
|
71
|
+
return inspect.Signature(constructor_parameters)
|
72
|
+
|
73
|
+
|
74
|
+
def _bind_instance_method(service_function: _Function, class_bound_method: _Function):
|
75
|
+
"""mdmd:hidden
|
76
|
+
|
77
|
+
Binds an "instance service function" to a specific method name.
|
78
|
+
This "dummy" _Function gets no unique object_id and isn't backend-backed at the moment, since all
|
79
|
+
it does it forward invocations to the underlying instance_service_function with the specified method,
|
80
|
+
and we don't support web_config for parameterized methods at the moment.
|
81
|
+
"""
|
82
|
+
# TODO(elias): refactor to not use `_from_loader()` as a crutch for lazy-loading the
|
83
|
+
# underlying instance_service_function. It's currently used in order to take advantage
|
84
|
+
# of resolver logic and get "chained" resolution of lazy loads, even though this thin
|
85
|
+
# object itself doesn't need any "loading"
|
86
|
+
assert service_function._obj
|
87
|
+
method_name = class_bound_method._use_method_name
|
88
|
+
|
89
|
+
def hydrate_from_instance_service_function(method_placeholder_fun):
|
90
|
+
method_placeholder_fun._hydrate_from_other(service_function)
|
91
|
+
method_placeholder_fun._obj = service_function._obj
|
92
|
+
method_placeholder_fun._web_url = (
|
93
|
+
class_bound_method._web_url
|
94
|
+
) # TODO: this shouldn't be set when actual parameters are used
|
95
|
+
method_placeholder_fun._function_name = f"{class_bound_method._function_name}[parameterized]"
|
96
|
+
method_placeholder_fun._is_generator = class_bound_method._is_generator
|
97
|
+
method_placeholder_fun._cluster_size = class_bound_method._cluster_size
|
98
|
+
method_placeholder_fun._use_method_name = method_name
|
99
|
+
method_placeholder_fun._is_method = True
|
100
|
+
|
101
|
+
async def _load(fun: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
|
102
|
+
# there is currently no actual loading logic executed to create each method on
|
103
|
+
# the *parameterized* instance of a class - it uses the parameter-bound service-function
|
104
|
+
# for the instance. This load method just makes sure to set all attributes after the
|
105
|
+
# `service_function` has been loaded (it's in the `_deps`)
|
106
|
+
hydrate_from_instance_service_function(fun)
|
107
|
+
|
108
|
+
def _deps():
|
109
|
+
if service_function.is_hydrated:
|
110
|
+
# without this check, the common service_function will be reloaded by all methods
|
111
|
+
# TODO(elias): Investigate if we can fix this multi-loader in the resolver - feels like a bug?
|
112
|
+
return []
|
113
|
+
return [service_function]
|
114
|
+
|
115
|
+
rep = f"Method({method_name})"
|
116
|
+
|
117
|
+
fun = _Function._from_loader(
|
118
|
+
_load,
|
119
|
+
rep,
|
120
|
+
deps=_deps,
|
121
|
+
hydrate_lazily=True,
|
122
|
+
)
|
123
|
+
if service_function.is_hydrated:
|
124
|
+
# Eager hydration (skip load) if the instance service function is already loaded
|
125
|
+
hydrate_from_instance_service_function(fun)
|
126
|
+
|
127
|
+
fun._info = class_bound_method._info
|
128
|
+
fun._obj = service_function._obj
|
129
|
+
fun._is_method = True
|
130
|
+
fun._app = class_bound_method._app
|
131
|
+
fun._spec = class_bound_method._spec
|
132
|
+
return fun
|
133
|
+
|
134
|
+
|
43
135
|
class _Obj:
|
44
136
|
"""An instance of a `Cls`, i.e. `Cls("foo", 42)` returns an `Obj`.
|
45
137
|
|
46
138
|
All this class does is to return `Function` objects."""
|
47
139
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
140
|
+
_cls: "_Cls" # parent
|
141
|
+
_functions: dict[str, _Function]
|
142
|
+
_has_entered: bool
|
143
|
+
_user_cls_instance: Optional[Any] = None
|
144
|
+
_args: tuple[Any, ...]
|
145
|
+
_kwargs: dict[str, Any]
|
146
|
+
|
147
|
+
_instance_service_function: Optional[_Function] = None # this gets set lazily
|
148
|
+
|
149
|
+
def _uses_common_service_function(self):
|
150
|
+
# Used for backwards compatibility checks with pre v0.63 classes
|
151
|
+
return self._cls._class_service_function is not None
|
53
152
|
|
54
153
|
def __init__(
|
55
154
|
self,
|
56
|
-
|
57
|
-
|
58
|
-
base_functions: Dict[str, _Function],
|
59
|
-
from_other_workspace: bool,
|
155
|
+
cls: "_Cls",
|
156
|
+
user_cls: Optional[type], # this would be None in case of lookups
|
60
157
|
options: Optional[api_pb2.FunctionOptions],
|
61
158
|
args,
|
62
159
|
kwargs,
|
@@ -65,179 +162,392 @@ class _Obj:
|
|
65
162
|
check_valid_cls_constructor_arg(i + 1, arg)
|
66
163
|
for key, kwarg in kwargs.items():
|
67
164
|
check_valid_cls_constructor_arg(key, kwarg)
|
68
|
-
|
69
|
-
self._functions = {}
|
70
|
-
for k, fun in base_functions.items():
|
71
|
-
self._functions[k] = fun.from_parametrized(self, from_other_workspace, options, args, kwargs)
|
72
|
-
self._functions[k]._set_output_mgr(output_mgr)
|
165
|
+
self._cls = cls
|
73
166
|
|
74
167
|
# Used for construction local object lazily
|
75
|
-
self.
|
76
|
-
self.
|
77
|
-
|
78
|
-
|
79
|
-
|
168
|
+
self._has_entered = False
|
169
|
+
self._user_cls = user_cls
|
170
|
+
|
171
|
+
# used for lazy construction in case of explicit constructors
|
172
|
+
self._args = args
|
173
|
+
self._kwargs = kwargs
|
174
|
+
self._options = options
|
175
|
+
|
176
|
+
def _cached_service_function(self) -> "modal.functions._Function":
|
177
|
+
# Returns a service function for this _Obj, serving all its methods
|
178
|
+
# In case of methods without parameters or options, this is simply proxying to the class service function
|
179
|
+
|
180
|
+
# only safe to call for 0.63+ classes (before then, all methods had their own services)
|
181
|
+
if not self._instance_service_function:
|
182
|
+
assert self._cls._class_service_function
|
183
|
+
self._instance_service_function = self._cls._class_service_function._bind_parameters(
|
184
|
+
self, self._options, self._args, self._kwargs
|
185
|
+
)
|
186
|
+
return self._instance_service_function
|
187
|
+
|
188
|
+
def _get_parameter_values(self) -> dict[str, Any]:
|
189
|
+
# binds args and kwargs according to the class constructor signature
|
190
|
+
# (implicit by parameters or explicit)
|
191
|
+
sig = _get_class_constructor_signature(self._user_cls)
|
192
|
+
bound_vars = sig.bind(*self._args, **self._kwargs)
|
193
|
+
bound_vars.apply_defaults()
|
194
|
+
return bound_vars.arguments
|
195
|
+
|
196
|
+
def _new_user_cls_instance(self):
|
197
|
+
if not _use_annotation_parameters(self._user_cls):
|
198
|
+
# TODO(elias): deprecate this code path eventually
|
199
|
+
user_cls_instance = self._user_cls(*self._args, **self._kwargs)
|
80
200
|
else:
|
81
|
-
|
201
|
+
# ignore constructor (assumes there is no custom constructor,
|
202
|
+
# which is guaranteed by _use_annotation_parameters)
|
203
|
+
# set the attributes on the class corresponding to annotations
|
204
|
+
# with = parameter() specifications
|
205
|
+
param_values = self._get_parameter_values()
|
206
|
+
user_cls_instance = self._user_cls.__new__(self._user_cls) # new instance without running __init__
|
207
|
+
user_cls_instance.__dict__.update(param_values)
|
208
|
+
|
209
|
+
# TODO: always use Obj instances instead of making modifications to user cls
|
210
|
+
# TODO: OR (if simpler for now) replace all the PartialFunctions on the user cls
|
211
|
+
# with getattr(self, method_name)
|
212
|
+
|
213
|
+
# user cls instances are only created locally, so we have all partial functions available
|
214
|
+
instance_methods = {}
|
215
|
+
for method_name in _find_partial_methods_for_user_cls(self._user_cls, _PartialFunctionFlags.FUNCTION):
|
216
|
+
instance_methods[method_name] = getattr(self, method_name)
|
217
|
+
|
218
|
+
user_cls_instance._modal_functions = instance_methods
|
219
|
+
return user_cls_instance
|
220
|
+
|
221
|
+
async def keep_warm(self, warm_pool_size: int) -> None:
|
222
|
+
"""Set the warm pool size for the class containers
|
223
|
+
|
224
|
+
Please exercise care when using this advanced feature!
|
225
|
+
Setting and forgetting a warm pool on functions can lead to increased costs.
|
226
|
+
|
227
|
+
Note that all Modal methods and web endpoints of a class share the same set
|
228
|
+
of containers and the warm_pool_size affects that common container pool.
|
229
|
+
|
230
|
+
```python notest
|
231
|
+
# Usage on a parametrized function.
|
232
|
+
Model = modal.Cls.lookup("my-app", "Model")
|
233
|
+
Model("fine-tuned-model").keep_warm(2)
|
234
|
+
```
|
235
|
+
"""
|
236
|
+
if not self._uses_common_service_function():
|
237
|
+
raise VersionError(
|
238
|
+
"Class instance `.keep_warm(...)` can't be used on classes deployed using client version <v0.63"
|
239
|
+
)
|
240
|
+
await self._cached_service_function().keep_warm(warm_pool_size)
|
82
241
|
|
83
|
-
def
|
84
|
-
"""
|
85
|
-
self._local_obj = self._local_obj_constr()
|
86
|
-
setattr(self._local_obj, "_modal_functions", self._functions) # Needed for PartialFunction.__get__
|
87
|
-
return self._local_obj
|
242
|
+
def _cached_user_cls_instance(self):
|
243
|
+
"""Get or construct the local object
|
88
244
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
self.get_obj() # Instantiate object
|
93
|
-
self._inited = True
|
245
|
+
Used for .local() calls and getting attributes of classes"""
|
246
|
+
if not self._user_cls_instance:
|
247
|
+
self._user_cls_instance = self._new_user_cls_instance() # Instantiate object
|
94
248
|
|
95
|
-
return self.
|
249
|
+
return self._user_cls_instance
|
96
250
|
|
97
|
-
def
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
251
|
+
def _enter(self):
|
252
|
+
assert self._user_cls
|
253
|
+
if not self._has_entered:
|
254
|
+
user_cls_instance = self._cached_user_cls_instance()
|
255
|
+
if hasattr(user_cls_instance, "__enter__"):
|
256
|
+
user_cls_instance.__enter__()
|
257
|
+
|
258
|
+
for method_flag in (
|
259
|
+
_PartialFunctionFlags.ENTER_PRE_SNAPSHOT,
|
260
|
+
_PartialFunctionFlags.ENTER_POST_SNAPSHOT,
|
261
|
+
):
|
262
|
+
for enter_method in _find_callables_for_obj(user_cls_instance, method_flag).values():
|
263
|
+
enter_method()
|
264
|
+
|
265
|
+
self._has_entered = True
|
102
266
|
|
103
267
|
@property
|
104
|
-
def
|
105
|
-
# needed because
|
106
|
-
return self.
|
268
|
+
def _entered(self) -> bool:
|
269
|
+
# needed because _aenter is nowrap
|
270
|
+
return self._has_entered
|
107
271
|
|
108
|
-
@
|
109
|
-
def
|
110
|
-
self.
|
272
|
+
@_entered.setter
|
273
|
+
def _entered(self, val: bool):
|
274
|
+
self._has_entered = val
|
111
275
|
|
112
276
|
@synchronizer.nowrap
|
113
|
-
async def
|
114
|
-
if not self.
|
115
|
-
|
116
|
-
if hasattr(
|
117
|
-
await
|
118
|
-
elif hasattr(
|
119
|
-
|
120
|
-
self.
|
277
|
+
async def _aenter(self):
|
278
|
+
if not self._entered: # use the property to get at the impl class
|
279
|
+
user_cls_instance = self._cached_user_cls_instance()
|
280
|
+
if hasattr(user_cls_instance, "__aenter__"):
|
281
|
+
await user_cls_instance.__aenter__()
|
282
|
+
elif hasattr(user_cls_instance, "__enter__"):
|
283
|
+
user_cls_instance.__enter__()
|
284
|
+
self._has_entered = True
|
121
285
|
|
122
286
|
def __getattr__(self, k):
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
287
|
+
# This is a bit messy and branchy because:
|
288
|
+
# * Support for pre-0.63 lookups *and* newer classes
|
289
|
+
# * Support .remote() on both hydrated (local or remote classes) or unhydrated classes (remote classes only)
|
290
|
+
# * Support .local() on both hydrated and unhydrated classes (assuming local access to code)
|
291
|
+
# * Support attribute access (when local cls is available)
|
292
|
+
|
293
|
+
def _get_method_bound_function() -> Optional["_Function"]:
|
294
|
+
"""Gets _Function object for method - either for a local or a hydrated remote class
|
295
|
+
|
296
|
+
* If class is neither local or hydrated - raise exception (should never happen)
|
297
|
+
* If attribute isn't a method - return None
|
298
|
+
"""
|
299
|
+
if self._cls._method_functions is None:
|
300
|
+
raise ExecutionError("Method is not local and not hydrated")
|
301
|
+
|
302
|
+
if class_bound_method := self._cls._method_functions.get(k, None):
|
303
|
+
# If we know the user is accessing a *method* and not another attribute,
|
304
|
+
# we don't have to create an instance of the user class yet.
|
305
|
+
# This is because it might just be a call to `.remote()` on it which
|
306
|
+
# doesn't require a local instance.
|
307
|
+
# As long as we have the service function or params, we can do remote calls
|
308
|
+
# without calling the constructor of the class in the calling context.
|
309
|
+
if self._cls._class_service_function is None:
|
310
|
+
# a <v0.63 lookup
|
311
|
+
return class_bound_method._bind_parameters(self, self._options, self._args, self._kwargs)
|
312
|
+
else:
|
313
|
+
return _bind_instance_method(self._cached_service_function(), class_bound_method)
|
314
|
+
|
315
|
+
return None # The attribute isn't a method
|
316
|
+
|
317
|
+
if self._cls._method_functions is not None:
|
318
|
+
# We get here with either a hydrated Cls or an unhydrated one with local definition
|
319
|
+
if method := _get_method_bound_function():
|
320
|
+
return method
|
321
|
+
elif self._user_cls:
|
322
|
+
# We have the local definition, and the attribute isn't a method
|
323
|
+
# so we instantiate if we don't have an instance, and try to get the attribute
|
324
|
+
user_cls_instance = self._cached_user_cls_instance()
|
325
|
+
return getattr(user_cls_instance, k)
|
326
|
+
else:
|
327
|
+
# This is the case for a *hydrated* class without the local definition, i.e. a lookup
|
328
|
+
# where the attribute isn't a registered method of the class
|
329
|
+
raise NotFoundError(
|
330
|
+
f"Class has no method `{k}` and attributes (or undecorated methods) can't be accessed for"
|
331
|
+
f" remote classes (`Cls.from_name` instances)"
|
332
|
+
)
|
333
|
+
|
334
|
+
# Not hydrated Cls, and we don't have the class - typically a Cls.from_name that
|
335
|
+
# has not yet been loaded. So use a special loader that loads it lazily:
|
336
|
+
|
337
|
+
async def method_loader(fun, resolver: Resolver, existing_object_id):
|
338
|
+
await resolver.load(self._cls) # load class so we get info about methods
|
339
|
+
method_function = _get_method_bound_function()
|
340
|
+
if method_function is None:
|
341
|
+
raise NotFoundError(
|
342
|
+
f"Class has no method {k}, and attributes can't be accessed for `Cls.from_name` instances"
|
343
|
+
)
|
344
|
+
await resolver.load(method_function) # get the appropriate method handle (lazy)
|
345
|
+
fun._hydrate_from_other(method_function)
|
346
|
+
|
347
|
+
# The reason we don't *always* use this lazy loader is because it precludes attribute access
|
348
|
+
# on local classes.
|
349
|
+
return _Function._from_loader(
|
350
|
+
method_loader,
|
351
|
+
repr,
|
352
|
+
deps=lambda: [], # TODO: use cls as dep instead of loading inside method_loader?
|
353
|
+
hydrate_lazily=True,
|
354
|
+
)
|
130
355
|
|
131
356
|
|
132
357
|
Obj = synchronize_api(_Obj)
|
133
358
|
|
134
359
|
|
135
360
|
class _Cls(_Object, type_prefix="cs"):
|
361
|
+
"""
|
362
|
+
Cls adds method pooling and [lifecycle hook](/docs/guide/lifecycle-functions) behavior
|
363
|
+
to [modal.Function](/docs/reference/modal.Function).
|
364
|
+
|
365
|
+
Generally, you will not construct a Cls directly.
|
366
|
+
Instead, use the [`@app.cls()`](/docs/reference/modal.App#cls) decorator on the App object.
|
367
|
+
"""
|
368
|
+
|
136
369
|
_user_cls: Optional[type]
|
137
|
-
|
370
|
+
_class_service_function: Optional[
|
371
|
+
_Function
|
372
|
+
] # The _Function serving *all* methods of the class, used for version >=v0.63
|
373
|
+
_method_functions: Optional[dict[str, _Function]] = None # Placeholder _Functions for each method
|
138
374
|
_options: Optional[api_pb2.FunctionOptions]
|
139
|
-
_callables:
|
140
|
-
_from_other_workspace: Optional[bool] # Functions require FunctionBindParams before invocation.
|
375
|
+
_callables: dict[str, Callable[..., Any]]
|
141
376
|
_app: Optional["modal.app._App"] = None # not set for lookups
|
377
|
+
_name: Optional[str]
|
142
378
|
|
143
379
|
def _initialize_from_empty(self):
|
144
380
|
self._user_cls = None
|
145
|
-
self.
|
381
|
+
self._class_service_function = None
|
146
382
|
self._options = None
|
147
383
|
self._callables = {}
|
148
|
-
self.
|
149
|
-
self._output_mgr: Optional[OutputManager] = None
|
384
|
+
self._name = None
|
150
385
|
|
151
386
|
def _initialize_from_other(self, other: "_Cls"):
|
387
|
+
super()._initialize_from_other(other)
|
152
388
|
self._user_cls = other._user_cls
|
153
|
-
self.
|
389
|
+
self._class_service_function = other._class_service_function
|
390
|
+
self._method_functions = other._method_functions
|
154
391
|
self._options = other._options
|
155
392
|
self._callables = other._callables
|
156
|
-
self.
|
157
|
-
self._output_mgr: Optional[OutputManager] = other._output_mgr
|
393
|
+
self._name = other._name
|
158
394
|
|
159
|
-
def
|
160
|
-
self.
|
395
|
+
def _get_partial_functions(self) -> dict[str, _PartialFunction]:
|
396
|
+
if not self._user_cls:
|
397
|
+
raise AttributeError("You can only get the partial functions of a local Cls instance")
|
398
|
+
return _find_partial_methods_for_user_cls(self._user_cls, _PartialFunctionFlags.all())
|
161
399
|
|
162
400
|
def _hydrate_metadata(self, metadata: Message):
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
401
|
+
assert isinstance(metadata, api_pb2.ClassHandleMetadata)
|
402
|
+
if (
|
403
|
+
self._class_service_function
|
404
|
+
and self._class_service_function._method_handle_metadata
|
405
|
+
and len(self._class_service_function._method_handle_metadata)
|
406
|
+
):
|
407
|
+
# The class only has a class service function and no method placeholders (v0.67+)
|
408
|
+
if self._method_functions:
|
409
|
+
# We're here when the Cls is loaded locally (e.g. _Cls.from_local) so the _method_functions mapping is
|
410
|
+
# populated with (un-hydrated) _Function objects
|
411
|
+
for (
|
412
|
+
method_name,
|
413
|
+
method_handle_metadata,
|
414
|
+
) in self._class_service_function._method_handle_metadata.items():
|
415
|
+
self._method_functions[method_name]._hydrate(
|
416
|
+
self._class_service_function.object_id, self._client, method_handle_metadata
|
417
|
+
)
|
168
418
|
else:
|
169
|
-
|
419
|
+
# We're here when the function is loaded remotely (e.g. _Cls.from_name)
|
420
|
+
self._method_functions = {}
|
421
|
+
for (
|
422
|
+
method_name,
|
423
|
+
method_handle_metadata,
|
424
|
+
) in self._class_service_function._method_handle_metadata.items():
|
425
|
+
self._method_functions[method_name] = _Function._new_hydrated(
|
426
|
+
self._class_service_function.object_id, self._client, method_handle_metadata
|
427
|
+
)
|
428
|
+
elif self._class_service_function and self._class_service_function.object_id:
|
429
|
+
# A class with a class service function and method placeholder functions
|
430
|
+
self._method_functions = {}
|
431
|
+
for method in metadata.methods:
|
432
|
+
self._method_functions[method.function_name] = _Function._new_hydrated(
|
433
|
+
self._class_service_function.object_id, self._client, method.function_handle_metadata
|
434
|
+
)
|
435
|
+
else:
|
436
|
+
# pre 0.63 class that does not have a class service function and only method functions
|
437
|
+
self._method_functions = {}
|
438
|
+
for method in metadata.methods:
|
439
|
+
self._method_functions[method.function_name] = _Function._new_hydrated(
|
170
440
|
method.function_id, self._client, method.function_handle_metadata
|
171
441
|
)
|
172
442
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
443
|
+
@staticmethod
|
444
|
+
def validate_construction_mechanism(user_cls):
|
445
|
+
"""mdmd:hidden"""
|
446
|
+
params = {k: v for k, v in user_cls.__dict__.items() if is_parameter(v)}
|
447
|
+
has_custom_constructor = user_cls.__init__ != object.__init__
|
448
|
+
if params and has_custom_constructor:
|
449
|
+
raise InvalidError(
|
450
|
+
"A class can't have both a custom __init__ constructor "
|
451
|
+
"and dataclass-style modal.parameter() annotations"
|
180
452
|
)
|
181
|
-
|
453
|
+
|
454
|
+
annotations = user_cls.__dict__.get("__annotations__", {}) # compatible with older pythons
|
455
|
+
missing_annotations = params.keys() - annotations.keys()
|
456
|
+
if missing_annotations:
|
457
|
+
raise InvalidError("All modal.parameter() specifications need to be type annotated")
|
458
|
+
|
459
|
+
annotated_params = {k: t for k, t in annotations.items() if k in params}
|
460
|
+
for k, t in annotated_params.items():
|
461
|
+
if t not in CLASS_PARAM_TYPE_MAP:
|
462
|
+
t_name = getattr(t, "__name__", repr(t))
|
463
|
+
supported = ", ".join(t.__name__ for t in CLASS_PARAM_TYPE_MAP.keys())
|
464
|
+
raise InvalidError(
|
465
|
+
f"{user_cls.__name__}.{k}: {t_name} is not a supported parameter type. Use one of: {supported}"
|
466
|
+
)
|
182
467
|
|
183
468
|
@staticmethod
|
184
|
-
def from_local(user_cls, app
|
469
|
+
def from_local(user_cls, app: "modal.app._App", class_service_function: _Function) -> "_Cls":
|
185
470
|
"""mdmd:hidden"""
|
186
|
-
|
187
|
-
|
188
|
-
|
471
|
+
# validate signature
|
472
|
+
_Cls.validate_construction_mechanism(user_cls)
|
473
|
+
|
474
|
+
method_functions: dict[str, _Function] = {}
|
475
|
+
partial_functions: dict[str, _PartialFunction] = _find_partial_methods_for_user_cls(
|
476
|
+
user_cls, _PartialFunctionFlags.FUNCTION
|
477
|
+
)
|
478
|
+
|
479
|
+
for method_name, partial_function in partial_functions.items():
|
480
|
+
method_function = class_service_function._bind_method(user_cls, method_name, partial_function)
|
481
|
+
if partial_function.webhook_config is not None:
|
482
|
+
app._web_endpoints.append(method_function.tag)
|
483
|
+
partial_function.wrapped = True
|
484
|
+
method_functions[method_name] = method_function
|
189
485
|
|
190
486
|
# Disable the warning that these are not wrapped
|
191
|
-
for partial_function in
|
487
|
+
for partial_function in _find_partial_methods_for_user_cls(user_cls, ~_PartialFunctionFlags.FUNCTION).values():
|
192
488
|
partial_function.wrapped = True
|
193
489
|
|
194
490
|
# Get all callables
|
195
|
-
callables:
|
491
|
+
callables: dict[str, Callable] = {
|
492
|
+
k: pf.raw_f for k, pf in _find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.all()).items()
|
493
|
+
}
|
196
494
|
|
197
|
-
def _deps() ->
|
198
|
-
return
|
495
|
+
def _deps() -> list[_Function]:
|
496
|
+
return [class_service_function]
|
199
497
|
|
200
498
|
async def _load(self: "_Cls", resolver: Resolver, existing_object_id: Optional[str]):
|
201
|
-
req = api_pb2.ClassCreateRequest(
|
202
|
-
|
203
|
-
|
499
|
+
req = api_pb2.ClassCreateRequest(
|
500
|
+
app_id=resolver.app_id, existing_class_id=existing_object_id, only_class_function=True
|
501
|
+
)
|
204
502
|
resp = await resolver.client.stub.ClassCreate(req)
|
205
503
|
self._hydrate(resp.class_id, resolver.client, resp.handle_metadata)
|
206
504
|
|
207
505
|
rep = f"Cls({user_cls.__name__})"
|
208
|
-
cls = _Cls._from_loader(_load, rep, deps=_deps)
|
506
|
+
cls: _Cls = _Cls._from_loader(_load, rep, deps=_deps)
|
209
507
|
cls._app = app
|
210
508
|
cls._user_cls = user_cls
|
211
|
-
cls.
|
509
|
+
cls._class_service_function = class_service_function
|
510
|
+
cls._method_functions = method_functions
|
212
511
|
cls._callables = callables
|
213
|
-
cls.
|
214
|
-
setattr(cls._user_cls, "_modal_functions", functions) # Needed for PartialFunction.__get__
|
512
|
+
cls._name = user_cls.__name__
|
215
513
|
return cls
|
216
514
|
|
515
|
+
def _uses_common_service_function(self):
|
516
|
+
# Used for backwards compatibility with version < 0.63
|
517
|
+
# where methods had individual top level functions
|
518
|
+
return self._class_service_function is not None
|
519
|
+
|
217
520
|
@classmethod
|
521
|
+
@renamed_parameter((2024, 12, 18), "tag", "name")
|
218
522
|
def from_name(
|
219
|
-
cls:
|
523
|
+
cls: type["_Cls"],
|
220
524
|
app_name: str,
|
221
|
-
|
525
|
+
name: str,
|
222
526
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
223
527
|
environment_name: Optional[str] = None,
|
224
528
|
workspace: Optional[str] = None,
|
225
529
|
) -> "_Cls":
|
226
|
-
"""
|
530
|
+
"""Reference a Cls from a deployed App by its name.
|
531
|
+
|
532
|
+
In contrast to `modal.Cls.lookup`, this is a lazy method
|
533
|
+
that defers hydrating the local object with metadata from
|
534
|
+
Modal servers until the first time it is actually used.
|
227
535
|
|
228
536
|
```python
|
229
|
-
|
537
|
+
Model = modal.Cls.from_name("other-app", "Model")
|
230
538
|
```
|
231
539
|
"""
|
232
540
|
|
233
541
|
async def _load_remote(obj: _Object, resolver: Resolver, existing_object_id: Optional[str]):
|
542
|
+
_environment_name = _get_environment_name(environment_name, resolver)
|
234
543
|
request = api_pb2.ClassGetRequest(
|
235
544
|
app_name=app_name,
|
236
|
-
object_tag=
|
545
|
+
object_tag=name,
|
237
546
|
namespace=namespace,
|
238
|
-
environment_name=
|
547
|
+
environment_name=_environment_name,
|
239
548
|
lookup_published=workspace is not None,
|
240
549
|
workspace_name=workspace,
|
550
|
+
only_class_function=True,
|
241
551
|
)
|
242
552
|
try:
|
243
553
|
response = await retry_transient_errors(resolver.client.stub.ClassGet, request)
|
@@ -249,49 +559,59 @@ class _Cls(_Object, type_prefix="cs"):
|
|
249
559
|
else:
|
250
560
|
raise
|
251
561
|
|
562
|
+
print_server_warnings(response.server_warnings)
|
563
|
+
|
564
|
+
class_service_name = f"{name}.*" # special name of the base service function for the class
|
565
|
+
|
566
|
+
class_service_function = _Function.from_name(
|
567
|
+
app_name,
|
568
|
+
class_service_name,
|
569
|
+
environment_name=_environment_name,
|
570
|
+
)
|
571
|
+
try:
|
572
|
+
obj._class_service_function = await resolver.load(class_service_function)
|
573
|
+
except modal.exception.NotFoundError:
|
574
|
+
# this happens when looking up classes deployed using <v0.63
|
575
|
+
# This try-except block can be removed when min supported version >= 0.63
|
576
|
+
pass
|
577
|
+
|
252
578
|
obj._hydrate(response.class_id, resolver.client, response.handle_metadata)
|
253
579
|
|
254
580
|
rep = f"Ref({app_name})"
|
255
|
-
cls = cls._from_loader(_load_remote, rep, is_another_app=True)
|
256
|
-
|
581
|
+
cls = cls._from_loader(_load_remote, rep, is_another_app=True, hydrate_lazily=True)
|
582
|
+
# TODO: when pre 0.63 is phased out, we can set class_service_function here instead
|
583
|
+
cls._name = name
|
257
584
|
return cls
|
258
585
|
|
259
586
|
def with_options(
|
260
587
|
self: "_Cls",
|
261
|
-
cpu: Optional[float] = None,
|
262
|
-
memory: Optional[Union[int,
|
588
|
+
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
589
|
+
memory: Optional[Union[int, tuple[int, int]]] = None,
|
263
590
|
gpu: GPU_T = None,
|
264
591
|
secrets: Collection[_Secret] = (),
|
265
|
-
volumes:
|
592
|
+
volumes: dict[Union[str, os.PathLike], _Volume] = {},
|
266
593
|
retries: Optional[Union[int, Retries]] = None,
|
267
594
|
timeout: Optional[int] = None,
|
268
595
|
concurrency_limit: Optional[int] = None,
|
269
596
|
allow_concurrent_inputs: Optional[int] = None,
|
270
597
|
container_idle_timeout: Optional[int] = None,
|
271
|
-
allow_background_volume_commits: bool = False,
|
272
598
|
) -> "_Cls":
|
273
599
|
"""
|
274
|
-
Beta
|
600
|
+
**Beta:** Allows for the runtime modification of a modal.Cls's configuration.
|
275
601
|
|
276
602
|
This is a beta feature and may be unstable.
|
277
603
|
|
278
604
|
**Usage:**
|
279
605
|
|
280
606
|
```python notest
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
)
|
285
|
-
Model2 = Model.with_options(
|
286
|
-
gpu=modal.gpu.A100(memory=40),
|
287
|
-
volumes={"/models": models_vol}
|
288
|
-
)
|
289
|
-
Model2().generate.remote(42)
|
607
|
+
Model = modal.Cls.lookup("my_app", "Model")
|
608
|
+
ModelUsingGPU = Model.with_options(gpu="A100")
|
609
|
+
ModelUsingGPU().generate.remote(42) # will run with an A100 GPU
|
290
610
|
```
|
291
611
|
"""
|
292
|
-
retry_policy = _parse_retries(retries)
|
612
|
+
retry_policy = _parse_retries(retries, f"Class {self.__name__}" if self._user_cls else "")
|
293
613
|
if gpu or cpu or memory:
|
294
|
-
resources = convert_fn_config_to_resources_config(cpu=cpu, memory=memory, gpu=gpu)
|
614
|
+
resources = convert_fn_config_to_resources_config(cpu=cpu, memory=memory, gpu=gpu, ephemeral_disk=None)
|
295
615
|
else:
|
296
616
|
resources = None
|
297
617
|
|
@@ -299,7 +619,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
299
619
|
api_pb2.VolumeMount(
|
300
620
|
mount_path=path,
|
301
621
|
volume_id=volume.object_id,
|
302
|
-
allow_background_commits=
|
622
|
+
allow_background_commits=True,
|
303
623
|
)
|
304
624
|
for path, volume in validate_volumes(volumes)
|
305
625
|
]
|
@@ -316,44 +636,111 @@ class _Cls(_Object, type_prefix="cs"):
|
|
316
636
|
task_idle_timeout_secs=container_idle_timeout,
|
317
637
|
replace_volume_mounts=replace_volume_mounts,
|
318
638
|
volume_mounts=volume_mounts,
|
319
|
-
|
639
|
+
target_concurrent_inputs=allow_concurrent_inputs,
|
320
640
|
)
|
321
641
|
|
322
642
|
return cls
|
323
643
|
|
324
644
|
@staticmethod
|
645
|
+
@renamed_parameter((2024, 12, 18), "tag", "name")
|
325
646
|
async def lookup(
|
326
647
|
app_name: str,
|
327
|
-
|
648
|
+
name: str,
|
328
649
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
329
650
|
client: Optional[_Client] = None,
|
330
651
|
environment_name: Optional[str] = None,
|
331
652
|
workspace: Optional[str] = None,
|
332
653
|
) -> "_Cls":
|
333
|
-
"""Lookup a
|
654
|
+
"""Lookup a Cls from a deployed App by its name.
|
334
655
|
|
335
|
-
|
336
|
-
|
656
|
+
In contrast to `modal.Cls.from_name`, this is an eager method
|
657
|
+
that will hydrate the local object with metadata from Modal servers.
|
658
|
+
|
659
|
+
```python notest
|
660
|
+
Model = modal.Cls.lookup("other-app", "Model")
|
661
|
+
model = Model()
|
662
|
+
model.inference(...)
|
337
663
|
```
|
338
664
|
"""
|
339
|
-
obj = _Cls.from_name(
|
665
|
+
obj = _Cls.from_name(
|
666
|
+
app_name, name, namespace=namespace, environment_name=environment_name, workspace=workspace
|
667
|
+
)
|
340
668
|
if client is None:
|
341
669
|
client = await _Client.from_env()
|
342
670
|
resolver = Resolver(client=client)
|
343
671
|
await resolver.load(obj)
|
344
672
|
return obj
|
345
673
|
|
674
|
+
@synchronizer.no_input_translation
|
346
675
|
def __call__(self, *args, **kwargs) -> _Obj:
|
347
676
|
"""This acts as the class constructor."""
|
348
677
|
return _Obj(
|
349
|
-
self
|
678
|
+
self,
|
679
|
+
self._user_cls,
|
680
|
+
self._options,
|
681
|
+
args,
|
682
|
+
kwargs,
|
350
683
|
)
|
351
684
|
|
352
685
|
def __getattr__(self, k):
|
353
686
|
# Used by CLI and container entrypoint
|
354
|
-
|
355
|
-
|
687
|
+
# TODO: remove this method - access to attributes on classes should be discouraged
|
688
|
+
if k in self._method_functions:
|
689
|
+
deprecation_warning(
|
690
|
+
(2025, 1, 13),
|
691
|
+
"Usage of methods directly on the class will soon be deprecated, "
|
692
|
+
"instantiate classes before using methods, e.g.:\n"
|
693
|
+
f"{self._name}().{k} instead of {self._name}.{k}",
|
694
|
+
pending=True,
|
695
|
+
)
|
696
|
+
return self._method_functions[k]
|
356
697
|
return getattr(self._user_cls, k)
|
357
698
|
|
358
699
|
|
359
700
|
Cls = synchronize_api(_Cls)
|
701
|
+
|
702
|
+
|
703
|
+
class _NO_DEFAULT:
|
704
|
+
def __repr__(self):
|
705
|
+
return "modal.cls._NO_DEFAULT()"
|
706
|
+
|
707
|
+
|
708
|
+
_no_default = _NO_DEFAULT()
|
709
|
+
|
710
|
+
|
711
|
+
class _Parameter:
|
712
|
+
default: Any
|
713
|
+
init: bool
|
714
|
+
|
715
|
+
def __init__(self, default: Any, init: bool):
|
716
|
+
self.default = default
|
717
|
+
self.init = init
|
718
|
+
|
719
|
+
def __get__(self, obj, obj_type=None) -> Any:
|
720
|
+
if obj:
|
721
|
+
if self.default is _no_default:
|
722
|
+
raise AttributeError("field has no default value and no specified value")
|
723
|
+
return self.default
|
724
|
+
return self
|
725
|
+
|
726
|
+
|
727
|
+
def is_parameter(p: Any) -> bool:
|
728
|
+
return isinstance(p, _Parameter) and p.init
|
729
|
+
|
730
|
+
|
731
|
+
def parameter(*, default: Any = _no_default, init: bool = True) -> Any:
|
732
|
+
"""Used to specify options for modal.cls parameters, similar to dataclass.field for dataclasses
|
733
|
+
```
|
734
|
+
class A:
|
735
|
+
a: str = modal.parameter()
|
736
|
+
|
737
|
+
```
|
738
|
+
|
739
|
+
If `init=False` is specified, the field is not considered a parameter for the
|
740
|
+
Modal class and not used in the synthesized constructor. This can be used to
|
741
|
+
optionally annotate the type of a field that's used internally, for example values
|
742
|
+
being set by @enter lifecycle methods, without breaking type checkers, but it has
|
743
|
+
no runtime effect on the class.
|
744
|
+
"""
|
745
|
+
# has to return Any to be assignable to any annotation (https://github.com/microsoft/pyright/issues/5102)
|
746
|
+
return _Parameter(default=default, init=init)
|