Skip to content

Commit 7f409be

Browse files
committed
feat: async engine parity, multi-target transitions, and SCXML send fixes
- Rewrite AsyncEngine to mirror SyncEngine's 3-phase macrostep architecture with full StateChart support - Extract pure computation helpers from BaseEngine so both engines share the same logic - Add on_error callback support to async dispatch methods - Support multi-target initial transitions for SCXML parallel regions in engine, IO layer, parser, and factory - Fix SCXML send error handling: error.communication for undispatchable session targets, error.execution for invalid targets/types, and namelist variable validation - Remove resolved xfail markers (test364, test496, test521, test553)
1 parent a710687 commit 7f409be

12 files changed

Lines changed: 513 additions & 393 deletions

File tree

AGENTS.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,15 @@ uv run pytest tests/test_signature.py::TestSignatureAdapter::test_wrap_fn_single
5151
uv run pytest -m "not slow"
5252
```
5353

54-
Tests include doctests from both source modules (`--doctest-modules`) and markdown docs
55-
(`--doctest-glob=*.md`). Coverage is enabled by default.
54+
When trying to run all tests, prefer to use xdist (`-n`) as some SCXML tests uses timeout of 30s to verify fallback mechanism.
55+
Don't specify the directory `tests/`, because this will exclude doctests from both source modules (`--doctest-modules`) and markdown docs
56+
(`--doctest-glob=*.md`) (enabled by default):
57+
58+
```bash
59+
uv run pytest -n auto
60+
```
61+
62+
Coverage is enabled by default.
5663

5764
## Linting and formatting
5865

statemachine/callbacks.py

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -298,20 +298,39 @@ def add(self, key: str, spec: CallbackSpec, builder: Callable[[], Callable]):
298298

299299
insort(self.items, wrapper)
300300

301-
async def async_call(self, *args, **kwargs):
302-
return await asyncio.gather(
303-
*(
304-
callback(*args, **kwargs)
305-
for callback in self
306-
if callback.condition(*args, **kwargs)
301+
async def async_call(
302+
self, *args, on_error: "Callable[[Exception], None] | None" = None, **kwargs
303+
):
304+
if on_error is None:
305+
return await asyncio.gather(
306+
*(
307+
callback(*args, **kwargs)
308+
for callback in self
309+
if callback.condition(*args, **kwargs)
310+
)
307311
)
308-
)
309312

310-
async def async_all(self, *args, **kwargs):
311-
coros = [condition(*args, **kwargs) for condition in self]
312-
for coro in asyncio.as_completed(coros):
313-
if not await coro:
314-
return False
313+
results = []
314+
for callback in self:
315+
if callback.condition(*args, **kwargs):
316+
try:
317+
results.append(await callback(*args, **kwargs))
318+
except Exception as e:
319+
on_error(e)
320+
return results
321+
322+
async def async_all(
323+
self, *args, on_error: "Callable[[Exception], None] | None" = None, **kwargs
324+
):
325+
for callback in self:
326+
try:
327+
if not await callback(*args, **kwargs):
328+
return False
329+
except Exception as e:
330+
if on_error is not None:
331+
on_error(e)
332+
return False
333+
raise
315334
return True
316335

317336
def call(self, *args, on_error: "Callable[[Exception], None] | None" = None, **kwargs):
@@ -388,8 +407,16 @@ def call(
388407
return []
389408
return self._registry[key].call(*args, on_error=on_error, **kwargs)
390409

391-
def async_call(self, key: str, *args, **kwargs):
392-
return self._registry[key].async_call(*args, **kwargs)
410+
async def async_call(
411+
self,
412+
key: str,
413+
*args,
414+
on_error: "Callable[[Exception], None] | None" = None,
415+
**kwargs,
416+
):
417+
if key not in self._registry:
418+
return []
419+
return await self._registry[key].async_call(*args, on_error=on_error, **kwargs)
393420

394421
def all(
395422
self,
@@ -402,8 +429,16 @@ def all(
402429
return True
403430
return self._registry[key].all(*args, on_error=on_error, **kwargs)
404431

405-
def async_all(self, key: str, *args, **kwargs):
406-
return self._registry[key].async_all(*args, **kwargs)
432+
async def async_all(
433+
self,
434+
key: str,
435+
*args,
436+
on_error: "Callable[[Exception], None] | None" = None,
437+
**kwargs,
438+
):
439+
if key not in self._registry:
440+
return True
441+
return await self._registry[key].async_all(*args, on_error=on_error, **kwargs)
407442

408443
def str(self, key: str) -> str:
409444
if key not in self._registry:

0 commit comments

Comments
 (0)