@@ -111,10 +111,6 @@ class InvokeConfig:
111111
112112 id : "str | None" = None
113113 idlocation : "str | None" = None
114- autoforward : bool = False
115- namelist : "str | None" = None
116- params : "List[Any]" = field (default_factory = list )
117- finalize : Any = None
118114
119115
120116@dataclass
@@ -199,6 +195,52 @@ def _apply_params(child_sm: "StateChart", params: "dict[str, Any]"):
199195 setattr (child_sm .model , name , value )
200196
201197
198+ # ---------------------------------------------------------------------------
199+ # Callback-based invoke adapters
200+ # ---------------------------------------------------------------------------
201+
202+
203+ class CallbackInvokeAdapter :
204+ """Wraps a resolved CallbackWrapper as an Invoker for InvokeManager.
205+
206+ When the state machine defines ``on_invoke_<state>()`` or passes a
207+ string/callable via ``State(invoke="method")``, the callback system
208+ resolves it into a ``CallbackWrapper``. This adapter bridges it to the
209+ invoke lifecycle so InvokeManager can spawn, track, and cancel it uniformly.
210+ """
211+
212+ def __init__ (self , wrapper ):
213+ self ._wrapper = wrapper
214+ self ._cancelled : "threading.Event | None" = None
215+
216+ def run (self , ctx : InvokeContext ) -> Any :
217+ self ._cancelled = threading .Event ()
218+ return self ._wrapper .call (
219+ machine = ctx ._parent_sm ,
220+ model = ctx ._parent_sm .model ,
221+ cancelled = self ._cancelled ,
222+ )
223+
224+ def on_cancel (self ):
225+ if self ._cancelled is not None :
226+ self ._cancelled .set ()
227+
228+
229+ class AsyncCallbackInvokeAdapter (CallbackInvokeAdapter ):
230+ """Async variant of :class:`CallbackInvokeAdapter`."""
231+
232+ async def run (self , ctx : InvokeContext ) -> Any :
233+ self ._cancelled = threading .Event ()
234+ result = self ._wrapper ._callback (
235+ machine = ctx ._parent_sm ,
236+ model = ctx ._parent_sm .model ,
237+ cancelled = self ._cancelled ,
238+ )
239+ if inspect .isawaitable (result ):
240+ result = await result
241+ return result
242+
243+
202244class _ParentBridge :
203245 """Listener attached to a child SM. Placeholder for future extensions."""
204246
@@ -211,16 +253,6 @@ def __init__(self, ctx: InvokeContext):
211253# ---------------------------------------------------------------------------
212254
213255
214- class _TriggerDataAdapter :
215- """Adapts a TriggerData to look like EventData for EventDataWrapper."""
216-
217- def __init__ (self , trigger_data : Any ):
218- self .trigger_data = trigger_data
219-
220- def __getattr__ (self , name : str ) -> Any :
221- return getattr (self .trigger_data , name )
222-
223-
224256class InvokeManager :
225257 """Manages active invocations for an engine instance.
226258
@@ -238,38 +270,51 @@ def __init__(self, engine: "BaseEngine"):
238270 def sm (self ) -> "StateChart" :
239271 return self ._engine .sm
240272
273+ # --- Config resolution ---
274+
275+ def _get_all_configs (self , state : "State" ) -> "List[InvokeConfig]" :
276+ """Combine InvokeConfig sources: static ``state.invocations`` and callback-based."""
277+ configs = list (getattr (state , "invocations" , None ) or [])
278+ callback_configs = getattr (self .sm , "_callback_invocations" , {})
279+ configs .extend (callback_configs .get (state .id , []))
280+ return configs
281+
282+ def _has_invocations (self , state : "State" ) -> bool :
283+ return bool (self ._get_all_configs (state ))
284+
241285 # --- Engine hooks ---
242286
243287 def on_state_entered (self , state : "State" ) -> None :
244288 """Track a state with invocations for post-macrostep spawning."""
245- if getattr (state , "invocations" , None ):
289+ if self . _has_invocations (state ):
246290 self ._states_to_invoke .add (state )
247291
248292 def on_state_exiting (self , state : "State" ) -> None :
249293 """Cancel invocations and remove from pending spawn set."""
250- if getattr (state , "invocations" , None ):
294+ if self . _has_invocations (state ):
251295 self .cancel_for_state (state )
252296 self ._states_to_invoke .discard (state )
253297
254298 def spawn_pending_sync (self , trigger_data : Any ) -> None :
255299 """Spawn invocations for states entered during this macrostep (sync)."""
256300 for state in sorted (self ._states_to_invoke , key = lambda s : s .document_order ):
257- for config in state . invocations :
301+ for config in self . _get_all_configs ( state ) :
258302 self .spawn_sync (state , config , trigger_data )
259303 self ._states_to_invoke .clear ()
260304
261305 async def spawn_pending_async (self , trigger_data : Any ) -> None :
262306 """Spawn invocations for states entered during this macrostep (async)."""
263307 for state in sorted (self ._states_to_invoke , key = lambda s : s .document_order ):
264- for config in state . invocations :
308+ for config in self . _get_all_configs ( state ) :
265309 await self .spawn_async (state , config , trigger_data )
266310 self ._states_to_invoke .clear ()
267311
268312 def handle_external_event (self , trigger_data : Any ) -> bool :
269313 """Process invoke-related aspects of an external event.
270314
271315 Handles forward_target (returns ``True`` if the event was consumed),
272- and applies finalize/autoforward as side-effects.
316+ and delegates finalize/autoforward to the invoker via optional
317+ protocol methods (``on_finalize``, ``on_event``).
273318
274319 Returns:
275320 ``True`` if the event was forwarded to another session and should
@@ -280,13 +325,23 @@ def handle_external_event(self, trigger_data: Any) -> bool:
280325 self ._forward_to_target (trigger_data )
281326 return True
282327
283- # Handle invoke finalize and autoforward
328+ # Delegate to invokers
284329 for state in self .sm .configuration :
285330 for inv in self .active_for_state (state ):
331+ handler = inv .handler
332+
333+ # Finalize: invoker handles it
286334 if trigger_data .invokeid and inv .invokeid == trigger_data .invokeid :
287- self .apply_finalize (inv , trigger_data )
288- if inv .config .autoforward and trigger_data .event :
289- self .forward_event (inv , str (trigger_data .event ), trigger_data )
335+ if hasattr (handler , "on_finalize" ):
336+ handler .on_finalize (trigger_data )
337+
338+ # Autoforward: invoker handles it
339+ if hasattr (handler , "on_event" ) and trigger_data .event :
340+ if getattr (handler , "autoforward" , False ):
341+ try :
342+ handler .on_event (str (trigger_data .event ), ** trigger_data .kwargs )
343+ except Exception :
344+ logger .exception ("Error forwarding event to %s" , inv .invokeid )
290345
291346 return False
292347
@@ -330,37 +385,6 @@ def _set_idlocation(self, config: InvokeConfig, invokeid: str):
330385 if config .idlocation :
331386 setattr (self .sm .model , config .idlocation , invokeid )
332387
333- # --- Param evaluation ---
334-
335- def _evaluate_params (self , config : InvokeConfig , trigger_data : Any ) -> "dict[str, Any]" :
336- """Evaluate namelist and param expressions in the parent's context."""
337- params : dict [str , Any ] = {}
338-
339- if config .namelist :
340- for name in config .namelist .strip ().split ():
341- if not hasattr (self .sm .model , name ):
342- raise NameError (f"Namelist variable '{ name } ' not found on parent model" )
343- params [name ] = getattr (self .sm .model , name )
344-
345- for param in config .params :
346- if param .expr is not None :
347- from .io .scxml .actions import _eval
348-
349- try :
350- kwargs = {"machine" : self .sm , "model" : self .sm .model }
351- kwargs .update (
352- {
353- k : v
354- for k , v in self .sm .model .__dict__ .items ()
355- if k not in {"_sessionid" , "_ioprocessors" , "_name" , "_event" }
356- }
357- )
358- params [param .name ] = _eval (param .expr , ** kwargs )
359- except Exception :
360- logger .exception ("Error evaluating param %s" , param .name )
361-
362- return params
363-
364388 # --- Handler resolution ---
365389
366390 def _resolve_handler (self , handler : Any ) -> Any :
@@ -385,16 +409,9 @@ def spawn_sync(self, state: "State", config: InvokeConfig, trigger_data: Any):
385409 invokeid = self ._generate_invokeid (state , config )
386410 self ._set_idlocation (config , invokeid )
387411
388- try :
389- params = self ._evaluate_params (config , trigger_data )
390- except Exception as e :
391- # SCXML spec: error in arg evaluation cancels the invocation
392- logger .debug ("Error evaluating invoke params for %s: %s" , invokeid , e )
393- self .sm .send ("error.execution" , error = e , internal = True )
394- return
395412 ctx = InvokeContext (
396413 invokeid = invokeid ,
397- params = params ,
414+ params = {} ,
398415 send = self ._make_parent_send (invokeid ),
399416 _parent_sm = self .sm ,
400417 )
@@ -470,15 +487,9 @@ async def spawn_async(self, state: "State", config: InvokeConfig, trigger_data:
470487 invokeid = self ._generate_invokeid (state , config )
471488 self ._set_idlocation (config , invokeid )
472489
473- try :
474- params = self ._evaluate_params (config , trigger_data )
475- except Exception as e :
476- logger .debug ("Error evaluating invoke params for %s: %s" , invokeid , e )
477- self .sm .send ("error.execution" , error = e , internal = True )
478- return
479490 ctx = InvokeContext (
480491 invokeid = invokeid ,
481- params = params ,
492+ params = {} ,
482493 send = self ._make_parent_send (invokeid ),
483494 _parent_sm = self .sm ,
484495 )
@@ -624,36 +635,6 @@ def get_invocation_for_child(self, child_sm: "StateChart") -> "Invocation | None
624635 return inv
625636 return None
626637
627- # --- Finalize & Autoforward ---
628-
629- def apply_finalize (self , invocation : Invocation , trigger_data : Any ):
630- """Execute the finalize block for an invocation before the event is processed."""
631- config = invocation .config
632- if config .finalize is None :
633- return
634- try :
635- from .io .scxml .actions import EventDataWrapper
636-
637- _event = EventDataWrapper (_TriggerDataAdapter (trigger_data ))
638- config .finalize (
639- machine = self .sm , model = self .sm .model , event_data = trigger_data , _event = _event
640- )
641- except Exception :
642- logger .exception ("Error in finalize for %s" , invocation .invokeid )
643-
644- def forward_event (self , invocation : Invocation , event_name : str , trigger_data : Any ):
645- """Forward an event to a child session (autoforward)."""
646- handler = invocation .handler
647- if handler is not None and hasattr (handler , "on_event" ):
648- try :
649- handler .on_event (event_name , ** trigger_data .kwargs )
650- except Exception :
651- logger .exception ("Error forwarding event to %s" , invocation .invokeid )
652- else :
653- child_sm = invocation .child_sm or getattr (handler , "_child_sm" , None )
654- if child_sm is not None and not invocation .terminated :
655- child_sm .send (event_name , ** trigger_data .kwargs )
656-
657638 # --- Cross-session sends ---
658639
659640 def send_to_child (self , event_name : str , ** kwargs ):
0 commit comments