@@ -51,23 +51,38 @@ class MyNexusService:
51
51
input_type : Optional [type [InputT ]] = dataclasses .field (default = None )
52
52
output_type : Optional [type [OutputT ]] = dataclasses .field (default = None )
53
53
54
- def __post_init__ (self ):
55
- if not self .name :
56
- raise ValueError ("Operation name cannot be empty" )
57
54
58
- def _validation_errors (self ) -> list [str ]:
59
- errors = []
60
- if not self .name :
61
- errors .append (
62
- f"Operation has no name (method_name is '{ self .method_name } ')"
55
+ @dataclass
56
+ class OperationDefinition (Generic [InputT , OutputT ]):
57
+ """
58
+ Internal representation of a user's :py:class:`Operation` definition.
59
+ """
60
+
61
+ name : str
62
+ method_name : str
63
+ input_type : type [InputT ]
64
+ output_type : type [OutputT ]
65
+
66
+ @classmethod
67
+ def from_operation (
68
+ cls , operation : Operation [InputT , OutputT ]
69
+ ) -> OperationDefinition [InputT , OutputT ]:
70
+ if not operation .name :
71
+ raise ValueError (
72
+ f"Operation has no name (method_name is '{ operation .method_name } ')"
63
73
)
64
- if not self .method_name :
65
- errors .append (f"Operation '{ self .name } ' has no method name" )
66
- if not self .input_type :
67
- errors .append (f"Operation '{ self .name } ' has no input type" )
68
- if not self .output_type :
69
- errors .append (f"Operation '{ self .name } ' has no output type" )
70
- return errors
74
+ if not operation .method_name :
75
+ raise ValueError (f"Operation '{ operation .name } ' has no method name" )
76
+ if not operation .input_type :
77
+ raise ValueError (f"Operation '{ operation .name } ' has no input type" )
78
+ if not operation .output_type :
79
+ raise ValueError (f"Operation '{ operation .name } ' has no output type" )
80
+ return cls (
81
+ name = operation .name ,
82
+ method_name = operation .method_name ,
83
+ input_type = operation .input_type ,
84
+ output_type = operation .output_type ,
85
+ )
71
86
72
87
73
88
@overload
@@ -124,16 +139,23 @@ def decorator(cls: type[ServiceT]) -> type[ServiceT]:
124
139
defn = ServiceDefinition .from_class (cls , name or cls .__name__ )
125
140
set_service_definition (cls , defn )
126
141
127
- # In order for callers to refer to operations at run-time, a decorated user
142
+ # In order for callers to refer to operation definitions at run-time, a decorated user
128
143
# service class must itself have a class attribute for every operation, even if
129
144
# declared only via a type annotation, and whether inherited from a parent class
130
145
# or not.
131
146
#
132
147
# TODO(preview): it is sufficient to do this setattr only for the subset of
133
148
# operations that were declared on *this* class. Currently however we are
134
149
# setting all inherited operations.
135
- for op_name , op in defn .operations .items ():
136
- setattr (cls , op_name , op )
150
+ for op_name , op_defn in defn .operation_definitions .items ():
151
+ if not hasattr (cls , op_name ):
152
+ op = Operation (
153
+ name = op_defn .name ,
154
+ method_name = op_defn .method_name ,
155
+ input_type = op_defn .input_type ,
156
+ output_type = op_defn .output_type ,
157
+ )
158
+ setattr (cls , op_name , op )
137
159
138
160
return cls
139
161
@@ -155,7 +177,7 @@ class ServiceDefinition:
155
177
"""
156
178
157
179
name : str
158
- operations : Mapping [str , Operation [Any , Any ]]
180
+ operation_definitions : Mapping [str , OperationDefinition [Any , Any ]]
159
181
160
182
def __post_init__ (self ):
161
183
if errors := self ._validation_errors ():
@@ -173,7 +195,7 @@ def from_class(user_class: type[ServiceT], name: str) -> ServiceDefinition:
173
195
If multiple service definitions define an operation with the same name, then the
174
196
usual mro() precedence rules apply.
175
197
"""
176
- operations = ServiceDefinition ._collect_operations (user_class )
198
+ operation_definitions = ServiceDefinition ._collect_operations (user_class )
177
199
178
200
# Obtain the set of operations to be inherited from ancestral service
179
201
# definitions. Operations are only inherited from classes that are also
@@ -186,47 +208,53 @@ def from_class(user_class: type[ServiceT], name: str) -> ServiceDefinition:
186
208
# 2. No inherited operation has the same method name as that of an operation
187
209
# defined here. If this were violated, there would be ambiguity in which
188
210
# operation handler is dispatched to.
189
- parent_defns = (
190
- defn
191
- for defn in (get_service_definition (cls ) for cls in user_class .mro ()[1 :])
192
- if defn
211
+ parent_service_defn = next (
212
+ (
213
+ defn
214
+ for defn in (
215
+ get_service_definition (cls ) for cls in user_class .mro ()[1 :]
216
+ )
217
+ if defn
218
+ ),
219
+ None ,
193
220
)
194
- method_names = { op . method_name for op in operations . values () if op . method_name }
195
- if parent_defn := next ( parent_defns , None ):
196
- for op in parent_defn . operations .values ():
197
- if op .method_name in method_names :
221
+ if parent_service_defn :
222
+ method_names = { op . method_name for op in operation_definitions . values ()}
223
+ for op_defn in parent_service_defn . operation_definitions .values ():
224
+ if op_defn .method_name in method_names :
198
225
raise ValueError (
199
- f"Operation method name '{ op .method_name } ' in class '{ user_class } ' "
226
+ f"Operation method name '{ op_defn .method_name } ' in class '{ user_class } ' "
200
227
f"also occurs in a service definition inherited from a parent class: "
201
- f"'{ parent_defn .name } '. This is not allowed."
228
+ f"'{ parent_service_defn .name } '. This is not allowed."
202
229
)
203
- if op .name in operations :
230
+ if op_defn .name in operation_definitions :
204
231
raise ValueError (
205
- f"Operation name '{ op .name } ' in class '{ user_class } ' "
232
+ f"Operation name '{ op_defn .name } ' in class '{ user_class } ' "
206
233
f"also occurs in a service definition inherited from a parent class: "
207
- f"'{ parent_defn .name } '. This is not allowed."
234
+ f"'{ parent_service_defn .name } '. This is not allowed."
208
235
)
209
- operations [ op .name ] = op
236
+ operation_definitions [ op_defn .name ] = op_defn
210
237
211
- return ServiceDefinition (name = name , operations = operations )
238
+ return ServiceDefinition (name = name , operation_definitions = operation_definitions )
212
239
213
240
def _validation_errors (self ) -> list [str ]:
214
241
errors = []
215
242
if not self .name :
216
243
errors .append ("Service has no name" )
217
244
seen_method_names = set ()
218
- for op in self .operations .values ():
219
- if op .method_name in seen_method_names :
220
- errors .append (f"Operation method name '{ op .method_name } ' is not unique" )
221
- seen_method_names .add (op .method_name )
222
- errors .extend (op ._validation_errors ()) # pyright: ignore[reportPrivateUsage]
245
+ for op_defn in self .operation_definitions .values ():
246
+ if op_defn .method_name in seen_method_names :
247
+ errors .append (
248
+ f"Operation method name '{ op_defn .method_name } ' is not unique"
249
+ )
250
+ seen_method_names .add (op_defn .method_name )
223
251
return errors
224
252
225
253
@staticmethod
226
254
def _collect_operations (
227
255
user_class : type [ServiceT ],
228
- ) -> dict [str , Operation [Any , Any ]]:
229
- """Collect operations from a user service definition class.
256
+ ) -> dict [str , OperationDefinition [Any , Any ]]:
257
+ """Collect operation definitions from a user service definition class.
230
258
231
259
Does not visit parent classes.
232
260
"""
@@ -301,11 +329,11 @@ def _collect_operations(
301
329
if op .method_name is None :
302
330
op .method_name = key
303
331
304
- operations_by_name = {}
332
+ op_defns = {}
305
333
for op in operations .values ():
306
- if op .name in operations_by_name :
307
- raise ValueError (
308
- f"Operation '{ op .name } ' in class '{ user_class } ' is defined multiple times"
334
+ if op .name in op_defns :
335
+ raise RuntimeError (
336
+ f"Operation '{ op .name } ' in service '{ user_class } ' is defined multiple times"
309
337
)
310
- operations_by_name [op .name ] = op
311
- return operations_by_name
338
+ op_defns [op .name ] = OperationDefinition . from_operation ( op )
339
+ return op_defns
0 commit comments