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.
Files changed (220) hide show
  1. modal/__init__.py +13 -9
  2. modal/__main__.py +41 -3
  3. modal/_clustered_functions.py +80 -0
  4. modal/_clustered_functions.pyi +22 -0
  5. modal/_container_entrypoint.py +402 -398
  6. modal/_ipython.py +3 -13
  7. modal/_location.py +17 -10
  8. modal/_output.py +243 -99
  9. modal/_pty.py +2 -2
  10. modal/_resolver.py +55 -60
  11. modal/_resources.py +26 -7
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1025 -0
  15. modal/{execution_context.py → _runtime/execution_context.py} +11 -2
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +123 -6
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +50 -14
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +386 -104
  24. modal/_utils/blob_utils.py +157 -186
  25. modal/_utils/bytes_io_segment_payload.py +97 -0
  26. modal/_utils/deprecation.py +89 -0
  27. modal/_utils/docker_utils.py +98 -0
  28. modal/_utils/function_utils.py +299 -98
  29. modal/_utils/grpc_testing.py +47 -34
  30. modal/_utils/grpc_utils.py +54 -21
  31. modal/_utils/hash_utils.py +51 -10
  32. modal/_utils/http_utils.py +39 -9
  33. modal/_utils/logger.py +2 -1
  34. modal/_utils/mount_utils.py +34 -16
  35. modal/_utils/name_utils.py +58 -0
  36. modal/_utils/package_utils.py +14 -1
  37. modal/_utils/pattern_utils.py +205 -0
  38. modal/_utils/rand_pb_testing.py +3 -3
  39. modal/_utils/shell_utils.py +15 -49
  40. modal/_vendor/a2wsgi_wsgi.py +62 -72
  41. modal/_vendor/cloudpickle.py +1 -1
  42. modal/_watcher.py +12 -10
  43. modal/app.py +561 -323
  44. modal/app.pyi +474 -262
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +22 -6
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +203 -42
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +61 -13
  51. modal/cli/dict.py +128 -0
  52. modal/cli/entry_point.py +26 -13
  53. modal/cli/environment.py +40 -9
  54. modal/cli/import_refs.py +21 -48
  55. modal/cli/launch.py +28 -14
  56. modal/cli/network_file_system.py +57 -21
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +34 -9
  59. modal/cli/programs/vscode.py +58 -8
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +199 -96
  62. modal/cli/secret.py +5 -4
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +74 -8
  65. modal/cli/volume.py +97 -56
  66. modal/client.py +248 -144
  67. modal/client.pyi +156 -124
  68. modal/cloud_bucket_mount.py +43 -30
  69. modal/cloud_bucket_mount.pyi +32 -25
  70. modal/cls.py +528 -141
  71. modal/cls.pyi +189 -145
  72. modal/config.py +32 -15
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +50 -54
  76. modal/dict.pyi +120 -164
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +30 -43
  80. modal/experimental.py +62 -2
  81. modal/file_io.py +537 -0
  82. modal/file_io.pyi +235 -0
  83. modal/file_pattern_matcher.py +196 -0
  84. modal/functions.py +846 -428
  85. modal/functions.pyi +446 -387
  86. modal/gpu.py +57 -44
  87. modal/image.py +943 -417
  88. modal/image.pyi +584 -245
  89. modal/io_streams.py +434 -0
  90. modal/io_streams.pyi +122 -0
  91. modal/mount.py +223 -90
  92. modal/mount.pyi +241 -243
  93. modal/network_file_system.py +85 -86
  94. modal/network_file_system.pyi +151 -110
  95. modal/object.py +66 -36
  96. modal/object.pyi +166 -143
  97. modal/output.py +63 -0
  98. modal/parallel_map.py +73 -47
  99. modal/parallel_map.pyi +51 -63
  100. modal/partial_function.py +272 -107
  101. modal/partial_function.pyi +219 -120
  102. modal/proxy.py +15 -12
  103. modal/proxy.pyi +3 -8
  104. modal/queue.py +96 -72
  105. modal/queue.pyi +210 -135
  106. modal/requirements/2024.04.txt +2 -1
  107. modal/requirements/2024.10.txt +16 -0
  108. modal/requirements/README.md +21 -0
  109. modal/requirements/base-images.json +22 -0
  110. modal/retries.py +45 -4
  111. modal/runner.py +325 -203
  112. modal/runner.pyi +124 -110
  113. modal/running_app.py +27 -4
  114. modal/sandbox.py +509 -231
  115. modal/sandbox.pyi +396 -169
  116. modal/schedule.py +2 -2
  117. modal/scheduler_placement.py +20 -3
  118. modal/secret.py +41 -25
  119. modal/secret.pyi +62 -42
  120. modal/serving.py +39 -49
  121. modal/serving.pyi +37 -43
  122. modal/stream_type.py +15 -0
  123. modal/token_flow.py +5 -3
  124. modal/token_flow.pyi +37 -32
  125. modal/volume.py +123 -137
  126. modal/volume.pyi +228 -221
  127. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
  128. modal-0.72.13.dist-info/RECORD +174 -0
  129. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
  130. modal_docs/gen_reference_docs.py +3 -1
  131. modal_docs/mdmd/mdmd.py +0 -1
  132. modal_docs/mdmd/signatures.py +1 -2
  133. modal_global_objects/images/base_images.py +28 -0
  134. modal_global_objects/mounts/python_standalone.py +2 -2
  135. modal_proto/__init__.py +1 -1
  136. modal_proto/api.proto +1231 -531
  137. modal_proto/api_grpc.py +750 -430
  138. modal_proto/api_pb2.py +2102 -1176
  139. modal_proto/api_pb2.pyi +8859 -0
  140. modal_proto/api_pb2_grpc.py +1329 -675
  141. modal_proto/api_pb2_grpc.pyi +1416 -0
  142. modal_proto/modal_api_grpc.py +149 -0
  143. modal_proto/modal_options_grpc.py +3 -0
  144. modal_proto/options_pb2.pyi +20 -0
  145. modal_proto/options_pb2_grpc.pyi +7 -0
  146. modal_proto/py.typed +0 -0
  147. modal_version/__init__.py +1 -1
  148. modal_version/_version_generated.py +2 -2
  149. modal/_asgi.py +0 -370
  150. modal/_container_exec.py +0 -128
  151. modal/_container_io_manager.py +0 -646
  152. modal/_container_io_manager.pyi +0 -412
  153. modal/_sandbox_shell.py +0 -49
  154. modal/app_utils.py +0 -20
  155. modal/app_utils.pyi +0 -17
  156. modal/execution_context.pyi +0 -37
  157. modal/shared_volume.py +0 -23
  158. modal/shared_volume.pyi +0 -24
  159. modal-0.62.115.dist-info/RECORD +0 -207
  160. modal_global_objects/images/conda.py +0 -15
  161. modal_global_objects/images/debian_slim.py +0 -15
  162. modal_global_objects/images/micromamba.py +0 -15
  163. test/__init__.py +0 -1
  164. test/aio_test.py +0 -12
  165. test/async_utils_test.py +0 -279
  166. test/blob_test.py +0 -67
  167. test/cli_imports_test.py +0 -149
  168. test/cli_test.py +0 -674
  169. test/client_test.py +0 -203
  170. test/cloud_bucket_mount_test.py +0 -22
  171. test/cls_test.py +0 -636
  172. test/config_test.py +0 -149
  173. test/conftest.py +0 -1485
  174. test/container_app_test.py +0 -50
  175. test/container_test.py +0 -1405
  176. test/cpu_test.py +0 -23
  177. test/decorator_test.py +0 -85
  178. test/deprecation_test.py +0 -34
  179. test/dict_test.py +0 -51
  180. test/e2e_test.py +0 -68
  181. test/error_test.py +0 -7
  182. test/function_serialization_test.py +0 -32
  183. test/function_test.py +0 -791
  184. test/function_utils_test.py +0 -101
  185. test/gpu_test.py +0 -159
  186. test/grpc_utils_test.py +0 -82
  187. test/helpers.py +0 -47
  188. test/image_test.py +0 -814
  189. test/live_reload_test.py +0 -80
  190. test/lookup_test.py +0 -70
  191. test/mdmd_test.py +0 -329
  192. test/mount_test.py +0 -162
  193. test/mounted_files_test.py +0 -327
  194. test/network_file_system_test.py +0 -188
  195. test/notebook_test.py +0 -66
  196. test/object_test.py +0 -41
  197. test/package_utils_test.py +0 -25
  198. test/queue_test.py +0 -115
  199. test/resolver_test.py +0 -59
  200. test/retries_test.py +0 -67
  201. test/runner_test.py +0 -85
  202. test/sandbox_test.py +0 -191
  203. test/schedule_test.py +0 -15
  204. test/scheduler_placement_test.py +0 -57
  205. test/secret_test.py +0 -89
  206. test/serialization_test.py +0 -50
  207. test/stub_composition_test.py +0 -10
  208. test/stub_test.py +0 -361
  209. test/test_asgi_wrapper.py +0 -234
  210. test/token_flow_test.py +0 -18
  211. test/traceback_test.py +0 -135
  212. test/tunnel_test.py +0 -29
  213. test/utils_test.py +0 -88
  214. test/version_test.py +0 -14
  215. test/volume_test.py +0 -397
  216. test/watcher_test.py +0 -58
  217. test/webhook_test.py +0 -145
  218. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
  219. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
  220. {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 typing import Any, Callable, Collection, Dict, List, Optional, Tuple, Type, TypeVar, Union
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
- PartialFunction,
27
- _find_callables_for_cls,
28
- _find_partial_methods_for_cls,
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
- _functions: Dict[str, _Function]
49
- _inited: bool
50
- _entered: bool
51
- _local_obj: Any
52
- _local_obj_constr: Optional[Callable[[], Any]]
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
- user_cls: type,
57
- output_mgr: Optional[OutputManager],
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._inited = False
76
- self._entered = False
77
- self._local_obj = None
78
- if user_cls:
79
- self._local_obj_constr = lambda: user_cls(*args, **kwargs)
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
- self._local_obj_constr = None
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 get_obj(self):
84
- """Constructs obj without any caching. Used by container entrypoint."""
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
- def get_local_obj(self):
90
- """Construct local object lazily. Used for .local() calls."""
91
- if not self._inited:
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._local_obj
249
+ return self._user_cls_instance
96
250
 
97
- def enter(self):
98
- if not self._entered:
99
- if hasattr(self._local_obj, "__enter__"):
100
- self._local_obj.__enter__()
101
- self._entered = True
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 entered(self):
105
- # needed because aenter is nowrap
106
- return self._entered
268
+ def _entered(self) -> bool:
269
+ # needed because _aenter is nowrap
270
+ return self._has_entered
107
271
 
108
- @entered.setter
109
- def entered(self, val):
110
- self._entered = val
272
+ @_entered.setter
273
+ def _entered(self, val: bool):
274
+ self._has_entered = val
111
275
 
112
276
  @synchronizer.nowrap
113
- async def aenter(self):
114
- if not self.entered:
115
- local_obj = self.get_local_obj()
116
- if hasattr(local_obj, "__aenter__"):
117
- await local_obj.__aenter__()
118
- elif hasattr(local_obj, "__enter__"):
119
- local_obj.__enter__()
120
- self.entered = True
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
- if k in self._functions:
124
- return self._functions[k]
125
- elif self._local_obj_constr:
126
- obj = self.get_local_obj()
127
- return getattr(obj, k)
128
- else:
129
- raise AttributeError(k)
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
- _functions: Dict[str, _Function]
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: Dict[str, Callable]
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._functions = {}
381
+ self._class_service_function = None
146
382
  self._options = None
147
383
  self._callables = {}
148
- self._from_other_workspace = None
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._functions = other._functions
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._from_other_workspace = other._from_other_workspace
157
- self._output_mgr: Optional[OutputManager] = other._output_mgr
393
+ self._name = other._name
158
394
 
159
- def _set_output_mgr(self, output_mgr: OutputManager):
160
- self._output_mgr = output_mgr
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
- for method in metadata.methods:
164
- if method.function_name in self._functions:
165
- self._functions[method.function_name]._hydrate(
166
- method.function_id, self._client, method.function_handle_metadata
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
- self._functions[method.function_name] = _Function._new_hydrated(
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
- def _get_metadata(self) -> api_pb2.ClassHandleMetadata:
174
- class_handle_metadata = api_pb2.ClassHandleMetadata()
175
- for f_name, f in self._functions.items():
176
- class_handle_metadata.methods.append(
177
- api_pb2.ClassMethod(
178
- function_name=f_name, function_id=f.object_id, function_handle_metadata=f._get_metadata()
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
- return class_handle_metadata
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, decorator: Callable[[PartialFunction, type], _Function]) -> "_Cls":
469
+ def from_local(user_cls, app: "modal.app._App", class_service_function: _Function) -> "_Cls":
185
470
  """mdmd:hidden"""
186
- functions: Dict[str, _Function] = {}
187
- for k, partial_function in _find_partial_methods_for_cls(user_cls, _PartialFunctionFlags.FUNCTION).items():
188
- functions[k] = decorator(partial_function, user_cls)
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 _find_partial_methods_for_cls(user_cls, ~_PartialFunctionFlags.FUNCTION).values():
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: Dict[str, Callable] = _find_callables_for_cls(user_cls, ~_PartialFunctionFlags(0))
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() -> List[_Function]:
198
- return list(functions.values())
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(app_id=resolver.app_id, existing_class_id=existing_object_id)
202
- for f_name, f in functions.items():
203
- req.methods.append(api_pb2.ClassMethod(function_name=f_name, function_id=f.object_id))
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._functions = functions
509
+ cls._class_service_function = class_service_function
510
+ cls._method_functions = method_functions
212
511
  cls._callables = callables
213
- cls._from_other_workspace = False
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: Type["_Cls"],
523
+ cls: type["_Cls"],
220
524
  app_name: str,
221
- tag: Optional[str] = None,
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
- """Retrieve a class with a given name and tag.
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
- Class = modal.Cls.from_name("other-app", "Class")
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=tag,
545
+ object_tag=name,
237
546
  namespace=namespace,
238
- environment_name=_get_environment_name(environment_name, resolver),
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
- cls._from_other_workspace = bool(workspace is not None)
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, Tuple[int, int]]] = None,
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: Dict[Union[str, os.PathLike], _Volume] = {},
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: Allows for the runtime modification of a modal.Cls's configuration.
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
- import modal
282
- Model = modal.Cls.lookup(
283
- "flywheel-generic", "Model", workspace="mk-1"
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=allow_background_volume_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
- allow_concurrent_inputs=allow_concurrent_inputs,
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
- tag: Optional[str] = None,
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 class with a given name and tag.
654
+ """Lookup a Cls from a deployed App by its name.
334
655
 
335
- ```python
336
- Class = modal.Cls.lookup("other-app", "Class")
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(app_name, tag, namespace=namespace, environment_name=environment_name, workspace=workspace)
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._user_cls, self._output_mgr, self._functions, self._from_other_workspace, self._options, args, kwargs
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
- if k in self._functions:
355
- return self._functions[k]
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)