modal 0.62.16__py3-none-any.whl → 0.72.11__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 +17 -13
  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 +420 -937
  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 -59
  11. modal/_resources.py +51 -0
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1036 -0
  15. modal/_runtime/execution_context.py +89 -0
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +134 -9
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +52 -16
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +479 -100
  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 +460 -171
  29. modal/_utils/grpc_testing.py +47 -31
  30. modal/_utils/grpc_utils.py +62 -109
  31. modal/_utils/hash_utils.py +61 -19
  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 +5 -7
  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 +14 -12
  43. modal/app.py +1003 -314
  44. modal/app.pyi +540 -264
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +63 -53
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +205 -45
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +62 -14
  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 +64 -58
  55. modal/cli/launch.py +32 -18
  56. modal/cli/network_file_system.py +64 -83
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +35 -10
  59. modal/cli/programs/vscode.py +60 -10
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +234 -131
  62. modal/cli/secret.py +8 -7
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +79 -10
  65. modal/cli/volume.py +110 -109
  66. modal/client.py +250 -144
  67. modal/client.pyi +157 -118
  68. modal/cloud_bucket_mount.py +108 -34
  69. modal/cloud_bucket_mount.pyi +32 -38
  70. modal/cls.py +535 -148
  71. modal/cls.pyi +190 -146
  72. modal/config.py +41 -19
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +111 -65
  76. modal/dict.pyi +136 -131
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +34 -43
  80. modal/experimental.py +61 -2
  81. modal/extensions/ipython.py +5 -5
  82. modal/file_io.py +537 -0
  83. modal/file_io.pyi +235 -0
  84. modal/file_pattern_matcher.py +197 -0
  85. modal/functions.py +906 -911
  86. modal/functions.pyi +466 -430
  87. modal/gpu.py +57 -44
  88. modal/image.py +1089 -479
  89. modal/image.pyi +584 -228
  90. modal/io_streams.py +434 -0
  91. modal/io_streams.pyi +122 -0
  92. modal/mount.py +314 -101
  93. modal/mount.pyi +241 -235
  94. modal/network_file_system.py +92 -92
  95. modal/network_file_system.pyi +152 -110
  96. modal/object.py +67 -36
  97. modal/object.pyi +166 -143
  98. modal/output.py +63 -0
  99. modal/parallel_map.py +434 -0
  100. modal/parallel_map.pyi +75 -0
  101. modal/partial_function.py +282 -117
  102. modal/partial_function.pyi +222 -129
  103. modal/proxy.py +15 -12
  104. modal/proxy.pyi +3 -8
  105. modal/queue.py +182 -65
  106. modal/queue.pyi +218 -118
  107. modal/requirements/2024.04.txt +29 -0
  108. modal/requirements/2024.10.txt +16 -0
  109. modal/requirements/README.md +21 -0
  110. modal/requirements/base-images.json +22 -0
  111. modal/retries.py +48 -7
  112. modal/runner.py +459 -156
  113. modal/runner.pyi +135 -71
  114. modal/running_app.py +38 -0
  115. modal/sandbox.py +514 -236
  116. modal/sandbox.pyi +397 -169
  117. modal/schedule.py +4 -4
  118. modal/scheduler_placement.py +20 -3
  119. modal/secret.py +56 -31
  120. modal/secret.pyi +62 -42
  121. modal/serving.py +51 -56
  122. modal/serving.pyi +44 -36
  123. modal/stream_type.py +15 -0
  124. modal/token_flow.py +5 -3
  125. modal/token_flow.pyi +37 -32
  126. modal/volume.py +285 -157
  127. modal/volume.pyi +249 -184
  128. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
  129. modal-0.72.11.dist-info/RECORD +174 -0
  130. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
  131. modal_docs/gen_reference_docs.py +3 -1
  132. modal_docs/mdmd/mdmd.py +0 -1
  133. modal_docs/mdmd/signatures.py +5 -2
  134. modal_global_objects/images/base_images.py +28 -0
  135. modal_global_objects/mounts/python_standalone.py +2 -2
  136. modal_proto/__init__.py +1 -1
  137. modal_proto/api.proto +1288 -533
  138. modal_proto/api_grpc.py +856 -456
  139. modal_proto/api_pb2.py +2165 -1157
  140. modal_proto/api_pb2.pyi +8859 -0
  141. modal_proto/api_pb2_grpc.py +1674 -855
  142. modal_proto/api_pb2_grpc.pyi +1416 -0
  143. modal_proto/modal_api_grpc.py +149 -0
  144. modal_proto/modal_options_grpc.py +3 -0
  145. modal_proto/options_pb2.pyi +20 -0
  146. modal_proto/options_pb2_grpc.pyi +7 -0
  147. modal_proto/py.typed +0 -0
  148. modal_version/__init__.py +1 -1
  149. modal_version/_version_generated.py +2 -2
  150. modal/_asgi.py +0 -370
  151. modal/_container_entrypoint.pyi +0 -378
  152. modal/_container_exec.py +0 -128
  153. modal/_sandbox_shell.py +0 -49
  154. modal/shared_volume.py +0 -23
  155. modal/shared_volume.pyi +0 -24
  156. modal/stub.py +0 -783
  157. modal/stub.pyi +0 -332
  158. modal-0.62.16.dist-info/RECORD +0 -198
  159. modal_global_objects/images/conda.py +0 -15
  160. modal_global_objects/images/debian_slim.py +0 -15
  161. modal_global_objects/images/micromamba.py +0 -15
  162. test/__init__.py +0 -1
  163. test/aio_test.py +0 -12
  164. test/async_utils_test.py +0 -262
  165. test/blob_test.py +0 -67
  166. test/cli_imports_test.py +0 -149
  167. test/cli_test.py +0 -659
  168. test/client_test.py +0 -194
  169. test/cls_test.py +0 -630
  170. test/config_test.py +0 -137
  171. test/conftest.py +0 -1420
  172. test/container_app_test.py +0 -32
  173. test/container_test.py +0 -1389
  174. test/cpu_test.py +0 -23
  175. test/decorator_test.py +0 -85
  176. test/deprecation_test.py +0 -34
  177. test/dict_test.py +0 -33
  178. test/e2e_test.py +0 -68
  179. test/error_test.py +0 -7
  180. test/function_serialization_test.py +0 -32
  181. test/function_test.py +0 -653
  182. test/function_utils_test.py +0 -101
  183. test/gpu_test.py +0 -159
  184. test/grpc_utils_test.py +0 -141
  185. test/helpers.py +0 -42
  186. test/image_test.py +0 -669
  187. test/live_reload_test.py +0 -80
  188. test/lookup_test.py +0 -70
  189. test/mdmd_test.py +0 -329
  190. test/mount_test.py +0 -162
  191. test/mounted_files_test.py +0 -329
  192. test/network_file_system_test.py +0 -181
  193. test/notebook_test.py +0 -66
  194. test/object_test.py +0 -41
  195. test/package_utils_test.py +0 -25
  196. test/queue_test.py +0 -97
  197. test/resolver_test.py +0 -58
  198. test/retries_test.py +0 -67
  199. test/runner_test.py +0 -85
  200. test/sandbox_test.py +0 -191
  201. test/schedule_test.py +0 -15
  202. test/scheduler_placement_test.py +0 -29
  203. test/secret_test.py +0 -78
  204. test/serialization_test.py +0 -42
  205. test/stub_composition_test.py +0 -10
  206. test/stub_test.py +0 -360
  207. test/test_asgi_wrapper.py +0 -234
  208. test/token_flow_test.py +0 -18
  209. test/traceback_test.py +0 -135
  210. test/tunnel_test.py +0 -29
  211. test/utils_test.py +0 -88
  212. test/version_test.py +0 -14
  213. test/volume_test.py +0 -341
  214. test/watcher_test.py +0 -30
  215. test/webhook_test.py +0 -146
  216. /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
  217. /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
  218. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
  219. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
  220. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/entry_points.txt +0 -0
