feat: invoke callback group — spawn external work from states#571
Merged
feat: invoke callback group — spawn external work from states#571
Conversation
Add invoke as a first-class callback group (CallbackGroup.INVOKE), following SCXML <invoke> semantics. States can spawn background work (API calls, file I/O, child state machines) on entry and cancel it on exit. - New CallbackGroup.INVOKE with convention naming (on_invoke_<state>), decorator (@state.invoke), and inline callables - IInvoke protocol for advanced handlers with InvokeContext (cancellation, send events to parent, machine reference) - StateChartInvoker adapter for child state machine invocation - invoke_group() for running multiple callables concurrently and waiting for all results as a single done.invoke event - InvokeManager lifecycle management integrated into both sync and async engines (sync: daemon threads, async: thread executor wrapped in asyncio.Task) - done_invoke_<state> factory prefix maps to done.invoke.<state> event family - visitor pattern (visit/async_visit) on CallbacksExecutor and CallbacksRegistry - __contains__ on CallbacksRegistry to avoid direct _registry access - Full test suite and documentation with practical file I/O examples
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #571 +/- ##
============================================
+ Coverage 99.94% 100.00% +0.05%
============================================
Files 32 33 +1
Lines 3768 4044 +276
Branches 583 635 +52
============================================
+ Hits 3766 4044 +278
+ Misses 1 0 -1
+ Partials 1 0 -1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
- Fix _InvokeCallableWrapper.on_cancel() to handle class handlers that haven't been instantiated yet (early return instead of calling unbound method) - Replace blocking threading.Event.wait() with sm_runner.sleep() in async tests to avoid freezing the event loop - Add tests for cancel_all(), cancel of terminated invocations, on_cancel exception suppression, StateChartInvoker.on_cancel(), normalize_invoke_callbacks edge cases, and _resolve_handler paths - Coverage: 90% → 96%
- Prefix unused ctx parameter in StateChartInvoker.run() with underscore - Remove unnecessary async from _spawn_one_async (no await in body) - Add explicit return after swallowing CancelledError with rationale comment - Add comment to empty on_cancel() in test to explain intent
- Test done_invoke_ prefix with Event() objects (factory.py L290-291) - Test visit/async_visit early return for missing registry keys (callbacks.py) - Test async_visit with awaitable visitor function (callbacks.py L378)
- Remove dead code: _needs_wrapping hasattr(item, "run") branch was unreachable because IInvoke protocol check catches it first - Add test for _InvokeCallableWrapper.__call__ (L74) - Add test for non-IInvoke class passing through normalize_invoke_callbacks - Add test for InvokeGroup.on_cancel() before run() - invoke.py: 0 missing lines, 4 remaining branch partials are all structurally unreachable (Protocol body, async race conditions)
Forward keyword arguments from the triggering event to invoke handlers:
- Plain callables receive them via SignatureAdapter dependency injection
- IInvoke handlers receive them via ctx.kwargs
This allows patterns like sm.send("start", file_name="config.json")
where the invoke handler reads file_name as a parameter.
…machine Move visit condition tests to test_callbacks.py and invalid state value test to test_statemachine.py. Add tests for async cancelled-during-execution, sync error-after-cancel, and spawn_pending_async empty paths. Add pragma: no branch to IInvoke Protocol method body (coverage.py limitation with Protocol classes).
|
Contributor
|
👏👏👏 great |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
CallbackGroup.INVOKE), following SCXML<invoke>semantics and UML's do activity (do/) conceptasyncio.Task) and cancel it on exiton_invoke_<state>), decorators (@state.invoke), inline callables,IInvokeprotocol, and childStateChartinvocationinvoke_group()runs multiple callables concurrently and waits for all results as a singledone.invokeeventdone_invoke_<state>factory prefix maps todone.invoke.<state>event familyvisit()/async_visit()visitor pattern and__contains__toCallbacksRegistryExample
For concurrent work with grouped results:
Closes #521