Skip to content

Commit 401e3bd

Browse files
committed
fix
1 parent b67f632 commit 401e3bd

6 files changed

Lines changed: 64 additions & 90 deletions

File tree

modal/_partial_function.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
125125
user_cls: Optional[type] = None # class
126126
flags: _PartialFunctionFlags
127127
params: _PartialFunctionParams
128-
registered: bool = False # Set to True when registered with an App to avoid warnings
129128

130129
def __init__(
131130
self,

modal/_server.py

Lines changed: 53 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ def _get_user_cls(self) -> type:
3939
return self._user_cls
4040

4141
def _get_app(self) -> "modal.app._App":
42-
assert self._app, "app can only be extracted for local Server (in container entrypoint)"
42+
assert self._app, "App can only be extracted for a local Server (in container entrypoint)"
4343
return self._app
4444

4545
def _get_service_function(self) -> _Function:
46-
assert self._service_function is not None # don't we need this?
46+
assert self._service_function is not None
4747
return self._service_function
4848

4949
@staticmethod
@@ -86,15 +86,63 @@ async def update_autoscaler(
8686

8787
# ============ Hydration ============
8888
async def hydrate(self, client: Optional[_Client] = None) -> "_Server":
89-
"""Hydrate the server by hydrating its underlying service function."""
9089
# This is required since we want to support @livemethod() decorated methods
91-
# and is normally handled by the _Object.hydrate() method
92-
# but we only want to hydrate the service function.
9390
service_function = self._get_service_function()
9491
await service_function.hydrate(client)
9592
return self
9693

9794
# ============ Construction ============
95+
@staticmethod
96+
def from_local(
97+
wrapped_user_cls: "type | _PartialFunction",
98+
app: "modal.app._App",
99+
service_function: _Function,
100+
) -> "_Server":
101+
"""Create a Server from a local class definition."""
102+
103+
# Note: Validation should be done by the caller (app.server()) BEFORE creating the Server.
104+
# Extract the underlying class if wrapped in a _PartialFunction (e.g., from @modal.clustered())
105+
user_cls = _Server._extract_user_cls(wrapped_user_cls)
106+
107+
server = _Server()
108+
server._app = app
109+
server._user_cls = user_cls
110+
server._service_function = service_function
111+
return server
112+
113+
@classmethod
114+
def from_name(
115+
cls: type["_Server"],
116+
app_name: str,
117+
name: str,
118+
*,
119+
environment_name: Optional[str] = None,
120+
client: Optional[_Client] = None,
121+
) -> "_Server":
122+
"""Reference a Server from a deployed App by its name.
123+
124+
This is a lazy method that defers hydrating the local
125+
object with metadata from Modal servers until the first
126+
time it is actually used.
127+
128+
TODO(claudia): Add examples
129+
"""
130+
131+
load_context_overrides = LoadContext(client=client, environment_name=environment_name)
132+
133+
server = _Server()
134+
server._service_function = _Function._from_name(
135+
app_name,
136+
name,
137+
load_context_overrides=load_context_overrides,
138+
)
139+
return server
140+
141+
def _is_local(self) -> bool:
142+
"""Returns True if this Server has local source code available."""
143+
return self._user_cls is not None
144+
145+
# ============ Validation ============
98146

99147
@staticmethod
100148
def _validate_wrapped_user_cls_decorators(
@@ -157,64 +205,3 @@ def validate_construction_mechanism(wrapped_user_cls: "type | _PartialFunction")
157205
f"Server class {user_cls.__name__} cannot have a custom __init__ method. "
158206
"Use @modal.enter() for initialization logic instead."
159207
)
160-
161-
@staticmethod
162-
def from_local(
163-
wrapped_user_cls: "type | _PartialFunction",
164-
app: "modal.app._App",
165-
service_function: _Function,
166-
) -> "_Server":
167-
"""Create a Server from a local class definition.
168-
169-
Note: Validation should be done by the caller (app.server()) BEFORE creating
170-
the service function, so we don't repeat it here.
171-
"""
172-
# Extract the underlying class if wrapped in a _PartialFunction (e.g., from @modal.clustered())
173-
user_cls = _Server._extract_user_cls(wrapped_user_cls)
174-
175-
# Mark lifecycle methods as registered to avoid warnings
176-
lifecycle_flags = ~_PartialFunctionFlags.interface_flags()
177-
lifecycle_partials = _find_partial_methods_for_user_cls(user_cls, lifecycle_flags)
178-
for partial_function in lifecycle_partials.values():
179-
partial_function.registered = True
180-
181-
server = _Server()
182-
server._app = app
183-
server._user_cls = user_cls
184-
server._service_function = service_function
185-
return server
186-
187-
@classmethod
188-
def from_name(
189-
cls: type["_Server"],
190-
app_name: str,
191-
name: str,
192-
*,
193-
environment_name: Optional[str] = None,
194-
client: Optional[_Client] = None,
195-
) -> "_Server":
196-
"""Reference a Server from a deployed App by its name.
197-
198-
This is a lazy method that defers hydrating the local
199-
object with metadata from Modal servers until the first
200-
time it is actually used.
201-
202-
TODO(claudia): Add examples
203-
"""
204-
205-
load_context_overrides = LoadContext(client=client, environment_name=environment_name)
206-
207-
# Server service functions use "#ClassName" naming convention
208-
service_function_name = f"#{name}"
209-
210-
server = _Server()
211-
server._service_function = _Function._from_name(
212-
app_name,
213-
service_function_name,
214-
load_context_overrides=load_context_overrides,
215-
)
216-
return server
217-
218-
def _is_local(self) -> bool:
219-
"""Returns True if this Server has local source code available."""
220-
return self._user_cls is not None

modal/app.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,19 +1286,19 @@ def wrapper(wrapped_user_cls: Union[CLS_T, _PartialFunction, Callable]) -> Serve
12861286
# Extract the underlying class if wrapped in a _PartialFunction (e.g., from @modal.clustered())
12871287
cluster_size = None
12881288
rdma = None
1289+
user_cls = wrapped_user_cls
1290+
12891291
if isinstance(wrapped_user_cls, _PartialFunction):
12901292
user_cls = wrapped_user_cls.user_cls
12911293
if wrapped_user_cls.flags & _PartialFunctionFlags.CLUSTERED:
12921294
cluster_size = wrapped_user_cls.params.cluster_size
12931295
rdma = wrapped_user_cls.params.rdma
1294-
else:
1295-
user_cls = wrapped_user_cls
12961296

12971297
local_state = self._local_state
12981298

12991299
# Create the FunctionInfo for the server, note we treat FunctionInfo as a class for servers
13001300
# Use "#ClassName" format to avoid collision with class_ids which use "ClassName"
1301-
info = FunctionInfo(None, serialized=serialized, user_cls=user_cls, name_override=f"#{user_cls.__name__}")
1301+
info = FunctionInfo(None, serialized=serialized, user_cls=user_cls, name_override=f"{user_cls.__name__}")
13021302
# Create the service function
13031303
service_function = _Function.from_local(
13041304
info,
@@ -1337,19 +1337,7 @@ def wrapper(wrapped_user_cls: Union[CLS_T, _PartialFunction, Callable]) -> Serve
13371337
)
13381338

13391339
self._add_function(service_function, is_web_endpoint=False)
1340-
1341-
# Create the Server object
13421340
server: Server = Server.from_local(wrapped_user_cls, self, service_function)
1343-
1344-
# Mark lifecycle methods as registered
1345-
for flag in (~_PartialFunctionFlags.interface_flags(),):
1346-
for partial in _find_partial_methods_for_user_cls(user_cls, flag).values():
1347-
partial.registered = True
1348-
1349-
# Note: We don't register the Server in classes - only the service_function is registered.
1350-
# The Server is just a local wrapper. The container side identifies servers via
1351-
# function_def.is_class and the function_ids.
1352-
13531341
return server # type: ignore
13541342

13551343
return wrapper

test/container_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2484,7 +2484,7 @@ def test_server_lifecycle_with_deployed_metadata(servicer, tmp_path, deployed_su
24842484
servicer,
24852485
tmp_path,
24862486
deployed_support_function_definitions,
2487-
"AppServerWithEnter.*", # function name as stored by isolated_deploy
2487+
"AppServerWithEnter",
24882488
inputs=[], # Server classes don't have method inputs
24892489
)
24902490
stdout, stderr = container_process.communicate(timeout=15)

test/flash_cls_test_with_app_server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,15 @@ def test_run_server(client, servicer):
6868
assert servicer.n_functions == 1
6969
objects = servicer.app_objects[app_id]
7070
# Servers use just the class name, not "ClassName.*"
71-
server_function_id = objects["#FlashClassDefault"]
71+
server_function_id = objects["FlashClassDefault"]
7272
assert servicer.precreated_functions == {server_function_id}
7373
assert method_handle_object_id == server_function_id
7474
assert len(objects) == 1 # just the service function
7575
assert server_function_id.startswith("fu-")
7676
assert servicer.app_functions[server_function_id].is_class
7777

7878
assert servicer.app_functions[server_function_id].module_name == "test.flash_cls_test_with_app_server"
79-
assert servicer.app_functions[server_function_id].function_name == "#FlashClassDefault"
79+
assert servicer.app_functions[server_function_id].function_name == "FlashClassDefault"
8080
assert servicer.app_functions[server_function_id].target_concurrent_inputs == 10
8181
assert servicer.app_functions[server_function_id].method_definitions_set
8282
assert servicer.app_functions[server_function_id].startup_timeout_secs == 30
@@ -106,7 +106,7 @@ def test_flash_params_override_experimental_options(client, servicer):
106106
app_id = flash_params_override_app.app_id
107107

108108
objects = servicer.app_objects[app_id]
109-
server_function_id = objects["#FlashParamsOverrideClass"]
109+
server_function_id = objects["FlashParamsOverrideClass"]
110110

111111
assert servicer.app_functions[server_function_id].target_concurrent_inputs == 11
112112
assert servicer.app_functions[server_function_id].experimental_options["flash"] == "us-east"

test/server_test.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,9 @@ def start(self):
247247

248248
with app.run(client=client):
249249
# This should not raise AttributeError: '_Server' object has no attribute 'hydrate'
250-
urls = URLServer.get_urls() # type: ignore[attr-defined]
250+
urls = URLServer._experimental_get_urls() # type: ignore[attr-defined]
251251
# URLs are generated by the mock servicer based on function name and proxy regions
252-
assert urls == ["https://modal-labs--#urlserver.modal-us-east.modal.direct"]
252+
assert urls == ["https://modal-labs--urlserver.modal-us-east.modal.direct"]
253253

254254

255255
def test_server_update_autoscaler(client, servicer):
@@ -444,9 +444,9 @@ def start(self):
444444
objects = servicer.app_objects[app_id]
445445

446446
# Servers use "#ClassName" naming convention
447-
assert "#ObjectsServer" in objects
447+
assert "ObjectsServer" in objects
448448

449-
server_id = objects["#ObjectsServer"]
449+
server_id = objects["ObjectsServer"]
450450
assert server_id.startswith("fu-")
451451

452452

0 commit comments

Comments
 (0)