modal/cls.py CHANGED
@@ -1,31 +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, 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
15
+ from ._resources import convert_fn_config_to_resources_config
13
16
  from ._serialization import check_valid_cls_constructor_arg
17
+ from ._traceback import print_server_warnings
14
18
  from ._utils.async_utils import synchronize_api, synchronizer
19
+ from ._utils.deprecation import deprecation_warning, renamed_parameter
15
20
  from ._utils.grpc_utils import retry_transient_errors
16
21
  from ._utils.mount_utils import validate_volumes
17
22
  from .client import _Client
18
- from .exception import InvalidError, NotFoundError
19
- from .functions import (
20
- _parse_retries,
21
- )
22
- from .gpu import GPU_T, parse_gpu_config
23
+ from .exception import ExecutionError, InvalidError, NotFoundError, VersionError
24
+ from .functions import _Function, _parse_retries
25
+ from .gpu import GPU_T
23
26
  from .object import _get_environment_name, _Object
24
27
  from .partial_function import (
25
- PartialFunction,
26
- _find_callables_for_cls,
27
- _find_partial_methods_for_cls,
28
- _Function,
28
+ _find_callables_for_obj,
29
+ _find_partial_methods_for_user_cls,
30
+ _PartialFunction,
29
31
  _PartialFunctionFlags,
30
32
  )
