Describe the bug
When using HierarchicalAsyncMachine with multiple models and nested states, state scoping becomes incorrect during async transitions. Specifically, nested state names are incorrectly prefixed multiple times, resulting in invalid state names.
In the example below, two models (model and model2) are attached to the same HierarchicalAsyncMachine with queued='model'. During execution, one model transitions correctly through the nested caffeinated states, but the second model fails with a ValueError because the machine attempts to resolve a doubly-scoped state name that was never registered.
This issue does not occur with a single model and appears to be specific to async machines with nested states.
Minimal working example
from transitions.extensions import HierarchicalAsyncMachine
states = ['standing', 'walking', {'name': 'caffeinated', 'initial': 'dithering', 'children':['dithering', 'running', {'name': 'finished', 'final': True}]}, {'name': 'end', 'final': True}]
transitions = [
['next_step', 'standing', 'walking'],
['next_step', 'walking', 'caffeinated'],
['next_step', 'caffeinated_dithering', 'caffeinated_running'],
['next_step', 'caffeinated_running', 'caffeinated_finished'],
['next_step', 'caffeinated', 'end']
]
class Model:
def __init__(self, name):
self.name = name
async def on_enter_standing(self):
print(f'{self.name} has entered standing state.')
async def on_enter_walking(self):
print(f'{self.name} has entered walking state.')
async def on_enter_caffeinated_dithering(self):
print(f'{self.name} has entered caffeinated dithering state.')
async def on_enter_caffeinated_running(self):
print(f'{self.name} has entered caffeinated running state.')
async def on_enter_caffeinated_finished(self):
print(f'{self.name} has entered caffeinated finished state.')
async def on_enter_end(self):
print(f'{self.name} has entered end state.')
async def run(self):
while not self.state == 'end':
await self.next_step()
model = Model('Model1')
model2 = Model('Model2')
machine = HierarchicalAsyncMachine(model=[model, model2], states=states, transitions=transitions, initial='standing', ignore_invalid_triggers=True, queued='model')
async def run():
asyncio.gather(model.run(), model2.run())
import asyncio
asyncio.run(run())
Expected behavior
Each model attached to a HierarchicalAsyncMachine should maintain an independent and correctly scoped state path. In the provided example, nested states should only be prefixed once (e.g., caffeinated_dithering) regardless of how many models are attached to the machine.
Additional context
- Transitions: Release 0.9.3
Traceback:
future: <_GatheringFuture finished exception=ValueError("State 'caffeinated_caffeinated_dithering' is not a registered state.")>
Traceback (most recent call last):
File "/home/user/.cache/pypoetry/virtualenvs/WwaJHQ-py3.11/lib/python3.11/site-packages/transitions/extensions/nesting.py", line 654, in get_state
with self(child):
^^^^^^^^^^^
File "/home/user/.cache/pypoetry/virtualenvs/WwaJHQ-py3.11/lib/python3.11/site-packages/transitions/extensions/nesting.py", line 422, in __call__
state = self.states[state_name]
~~~~~~~~~~~^^^^^^^^^^^^
KeyError: 'caffeinated'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/user/.cache/pypoetry/virtualenvs/WwaJHQ-py3.11/lib/python3.11/site-packages/transitions/extensions/nesting.py", line 661, in get_state
state = state.states[elem]
~~~~~~~~~~~~^^^^^^
KeyError: 'caffeinated'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/user/.cache/pypoetry/virtualenvs/WwaJHQ-py3.11/lib/python3.11/site-packages/transitions/extensions/nesting.py", line 655, in get_state
return self.get_state(state, hint)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/user/.cache/pypoetry/virtualenvs/p2pfl---WwaJHQ-py3.11/lib/python3.11/site-packages/transitions/extensions/nesting.py", line 664, in get_state
raise ValueError(
ValueError: State 'caffeinated_caffeinated_dithering' is not a registered state.
Describe the bug
When using HierarchicalAsyncMachine with multiple models and nested states, state scoping becomes incorrect during async transitions. Specifically, nested state names are incorrectly prefixed multiple times, resulting in invalid state names.
In the example below, two models (model and model2) are attached to the same HierarchicalAsyncMachine with queued='model'. During execution, one model transitions correctly through the nested caffeinated states, but the second model fails with a ValueError because the machine attempts to resolve a doubly-scoped state name that was never registered.
This issue does not occur with a single model and appears to be specific to async machines with nested states.
Minimal working example
Expected behavior
Each model attached to a HierarchicalAsyncMachine should maintain an independent and correctly scoped state path. In the provided example, nested states should only be prefixed once (e.g., caffeinated_dithering) regardless of how many models are attached to the machine.
Additional context
Traceback: