11# Copyright Modal Labs 2025
22import inspect
33import typing
4- from collections .abc import Collection
54from typing import Any , Optional
65
76from google .protobuf .message import Message
@@ -52,43 +51,44 @@ def stop(self):
5251 ```
5352 """
5453
55- # Backend returns "cs-" IDs for servers (since it is defined on classes).
56- # Set directly to avoid overwriting _Cls's registration in _Object._prefix_to_type.
54+ # Maps 1-1 w function
5755 _type_prefix = "cs"
5856
5957 _app : Optional ["modal.app._App" ] = None
6058 _name : Optional [str ] = None
61- _user_server : Optional [type ] = None
62- _user_server_function : Optional [_Function ] = None
63- # Runtime state for lifecycle management
64- _user_server_instance : Optional [Any ] = None
59+ # Raw user defined class
60+ _user_cls : Optional [type ] = None
61+ # Instantiated raw user class
62+ _user_cls_instance : Optional [Any ] = None
63+ # Function interface with server backend
64+ _service_function : Optional [_Function ] = None
6565 _has_entered : bool = False
6666
6767 def _initialize_from_empty (self ):
68- self ._user_server = None
69- self ._user_server_function = None
68+ self ._user_cls = None
7069 self ._name = None
7170 self ._app = None
72- self ._user_server_instance = None
71+ self ._user_cls_instance = None
72+ self ._service_function = None
7373 self ._has_entered = False
7474
7575 def _initialize_from_other (self , other : "_Server" ):
7676 super ()._initialize_from_other (other )
7777 self ._app = other ._app
78- self ._user_server = other ._user_server
79- self ._user_server_function = other ._user_server_function
78+ self ._user_cls = other ._user_cls
79+ self ._user_cls_instance = other ._user_cls_instance
80+ self ._service_function = other ._service_function
8081 self ._name = other ._name
8182 self ._load_context_overrides = other ._load_context_overrides
82- self ._user_server_instance = other ._user_server_instance
8383 self ._has_entered = other ._has_entered
8484
8585 def _get_app (self ) -> "modal.app._App" :
8686 assert self ._app is not None
8787 return self ._app
8888
89- def _get_user_server (self ) -> type :
90- assert self ._user_server is not None
91- return self ._user_server
89+ def _get_user_cls (self ) -> type :
90+ assert self ._user_cls is not None
91+ return self ._user_cls
9292
9393 def _get_name (self ) -> str :
9494 assert self ._name is not None
@@ -99,33 +99,29 @@ def __name__(self) -> str:
9999 """Return the name of the server class for compatibility with code expecting class-like objects."""
100100 return self ._name or ""
101101
102- def _get_method_names (self ) -> Collection [str ]:
103- """Servers don't have methods - return empty collection for compatibility."""
104- return []
105-
106102 def _get_service_function (self ) -> _Function :
107- assert self ._user_server_function is not None
108- return self ._user_server_function
103+ assert self ._service_function is not None
104+ return self ._service_function
109105
110106 # ============ Lifecycle Management ============
111107
112- def _get_or_create_instance (self ) -> Any :
108+ def _get_or_create_user_cls_instance (self ) -> Any :
113109 """Get or construct the local server instance."""
114- if self ._user_server_instance is None :
115- assert self ._user_server is not None
116- self ._user_server_instance = object .__new__ (self ._user_server )
117- return self ._user_server_instance
110+ if self ._user_cls_instance is None :
111+ assert self ._user_cls is not None
112+ self ._user_cls_instance = object .__new__ (self ._user_cls )
113+ return self ._user_cls_instance
118114
119115 def _enter (self ):
120116 """Run @enter lifecycle hooks (sync version)."""
121- assert self ._user_server is not None
117+ assert self ._user_cls is not None
122118 if self ._has_entered :
123119 return
124120
125- user_server_instance = self ._get_or_create_instance ()
121+ user_cls_instance = self ._get_or_create_user_cls_instance ()
126122
127123 # Support __enter__ context manager protocol
128- enter_method = getattr (user_server_instance , "__enter__" , None )
124+ enter_method = getattr (user_cls_instance , "__enter__" , None )
129125 if enter_method is not None :
130126 enter_method ()
131127
@@ -134,7 +130,7 @@ def _enter(self):
134130 _PartialFunctionFlags .ENTER_PRE_SNAPSHOT ,
135131 _PartialFunctionFlags .ENTER_POST_SNAPSHOT ,
136132 ):
137- for enter_method in _find_callables_for_obj (user_server_instance , method_flag ).values ():
133+ for enter_method in _find_callables_for_obj (user_cls_instance , method_flag ).values ():
138134 enter_method ()
139135
140136 self ._has_entered = True
@@ -145,10 +141,10 @@ async def _aenter(self):
145141 if self ._has_entered :
146142 return
147143
148- user_server_instance = self ._get_or_create_instance ()
144+ user_cls_instance = self ._get_or_create_user_cls_instance ()
149145
150- aenter_method = getattr (user_server_instance , "__aenter__" , None )
151- enter_method = getattr (user_server_instance , "__enter__" , None )
146+ aenter_method = getattr (user_cls_instance , "__aenter__" , None )
147+ enter_method = getattr (user_cls_instance , "__enter__" , None )
152148 if aenter_method is not None :
153149 await aenter_method ()
154150 elif enter_method is not None :
@@ -159,7 +155,7 @@ async def _aenter(self):
159155 _PartialFunctionFlags .ENTER_PRE_SNAPSHOT ,
160156 _PartialFunctionFlags .ENTER_POST_SNAPSHOT ,
161157 ):
162- for enter_method in _find_callables_for_obj (user_server_instance , method_flag ).values ():
158+ for enter_method in _find_callables_for_obj (user_cls_instance , method_flag ).values ():
163159 enter_method ()
164160
165161 self ._has_entered = True
@@ -196,8 +192,6 @@ async def update_autoscaler(
196192 TODO(claudia): Add examples
197193
198194 """
199- # if target_concurrency is not None:
200- # await self._get_service_function().update_autoscaler(target_concurrency=target_concurrency)
201195 return await self ._get_service_function ().update_autoscaler (
202196 min_containers = min_containers ,
203197 max_containers = max_containers ,
@@ -208,75 +202,72 @@ async def update_autoscaler(
208202 # ============ Hydration ============
209203
210204 def _hydrate_metadata (self , metadata : Optional [Message ]):
211- # Servers don't have method metadata like Cls does
212- if metadata is not None :
213- assert isinstance (metadata , api_pb2 .ClassHandleMetadata )
214- # Just verify the service function is hydrated
215205 service_function = self ._get_service_function ()
216206 assert service_function .is_hydrated
217207
218208 # ============ Construction ============
219209
220210 @staticmethod
221- def validate_wrapped_server_decorators (wrapped_server : type , enable_memory_snapshot : bool ):
222- if not inspect .isclass (wrapped_server ):
211+ def validate_wrapped_user_cls_decorators (wrapped_user_cls : type , enable_memory_snapshot : bool ):
212+ # TODO(claudia): Add tests for this, ensure that parametrization is not allowed.
213+ if not inspect .isclass (wrapped_user_cls ):
223214 raise TypeError ("The @app.server() decorator must be used on a class." )
224215 if not _find_partial_methods_for_user_cls (
225- wrapped_server , _PartialFunctionFlags .ENTER_PRE_SNAPSHOT
226- ) and not _find_partial_methods_for_user_cls (wrapped_server , _PartialFunctionFlags .ENTER_POST_SNAPSHOT ):
216+ wrapped_user_cls , _PartialFunctionFlags .ENTER_PRE_SNAPSHOT
217+ ) and not _find_partial_methods_for_user_cls (wrapped_user_cls , _PartialFunctionFlags .ENTER_POST_SNAPSHOT ):
227218 raise InvalidError ("Server class must have an @modal.enter() to setup the server." )
228219
229220 # Check for disallowed decorators
230221 # @modal.method() not allowed
231- if _find_partial_methods_for_user_cls (wrapped_server , _PartialFunctionFlags .CALLABLE_INTERFACE ).values ():
222+ if _find_partial_methods_for_user_cls (wrapped_user_cls , _PartialFunctionFlags .CALLABLE_INTERFACE ).values ():
232223 raise InvalidError (
233- f"Server class { wrapped_server .__name__ } cannot have @method() decorated functions. "
224+ f"Server class { wrapped_user_cls .__name__ } cannot have @method() decorated functions. "
234225 "Servers only expose HTTP endpoints."
235226 )
236227 # @enter with snap=True without enable_memory_snapshot
237228 if (
238- _find_partial_methods_for_user_cls (wrapped_server , _PartialFunctionFlags .ENTER_PRE_SNAPSHOT )
229+ _find_partial_methods_for_user_cls (wrapped_user_cls , _PartialFunctionFlags .ENTER_PRE_SNAPSHOT )
239230 and not enable_memory_snapshot
240231 ):
241232 raise InvalidError ("Server must have `enable_memory_snapshot=True` to use `snap=True` on @enter methods." )
242233
243- if isinstance (wrapped_server , _PartialFunction ):
234+ if isinstance (wrapped_user_cls , _PartialFunction ):
244235 # @modal.concurrent not allowed on server classes
245- if wrapped_server .flags & _PartialFunctionFlags .CONCURRENT :
236+ if wrapped_user_cls .flags & _PartialFunctionFlags .CONCURRENT :
246237 raise InvalidError (
247- f"Server class { wrapped_server .__name__ } cannot have @concurrent() decorated functions. "
238+ f"Server class { wrapped_user_cls .__name__ } cannot have @concurrent() decorated functions. "
248239 "Please use `target_concurrency` param instead."
249240 )
250241 # @modal.http_server not allowed on server classes
251- if wrapped_server .flags & _PartialFunctionFlags .HTTP_WEB_INTERFACE :
242+ if wrapped_user_cls .flags & _PartialFunctionFlags .HTTP_WEB_INTERFACE :
252243 raise InvalidError (
253- f"Server class { wrapped_server .__name__ } cannot have @modal.experimental .http_server() decorator. "
244+ f"Server class { wrapped_user_cls .__name__ } cannot have @modal.http_server() decorator. "
254245 "Servers already expose HTTP endpoints."
255246 )
256247
257248 @staticmethod
258- def validate_construction_mechanism (user_server : type ):
249+ def validate_construction_mechanism (user_cls : type ):
259250 """Validate that the server class doesn't have a custom constructor."""
260- if user_server .__init__ != object .__init__ :
251+ if user_cls .__init__ != object .__init__ :
261252 raise InvalidError (
262- f"Server class { user_server .__name__ } cannot have a custom __init__ method. "
253+ f"Server class { user_cls .__name__ } cannot have a custom __init__ method. "
263254 "Use @modal.enter() for initialization logic instead."
264255 )
265256
266257 @staticmethod
267258 def from_local (
268- user_server : type ,
259+ user_cls : type ,
269260 app : "modal.app._App" ,
270261 service_function : _Function ,
271262 ) -> "_Server" :
272263 """Create a Server from a local class definition."""
273- _Server .validate_wrapped_server_decorators ( user_server , enable_memory_snapshot = False )
264+ _Server .validate_wrapped_user_cls_decorators ( user_cls , enable_memory_snapshot = False )
274265 # Validate - no custom constructors allowed
275- _Server .validate_construction_mechanism (user_server )
266+ _Server .validate_construction_mechanism (user_cls )
276267
277268 # Mark lifecycle methods as registered to avoid warnings
278269 lifecycle_flags = ~ _PartialFunctionFlags .interface_flags ()
279- lifecycle_partials = _find_partial_methods_for_user_cls (user_server , lifecycle_flags )
270+ lifecycle_partials = _find_partial_methods_for_user_cls (user_cls , lifecycle_flags )
280271 for partial_function in lifecycle_partials .values ():
281272 partial_function .registered = True
282273
@@ -298,17 +289,17 @@ async def _load(
298289 resp = await load_context .client .stub .ClassCreate (req )
299290 self ._hydrate (resp .class_id , load_context .client , resp .handle_metadata )
300291
301- rep = f"Server({ user_server .__name__ } )"
292+ rep = f"Server({ user_cls .__name__ } )"
302293 server : _Server = _Server ._from_loader (
303294 _load ,
304295 rep ,
305296 deps = _deps ,
306297 load_context_overrides = app ._root_load_context ,
307298 )
308299 server ._app = app
309- server ._user_server = user_server
310- server ._user_server_function = service_function
311- server ._name = user_server .__name__
300+ server ._user_cls = user_cls
301+ server ._service_function = service_function
302+ server ._name = user_cls .__name__
312303 return server
313304
314305 @classmethod
0 commit comments