31
33
  from .retries import Retries
@@ -36,7 +38,98 @@ T = TypeVar("T")
36
38
 
37
39
 
38
40
  if typing.TYPE_CHECKING:
39
- import modal.stub
41
+ import modal.app
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
40
133
 
41
134
 
42
135
  class _Obj:
@@ -44,18 +137,23 @@ class _Obj:
44
137
 
45
138
  All this class does is to return `Function` objects."""
46
139
 
47
- _functions: Dict[str, _Function]
48
- _inited: bool
49
- _entered: bool
50
- _local_obj: Any
51
- _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
52
152
 
53
153
  def __init__(
54
154
  self,
55
- user_cls: type,
56
- output_mgr: Optional[OutputManager],
57
- base_functions: Dict[str, _Function],
58
- from_other_workspace: bool,
155
+ cls: "_Cls",
156
+ user_cls: Optional[type], # this would be None in case of lookups
59
157
  options: Optional[api_pb2.FunctionOptions],
60
158
  args,
61
159
  kwargs,
@@ -64,179 +162,392 @@ class _Obj:
64
162
  check_valid_cls_constructor_arg(i + 1, arg)
65
163
  for key, kwarg in kwargs.items():
66
164
  check_valid_cls_constructor_arg(key, kwarg)
67
-
68
- self._functions = {}
69
- for k, fun in base_functions.items():
70
- self._functions[k] = fun.from_parametrized(self, from_other_workspace, options, args, kwargs)
71
- self._functions[k]._set_output_mgr(output_mgr)
165
+ self._cls = cls
72
166
 
73
167
  # Used for construction local object lazily
74
- self._inited = False
75
- self._entered = False
76
- self._local_obj = None
77
- if user_cls:
78
- 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)
79
200
  else:
80
- 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)
241
+
242
+ def _cached_user_cls_instance(self):
243
+ """Get or construct the local object
244
+
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
81
248
 
82
- def get_obj(self):
83
- """Constructs obj without any caching. Used by container entrypoint."""
84
- self._local_obj = self._local_obj_constr()
85
- setattr(self._local_obj, "_modal_functions", self._functions) # Needed for PartialFunction.__get__
86
- return self._local_obj
249
+ return self._user_cls_instance
87
250
 
88
- def get_local_obj(self):
89
- """Construct local object lazily. Used for .local() calls."""
90
- if not self._inited:
91
- self.get_obj() # Instantiate object
92
- self._inited = 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__()
93
257
 
94
- return self._local_obj
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()
95
264
 
96
- def enter(self):
97
- if not self._entered:
98
- if hasattr(self._local_obj, "__enter__"):
99
- self._local_obj.__enter__()
100
- self._entered = True
265
+ self._has_entered = True
101
266
 
102
267
  @property
103
- def entered(self):
104
- # needed because aenter is nowrap
105
- return self._entered
268
+ def _entered(self) -> bool:
269
+ # needed because _aenter is nowrap
270
+ return self._has_entered
106
271
 
107
- @entered.setter
108
- def entered(self, val):
109
- self._entered = val
272
+ @_entered.setter
273
+ def _entered(self, val: bool):
274
+ self._has_entered = val
110
275
 
111
276
  @synchronizer.nowrap
112
- async def aenter(self):
113
- if not self.entered:
114
- local_obj = self.get_local_obj()
115
- if hasattr(local_obj, "__aenter__"):
116
- await local_obj.__aenter__()
117
- elif hasattr(local_obj, "__enter__"):
118
- local_obj.__enter__()
119
- 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
120
285
 
121
286
  def __getattr__(self, k):
122
- if k in self._functions:
123
- return self._functions[k]
124
- elif self._local_obj_constr:
125
- obj = self.get_local_obj()
126
- return getattr(obj, k)
127
- else:
128
- 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
+ )
129
355
 
130
356
 
131
357
  Obj = synchronize_api(_Obj)
132
358
 
133
359
 
134
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
+
135
369
  _user_cls: Optional[type]
136
- _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
137
374
  _options: Optional[api_pb2.FunctionOptions]
138
- _callables: Dict[str, Callable]
139
- _from_other_workspace: Optional[bool] # Functions require FunctionBindParams before invocation.
140
- _stub: Optional["modal.stub._Stub"] = None # not set for lookups
375
+ _callables: dict[str, Callable[..., Any]]
376
+ _app: Optional["modal.app._App"] = None # not set for lookups
377
+ _name: Optional[str]
141
378
 
142
379
  def _initialize_from_empty(self):
143
380
  self._user_cls = None
144
- self._functions = {}
381
+ self._class_service_function = None
145
382
  self._options = None
146
383
  self._callables = {}
147
- self._from_other_workspace = None
148
- self._output_mgr: Optional[OutputManager] = None
384
+ self._name = None
149
385
 
150
386
  def _initialize_from_other(self, other: "_Cls"):
387
+ super()._initialize_from_other(other)
151
388
  self._user_cls = other._user_cls
152
- self._functions = other._functions
389
+ self._class_service_function = other._class_service_function
390
+ self._method_functions = other._method_functions
153
391
  self._options = other._options
154
392
  self._callables = other._callables
155
- self._from_other_workspace = other._from_other_workspace
156
- self._output_mgr: Optional[OutputManager] = other._output_mgr
393
+ self._name = other._name
157
394
 
158
- def _set_output_mgr(self, output_mgr: OutputManager):
159
- 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())
160
399
 
161
400
  def _hydrate_metadata(self, metadata: Message):
162
- for method in metadata.methods:
163
- if method.function_name in self._functions:
164
- self._functions[method.function_name]._hydrate(
165
- method.function_id, self._client, method.function_handle_metadata
166
- )
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
+ )
167
418
  else:
168
- 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(
169
440
  method.function_id, self._client, method.function_handle_metadata
170
441
  )
171
442
 
172
- def _get_metadata(self) -> api_pb2.ClassHandleMetadata:
173
- class_handle_metadata = api_pb2.ClassHandleMetadata()
174
- for f_name, f in self._functions.items():
175
- class_handle_metadata.methods.append(
176
- api_pb2.ClassMethod(
177
- function_name=f_name, function_id=f.object_id, function_handle_metadata=f._get_metadata()
178
- )
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"
179
452
  )
180
- 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
+ )
181
467
 
182
468
  @staticmethod
183
- def from_local(user_cls, stub, decorator: Callable[[PartialFunction, type], _Function]) -> "_Cls":
469
+ def from_local(user_cls, app: "modal.app._App", class_service_function: _Function) -> "_Cls":
184
470
  """mdmd:hidden"""
185
- functions: Dict[str, _Function] = {}
186
- for k, partial_function in _find_partial_methods_for_cls(user_cls, _PartialFunctionFlags.FUNCTION).items():
187
- 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
188
485
 
189
486
  # Disable the warning that these are not wrapped
190
- 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():
191
488
  partial_function.wrapped = True
192
489
 
193
490
  # Get all callables
194
- 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
+ }
195
494
 
196
- def _deps() -> List[_Function]:
197
- return list(functions.values())
495
+ def _deps() -> list[_Function]:
496
+ return [class_service_function]
198
497
 
199
498
  async def _load(self: "_Cls", resolver: Resolver, existing_object_id: Optional[str]):
200
- req = api_pb2.ClassCreateRequest(app_id=resolver.app_id, existing_class_id=existing_object_id)
201
- for f_name, f in functions.items():
202
- 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
+ )
203
502
  resp = await resolver.client.stub.ClassCreate(req)
204
503
  self._hydrate(resp.class_id, resolver.client, resp.handle_metadata)
205
504
 
206
505
  rep = f"Cls({user_cls.__name__})"
207
- cls = _Cls._from_loader(_load, rep, deps=_deps)
208
- cls._stub = stub
506
+ cls: _Cls = _Cls._from_loader(_load, rep, deps=_deps)
507
+ cls._app = app
209
508
  cls._user_cls = user_cls
