Skip to content

Incorrect nested state scoping for multiple models in HierarchicalAsyncMachine #735

@japgarrido

Description

@japgarrido

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.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions