-
Notifications
You must be signed in to change notification settings - Fork 70
Make all "constructors" take a client optional arg
#3669
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
761c08d
fe730c6
85613dd
12f923e
9636c3e
2a688c6
8df8a97
a2cef05
72f72a8
383f2cf
4073520
b151f64
40d2b6b
00de1d6
97fc242
21b9e0e
5e577b9
24acd45
1652204
97a9f80
46af298
8d47cd2
bcf376c
c324d25
38ae900
98bc369
333cf74
ea83ede
3b60952
fe10c23
f6da666
130d560
baf2cad
c5012df
e84061d
03921ce
4d6a6fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,8 @@ | |
| from modal_proto import api_pb2 | ||
| from modal_proto.modal_api_grpc import ModalClientModal | ||
|
|
||
| from ._object import _get_environment_name, _Object, live_method, live_method_gen | ||
| from ._load_context import LoadContext | ||
| from ._object import _Object, live_method, live_method_gen | ||
| from ._pty import get_pty_info | ||
| from ._resolver import Resolver | ||
| from ._resources import convert_fn_config_to_resources_config | ||
|
|
@@ -656,7 +657,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type | |
| @staticmethod | ||
| def from_local( | ||
| info: FunctionInfo, | ||
| app, | ||
| app: Optional["modal.app._App"], # App here should only be None in case of Image.run_function | ||
| image: _Image, | ||
| env: Optional[dict[str, Optional[str]]] = None, | ||
| secrets: Optional[Collection[_Secret]] = None, | ||
|
|
@@ -882,12 +883,12 @@ def _deps(only_explicit_mounts=False) -> list[_Object]: | |
| is_web_endpoint, is_generator, restrict_output | ||
| ) | ||
|
|
||
| async def _preload(self: _Function, resolver: Resolver, existing_object_id: Optional[str]): | ||
| assert resolver.client and resolver.client.stub | ||
|
|
||
| assert resolver.app_id | ||
| async def _preload( | ||
| self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str] | ||
freider marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ): | ||
| assert load_context.app_id | ||
| req = api_pb2.FunctionPrecreateRequest( | ||
| app_id=resolver.app_id, | ||
| app_id=load_context.app_id, | ||
| function_name=info.function_name, | ||
| function_type=function_type, | ||
| existing_function_id=existing_object_id or "", | ||
|
|
@@ -903,11 +904,12 @@ async def _preload(self: _Function, resolver: Resolver, existing_object_id: Opti | |
| elif webhook_config: | ||
| req.webhook_config.CopyFrom(webhook_config) | ||
|
|
||
| response = await resolver.client.stub.FunctionPrecreate(req) | ||
| self._hydrate(response.function_id, resolver.client, response.handle_metadata) | ||
| response = await load_context.client.stub.FunctionPrecreate(req) | ||
| self._hydrate(response.function_id, load_context.client, response.handle_metadata) | ||
|
|
||
| async def _load(self: _Function, resolver: Resolver, existing_object_id: Optional[str]): | ||
| assert resolver.client and resolver.client.stub | ||
| async def _load( | ||
| self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str] | ||
| ): | ||
| with FunctionCreationStatus(resolver, tag) as function_creation_status: | ||
| timeout_secs = timeout | ||
|
|
||
|
|
@@ -1103,16 +1105,16 @@ async def _load(self: _Function, resolver: Resolver, existing_object_id: Optiona | |
| ), | ||
| ) | ||
|
|
||
| assert resolver.app_id | ||
| assert load_context.app_id | ||
| assert (function_definition is None) != (function_data is None) # xor | ||
| request = api_pb2.FunctionCreateRequest( | ||
| app_id=resolver.app_id, | ||
| app_id=load_context.app_id, | ||
| function=function_definition, | ||
| function_data=function_data, | ||
| existing_function_id=existing_object_id or "", | ||
| ) | ||
| try: | ||
| response: api_pb2.FunctionCreateResponse = await resolver.client.stub.FunctionCreate(request) | ||
| response: api_pb2.FunctionCreateResponse = await load_context.client.stub.FunctionCreate(request) | ||
| except GRPCError as exc: | ||
| if exc.status == Status.INVALID_ARGUMENT: | ||
| raise InvalidError(exc.message) | ||
|
|
@@ -1127,10 +1129,14 @@ async def _load(self: _Function, resolver: Resolver, existing_object_id: Optiona | |
| serve_mounts = {m for m in all_mounts if m.is_local()} | ||
| serve_mounts |= image._serve_mounts | ||
| obj._serve_mounts = frozenset(serve_mounts) | ||
| self._hydrate(response.function_id, resolver.client, response.handle_metadata) | ||
| self._hydrate(response.function_id, load_context.client, response.handle_metadata) | ||
|
|
||
| rep = f"Function({tag})" | ||
| obj = _Function._from_loader(_load, rep, preload=_preload, deps=_deps) | ||
| # Pass a *reference* to the App's LoadContext - this is important since the App is | ||
| # the only way to infer a LoadContext for an `@app.function`, and the App doesn't | ||
| # get its client until *after* the Function is created. | ||
| load_context = app._root_load_context if app else LoadContext.empty() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: LoadContext Sharing Breaks When App Is NoneWhen |
||
| obj = _Function._from_loader(_load, rep, preload=_preload, deps=_deps, load_context_overrides=load_context) | ||
|
|
||
| obj._raw_f = info.raw_f | ||
| obj._info = info | ||
|
|
@@ -1172,7 +1178,12 @@ def _bind_parameters( | |
|
|
||
| parent = self | ||
|
|
||
| async def _load(param_bound_func: _Function, resolver: Resolver, existing_object_id: Optional[str]): | ||
| async def _load( | ||
| param_bound_func: _Function, | ||
| resolver: Resolver, | ||
| load_context: LoadContext, | ||
| existing_object_id: Optional[str], | ||
| ): | ||
| if not parent.is_hydrated: | ||
| # While the base Object.hydrate() method appears to be idempotent, it's not always safe | ||
| await parent.hydrate() | ||
|
|
@@ -1205,7 +1216,6 @@ async def _load(param_bound_func: _Function, resolver: Resolver, existing_object | |
| param_bound_func._hydrate_from_other(parent) | ||
| return | ||
|
|
||
| environment_name = _get_environment_name(None, resolver) | ||
| assert parent is not None and parent.is_hydrated | ||
|
|
||
| if options: | ||
|
|
@@ -1245,7 +1255,7 @@ async def _load(param_bound_func: _Function, resolver: Resolver, existing_object | |
| function_id=parent.object_id, | ||
| serialized_params=serialized_params, | ||
| function_options=options_pb, | ||
| environment_name=environment_name | ||
| environment_name=load_context.environment_name | ||
| or "", # TODO: investigate shouldn't environment name always be specified here? | ||
| ) | ||
|
|
||
|
|
@@ -1262,7 +1272,13 @@ def _deps(): | |
| return [dep for dep in all_deps if not dep.is_hydrated] | ||
| return [] | ||
|
|
||
| fun: _Function = _Function._from_loader(_load, "Function(parametrized)", hydrate_lazily=True, deps=_deps) | ||
| fun: _Function = _Function._from_loader( | ||
| _load, | ||
| "Function(parametrized)", | ||
| hydrate_lazily=True, | ||
| deps=_deps, | ||
| load_context_overrides=self._load_context_overrides, | ||
| ) | ||
|
|
||
| fun._info = self._info | ||
| fun._obj = obj | ||
|
|
@@ -1360,34 +1376,43 @@ def _from_name( | |
| cls, | ||
| app_name: str, | ||
| name: str, | ||
| namespace=None, # mdmd:line-hidden | ||
| environment_name: Optional[str] = None, | ||
| *, | ||
| load_context_overrides: LoadContext, | ||
| ): | ||
| # internal function lookup implementation that allows lookup of class "service functions" | ||
| # in addition to non-class functions | ||
| async def _load_remote(self: _Function, resolver: Resolver, existing_object_id: Optional[str]): | ||
| assert resolver.client and resolver.client.stub | ||
| async def _load_remote( | ||
| self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str] | ||
| ): | ||
| request = api_pb2.FunctionGetRequest( | ||
| app_name=app_name, | ||
| object_tag=name, | ||
| environment_name=_get_environment_name(environment_name, resolver) or "", | ||
| environment_name=load_context.environment_name, | ||
| ) | ||
| try: | ||
| response = await resolver.client.stub.FunctionGet(request) | ||
| response = await load_context.client.stub.FunctionGet(request) | ||
| except NotFoundError as exc: | ||
| # refine the error message | ||
| env_context = f" (in the '{environment_name}' environment)" if environment_name else "" | ||
| env_context = ( | ||
| f" (in the '{load_context.environment_name}' environment)" if load_context.environment_name else "" | ||
| ) | ||
| raise NotFoundError( | ||
| f"Lookup failed for Function '{name}' from the '{app_name}' app{env_context}: {exc}." | ||
| ) from None | ||
|
|
||
| print_server_warnings(response.server_warnings) | ||
|
|
||
| self._hydrate(response.function_id, resolver.client, response.handle_metadata) | ||
| self._hydrate(response.function_id, load_context.client, response.handle_metadata) | ||
|
|
||
| environment_rep = f", environment_name={environment_name!r}" if environment_name else "" | ||
| environment_rep = ( | ||
| f", environment_name={load_context_overrides.environment_name!r}" | ||
| if load_context_overrides._environment_name # slightly ugly - checking if _environment_name is overridden | ||
| else "" | ||
| ) | ||
| rep = f"modal.Function.from_name('{app_name}', '{name}'{environment_rep})" | ||
| return cls._from_loader(_load_remote, rep, is_another_app=True, hydrate_lazily=True) | ||
| return cls._from_loader( | ||
| _load_remote, rep, is_another_app=True, hydrate_lazily=True, load_context_overrides=load_context_overrides | ||
| ) | ||
|
|
||
| @classmethod | ||
| def from_name( | ||
|
|
@@ -1397,6 +1422,7 @@ def from_name( | |
| *, | ||
| namespace=None, # mdmd:line-hidden | ||
| environment_name: Optional[str] = None, | ||
| client: Optional[_Client] = None, | ||
| ) -> "_Function": | ||
| """Reference a Function from a deployed App by its name. | ||
|
|
||
|
|
@@ -1420,7 +1446,9 @@ def from_name( | |
| ) | ||
|
|
||
| warn_if_passing_namespace(namespace, "modal.Function.from_name") | ||
| return cls._from_name(app_name, name, environment_name=environment_name) | ||
| return cls._from_name( | ||
| app_name, name, load_context_overrides=LoadContext(environment_name=environment_name, client=client) | ||
| ) | ||
|
|
||
| @property | ||
| def tag(self) -> str: | ||
|
|
@@ -2018,16 +2046,20 @@ async def from_id(function_call_id: str, client: Optional[_Client] = None) -> "_ | |
| if you no longer have access to the original object returned from `Function.spawn`. | ||
|
|
||
| """ | ||
| if client is None: | ||
| client = await _Client.from_env() | ||
|
|
||
| async def _load(self: _FunctionCall, resolver: Resolver, existing_object_id: Optional[str]): | ||
| async def _load( | ||
| self: _FunctionCall, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str] | ||
| ): | ||
| request = api_pb2.FunctionCallFromIdRequest(function_call_id=function_call_id) | ||
| resp = await resolver.client.stub.FunctionCallFromId(request) | ||
| self._hydrate(function_call_id, resolver.client, resp) | ||
| resp = await load_context.client.stub.FunctionCallFromId(request) | ||
| self._hydrate(function_call_id, load_context.client, resp) | ||
|
|
||
| rep = f"FunctionCall.from_id({function_call_id!r})" | ||
| fc: _FunctionCall[Any] = _FunctionCall._from_loader(_load, rep, hydrate_lazily=True) | ||
| fc: _FunctionCall[Any] = _FunctionCall._from_loader( | ||
| _load, rep, hydrate_lazily=True, load_context_overrides=LoadContext(client=client) | ||
| ) | ||
|
|
||
| # TODO(elias): Remove these | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # We already know the object ID, so we can set it directly | ||
| fc._object_id = function_call_id | ||
| fc._client = client | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| # Copyright Modal Labs 2025 | ||
| from typing import Optional | ||
|
|
||
| from .client import _Client | ||
| from .config import config | ||
|
|
||
|
|
||
| class LoadContext: | ||
| """Encapsulates optional metadata values used during object loading. | ||
|
|
||
| This metadata is set during object construction and propagated through | ||
| parent-child relationships (e.g., App -> Function, Cls -> Obj -> bound methods). | ||
| """ | ||
|
|
||
| _client: Optional[_Client] = None | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At a glance, it looks like @dataclass
class ResolvedContext:
client: _Client
environment_name: str
app_id: Optiona[str]I would even rename
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that could be nice! One argument against would be if we think ahead to when the Resolver doesn't exist anymore and all contexts are just propagated from their "parents" explicitly (instead of using Making each property resolve individually would fix the case where we have loaders that may raise exceptions immediately without actually needing a functional context (like calling methods directly on un-instantiated classes). Maybe we should actually already make that change, e.g. having LoadContext have |
||
| _environment_name: Optional[str] = None | ||
| _app_id: Optional[str] = None | ||
|
|
||
| def __init__( | ||
| self, | ||
| *, | ||
| client: Optional[_Client] = None, | ||
| environment_name: Optional[str] = None, | ||
| app_id: Optional[str] = None, | ||
| ): | ||
| self._client = client | ||
| self._environment_name = environment_name | ||
| self._app_id = app_id | ||
|
|
||
| @property | ||
| def client(self) -> _Client: | ||
| assert self._client is not None | ||
| return self._client | ||
|
|
||
| @property | ||
| def environment_name(self) -> str: | ||
| assert self._environment_name is not None | ||
| return self._environment_name | ||
|
|
||
| @property | ||
| def app_id(self) -> Optional[str]: | ||
| return self._app_id | ||
|
|
||
| @classmethod | ||
| def empty(cls) -> "LoadContext": | ||
| """Create an empty LoadContext with all fields set to None. | ||
|
|
||
| Used when loading objects that don't have a parent context. | ||
| """ | ||
| return cls(client=None, environment_name=None, app_id=None) | ||
|
|
||
| def merged_with(self, parent: "LoadContext") -> "LoadContext": | ||
| """Create a new LoadContext with parent values filling in None fields. | ||
|
|
||
| Returns a new LoadContext without mutating self or parent. | ||
| Values from self take precedence over values from parent. | ||
| """ | ||
| return LoadContext( | ||
| client=self._client if self._client is not None else parent._client, | ||
| environment_name=self._environment_name if self._environment_name is not None else parent._environment_name, | ||
| app_id=self._app_id if self._app_id is not None else parent._app_id, | ||
| ) # TODO (elias): apply_defaults? | ||
|
|
||
| async def apply_defaults(self) -> "LoadContext": | ||
| """Infer default client and environment_name if not present | ||
|
|
||
| Returns a new instance (no in place mutation)""" | ||
|
|
||
| return LoadContext( | ||
| client=await _Client.from_env() if self._client is None else self.client, | ||
| environment_name=config.get("environment") if self._environment_name is None else self._environment_name, | ||
| app_id=self._app_id, | ||
| ) | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def reset(self) -> "LoadContext": | ||
| self._client = None | ||
| self._environment_name = None | ||
| self._app_id = None | ||
| return self | ||
|
|
||
| async def in_place_upgrade( | ||
| self, client: Optional[_Client] = None, environment_name: Optional[str] = None, app_id: Optional[str] = None | ||
| ) -> "LoadContext": | ||
| """In-place set values if they aren't already set, or set default values | ||
|
|
||
| Intended for Function/Cls hydration specifically | ||
|
|
||
| In those cases, it's important to in-place upgrade/apply_defaults since any "sibling" of the function/cls | ||
| would share the load context with its parent, and the initial load context overrides may not be sufficient | ||
| since an `app.deploy()` etc could get arguments that set a new client etc. | ||
|
|
||
| E.g. | ||
| @app.function() | ||
| def f(): | ||
| ... | ||
|
|
||
| f2 = Function.with_options(...) | ||
|
|
||
| with app.run(client=...): # hydrates f and f2 at this point | ||
| ... | ||
| """ | ||
| self._client = self._client or client or await _Client.from_env() | ||
| self._environment_name = self._environment_name or environment_name or config.get("environment") | ||
| self._app_id = self._app_id or app_id | ||
| return self | ||
Uh oh!
There was an error while loading. Please reload this page.