210
- cls._functions = functions
509
+ cls._class_service_function = class_service_function
510
+ cls._method_functions = method_functions
211
511
  cls._callables = callables
212
- cls._from_other_workspace = False
213
- setattr(cls._user_cls, "_modal_functions", functions) # Needed for PartialFunction.__get__
512
+ cls._name = user_cls.__name__
214
513
  return cls
215
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
+
216
520
  @classmethod
521
+ @renamed_parameter((2024, 12, 18), "tag", "name")
217
522
  def from_name(
218
- cls: Type["_Cls"],
523
+ cls: type["_Cls"],
219
524
  app_name: str,
220
- tag: Optional[str] = None,
525
+ name: str,
221
526
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
222
527
  environment_name: Optional[str] = None,
223
528
  workspace: Optional[str] = None,
224
529
  ) -> "_Cls":
225
- """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.
226
535
 
227
536
  ```python
228
- Class = modal.Cls.from_name("other-app", "Class")
537
+ Model = modal.Cls.from_name("other-app", "Model")
229
538
  ```
230
539
  """
231
540
 
232
541
  async def _load_remote(obj: _Object, resolver: Resolver, existing_object_id: Optional[str]):
542
+ _environment_name = _get_environment_name(environment_name, resolver)
233
543
  request = api_pb2.ClassGetRequest(
234
544
  app_name=app_name,
235
- object_tag=tag,
545
+ object_tag=name,
236
546
  namespace=namespace,
237
- environment_name=_get_environment_name(environment_name, resolver),
547
+ environment_name=_environment_name,
238
548
  lookup_published=workspace is not None,
239
549
  workspace_name=workspace,
550
+ only_class_function=True,
240
551
  )
241
552
  try:
242
553
  response = await retry_transient_errors(resolver.client.stub.ClassGet, request)
@@ -248,50 +559,59 @@ class _Cls(_Object, type_prefix="cs"):
248
559
  else:
249
560
  raise
250
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
+
251
578
  obj._hydrate(response.class_id, resolver.client, response.handle_metadata)
252
579
 
253
580
  rep = f"Ref({app_name})"
254
- cls = cls._from_loader(_load_remote, rep, is_another_app=True)
255
- 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
256
584
  return cls
257
585
 
258
586
  def with_options(
259
587
  self: "_Cls",
260
- cpu: Optional[float] = None,
261
- memory: Optional[int] = None,
588
+ cpu: Optional[Union[float, tuple[float, float]]] = None,
589
+ memory: Optional[Union[int, tuple[int, int]]] = None,
262
590
  gpu: GPU_T = None,
263
591
  secrets: Collection[_Secret] = (),
264
- volumes: Dict[Union[str, os.PathLike], _Volume] = {},
592
+ volumes: dict[Union[str, os.PathLike], _Volume] = {},
265
593
  retries: Optional[Union[int, Retries]] = None,
266
594
  timeout: Optional[int] = None,
267
595
  concurrency_limit: Optional[int] = None,
268
596
  allow_concurrent_inputs: Optional[int] = None,
269
597
  container_idle_timeout: Optional[int] = None,
270
- allow_background_volume_commits: bool = False,
271
598
  ) -> "_Cls":
272
599
  """
273
- Allows for the runtime modification of a modal.Cls's configuration.
274
- Designed for usage in the [MK1 Flywheel](/docs/guide/mk1).
600
+ **Beta:** Allows for the runtime modification of a modal.Cls's configuration.
601
+
602
+ This is a beta feature and may be unstable.
275
603
 
276
604
  **Usage:**
277
605
 
278
606
  ```python notest
279
- import modal
280
- Model = modal.Cls.lookup(
281
- "flywheel-generic", "Model", workspace="mk-1"
282
- )
283
- Model2 = Model.with_options(
284
- gpu=modal.gpu.A100(memory=40),
285
- volumes={"/models": models_vol}
286
- )
287
- 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
288
610
  ```
289
611
  """
290
- retry_policy = _parse_retries(retries)
612
+ retry_policy = _parse_retries(retries, f"Class {self.__name__}" if self._user_cls else "")
291
613
  if gpu or cpu or memory:
292
- milli_cpu = int(1000 * cpu) if cpu is not None else None
293
- gpu_config = parse_gpu_config(gpu)
294
- resources = api_pb2.Resources(milli_cpu=milli_cpu, gpu_config=gpu_config, memory_mb=memory)
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)